diff --git a/.github/workflows/dynamic-contracts.yml b/.github/workflows/dynamic-contracts.yml index d5c0c53f..1115398b 100644 --- a/.github/workflows/dynamic-contracts.yml +++ b/.github/workflows/dynamic-contracts.yml @@ -137,8 +137,20 @@ jobs: fi sleep 2 done + # Assert the mainnet nesting, not just that a tick advanced. + shape_ok=0 + if [ "$ticked" -eq 1 ]; then + ti=$(wget -qO- http://127.0.0.1:41841/live/v1/tick-info 2>/dev/null) || true + bh=$(wget -qO- http://127.0.0.1:41841/live/v1/block-height 2>/dev/null) || true + nested=$(printf "%s" "$ti" | sed -n "s/.*\"tickInfo\":{\([^}]*\)}.*/\1/p") + shape_ok=1 + for field in tick duration epoch initialTick; do + printf "%s" "$nested" | grep -q "\"$field\":" || { echo "tick-info: tickInfo.$field missing"; shape_ok=0; } + done + printf "%s" "$bh" | grep -q "\"blockHeight\":{" || { echo "block-height: blockHeight wrapper missing"; shape_ok=0; } + fi kill "$node_pid" 2>/dev/null || true - if [ "$ticked" -ne 1 ]; then + if [ "$ticked" -ne 1 ] || [ "$shape_ok" -ne 1 ]; then cat node-smoke.log exit 1 fi diff --git a/src/extensions/http/controller/rpc_live_controller.h b/src/extensions/http/controller/rpc_live_controller.h index 9530edad..4f8be75b 100644 --- a/src/extensions/http/controller/rpc_live_controller.h +++ b/src/extensions/http/controller/rpc_live_controller.h @@ -14,6 +14,16 @@ #include "extensions/rpc/rpc_core.h" // ============================ live (/live/v1/...) ============================ +// Mainnet returns errors as HTTP 4xx/5xx, never 200: clients branch on res.ok. +static RpcResp rpcErr(int code, const std::string& message, int status = 400) +{ + Json::Value e; + e["code"] = code; + e["message"] = message; + e["details"] = Json::Value(Json::arrayValue); + return jsonResp(e, status); +} + RPC_ROUTE("GET", "/live/v1/assets/issuances") { std::string issuerIdentity = req.getParameter("issuerIdentity"); @@ -37,14 +47,15 @@ RPC_ROUTE("GET", "/live/v1/assets/issuances") Json::Value root; Json::Value assetJson = HttpUtils::issuanceToJson((HttpUtils::AssetIssuanceType *)&asset); root["data"] = assetJson; + root["tick"] = system.tick; + root["universeIndex"] = Json::UInt64(i); assetsArray.append(root); targetUniverseIndex = i; break; } } + (void)targetUniverseIndex; result["assets"] = assetsArray; - result["tick"] = system.tick; - result["universeIndex"] = Json::UInt64(targetUniverseIndex); return jsonResp(result); } @@ -54,15 +65,11 @@ RPC_ROUTE("GET", "/live/v1/assets/issuances/:index") unsigned long long index = std::stoull(req.getParameter("index")); if (index >= ASSETS_CAPACITY) { - result["code"] = 3; - result["message"] = "Index out of range"; - return jsonResp(result); + return rpcErr(3, "Index out of range", 400); } if (assets[index].varStruct.issuance.type != ISSUANCE) { - result["code"] = 3; - result["message"] = "No asset issuance at the given index"; - return jsonResp(result); + return rpcErr(3, "No asset issuance at the given index", 400); } auto &asset = assets[index].varStruct.issuance; Json::Value assetJson = HttpUtils::issuanceToJson((HttpUtils::AssetIssuanceType *)&asset); @@ -118,15 +125,11 @@ RPC_ROUTE("GET", "/live/v1/assets/ownerships/:index") unsigned long long index = std::stoull(req.getParameter("index")); if (index >= ASSETS_CAPACITY) { - result["code"] = 3; - result["message"] = "Index out of range"; - return jsonResp(result); + return rpcErr(3, "Index out of range", 400); } if (assets[index].varStruct.ownership.type != OWNERSHIP) { - result["code"] = 3; - result["message"] = "No asset ownership at the given index"; - return jsonResp(result); + return rpcErr(3, "No asset ownership at the given index", 400); } auto &asset = assets[index].varStruct.ownership; Json::Value assetJson = HttpUtils::ownershipToJson((HttpUtils::AssetOwnershipType *)&asset); @@ -189,15 +192,11 @@ RPC_ROUTE("GET", "/live/v1/assets/possessions/:index") unsigned long long index = std::stoull(req.getParameter("index")); if (index >= ASSETS_CAPACITY) { - result["code"] = 3; - result["message"] = "Index out of range"; - return jsonResp(result); + return rpcErr(3, "Index out of range", 400); } if (assets[index].varStruct.possession.type != POSSESSION) { - result["code"] = 3; - result["message"] = "No asset possession at the given index"; - return jsonResp(result); + return rpcErr(3, "No asset possession at the given index", 400); } auto &asset = assets[index].varStruct.possession; Json::Value assetJson = HttpUtils::possessionToJson((HttpUtils::AssetPossessionType *)&asset); @@ -258,6 +257,8 @@ RPC_ROUTE("GET", "/live/v1/assets/:identity/owned") Json::Value root; Json::Value assetJson = HttpUtils::ownershipToJson((HttpUtils::AssetOwnershipType *)&asset); + // Only the per-identity routes carry padding; the universe-index ones do not. + assetJson["padding"] = (int)asset.padding[0]; assetJson["issuedAsset"] = HttpUtils::issuanceToJson((HttpUtils::AssetIssuanceType *)issuanceAsset); root["data"] = assetJson; Json::Value info(Json::objectValue); @@ -292,7 +293,12 @@ RPC_ROUTE("GET", "/live/v1/assets/:identity/possessed") Json::Value root; Json::Value assetJson = HttpUtils::possessionToJson((HttpUtils::AssetPossessionType *)&asset); + // Per-identity possessions report the owned asset's issuanceIndex, not ownershipIndex. + assetJson["padding"] = (int)asset.padding[0]; + assetJson.removeMember("ownershipIndex"); + assetJson["issuanceIndex"] = ownershipAsset->issuanceIndex; assetJson["ownedAsset"] = HttpUtils::ownershipToJson((HttpUtils::AssetOwnershipType *)ownershipAsset); + assetJson["ownedAsset"]["padding"] = (int)ownershipAsset->padding[0]; assetJson["ownedAsset"]["issuedAsset"] = HttpUtils::issuanceToJson((HttpUtils::AssetIssuanceType *)issuanceAsset); root["data"] = assetJson; Json::Value info(Json::objectValue); @@ -327,23 +333,24 @@ RPC_ROUTE("GET", "/live/v1/balances/:id") return jsonResp(result); } -// block-height + tick-info share the same body in the drogon controller. -static RpcResp rpcLiveTickInfo(const RpcReq& req) +static RpcResp rpcLiveTickInfo(const RpcReq& req, const char* wrapperKey) { (void)req; + Json::Value tickInfo; + tickInfo["tick"] = system.tick; + tickInfo["duration"] = 0; + tickInfo["epoch"] = system.epoch; + tickInfo["initialTick"] = system.initialTick; + Json::Value json; - json["epoch"] = system.epoch; - json["tick"] = system.tick; - json["initialTick"] = system.initialTick; + json[wrapperKey] = tickInfo; json["alignedVotes"] = gTickNumberOfComputors; json["misalignedVotes"] = gTickTotalNumberOfComputors - gTickNumberOfComputors; json["mainAuxStatus"] = mainAuxStatus; - json["duration"] = 0; - json["tickInfo"]["tick"] = system.tick; return jsonResp(json); } -RPC_ROUTE("GET", "/live/v1/block-height") { return rpcLiveTickInfo(req); } -RPC_ROUTE("GET", "/live/v1/tick-info") { return rpcLiveTickInfo(req); } +RPC_ROUTE("GET", "/live/v1/block-height") { return rpcLiveTickInfo(req, "blockHeight"); } +RPC_ROUTE("GET", "/live/v1/tick-info") { return rpcLiveTickInfo(req, "tickInfo"); } RPC_ROUTE("POST", "/live/v1/broadcast-transaction") { @@ -353,9 +360,7 @@ RPC_ROUTE("POST", "/live/v1/broadcast-transaction") auto json = rpcJsonBody(req.body); if (!json) { - result["code"] = 3; - result["message"] = "Invalid JSON"; - return jsonResp(result); + return rpcErr(3, "Invalid JSON", 400); } std::string txBase64 = (*json)["encodedTransaction"].asString(); @@ -364,9 +369,7 @@ RPC_ROUTE("POST", "/live/v1/broadcast-transaction") Transaction *tx = (Transaction*)txData.data(); if (!tx->checkValidity()) { - result["code"] = 3; - result["message"] = "Invalid validity"; - return jsonResp(result); + return rpcErr(3, "Invalid validity", 400); } std::cout << "tx json" << HttpUtils::transactionToJson(tx, false) << std::endl; { @@ -374,9 +377,7 @@ RPC_ROUTE("POST", "/live/v1/broadcast-transaction") KangarooTwelve(txData.data(), tx->totalSize() - SIGNATURE_SIZE, digest, sizeof(digest)); if (!verify(tx->sourcePublicKey.m256i_u8, digest, tx->signaturePtr())) { - result["code"] = 3; - result["message"] = "Invalid signature"; - return jsonResp(result); + return rpcErr(3, "Invalid signature", 400); } } @@ -400,9 +401,7 @@ RPC_ROUTE("POST", "/live/v1/broadcast-transaction") } catch (const std::exception &e) { - result["code"] = -1; - result["message"] = "Exception: " + std::string(e.what()); - return jsonResp(result); + return rpcErr(-1, "Exception: " + std::string(e.what()), 500); } } @@ -433,17 +432,13 @@ RPC_ROUTE("POST", "/live/v1/querySmartContract") auto json = rpcJsonBody(req.body); if (!json) { - result["code"] = 3; - result["message"] = "Invalid JSON"; - return jsonResp(result); + return rpcErr(3, "Invalid JSON", 400); } unsigned int contractIndex = (*json)["contractIndex"].asUInt(); if (contractIndex < 1 || contractIndex >= contractCount) { - result["code"] = 3; - result["message"] = "contractIndex out of range"; - return jsonResp(result, 400); + return rpcErr(3, "contractIndex out of range", 400); } unsigned short inputType = (*json)["inputType"].asUInt(); unsigned short inputSize = (*json)["inputSize"].asUInt(); @@ -451,9 +446,7 @@ RPC_ROUTE("POST", "/live/v1/querySmartContract") std::vector inputData = base64_decode(requestData); if (inputData.size() != inputSize) { - result["code"] = 3; - result["message"] = "Input size mismatch"; - return jsonResp(result, 400); + return rpcErr(3, "Input size mismatch", 400); } QpiContextUserFunctionCall qpiContext(contractIndex); auto errorCode = qpiContext.call(inputType, inputData.data(), inputSize); @@ -466,16 +459,12 @@ RPC_ROUTE("POST", "/live/v1/querySmartContract") } else { - result["code"] = -1; - result["message"] = "Error calling smart contract function: " + std::to_string(errorCode); - return jsonResp(result, 500); + return rpcErr(-1, "Error calling smart contract function: " + std::to_string(errorCode), 500); } } catch (const std::exception &e) { - result["code"] = -1; - result["message"] = "Exception: " + std::string(e.what()); - return jsonResp(result, 500); + return rpcErr(-1, "Exception: " + std::string(e.what()), 500); } } diff --git a/src/extensions/http/controller/rpc_queryv2_controller.h b/src/extensions/http/controller/rpc_queryv2_controller.h index 7711bec9..be898a4b 100644 --- a/src/extensions/http/controller/rpc_queryv2_controller.h +++ b/src/extensions/http/controller/rpc_queryv2_controller.h @@ -138,7 +138,7 @@ RPC_ROUTE("POST", "/query/v1/getTickData") jsonObject["tickNumber"] = localTickData.tick; jsonObject["epoch"] = localTickData.epoch; jsonObject["computorIndex"] = localTickData.computorIndex; - jsonObject["timelock"] = base64_encode(localTickData.timelock.m256i_u8, 32); + jsonObject["timeLock"] = base64_encode(localTickData.timelock.m256i_u8, 32); jsonObject["timestamp"] = HttpUtils::formatTimestamp(localTickData.millisecond, localTickData.second, localTickData.minute, localTickData.hour, localTickData.day, localTickData.month, localTickData.year); jsonObject["varStruct"] = ""; @@ -152,10 +152,12 @@ RPC_ROUTE("POST", "/query/v1/getTickData") txDigestsJson.append(wchar_to_string(id)); } } - jsonObject["transactionDigests"] = txDigestsJson; + jsonObject["transactionHashes"] = txDigestsJson; Json::Value contractFeesJson(Json::arrayValue); + // Zero fees are dropped, as go-archiver's contractFeesToProto() does. for (unsigned int i = 0; i < MAX_NUMBER_OF_CONTRACTS; i++) - contractFeesJson.append(Json::UInt64(localTickData.contractFees[i])); + if (localTickData.contractFees[i] != 0) + contractFeesJson.append(std::to_string(localTickData.contractFees[i])); jsonObject["contractFees"] = contractFeesJson; jsonObject["signature"] = base64_encode(localTickData.signature, SIGNATURE_SIZE); return jsonResp(jsonObject); diff --git a/src/extensions/http/controller/rpc_stats_controller.h b/src/extensions/http/controller/rpc_stats_controller.h index 7453d545..fe1927ae 100644 --- a/src/extensions/http/controller/rpc_stats_controller.h +++ b/src/extensions/http/controller/rpc_stats_controller.h @@ -219,7 +219,7 @@ RPC_ROUTE("GET", "/v1/latest-stats") } TickStorage::tickData.releaseLock(); - data["circulatingSupply"] = Json::UInt64(spectrumInfo.totalAmount); + data["circulatingSupply"] = std::to_string(spectrumInfo.totalAmount); data["activeAddresses"] = spectrumInfo.numberOfEntities; data["price"] = 0; data["marketCap"] = "0"; @@ -237,7 +237,7 @@ RPC_ROUTE("GET", "/v1/latest-stats") } data["emptyTicksInCurrentEpoch"] = emptyTicks; data["epochTickQuality"] = system.tick - system.initialTick == 0 ? 0 : std::roundf((float)(system.tick - system.initialTick - emptyTicks) / (float)(system.tick - system.initialTick) * 100000.0f) / 100000.0f; - data["burnedQus"] = 0; + data["burnedQus"] = "0"; result["data"] = data; return jsonResp(result); } diff --git a/src/extensions/http/static/explorer/app.js b/src/extensions/http/static/explorer/app.js index 0f83a3a8..4d47f356 100644 --- a/src/extensions/http/static/explorer/app.js +++ b/src/extensions/http/static/explorer/app.js @@ -454,7 +454,7 @@ async function renderTick(n) { ${tx.hash ? `` : ''} `).join(''); - const digests = (td.transactionDigests || []); + const digests = (td.transactionHashes || []); $view().innerHTML = `

