Skip to content

feat: add MPP support#13934

Draft
grandizzy wants to merge 24 commits intomasterfrom
grandizzy/add-mpp-support
Draft

feat: add MPP support#13934
grandizzy wants to merge 24 commits intomasterfrom
grandizzy/add-mpp-support

Conversation

@grandizzy
Copy link
Copy Markdown
Collaborator

Transparent HTTP 402 payment handling via the Machine Payments Protocol (mpp-rs). When an RPC endpoint returns 402, the transport automatically pays the challenge and retries.

Supersedes #13851.

Changes

  • MPP transportMppHttpTransport wraps the HTTP transport with automatic 402 handling, 410 channel recovery, and key_authorization retry logic
  • Session provider — manages signing mode (Direct vs Keychain), channel lifecycle, authorized signer
  • Persistent channels — saves open channels to ~/.tempo/foundry/channels.json for cross-process reuse
  • Key discovery — auto-discovers signing keys from TEMPO_PRIVATE_KEY env var or ~/.tempo/wallet/keys.toml
  • Consolidated Tempo types — unified KeyEntry, KeysFile, decode_key_authorization in common/tempo.rs, shared by both wallets and mpp
  • E2E test scriptscripts/mpp-test.sh + CI workflow

Dependencies

Depends on alloy-2.0 branches (not yet merged to main):

  • mpp-rs: https://github.com/tempoxyz/mpp-rs branch alloy-2.0
  • tempo: https://github.com/tempoxyz/tempo branch alloy-2.0

These branches exist because foundry uses alloy 2.0.0-rc.0 while tempo/mpp-rs main branches use alloy v1. The alloy rev is patched from 100a3324031dad to match.

Testing

cargo build --bin cast
./scripts/mpp-test.sh ./target/debug/cast

Unit tests: cargo test -p foundry-common -- mpp

decofe and others added 23 commits March 25, 2026 18:39
* feat: add MPP support for 402-gated RPC endpoints

Adds transparent HTTP 402 payment handling via the Machine Payments
Protocol (mpp-rs). When --mpp-key is set (or MPP_KEY env var), the
transport automatically intercepts 402 responses, pays the challenge,
and retries with the credential.

- Add MppHttpTransport: custom HTTP transport using mpp's PaymentExt
- Wire --mpp-key through RpcOpts -> Config -> ProviderBuilder -> RuntimeTransport
- Gate all MPP code behind cfg(feature = "mpp"), enabled by default

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d0530-3e86-726f-9d92-f4c35e965e4f

* fix: disable mpp feature by default, fix fmt and clippy

The mpp feature is disabled by default since mpp-rs needs to be updated
from alloy v1 to alloy v2 to match foundry's current dependencies.
All MPP code is behind cfg(feature = "mpp") so it compiles cleanly.

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d0530-3e86-726f-9d92-f4c35e965e4f

* fix: enable mpp feature, use alloy patches, fix deny sources

- Enable mpp feature by default in workspace
- Use only mpp client feature (no evm/tempo) to avoid pulling openssl
- Implement EvmSigningProvider inline using foundry's own alloy deps
- Handle 402 protocol directly instead of via mpp's Fetch trait
  (avoids reqwest v0.12 vs v0.13 conflict)
- Add mpp-rs to deny.toml allow-git list
- Fix clippy (io::Error::other) and fmt

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d0530-3e86-726f-9d92-f4c35e965e4f

* fix: add mpp_key to config test fixture

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d0530-3e86-726f-9d92-f4c35e965e4f

* fix: drop mpp client feature to avoid reqwest/openssl conflict

Use mpp with no features (protocol types only). Implement 402
challenge-response and signing directly in MppHttpTransport/MppSigner
instead of depending on mpp's client module which pulls reqwest 0.12
with native-tls/openssl.

Also fixes missing mpp_key field in forge config test.

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d0530-3e86-726f-9d92-f4c35e965e4f

* refactor: remove mpp feature flag, make unconditional

MPP support is now always compiled in. Removes all cfg(feature = "mpp")
gates, the [features] section from foundry-common, and makes mpp,
alloy-signer, and alloy-signer-local non-optional dependencies.

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d0530-3e86-726f-9d92-f4c35e965e4f

* test: add MPP transport tests

- test_mpp_signer_produces_valid_credential: verifies MppSigner echoes
  challenge fields and produces valid payload with DID source
