diff --git a/src/node/api/http.py b/src/node/api/http.py index e5f8944..dd75c13 100644 --- a/src/node/api/http.py +++ b/src/node/api/http.py @@ -21,6 +21,21 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +def normalize_info_payload(data: dict) -> dict: + """Normalize cryptarchia/info responses. + + Current nodes wrap the fields under "cryptarchia_info" and report mode as + a (possibly nested) object like {"Started": "Online"}; flat payloads with a + plain string mode pass through unchanged. + """ + info = dict(data.get("cryptarchia_info", data)) + mode = data.get("mode", info.get("mode")) + while isinstance(mode, dict): + mode = next(iter(mode.values()), "Unknown") if mode else "Unknown" + info["mode"] = mode if isinstance(mode, str) else str(mode) + return info + + class HttpNodeApi(NodeApi): # Paths can't have a leading slash since they are relative to the base URL ENDPOINT_INFO = "cryptarchia/info" @@ -79,7 +94,7 @@ class HttpNodeApi(NodeApi): url = urljoin(self.base_url, self.ENDPOINT_INFO) response = requests.get(url, auth=self.authentication, timeout=60) response.raise_for_status() - return InfoSerializer.model_validate(response.json()) + return InfoSerializer.model_validate(normalize_info_payload(response.json())) async def get_block_by_hash(self, block_hash: str) -> Optional[BlockSerializer]: url = urljoin(self.base_url, self.ENDPOINT_BLOCK_BY_HASH + block_hash) diff --git a/src/node/api/serializers/fields.py b/src/node/api/serializers/fields.py index c687776..894fb95 100644 --- a/src/node/api/serializers/fields.py +++ b/src/node/api/serializers/fields.py @@ -28,6 +28,17 @@ def bytes_from_int(data: int) -> bytes: return data.to_bytes((data.bit_length() + 7) // 8) # TODO: Ensure endianness is correct. +def bytes_from_hex_or_intarray(data: str | list[int]) -> bytes: + """Node versions drifted between int arrays and hex strings for byte fields. + + Older nodes (<= 0.1.2) serialize bytes as int arrays; newer nodes serialize + them as hex strings. Accept both so the explorer works across node versions. + """ + if isinstance(data, str): + return bytes_from_hex(data) + return bytes_from_intarray(data) + + def bytes_into_hex(data: bytes) -> str: return data.hex() @@ -35,3 +46,6 @@ def bytes_into_hex(data: bytes) -> str: BytesFromIntArray = Annotated[bytes, BeforeValidator(bytes_from_intarray), PlainSerializer(bytes_into_hex)] BytesFromHex = Annotated[bytes, BeforeValidator(bytes_from_hex), PlainSerializer(bytes_into_hex)] BytesFromInt = Annotated[bytes, BeforeValidator(bytes_from_int), PlainSerializer(bytes_into_hex)] +BytesFromHexOrIntArray = Annotated[ + bytes, BeforeValidator(bytes_from_hex_or_intarray), PlainSerializer(bytes_into_hex) +] diff --git a/src/node/api/serializers/transaction.py b/src/node/api/serializers/transaction.py index 3ffab43..0f156dd 100644 --- a/src/node/api/serializers/transaction.py +++ b/src/node/api/serializers/transaction.py @@ -1,17 +1,21 @@ from random import randint -from typing import List, Self +from typing import List, Optional, Self from pydantic import Field from core.models import NbeSerializer +from node.api.serializers.fields import BytesFromHex from node.api.serializers.operation import LedgerOpSerializer, MantleOpSerializerField from utils.protocols import FromRandom class TransactionSerializer(NbeSerializer, FromRandom): ops: List[MantleOpSerializerField] - execution_gas_price: int = Field(description="Integer in u64 format.") - storage_gas_price: int = Field(description="Integer in u64 format.") + # Newer nodes include the canonical tx hash in mantle_tx; older ones don't. + hash: Optional[BytesFromHex] = Field(default=None, description="Canonical tx hash (newer nodes only).") + # Gas prices were dropped from mantle_tx on newer nodes; default to 0 there. + execution_gas_price: int = Field(default=0, description="Integer in u64 format.") + storage_gas_price: int = Field(default=0, description="Integer in u64 format.") @classmethod def from_random(cls) -> Self: diff --git a/tests/fixtures/block_new_format.json b/tests/fixtures/block_new_format.json new file mode 100644 index 0000000..44332da --- /dev/null +++ b/tests/fixtures/block_new_format.json @@ -0,0 +1,37 @@ +{ + "header": { + "id": "d266fec335ecf2bfb713db1c5bf389e21fd80d208a9ef01989da6aef3deae7b4", + "parent_block": "c4afbf0d3a99fbcb624295a6626ad2786ec181e02522467c55489eb72fe81ce1", + "slot": 13240353, + "block_root": "ac4d5d879a87da6b62a1e1c97840a73372cfef8fd8b7b454d5fbef66e21b1365", + "proof_of_leadership": { + "proof": "bf5f85bf2f664ac55bc0116e75d5ba44d36935935ea53df24cc23bdff10b16a87ec8a60cfd8414b29a50e3467f091c00ae7b2ad4edeedfa6c610de50b4051d249c62e8a0b99e99005ba4b1342c524569150bc8cd58a9d31bda86f5a23d7d858e1094b87b9db5866f998e4aea806b9c3c346b945cb10ba2e4b51c22931bbe901a", + "entropy_contribution": "1a5b15d2ef474acf2f1ef7e46e40967f9bd6ccd5bf1877a85350e6294ddad90a", + "leader_key": "a38c8d8d2e560cd21cf9cf856bb7e93fff41fa6a3ab0991945c3761ee7a20d50", + "voucher_cm": "c4ecf502b774020f2a70474720b4de25653fe8ad56853085725dc6c56b00a405" + } + }, + "transactions": [ + { + "mantle_tx": { + "hash": "9e00ebfc1d641adaeecaf0e20c09ffed78a84fd2f36b790b94cf64f758260348", + "ops": [ + { + "opcode": 17, + "payload": { + "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", + "inscription": "44160000000000006508e79b06e19340759182b8c17ee4f490ad8ffea5bf89ad06e6f10e843932af84e707c7cfa10873b5bd312345a6419de751ed433b6cf2fff0f6a1cefb965d422e0910bc9e010000ca2b0f5396f61a85e56f499c16dfe7b9f56ca86e25c5059cc5e685f59b7b1be5e67e9aaae67b3cb667a56e16454d661365e207f9c35453b792ca98abb16702bb0100000000cf8fdba1d3ac5dbd6b28315cfd7eb206c1a8cab5206a1d48ebb3cd26d4d36f68030000002f4c455a2f436c6f636b50726f6772616d4163636f756e742f303030303030312f4c455a2f436c6f636b50726f6772616d4163636f756e742f303030303031302f4c455a2f436c6f636b50726f6772616d4163636f756e742f3030303030353000000000020000002e0910bc9e0100000000000000", + "parent": "77007291cc00a4ad150d79afe4068008ec662dc3f7e1b70358a427cb03ba6de7", + "signer": "7d1726d002f952db5be1a2f534f8b4bafe429c8e56f6a42c8c6f9faa3aa59b06" + } + } + ] + }, + "ops_proofs": [ + { + "Ed25519Sig": "307e8225cc3ccd3ea4a2a0de7509aa90a85453cb9d4fb2f56e1c36f83cd00ebf33805807f45fb6ddb332a1caa1c096ea8a95bb2135a500014174a71a256fa50a" + } + ] + } + ] +} \ No newline at end of file