TICK ${fmt.n(td.tickNumber ?? n)}

@@ -482,7 +482,7 @@ async function renderTick(n) {

TIMELOCK

-
${fmt.esc(td.timelock || '—')}
+
${fmt.esc(td.timeLock || '—')}

SIGNATURE

diff --git a/src/extensions/http/utils.h b/src/extensions/http/utils.h index 2a1495ed..51eccdfe 100644 --- a/src/extensions/http/utils.h +++ b/src/extensions/http/utils.h @@ -62,7 +62,8 @@ class HttpUtils assetJson["name"] = std::string(asset->name); assetJson["type"] = ASSET_ISSUANCE; assetJson["numberOfDecimalPlaces"] = asset->numberOfDecimalPlaces; - for (int i = 0; i < 8; i++) + // char[7]: the old i < 8 ran past the struct. + for (size_t i = 0; i < sizeof(asset->unitOfMeasurement); i++) { unitOfMeasurementArray.append(asset->unitOfMeasurement[i]); } @@ -124,7 +125,8 @@ class HttpUtils getIdentity(digest, txHashStr, true); CHAR16 humanId[61] = {0}; jsonObject["hash"] = wchar_to_string(txHashStr); - jsonObject["amount"] = Json::UInt64(tx->amount); + // int64 as string, per protobuf JSON: a number loses precision past 2^53. + jsonObject["amount"] = std::to_string(tx->amount); getIdentity((const unsigned char *)&tx->sourcePublicKey, humanId, false); jsonObject["source"] = wchar_to_string(humanId); getIdentity((const unsigned char *)&tx->destinationPublicKey, humanId, false);