From 8e7a8b82ec49b83bbc284a7df8b1829025faa7b0 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 1 Apr 2026 12:28:24 -0700 Subject: [PATCH 1/4] fix: default chain_id to 4217 (mainnet), matching mppx and mpp-rs When chain_id was omitted from tempo(), the challenge's methodDetails lacked a chainId field. The Rust CLI (mpp-rs) strictly requires chainId and rejects challenges without it: 'Malformed payment request: missing chainId'. Both mppx and mpp-rs default to 4217 (mainnet). This makes pympp consistent so servers work out of the box without explicitly passing chain_id. --- src/mpp/methods/tempo/__init__.py | 1 - src/mpp/methods/tempo/client.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mpp/methods/tempo/__init__.py b/src/mpp/methods/tempo/__init__.py index a7c6ff3..5c2f735 100644 --- a/src/mpp/methods/tempo/__init__.py +++ b/src/mpp/methods/tempo/__init__.py @@ -20,7 +20,6 @@ server = Mpp.create( method=tempo( - chain_id=42431, intents={"charge": ChargeIntent()}, ), ) diff --git a/src/mpp/methods/tempo/client.py b/src/mpp/methods/tempo/client.py index 4f14cbc..55ec037 100644 --- a/src/mpp/methods/tempo/client.py +++ b/src/mpp/methods/tempo/client.py @@ -12,6 +12,7 @@ from mpp import Challenge, Credential from mpp.methods.tempo._attribution import encode as encode_attribution from mpp.methods.tempo._defaults import ( + CHAIN_ID, CHAIN_RPC_URLS, RPC_URL, default_currency_for_chain, @@ -303,7 +304,7 @@ def tempo( intents: dict[str, Intent], account: TempoAccount | None = None, fee_payer: TempoAccount | None = None, - chain_id: int | None = None, + chain_id: int = CHAIN_ID, rpc_url: str | None = None, root_account: str | None = None, currency: str | None = None, @@ -320,8 +321,8 @@ def tempo( (server-side). When set, the server signs with domain ``0x78`` and broadcasts directly — no external fee payer service needed. - chain_id: Tempo chain ID (4217 for mainnet, 42431 for testnet). - Resolves the RPC URL automatically from known chains. + chain_id: Tempo chain ID (default: 4217 for mainnet, use 42431 + for testnet). Resolves the RPC URL automatically from known chains. rpc_url: Tempo RPC endpoint URL. Overrides the URL resolved from ``chain_id``. Defaults to mainnet if neither is set. root_account: Root account address for access key signing. @@ -350,7 +351,7 @@ def tempo( ) """ if rpc_url is None: - rpc_url = rpc_url_for_chain(chain_id) if chain_id else RPC_URL + rpc_url = rpc_url_for_chain(chain_id) if currency is None: currency = default_currency_for_chain(chain_id) From be5a66c02282afde0445191540c8306771a072c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Apr 2026 19:28:58 +0000 Subject: [PATCH 2/4] chore: add changelog --- .changelog/gentle-eagles-whisper.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changelog/gentle-eagles-whisper.md diff --git a/.changelog/gentle-eagles-whisper.md b/.changelog/gentle-eagles-whisper.md new file mode 100644 index 0000000..fb37dfb --- /dev/null +++ b/.changelog/gentle-eagles-whisper.md @@ -0,0 +1,5 @@ +--- +pympp: patch +--- + +Defaulted `chain_id` to 4217 (mainnet) in the `tempo()` function, removing the need to pass it explicitly. Updated docs and example code accordingly. From c8f74c75b921d0ed95794b313dc0fc5ad05e83ab Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 1 Apr 2026 12:41:21 -0700 Subject: [PATCH 3/4] fix: remove hardcoded testnet fee payer URL, require explicit config The DEFAULT_FEE_PAYER_URL (sponsor.moderato.tempo.xyz) was a testnet-only service silently used as fallback when no fee payer was configured. With chain_id now defaulting to mainnet (4217), this would route mainnet transactions to a testnet sponsor. Align with mppx and mpp-rs: require explicit fee payer configuration (either a feePayer account on the method, or a feePayerUrl in methodDetails). Raise a clear error if neither is set. --- src/mpp/methods/tempo/_defaults.py | 4 ---- src/mpp/methods/tempo/intents.py | 9 +++++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/mpp/methods/tempo/_defaults.py b/src/mpp/methods/tempo/_defaults.py index fdcd160..e5d9fe2 100644 --- a/src/mpp/methods/tempo/_defaults.py +++ b/src/mpp/methods/tempo/_defaults.py @@ -13,10 +13,6 @@ TESTNET_CHAIN_ID = 42431 TESTNET_RPC_URL = "https://rpc.moderato.tempo.xyz" -# Testnet only — the fee payer service sponsors gas on testnet. -# On mainnet, the server itself must pay gas or provide its own fee payer. -DEFAULT_FEE_PAYER_URL = "https://sponsor.moderato.tempo.xyz" - # Chain ID -> default currency mapping # Mainnet defaults to USDC, testnet defaults to pathUSD DEFAULT_CURRENCIES: MappingProxyType[int, str] = MappingProxyType( diff --git a/src/mpp/methods/tempo/intents.py b/src/mpp/methods/tempo/intents.py index 57cb111..5b05e00 100644 --- a/src/mpp/methods/tempo/intents.py +++ b/src/mpp/methods/tempo/intents.py @@ -14,7 +14,7 @@ from mpp import Credential, Receipt from mpp.errors import VerificationError -from mpp.methods.tempo._defaults import DEFAULT_FEE_PAYER_URL, PATH_USD, rpc_url_for_chain +from mpp.methods.tempo._defaults import PATH_USD, rpc_url_for_chain from mpp.methods.tempo.schemas import ( ChargeRequest, CredentialPayload, @@ -366,7 +366,12 @@ async def _verify_transaction( if self.fee_payer is not None: raw_tx = self._cosign_as_fee_payer(raw_tx, request.currency, request=request) else: - fee_payer_url = request.methodDetails.feePayerUrl or DEFAULT_FEE_PAYER_URL + fee_payer_url = request.methodDetails.feePayerUrl + if not fee_payer_url: + raise VerificationError( + "No fee payer configured: set feePayer on the tempo() method " + "or provide a feePayerUrl in methodDetails" + ) sign_response = await client.post( fee_payer_url, From 0402ecd45b709af62d6ddf4d1847ebd993edbb35 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Wed, 1 Apr 2026 12:50:04 -0700 Subject: [PATCH 4/4] test: update tests for chain_id=4217 default and fee payer changes --- tests/test_mpp_create.py | 8 ++++---- tests/test_tempo.py | 42 ++++++++++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/tests/test_mpp_create.py b/tests/test_mpp_create.py index 63a75af..dedbf50 100644 --- a/tests/test_mpp_create.py +++ b/tests/test_mpp_create.py @@ -239,12 +239,12 @@ async def test_charge_override_currency_recipient(self) -> None: assert result.request["recipient"] == "0xother" @pytest.mark.asyncio - async def test_charge_defaults_currency_to_pathusd(self) -> None: - """Currency defaults to pathUSD when chain_id is not set.""" - from mpp.methods.tempo import PATH_USD + async def test_charge_defaults_currency_to_usdc(self) -> None: + """Currency defaults to USDC when chain_id defaults to mainnet (4217).""" + from mpp.methods.tempo import USDC method = tempo(intents={"charge": ChargeIntent()}) - assert method.currency == PATH_USD + assert method.currency == USDC @pytest.mark.asyncio async def test_charge_defaults_currency_to_usdc_on_mainnet(self) -> None: diff --git a/tests/test_tempo.py b/tests/test_tempo.py index 492c9b9..0de370d 100644 --- a/tests/test_tempo.py +++ b/tests/test_tempo.py @@ -612,9 +612,10 @@ async def test_client_builds_sponsored_transaction(self, httpx_mock: HTTPXMock) intents={"charge": ChargeIntent()}, ) + # eth_chainId httpx_mock.add_response( url="https://rpc.test", - json={"jsonrpc": "2.0", "result": "0x1", "id": 1}, + json={"jsonrpc": "2.0", "result": "0x1079", "id": 1}, ) httpx_mock.add_response( url="https://rpc.test", @@ -1191,10 +1192,10 @@ def test_tempo_factory_stores_chain_id(self) -> None: method = tempo(chain_id=42431, intents={"charge": ChargeIntent()}) assert method.chain_id == 42431 - def test_tempo_factory_chain_id_defaults_none(self) -> None: - """tempo() without chain_id should default to None.""" + def test_tempo_factory_chain_id_defaults_mainnet(self) -> None: + """tempo() without chain_id should default to 4217 (mainnet).""" method = tempo(intents={"charge": ChargeIntent()}) - assert method.chain_id is None + assert method.chain_id == 4217 def test_tempo_factory_chain_id_resolves_rpc(self) -> None: """tempo(chain_id=42431) should resolve testnet RPC URL.""" @@ -1275,9 +1276,10 @@ async def test_client_falls_back_to_method_rpc_for_unknown_chain( intents={"charge": ChargeIntent()}, ) + # eth_chainId httpx_mock.add_response( url="https://rpc.custom", - json={"jsonrpc": "2.0", "result": "0x1", "id": 1}, + json={"jsonrpc": "2.0", "result": "0x1079", "id": 1}, ) httpx_mock.add_response( url="https://rpc.custom", @@ -1323,9 +1325,10 @@ async def test_client_ignores_non_numeric_chain_id(self, httpx_mock: HTTPXMock) intents={"charge": ChargeIntent()}, ) + # eth_chainId httpx_mock.add_response( url="https://rpc.custom", - json={"jsonrpc": "2.0", "result": "0x1", "id": 1}, + json={"jsonrpc": "2.0", "result": "0x1079", "id": 1}, ) httpx_mock.add_response( url="https://rpc.custom", @@ -1415,8 +1418,12 @@ async def test_access_key_builds_keychain_signature(self, httpx_mock: HTTPXMock) intents={"charge": ChargeIntent()}, ) - # Mock RPC: chain_id, nonce, gas_price, estimateGas - for _ in range(4): + # Mock RPC: chain_id (4217=0x1079), nonce, gas_price, estimateGas + httpx_mock.add_response( + url="https://rpc.test", + json={"jsonrpc": "2.0", "result": "0x1079", "id": 1}, + ) + for _ in range(3): httpx_mock.add_response( url="https://rpc.test", json={"jsonrpc": "2.0", "result": "0x1", "id": 1}, @@ -1455,9 +1462,15 @@ async def test_access_key_with_fee_payer(self, httpx_mock: HTTPXMock) -> None: intents={"charge": ChargeIntent()}, ) - for _ in range(4): + # Mock RPC: chain_id (4217=0x1079), nonce, gas_price, estimateGas + # Challenge chainId=4217 resolves to rpc.tempo.xyz + httpx_mock.add_response( + url="https://rpc.tempo.xyz", + json={"jsonrpc": "2.0", "result": "0x1079", "id": 1}, + ) + for _ in range(3): httpx_mock.add_response( - url="https://rpc.test", + url="https://rpc.tempo.xyz", json={"jsonrpc": "2.0", "result": "0x1", "id": 1}, ) @@ -1469,7 +1482,7 @@ async def test_access_key_with_fee_payer(self, httpx_mock: HTTPXMock) -> None: "amount": "1000000", "currency": "0x20c0000000000000000000000000000000000000", "recipient": "0x742d35Cc6634c0532925a3b844bC9e7595F8fE00", - "methodDetails": {"feePayer": True, "chainId": 1}, + "methodDetails": {"feePayer": True, "chainId": 4217}, }, realm="test.example.com", request_b64="e30", @@ -1492,7 +1505,12 @@ async def test_no_root_account_uses_regular_signing(self, httpx_mock: HTTPXMock) intents={"charge": ChargeIntent()}, ) - for _ in range(4): + # Mock RPC: chain_id (4217=0x1079), nonce, gas_price, estimateGas + httpx_mock.add_response( + url="https://rpc.test", + json={"jsonrpc": "2.0", "result": "0x1079", "id": 1}, + ) + for _ in range(3): httpx_mock.add_response( url="https://rpc.test", json={"jsonrpc": "2.0", "result": "0x1", "id": 1},