@@ -19,7 +19,10 @@ use tower::Service;
1919use tracing:: { Instrument , debug, debug_span, trace} ;
2020use url:: Url ;
2121
22- use super :: { keys:: discover_mpp_config_for_chain, session:: SessionProvider } ;
22+ use super :: {
23+ keys:: { DiscoverOptions , discover_mpp_config} ,
24+ session:: SessionProvider ,
25+ } ;
2326
2427/// Default deposit amount for new channels (in base units).
2528const DEFAULT_DEPOSIT : u128 = 100_000 ;
@@ -64,16 +67,17 @@ impl LazySessionProvider {
6467 return Ok ( provider. clone ( ) ) ;
6568 }
6669
67- let config = discover_mpp_config_for_chain ( chain_id) . ok_or_else ( || {
68- TransportErrorKind :: custom ( std:: io:: Error :: other (
69- "RPC endpoint returned HTTP 402 Payment Required. \
70+ let config = discover_mpp_config ( DiscoverOptions { chain_id, ..Default :: default ( ) } )
71+ . ok_or_else ( || {
72+ TransportErrorKind :: custom ( std:: io:: Error :: other (
73+ "RPC endpoint returned HTTP 402 Payment Required. \
7074 This endpoint requires payment via the Machine Payments Protocol (MPP).\n \n \
7175 To configure MPP, install the Tempo wallet CLI and create a key:\n \
7276 \n curl -sSL https://tempo.xyz/install.sh | bash\
7377 \n tempo wallet login\
7478 \n \n See https://docs.tempo.xyz for more information.",
75- ) )
76- } ) ?;
79+ ) )
80+ } ) ?;
7781
7882 let signer: mpp:: PrivateKeySigner = config. key . parse ( ) . map_err ( |e| {
7983 TransportErrorKind :: custom ( std:: io:: Error :: other ( format ! ( "invalid MPP key: {e}" ) ) )
@@ -190,24 +194,19 @@ where
190194 . filter_map ( |r| r. ok ( ) )
191195 . collect ( ) ;
192196
193- // Extract chainId from the first Tempo challenge to select the correct
194- // key when multiple keys (mainnet/testnet) are present in keys.toml.
195- let challenge_chain_id = challenges. iter ( ) . find_map ( |c| {
196- if c. method . as_str ( ) == "tempo" {
197- c. request
198- . decode_value ( )
199- . ok ( )
200- . and_then ( |v| v. get ( "methodDetails" ) ?. get ( "chainId" ) ?. as_u64 ( ) )
201- } else {
202- None
203- }
204- } ) ;
205-
206- let resolved = self . provider . resolve_for_chain ( challenge_chain_id) ?;
207-
208- let challenge = challenges
197+ // Try each challenge until we find one with a matching key (chain + currency)
198+ // in keys.toml. This handles servers that offer multiple chains and currencies
199+ // (e.g. mainnet + testnet) — we pick the first one the user has a key for.
200+ let ( resolved, challenge) = challenges
209201 . iter ( )
210- . find ( |c| resolved. supports ( c. method . as_str ( ) , c. intent . as_str ( ) ) )
202+ . find_map ( |c| {
203+ let ( chain_id, currency) = extract_challenge_chain_and_currency ( c) ;
204+ if !self . provider . supports_challenge ( chain_id, currency. as_deref ( ) ) {
205+ return None ;
206+ }
207+ let provider = self . provider . resolve_for_chain ( chain_id) . ok ( ) ?;
208+ provider. supports ( c. method . as_str ( ) , c. intent . as_str ( ) ) . then_some ( ( provider, c) )
209+ } )
211210 . ok_or_else ( || {
212211 let offered: Vec < _ > =
213212 challenges. iter ( ) . map ( |c| format ! ( "{}.{}" , c. method, c. intent) ) . collect ( ) ;
@@ -401,13 +400,32 @@ where
401400 }
402401}
403402
403+ /// Extract `(chainId, currency)` from a parsed MPP challenge.
404+ fn extract_challenge_chain_and_currency (
405+ c : & mpp:: protocol:: core:: PaymentChallenge ,
406+ ) -> ( Option < u64 > , Option < String > ) {
407+ if c. method . as_str ( ) == "tempo" {
408+ let val = c. request . decode_value ( ) . ok ( ) ;
409+ let chain_id = val. as_ref ( ) . and_then ( |v| v. get ( "methodDetails" ) ?. get ( "chainId" ) ?. as_u64 ( ) ) ;
410+ let currency = val. as_ref ( ) . and_then ( |v| v. get ( "currency" ) ?. as_str ( ) . map ( String :: from) ) ;
411+ ( chain_id, currency)
412+ } else {
413+ ( None , None )
414+ }
415+ }
416+
404417/// Trait for resolving a concrete `PaymentProvider` from a potentially lazy wrapper.
405418pub ( crate ) trait ResolveProvider {
406419 type Provider : PaymentProvider ;
407420 fn resolve ( & self ) -> TransportResult < Self :: Provider > {
408421 self . resolve_for_chain ( None )
409422 }
410423 fn resolve_for_chain ( & self , _chain_id : Option < u64 > ) -> TransportResult < Self :: Provider > ;
424+ /// Check if this provider can handle a challenge with the given chain and currency
425+ /// without initializing/caching state. Returns `true` by default.
426+ fn supports_challenge ( & self , _chain_id : Option < u64 > , _currency : Option < & str > ) -> bool {
427+ true
428+ }
411429 fn set_key_provisioned ( & self , _provisioned : bool ) { }
412430 fn clear_channels ( & self ) { }
413431}
@@ -424,6 +442,10 @@ impl ResolveProvider for LazySessionProvider {
424442 fn resolve_for_chain ( & self , chain_id : Option < u64 > ) -> TransportResult < SessionProvider > {
425443 self . get_or_init ( chain_id)
426444 }
445+ fn supports_challenge ( & self , chain_id : Option < u64 > , currency : Option < & str > ) -> bool {
446+ let currency = currency. and_then ( |s| s. parse ( ) . ok ( ) ) ;
447+ discover_mpp_config ( DiscoverOptions { chain_id, currency } ) . is_some ( )
448+ }
427449 fn set_key_provisioned ( & self , provisioned : bool ) {
428450 Self :: set_key_provisioned ( self , provisioned)
429451 }
@@ -744,12 +766,8 @@ mod tests {
744766 let msg = err. to_string ( ) ;
745767
746768 assert ! (
747- msg. contains( "402 Payment Required" ) ,
748- "expected 402 Payment Required in error, got: {msg}"
749- ) ;
750- assert ! (
751- msg. contains( "tempo wallet login" ) ,
752- "expected setup instructions in error, got: {msg}"
769+ msg. contains( "no supported MPP challenge" ) ,
770+ "expected 'no supported MPP challenge' in error, got: {msg}"
753771 ) ;
754772
755773 handle. abort ( ) ;
@@ -796,7 +814,7 @@ mod tests {
796814 "no MPP key found; set TEMPO_PRIVATE_KEY or configure ~/.tempo/wallet/keys.toml" ,
797815 ) ;
798816
799- let config = discover_mpp_config_for_chain ( None )
817+ let config = discover_mpp_config ( Default :: default ( ) )
800818 . expect ( "no MPP config found; configure ~/.tempo/wallet/keys.toml" ) ;
801819
802820 let signer: mpp:: PrivateKeySigner =
@@ -863,47 +881,38 @@ mod tests {
863881 }
864882
865883 #[ test]
866- fn challenge_chain_id_extraction ( ) {
867- let extract_chain_id = |headers : Vec < & str > | -> Option < u64 > {
884+ fn challenge_chain_and_currency_extraction ( ) {
885+ let extract = |headers : Vec < & str > | -> Vec < ( Option < u64 > , Option < String > ) > {
868886 let challenges: Vec < _ > =
869887 parse_www_authenticate_all ( headers) . into_iter ( ) . filter_map ( |r| r. ok ( ) ) . collect ( ) ;
870- challenges. iter ( ) . find_map ( |c| {
871- if c. method . as_str ( ) == "tempo" {
872- c. request
873- . decode_value ( )
874- . ok ( )
875- . and_then ( |v| v. get ( "methodDetails" ) ?. get ( "chainId" ) ?. as_u64 ( ) )
876- } else {
877- None
878- }
879- } )
888+ challenges. iter ( ) . map ( |c| extract_challenge_chain_and_currency ( c) ) . collect ( )
880889 } ;
881890
882891 let b64 = |v : serde_json:: Value | -> String {
883892 Base64UrlJson :: from_value ( & v) . unwrap ( ) . raw ( ) . to_string ( )
884893 } ;
885894
886- // Tempo challenge with chainId → extracts it
895+ // Tempo challenge with chainId + currency
887896 let tempo_header = format ! (
888897 r#"Payment id="abc", realm="api", method="tempo", intent="charge", request="{}""# ,
889898 b64(
890899 serde_json:: json!( { "amount" : "1000" , "currency" : "0x20c0" , "methodDetails" : { "chainId" : 42431 } , "recipient" : "0xabc" } )
891900 )
892901 ) ;
893- assert_eq ! ( extract_chain_id ( vec![ & tempo_header] ) , Some ( 42431 ) ) ;
902+ assert_eq ! ( extract ( vec![ & tempo_header] ) , vec! [ ( Some ( 42431 ) , Some ( "0x20c0" . into ( ) ) ) ] ) ;
894903
895- // Non-tempo challenge → None
904+ // Non-tempo challenge → ( None, None)
896905 let stripe_header = format ! (
897906 r#"Payment id="xyz", realm="api", method="stripe", intent="charge", request="{}""# ,
898907 b64( serde_json:: json!( { "amount" : "100" } ) )
899908 ) ;
900- assert_eq ! ( extract_chain_id ( vec![ & stripe_header] ) , None ) ;
909+ assert_eq ! ( extract ( vec![ & stripe_header] ) , vec! [ ( None , None ) ] ) ;
901910
902- // Tempo challenge without methodDetails → None
911+ // Tempo challenge without methodDetails → chainId None, currency present
903912 let no_details = format ! (
904913 r#"Payment id="def", realm="api", method="tempo", intent="charge", request="{}""# ,
905914 b64( serde_json:: json!( { "amount" : "1000" , "currency" : "0x20c0" , "recipient" : "0xabc" } ) )
906915 ) ;
907- assert_eq ! ( extract_chain_id ( vec![ & no_details] ) , None ) ;
916+ assert_eq ! ( extract ( vec![ & no_details] ) , vec! [ ( None , Some ( "0x20c0" . into ( ) ) ) ] ) ;
908917 }
909918}
0 commit comments