- test_mpp_transport_non_402_passthrough: non-402 responses pass through
  without payment flow
- test_mpp_transport_402_then_200: full 402 → pay → retry → 200 flow,
  verifies exactly 2 HTTP calls
- test_mpp_transport_402_credential_is_valid: server-side validation
  that the credential parses correctly with matching challenge fields
- test_mpp_transport_402_missing_www_authenticate: 402 without
  WWW-Authenticate header returns clear error
- test_mpp_transport_via_runtime_transport: end-to-end through
  RuntimeTransport with --mpp-key wiring

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d0530-3e86-726f-9d92-f4c35e965e4f

---------

Co-authored-by: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
…ssion support

- Make MppHttpTransport generic over PaymentProvider trait (default: TempoProvider)
- Use MultiProvider in connect_http() for both charge and session intents
- Add keychain signing mode support via discover_mpp_config()
- Add MockPaymentProvider for unit tests (no real RPC calls)
- Add live integration test (test_mpp_live_pay) against rpc.mpp.tempo.xyz
- Remove unused alloy-signer/alloy-signer-local deps from foundry-common
- Enable mpp crate features: tempo, client

Amp-Thread-ID: https://ampcode.com/threads/T-019d06b8-fcca-7402-8e45-09082c690a8c
Co-authored-by: Amp <amp@ampcode.com>
When a plain HTTP transport receives a 402 Payment Required response
(no Tempo wallet configured), show a clear error with setup instructions
instead of a generic HTTP error.

Also:
- Add discover_mpp_config() for richer key discovery (wallet/key addresses)
- Use MultiProvider in connect_http() for charge + session support
- Add keychain signing mode for smart wallet keys
- Remove test_mpp_transport_via_runtime_transport (untestable without funds)
- Move all test imports to module level

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d06b8-fcca-7402-8e45-09082c690a8c
…ndpoint

