diff --git a/prime_sdk/__init__.py b/prime_sdk/__init__.py index 2bf8152..1825dc8 100644 --- a/prime_sdk/__init__.py +++ b/prime_sdk/__init__.py @@ -48,6 +48,8 @@ from .models import ( BlockHeader, BlockProof, GrpcConnectionInfo, + InterchainTrace, + InterchainTransaction, ListResponse, ListTransactionsResponse, SmartContract, @@ -67,6 +69,7 @@ from .models import ( TransactionType, TransactionTypeCreateRequest, TransactionTypeCreateResponse, + VerificationBlock, ) from .system import SystemClient from .transaction import TransactionClient @@ -125,6 +128,8 @@ __all__ = [ "BlockHeader", "BlockProof", "GrpcConnectionInfo", + "InterchainTrace", + "InterchainTransaction", "ListResponse", "ListTransactionsResponse", "SmartContract", @@ -144,4 +149,5 @@ __all__ = [ "TransactionType", "TransactionTypeCreateRequest", "TransactionTypeCreateResponse", + "VerificationBlock", ] diff --git a/prime_sdk/block.py b/prime_sdk/block.py index e0fddcb..dd854db 100644 --- a/prime_sdk/block.py +++ b/prime_sdk/block.py @@ -1,7 +1,7 @@ """Block endpoints.""" from .client import Client -from .models import Block +from .models import Block, InterchainTrace class BlockClient: @@ -10,3 +10,11 @@ class BlockClient: def get(self, block_id: str) -> Block: return self._client.get(f"/api/v1/block/{block_id}", Block) + + def get_interchain(self, block_id: str) -> InterchainTrace: + """Trace a block to the validator (verification) blocks that validated it + and the public-chain interchain anchors those validator blocks were + bundled into.""" + return self._client.get( + f"/api/v1/block/{block_id}/interchain", InterchainTrace + ) diff --git a/prime_sdk/models.py b/prime_sdk/models.py index 44e6d44..90c85ef 100644 --- a/prime_sdk/models.py +++ b/prime_sdk/models.py @@ -350,6 +350,92 @@ class Block: ) +# --------------------------------------------------------------------------- # +# Interchain trace +# --------------------------------------------------------------------------- # + + +@dataclass +class VerificationBlock: + """A validator's verification of a prime block.""" + + version: str = "" + prime_chain_id: str = "" + prime_block_id: str = "" + timestamp: str = "" + verifier_public_key: str = "" + verifier_signature: str = "" + + @classmethod + def from_dict(cls, d: Dict[str, Any]) -> "VerificationBlock": + return cls( + version=d.get("version", ""), + prime_chain_id=d.get("primeChainId", ""), + prime_block_id=d.get("primeBlockId", ""), + timestamp=d.get("timestamp", ""), + verifier_public_key=d.get("verifierPublicKey", ""), + verifier_signature=d.get("verifierSignature", ""), + ) + + +@dataclass +class InterchainTransaction: + """An anchor broadcast to a public blockchain (e.g. ETH/BTC) bundling one or + more validator blocks. ``validator_blocks`` holds the covered prime block ids; + ``covered_prime_chain_ids`` the prime chains they belong to.""" + + id: int = 0 + version: str = "" + timestamp: str = "" + chain_id: str = "" + trans_hash: str = "" + block_id: str = "" + validator_blocks: List[str] = field(default_factory=list) + validator_blockhash: str = "" + signature: str = "" + covered_prime_chain_ids: List[str] = field(default_factory=list) + + @classmethod + def from_dict(cls, d: Dict[str, Any]) -> "InterchainTransaction": + return cls( + id=d.get("id", 0), + version=d.get("version", ""), + timestamp=d.get("timestamp", ""), + chain_id=d.get("chainId", ""), + trans_hash=d.get("transHash", ""), + block_id=d.get("blockId", ""), + validator_blocks=d.get("validatorBlocks") or [], + validator_blockhash=d.get("validatorBlockhash", ""), + signature=d.get("signature", ""), + covered_prime_chain_ids=d.get("coveredPrimeChainIds") or [], + ) + + +@dataclass +class InterchainTrace: + """Links a prime block to the validator (verification) blocks that validated + it and the public-chain interchain anchors those validator blocks were + bundled into. Returned by ``transaction.get_interchain`` and + ``block.get_interchain``.""" + + block_id: str = "" + validator_blocks: List[VerificationBlock] = field(default_factory=list) + interchain_transactions: List[InterchainTransaction] = field(default_factory=list) + + @classmethod + def from_dict(cls, d: Dict[str, Any]) -> "InterchainTrace": + return cls( + block_id=d.get("blockId", ""), + validator_blocks=[ + VerificationBlock.from_dict(v) for v in (d.get("validatorBlocks") or []) + ], + interchain_transactions=[ + InterchainTransaction.from_dict(v) + for v in (d.get("interchainTransactions") or []) + ], + ) + + # --------------------------------------------------------------------------- # # System / generic # --------------------------------------------------------------------------- # diff --git a/prime_sdk/transaction.py b/prime_sdk/transaction.py index 89010a1..13f02f6 100644 --- a/prime_sdk/transaction.py +++ b/prime_sdk/transaction.py @@ -2,6 +2,7 @@ from .client import CONTENT_TYPE_JSON, Client from .models import ( + InterchainTrace, ListTransactionsResponse, Transaction, TransactionBulkRequest, @@ -39,5 +40,14 @@ class TransactionClient: def get(self, transaction_id: str) -> Transaction: return self._client.get(f"/api/v1/transaction/{transaction_id}", Transaction) + def get_interchain(self, transaction_id: str) -> InterchainTrace: + """Trace a transaction to the validator (verification) blocks that + validated its prime block and the public-chain interchain anchors those + validator blocks were bundled into. If the transaction is still pending + (not yet in a block) the trace's lists are empty.""" + return self._client.get( + f"/api/v1/transaction/{transaction_id}/interchain", InterchainTrace + ) + def list(self) -> ListTransactionsResponse: return self._client.get("/api/v1/transaction/", ListTransactionsResponse) diff --git a/tests/test_client.py b/tests/test_client.py index bcc2f51..4ff5a60 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,6 +16,7 @@ from prime_sdk import ( Block, DragonchainAPIError, DragonchainSDK, + InterchainTrace, SmartContractCreateRequest, Transaction, TransactionCreateRequest, @@ -191,6 +192,42 @@ def test_block_from_dict_nested_header(): assert len(blk.transactions) == 3 +def test_interchain_trace_from_dict_nested(): + # Server shape: validatorBlocks + interchainTransactions arrays, camelCase. + raw = { + "blockId": "69636602", + "validatorBlocks": [ + { + "version": "1", + "primeChainId": "zDYr", + "primeBlockId": "69636602", + "verifierPublicKey": "02c4...", + "verifierSignature": "MEUC...", + } + ], + "interchainTransactions": [ + { + "id": 19, + "chainId": "1", + "transHash": "0xd46e", + "validatorBlocks": ["69636602"], + "coveredPrimeChainIds": ["zDYr"], + } + ], + } + trace = InterchainTrace.from_dict(raw) + assert trace.block_id == "69636602" + assert len(trace.validator_blocks) == 1 + assert trace.validator_blocks[0].prime_block_id == "69636602" + assert trace.validator_blocks[0].verifier_public_key == "02c4..." + assert len(trace.interchain_transactions) == 1 + assert trace.interchain_transactions[0].id == 19 + assert trace.interchain_transactions[0].trans_hash == "0xd46e" + assert trace.interchain_transactions[0].chain_id == "1" + assert trace.interchain_transactions[0].validator_blocks == ["69636602"] + assert trace.interchain_transactions[0].covered_prime_chain_ids == ["zDYr"] + + def test_contract_create_request_omitempty(): req = SmartContractCreateRequest( environment="python3.8",