From d425b58cfe29b9cbffcaf980fdaaad0c4a314fbc Mon Sep 17 00:00:00 2001 From: Andrew Miller Date: Fri, 5 Jun 2026 10:56:33 -0400 Subject: [PATCH] get_interchain: per_chain + chains options (default first anchor per chain); bump 0.5.0 transaction.get_interchain / block.get_interchain take per_chain= and chains= kwargs mapping to prime-node's ?perChain=&chains= params. Default (no kwargs) returns one anchor per chain. Shared interchain_query() helper, exported; pytest. --- README.md | 18 ++++++++++++++++++ prime_sdk/__init__.py | 4 +++- prime_sdk/block.py | 20 +++++++++++++++++--- prime_sdk/interchain.py | 27 +++++++++++++++++++++++++++ prime_sdk/transaction.py | 21 ++++++++++++++++++--- pyproject.toml | 2 +- tests/test_interchain.py | 22 ++++++++++++++++++++++ 7 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 prime_sdk/interchain.py create mode 100644 tests/test_interchain.py diff --git a/README.md b/README.md index 2dbf9ee..cf5ee84 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,24 @@ client = DragonchainSDK( - `proof_measure.report(req)` — Per-transaction "securedBy" report over interchain anchors. - `proof_measure.health()` — Service liveness. +## Interchain trace + +`transaction.get_interchain` / `block.get_interchain` trace a prime block to the +public-chain anchors covering it. By default they return the **first anchor per +chain** (anchor proofs are chained, so the earliest per chain is the meaningful +one): + +```python +# Default: first anchor per chain (ETH, BTC, …) +trace = client.block.get_interchain("42") + +# Up to 3 anchors per chain +trace = client.block.get_interchain("42", per_chain=3) + +# All anchors, only the ETH-mainnet chain ("1"); "0" = BTC +trace = client.block.get_interchain("42", per_chain=0, chains=["1"]) +``` + ## Proof Measure `proof-measure` is a separate, **public, unauthenticated** Dragonchain service diff --git a/prime_sdk/__init__.py b/prime_sdk/__init__.py index b5c23ef..1198e6f 100644 --- a/prime_sdk/__init__.py +++ b/prime_sdk/__init__.py @@ -81,6 +81,7 @@ from .models import ( SecurityResult, TransactionReport, ) +from .interchain import interchain_query from .proof_measure import DEFAULT_BASE_URL as PROOF_MEASURE_DEFAULT_BASE_URL from .proof_measure import ProofMeasureClient from .system import SystemClient @@ -88,7 +89,7 @@ from .transaction import TransactionClient from .transaction_type import TransactionTypeClient from .unauthenticated_client import UnauthenticatedClient -__version__ = "0.4.0" +__version__ = "0.5.0" class DragonchainSDK: @@ -148,6 +149,7 @@ __all__ = [ "ProofMeasureClient", "UnauthenticatedClient", "PROOF_MEASURE_DEFAULT_BASE_URL", + "interchain_query", # models "Block", "BlockHeader", diff --git a/prime_sdk/block.py b/prime_sdk/block.py index dd854db..0aaf509 100644 --- a/prime_sdk/block.py +++ b/prime_sdk/block.py @@ -1,6 +1,9 @@ """Block endpoints.""" +from typing import List, Optional + from .client import Client +from .interchain import interchain_query from .models import Block, InterchainTrace @@ -11,10 +14,21 @@ 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: + def get_interchain( + self, + block_id: str, + per_chain: Optional[int] = None, + chains: Optional[List[str]] = None, + ) -> InterchainTrace: """Trace a block to the validator (verification) blocks that validated it and the public-chain interchain anchors those validator blocks were - bundled into.""" + bundled into. + + By default returns the first anchor per public chain (anchor proofs are + chained, so the earliest per chain is the meaningful one). ``per_chain`` + caps anchors per chain (1 = first per chain; 0 = all); ``chains`` + restricts to specific chain ids.""" return self._client.get( - f"/api/v1/block/{block_id}/interchain", InterchainTrace + f"/api/v1/block/{block_id}/interchain{interchain_query(per_chain, chains)}", + InterchainTrace, ) diff --git a/prime_sdk/interchain.py b/prime_sdk/interchain.py new file mode 100644 index 0000000..e4df14a --- /dev/null +++ b/prime_sdk/interchain.py @@ -0,0 +1,27 @@ +"""Query builder for the interchain-trace endpoints. + +Anchor proofs are chained, so by default the trace returns the first anchor per +public chain; these options change how many and which chains are returned. +""" + +from typing import List, Optional +from urllib.parse import quote + + +def interchain_query( + per_chain: Optional[int] = None, chains: Optional[List[str]] = None +) -> str: + """Build the "?perChain=...&chains=..." suffix for the interchain trace + endpoints. + + Returns "" when nothing is set (the server then applies its defaults: one + anchor per chain, all chains). ``per_chain`` caps anchors per public chain, + earliest-first (1 = first per chain; 0 = all). ``chains`` restricts to the + given chain ids ("1" = ETH mainnet, "0" = BTC; testnet ids differ). + """ + parts: List[str] = [] + if per_chain is not None: + parts.append(f"perChain={int(per_chain)}") + if chains: + parts.append("chains=" + ",".join(quote(str(c), safe="") for c in chains)) + return "?" + "&".join(parts) if parts else "" diff --git a/prime_sdk/transaction.py b/prime_sdk/transaction.py index 13f02f6..f8ce5e1 100644 --- a/prime_sdk/transaction.py +++ b/prime_sdk/transaction.py @@ -1,6 +1,9 @@ """Transaction endpoints.""" +from typing import List, Optional + from .client import CONTENT_TYPE_JSON, Client +from .interchain import interchain_query from .models import ( InterchainTrace, ListTransactionsResponse, @@ -40,13 +43,25 @@ 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: + def get_interchain( + self, + transaction_id: str, + per_chain: Optional[int] = None, + chains: Optional[List[str]] = None, + ) -> 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.""" + (not yet in a block) the trace's lists are empty. + + By default returns the first anchor per public chain (anchor proofs are + chained, so the earliest per chain is the meaningful one). ``per_chain`` + caps anchors per chain (1 = first per chain; 0 = all); ``chains`` + restricts to specific chain ids.""" return self._client.get( - f"/api/v1/transaction/{transaction_id}/interchain", InterchainTrace + f"/api/v1/transaction/{transaction_id}/interchain" + f"{interchain_query(per_chain, chains)}", + InterchainTrace, ) def list(self) -> ListTransactionsResponse: diff --git a/pyproject.toml b/pyproject.toml index 54cc175..7fd6a0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "prime-sdk-python" -version = "0.4.0" +version = "0.5.0" description = "A self-contained Python SDK for interacting with Dragonchain nodes." readme = "README.md" requires-python = ">=3.9" diff --git a/tests/test_interchain.py b/tests/test_interchain.py new file mode 100644 index 0000000..3b62304 --- /dev/null +++ b/tests/test_interchain.py @@ -0,0 +1,22 @@ +"""Offline tests for the interchain-trace query builder.""" + +from prime_sdk import interchain_query + + +def test_empty_when_nothing_set(): + assert interchain_query() == "" + assert interchain_query(None, None) == "" + assert interchain_query(chains=[]) == "" + + +def test_per_chain_including_zero(): + assert interchain_query(per_chain=1) == "?perChain=1" + assert interchain_query(per_chain=0) == "?perChain=0" + + +def test_chains_joined_with_commas(): + assert interchain_query(chains=["1", "0"]) == "?chains=1,0" + + +def test_combined(): + assert interchain_query(per_chain=2, chains=["1", "0"]) == "?perChain=2&chains=1,0"