Pass self.url (the user's --rpc-url) to TempoProvider/TempoSessionProvider
instead of hardcoding https://rpc.tempo.xyz. This makes MPP work with any
402-enabled RPC endpoint (e.g. https://mpp.alchemy.com).

Also updates mpp-rs to latest alloy-2.0 branch.

Amp-Thread-ID: https://ampcode.com/threads/T-019d06ed-d3eb-719e-bb41-3cff0dcb2443
Co-authored-by: Amp <amp@ampcode.com>
…trings

Replace hardcoded 'https://rpc.tempo.xyz' with mpp-rs's own DEFAULT_RPC_URL
constant. This way the RPC URL is owned by mpp-rs and updates automatically
when the upstream library changes it.

Amp-Thread-ID: https://ampcode.com/threads/T-019d06ed-d3eb-719e-bb41-3cff0dcb2443
Co-authored-by: Amp <amp@ampcode.com>
…oded RPC

Replace mpp-rs TempoSessionProvider with a custom SessionProvider that
uses expiring nonces (nonce=0, nonceKey=MAX) for channel open
transactions — matching how tempoxyz/wallet works. This eliminates:

- The need for a separate chain RPC URL (no eth_getTransactionCount)
- The chicken-and-egg problem when the RPC is itself 402-gated
- Any hardcoded RPC URLs in the codebase

Users just need 'tempo wallet login' and any 402-enabled --rpc-url
works automatically.

Amp-Thread-ID: https://ampcode.com/threads/T-019d06ed-d3eb-719e-bb41-3cff0dcb2443
Co-authored-by: Amp <amp@ampcode.com>
…tion

- MppHttpTransport<P> is now generic over any PaymentProvider
- LazyMppHttpTransport (type alias) lazily discovers Tempo keys on first 402
- Tests use MppHttpTransport<MockPaymentProvider> directly
- ResolveProvider trait bridges lazy init and direct providers
- InnerTransport::Http uses LazyMppHttpTransport via ::lazy() constructor

Amp-Thread-ID: https://ampcode.com/threads/T-019d074c-484c-742a-82bd-385576eaec98
Co-authored-by: Amp <amp@ampcode.com>
Remove MppKeyEntry/MppKeysFile duplicates from mpp/keys.rs and use
the shared types from crate::tempo (KeyEntry with wallet_type,
KeysFile, read_tempo_keys_file, constants) directly.

Only MppKeyConfig and the primary key selection logic remain
MPP-specific.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d1f7f-764a-7288-a2e5-ba7f7f1447c3
…13920)

* fix(mpp): pass key_authorization from keys.toml, handle KeyAlreadyExists

Two bugs prevented MPP from working with passkey/keychain wallets:

1. key_authorization was never read from keys.toml or passed to
   TempoSigningMode::Keychain — hardcoded to None. Without it, the
   on-chain keychain validation fails with 'access key does not exist'.

2. SESSION_OPEN_GAS_LIMIT (2M) was too low for key authorization
   provisioning with WebAuthn signature verification. Bumped to 10M.

Additionally, key_authorization should only be included on the first
channel open (to provision the key on-chain). Subsequent opens would
fail with KeyAlreadyExists. Fix: optimistically skip key_authorization
(assume key exists), and retry with it only when the gateway reports
'access key does not exist'.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d23b1-d969-7797-862b-d20db85e7c47

* feat(mpp): persist payment channels to disk for cross-process reuse

Store open channel state in ~/.tempo/foundry/channels.json so that
subsequent cast/forge invocations reuse existing channels instead of
opening new ones on-chain.

- Add persist.rs: JSON channel store with load/save/upsert
- SessionProvider loads persisted channels on init, saves on updates
- Channels keyed by payee:currency:escrow, tracked with deposit/amount
- Spent/inactive channels evicted on load
- No secrets stored — only public on-chain data and bookkeeping

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d23b1-d969-7797-862b-d20db85e7c47

* feat(mpp): topUp, 410 recovery, e2e tests for all tools, CI workflow

- TopUp existing channel when deposit is exhausted (approve + topUp tx)
- Handle 204 No Content after topUp, retry with voucher
- Handle 410 Gone (stale channel), clear state and open new channel
- Add anvil fork and chisel fork steps to mpp-test.sh
- Show voucher count after forge/anvil/chisel steps
- Add mpp-e2e.yml CI workflow (TEMPO_KEYS_TOML_B64 secret)

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d23b1-d969-7797-862b-d20db85e7c47

* Fix CI

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d23b1-d969-7797-862b-d20db85e7c47

* feat(config): add built-in RPC endpoint aliases as fallbacks (#13929)

Amp-Thread-ID: https://ampcode.com/threads/T-019d2524-f05b-74e8-b880-deb3d6f0d227

Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com>
Co-authored-by: onbjerg <8862627+onbjerg@users.noreply.github.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: onbjerg <onbjerg@users.noreply.github.com>
Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com>
Co-authored-by: onbjerg <8862627+onbjerg@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d25af-8b2b-7709-b8cf-d5226c13ccf8
- Restore common/tempo.rs as canonical source for shared key types
- Add generic decode_key_authorization<T: Decodable> to common/tempo
- Remove duplicate WalletType, keys_path(), decode_key_authorization()
  from wallets/tempo.rs — now imports from foundry_common::tempo
- Replace inline hex+RLP decode in mpp/transport.rs with shared helper
- Replace manual toml::Value parsing in transport test with discover_mpp_config()

Amp-Thread-ID: https://ampcode.com/threads/T-019d25af-8b2b-7709-b8cf-d5226c13ccf8
Co-authored-by: Amp <amp@ampcode.com>
- Move KeyEntry, KeysFile, KeyType, StoredTokenLimit to common/tempo.rs
  as the single source of truth, using Address types
- wallets/tempo.rs now imports from foundry_common::tempo, removing all
  duplicate type definitions
- lookup_signer() uses read_tempo_keys_file() instead of manual file I/O
- MppKeyConfig uses Address instead of String, simplifying transport.rs
  (no more string-to-Address parsing)
- Update mpp/keys.rs tests to use valid Address values
- Remove unused toml dependency from foundry-wallets

Amp-Thread-ID: https://ampcode.com/threads/T-019d25af-8b2b-7709-b8cf-d5226c13ccf8
Co-authored-by: Amp <amp@ampcode.com>
@grandizzy grandizzy force-pushed the grandizzy/add-mpp-support branch from a85fbd4 to 46db5e4 Compare March 25, 2026 19:30
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d25af-8b2b-7709-b8cf-d5226c13ccf8
@grandizzy grandizzy force-pushed the grandizzy/add-mpp-support branch from 46db5e4 to c4d1d8a Compare March 25, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants