1use super::persist;
10use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
11use alloy_sol_types::SolCall as _;
12use foundry_wallets::Channel;
13use mpp::{
14 client::{
15 PaymentProvider,
16 channel_ops::{
17 ChannelEntry, OpenPayloadOptions, build_credential, compute_expiring_nonce_hash,
18 create_voucher_payload, is_precompile_escrow, resolve_chain_id, resolve_escrow,
19 },
20 tempo::signing::{
21 TempoSigningMode, sign_and_encode_async, sign_and_encode_fee_payer_envelope_async,
22 sign_and_encode_fee_payer_request_async,
23 },
24 },
25 error::MppError,
26 protocol::{
27 core::{PaymentChallenge, PaymentCredential},
28 intents::SessionRequest,
29 methods::tempo::{
30 precompile_voucher::{
31 PRECOMPILE_MAX_CUMULATIVE_AMOUNT, compute_precompile_channel_id,
32 sign_precompile_voucher,
33 },
34 session::{ChannelDescriptor, TempoSessionExt},
35 },
36 },
37 tempo::{Call, SessionCredentialPayload, compute_channel_id, sign_voucher},
38};
39use std::{
40 collections::HashMap,
41 sync::{Arc, Mutex, OnceLock},
42};
43use tempo_alloy::contracts::precompiles::{ITIP20ChannelReserve, TIP20_CHANNEL_RESERVE_ADDRESS};
44
45type SharedChannelState = (
47 Arc<Mutex<HashMap<String, ChannelEntry>>>,
48 Arc<Mutex<HashMap<String, ChannelDescriptor>>>,
49 Arc<Mutex<bool>>,
50);
51
52static GLOBAL_CHANNELS: OnceLock<Mutex<HashMap<String, SharedChannelState>>> = OnceLock::new();
56
57static GLOBAL_PERSISTED: OnceLock<Arc<Mutex<HashMap<String, Channel>>>> = OnceLock::new();
62
63#[derive(Clone, Debug)]
68enum PendingAction {
69 Open { key: String },
71 TopUp { key: String, old_deposit: String },
73 Voucher { key: String, old_cumulative: u128 },
75}
76
77const EXPIRING_NONCE_KEY: U256 = U256::MAX;
79
80const VALID_BEFORE_SECS: u64 = 25;
82
83const SESSION_OPEN_GAS_LIMIT: u64 = 10_000_000;
85
86const SESSION_OPEN_FEE_PAYER_GAS_LIMIT: u64 = 2_000_000;
94
95const MAX_FEE_PER_GAS: u128 = 20_000_000_000;
97
98const MAX_PRIORITY_FEE_PER_GAS: u128 = 20_000_000_000;
100
101const MAX_PRIORITY_FEE_PER_GAS_FEE_PAYER: u128 = 1_000_000_000;
105
106#[derive(Clone)]
112pub struct SessionProvider {
113 signer: mpp::PrivateKeySigner,
114 signing_mode: TempoSigningMode,
115 authorized_signer: Option<Address>,
116 default_deposit: Option<u128>,
117 channels: Arc<Mutex<HashMap<String, ChannelEntry>>>,
118 precompile_descriptors: Arc<Mutex<HashMap<String, ChannelDescriptor>>>,
119 key_provisioned: Arc<Mutex<bool>>,
120 persisted: Arc<Mutex<HashMap<String, Channel>>>,
121 pending: Arc<Mutex<Option<PendingAction>>>,
123 key_chain_id: Option<u64>,
126 key_currencies: Vec<Address>,
129 origin: String,
130}
131
132impl std::fmt::Debug for SessionProvider {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.debug_struct("SessionProvider")
135 .field("signing_mode", &self.signing_mode)
136 .field("authorized_signer", &self.authorized_signer)
137 .field("default_deposit", &self.default_deposit)
138 .finish_non_exhaustive()
139 }
140}
141
142impl SessionProvider {
143 pub fn new(signer: mpp::PrivateKeySigner, origin: String) -> Self {
151 let persisted =
153 GLOBAL_PERSISTED.get_or_init(|| Arc::new(Mutex::new(persist::load_channels()))).clone();
154
155 let (channels, precompile_descriptors, key_provisioned) = {
157 let global = GLOBAL_CHANNELS.get_or_init(|| Mutex::new(HashMap::new()));
158 let mut map = global.lock().unwrap();
159 map.entry(origin.clone())
160 .or_insert_with(|| {
161 let mut channels: HashMap<String, ChannelEntry> = HashMap::new();
163 for (key, ch) in persisted.lock().unwrap().iter() {
164 if ch.origin == origin
165 && let Some(entry) = persist::to_channel_entry(ch)
166 {
167 channels.insert(key.clone(), entry);
168 }
169 }
170 (
171 Arc::new(Mutex::new(channels)),
172 Arc::new(Mutex::new(HashMap::new())),
173 Arc::new(Mutex::new(true)),
174 )
175 })
176 .clone()
177 };
178
179 Self {
180 signer,
181 signing_mode: TempoSigningMode::Direct,
182 authorized_signer: None,
183 default_deposit: None,
184 channels,
185 precompile_descriptors,
186 key_provisioned,
187 persisted,
188 pending: Arc::new(Mutex::new(None)),
189 key_chain_id: None,
190 key_currencies: vec![],
191 origin,
192 }
193 }
194
195 pub fn with_signing_mode(mut self, mode: TempoSigningMode) -> Self {
197 self.signing_mode = mode;
198 self
199 }
200
201 pub const fn with_authorized_signer(mut self, addr: Address) -> Self {
203 self.authorized_signer = Some(addr);
204 self
205 }
206
207 pub const fn with_default_deposit(mut self, deposit: u128) -> Self {
209 self.default_deposit = Some(deposit);
210 self
211 }
212
213 pub fn funding_wallet_address(&self) -> Address {
215 self.signing_mode.from_address(self.signer.address())
216 }
217
218 pub const fn key_chain_id(&self) -> Option<u64> {
220 self.key_chain_id
221 }
222
223 pub fn with_key_filters(mut self, chain_id: Option<u64>, currencies: Vec<Address>) -> Self {
227 self.key_chain_id = chain_id;
228 self.key_currencies = currencies;
229 self
230 }
231
232 pub fn matches_challenge(&self, chain_id: Option<u64>, currency: Option<Address>) -> bool {
235 if let Some(cid) = chain_id
236 && self.key_chain_id.is_some_and(|k| k != cid)
237 {
238 return false;
239 }
240 if let Some(cur) = currency
241 && !self.key_currencies.is_empty()
242 && !self.key_currencies.contains(&cur)
243 {
244 return false;
245 }
246 true
247 }
248
249 pub fn clear_channels(&self) {
254 let origin = &self.origin;
255 let mut channels = self.channels.lock().unwrap();
257 let mut persisted = self.persisted.lock().unwrap();
258 let keys_to_remove: Vec<(String, String)> = persisted
259 .iter()
260 .filter(|(_, ch)| ch.origin == *origin)
261 .map(|(k, ch): (&String, &Channel)| (k.clone(), ch.channel_id.clone()))
262 .collect();
263 for (key, channel_id) in &keys_to_remove {
264 channels.remove(key);
265 self.precompile_descriptors.lock().unwrap().remove(key);
266 persisted.remove(key);
267 persist::delete_channel_from_db(channel_id);
268 }
269 }
270
271 pub fn set_key_provisioned(&self, provisioned: bool) {
273 *self.key_provisioned.lock().unwrap() = provisioned;
274 }
275
276 pub fn is_key_provisioned(&self) -> bool {
278 *self.key_provisioned.lock().unwrap()
279 }
280
281 pub fn flush_pending(&self) {
285 let pending = self.pending.lock().unwrap().take();
286 if pending.is_some() {
287 persist::save_channels(&self.persisted.lock().unwrap());
288 }
289 }
290
291 pub fn commit_topup_and_track_voucher(&self) {
297 let pending = self.pending.lock().unwrap().take();
298 if let Some(PendingAction::TopUp { key, .. }) = pending {
299 let old_cumulative =
302 self.channels.lock().unwrap().get(&key).map(|e| e.cumulative_amount).unwrap_or(0);
303 *self.pending.lock().unwrap() = Some(PendingAction::Voucher { key, old_cumulative });
304 }
305 }
306
307 pub fn rollback_pending(&self) {
311 let pending = self.pending.lock().unwrap().take();
312 if let Some(action) = pending {
313 match action {
314 PendingAction::Open { key } => {
315 self.channels.lock().unwrap().remove(&key);
316 self.precompile_descriptors.lock().unwrap().remove(&key);
317 self.persisted.lock().unwrap().remove(&key);
318 }
319 PendingAction::TopUp { key, old_deposit } => {
320 if let Some(p) = self.persisted.lock().unwrap().get_mut(&key) {
321 p.deposit = old_deposit;
322 }
323 }
324 PendingAction::Voucher { key, old_cumulative } => {
325 if let Some(entry) = self.channels.lock().unwrap().get_mut(&key) {
326 entry.cumulative_amount = old_cumulative;
327 }
328 if let Some(p) = self.persisted.lock().unwrap().get_mut(&key) {
329 p.cumulative_amount = old_cumulative.to_string();
330 }
331 }
332 }
333 }
334 }
335
336 #[allow(clippy::too_many_arguments)]
337 fn channel_key(
338 origin: &str,
339 payer: &Address,
340 authorized_signer: Option<Address>,
341 payee: &Address,
342 currency: &Address,
343 escrow: &Address,
344 chain_id: u64,
345 operator: Address,
346 ) -> String {
347 let origin_hash = &alloy_primitives::keccak256(origin.as_bytes()).to_string()[..18];
351 let signer = authorized_signer.unwrap_or(*payer);
352 format!("{origin_hash}:{chain_id}:{payer}:{signer}:{payee}:{currency}:{escrow}:{operator}")
353 .to_lowercase()
354 }
355
356 fn compute_fee_payer_expiring_nonce_hash(
362 unsigned_tx: &tempo_primitives::transaction::TempoTransaction,
363 sender: Address,
364 ) -> B256 {
365 let mut tx = unsigned_tx.clone();
366 tx.fee_payer_signature =
367 Some(alloy_primitives::Signature::new(U256::ZERO, U256::ZERO, false));
368 compute_expiring_nonce_hash(&tx, sender)
369 }
370
371 fn resolve_deposit(&self, suggested: Option<&str>) -> Result<u128, MppError> {
372 let suggested_val = suggested.and_then(|s| s.parse::<u128>().ok());
373
374 if let (Some(sv), Some(local)) = (suggested_val, self.default_deposit)
377 && sv > local
378 {
379 let _ = sh_warn!(
380 "server-suggested deposit ({sv}) exceeds local default ({local}); \
381 set MPP_DEPOSIT to override"
382 );
383 }
384
385 let amount = self.default_deposit.or(suggested_val);
386
387 amount.ok_or_else(|| {
388 MppError::InvalidConfig("no deposit amount: set default_deposit".to_string())
389 })
390 }
391
392 async fn create_open_tx(
393 &self,
394 payer: Address,
395 options: OpenPayloadOptions,
396 operator: Address,
397 ) -> Result<(ChannelEntry, SessionCredentialPayload), MppError> {
398 if is_precompile_escrow(options.escrow_contract) {
399 return self.create_precompile_open_tx(payer, options, operator).await;
400 }
401
402 let authorized_signer = options.authorized_signer.unwrap_or(payer);
403 let salt = B256::random();
404
405 let channel_id = compute_channel_id(
406 payer,
407 options.payee,
408 options.currency,
409 salt,
410 authorized_signer,
411 options.escrow_contract,
412 options.chain_id,
413 );
414
415 alloy_sol_types::sol! {
416 interface ITIP20 {
417 function approve(address spender, uint256 amount) external returns (bool);
418 }
419 interface IEscrow {
420 function open(
421 address payee,
422 address token,
423 uint128 deposit,
424 bytes32 salt,
425 address authorizedSigner
426 ) external;
427 }
428 }
429
430 let approve_data =
431 ITIP20::approveCall::new((options.escrow_contract, U256::from(options.deposit)))
432 .abi_encode();
433
434 let open_data = IEscrow::openCall::new((
435 options.payee,
436 options.currency,
437 options.deposit,
438 salt,
439 authorized_signer,
440 ))
441 .abi_encode();
442
443 let calls = vec![
444 Call {
445 to: TxKind::Call(options.currency),
446 value: U256::ZERO,
447 input: Bytes::from(approve_data),
448 },
449 Call {
450 to: TxKind::Call(options.escrow_contract),
451 value: U256::ZERO,
452 input: Bytes::from(open_data),
453 },
454 ];
455
456 let valid_before = {
457 let now = std::time::SystemTime::now()
458 .duration_since(std::time::UNIX_EPOCH)
459 .unwrap_or_default()
460 .as_secs();
461 Some(now + VALID_BEFORE_SECS)
462 };
463
464 let tx = mpp::client::tempo::charge::tx_builder::build_tempo_tx(
465 mpp::client::tempo::charge::tx_builder::TempoTxOptions {
466 calls,
467 chain_id: options.chain_id,
468 fee_token: options.currency,
469 nonce: 0,
470 nonce_key: EXPIRING_NONCE_KEY,
471 gas_limit: if options.fee_payer {
472 SESSION_OPEN_FEE_PAYER_GAS_LIMIT
473 } else {
474 SESSION_OPEN_GAS_LIMIT
475 },
476 max_fee_per_gas: MAX_FEE_PER_GAS,
477 max_priority_fee_per_gas: if options.fee_payer {
478 MAX_PRIORITY_FEE_PER_GAS_FEE_PAYER
479 } else {
480 MAX_PRIORITY_FEE_PER_GAS
481 },
482 fee_payer: options.fee_payer,
483 valid_before,
484 key_authorization: (!*self.key_provisioned.lock().unwrap())
485 .then(|| self.signing_mode.key_authorization().cloned())
486 .flatten(),
487 },
488 );
489
490 let signed_tx = if options.fee_payer {
491 sign_and_encode_fee_payer_envelope_async(tx, &self.signer, &self.signing_mode).await?
492 } else {
493 sign_and_encode_async(tx, &self.signer, &self.signing_mode).await?
494 };
495
496 let voucher = sign_voucher(
497 &self.signer,
498 channel_id,
499 options.initial_amount,
500 options.escrow_contract,
501 options.chain_id,
502 )
503 .await?;
504
505 let entry = ChannelEntry {
506 channel_id,
507 salt,
508 cumulative_amount: options.initial_amount,
509 escrow_contract: options.escrow_contract,
510 chain_id: options.chain_id,
511 opened: true,
512 };
513
514 let signed_tx_hex = alloy_primitives::hex::encode_prefixed(&signed_tx);
515 let voucher_sig_hex = alloy_primitives::hex::encode_prefixed(&voucher);
516
517 Ok((
518 entry,
519 SessionCredentialPayload::Open {
520 payload_type: "transaction".to_string(),
521 channel_id: channel_id.to_string(),
522 transaction: signed_tx_hex,
523 descriptor: None,
524 authorized_signer: Some(format!("{authorized_signer}")),
525 cumulative_amount: options.initial_amount.to_string(),
526 signature: voucher_sig_hex,
527 },
528 ))
529 }
530
531 async fn create_precompile_open_tx(
535 &self,
536 payer: Address,
537 options: OpenPayloadOptions,
538 operator: Address,
539 ) -> Result<(ChannelEntry, SessionCredentialPayload), MppError> {
540 if options.deposit > PRECOMPILE_MAX_CUMULATIVE_AMOUNT
541 || options.initial_amount > PRECOMPILE_MAX_CUMULATIVE_AMOUNT
542 {
543 return Err(MppError::InvalidConfig(
544 "precompile escrow deposit/initial_amount must fit uint96".to_string(),
545 ));
546 }
547
548 let authorized_signer = options.authorized_signer.unwrap_or(payer);
549 let salt = B256::random();
550
551 let open_data = ITIP20ChannelReserve::openCall::new((
552 options.payee,
553 operator,
554 options.currency,
555 alloy_primitives::Uint::<96, 2>::from(options.deposit),
556 salt,
557 authorized_signer,
558 ))
559 .abi_encode();
560
561 let calls = vec![Call {
562 to: TxKind::Call(TIP20_CHANNEL_RESERVE_ADDRESS),
563 value: U256::ZERO,
564 input: Bytes::from(open_data),
565 }];
566
567 let valid_before = {
568 let now = std::time::SystemTime::now()
569 .duration_since(std::time::UNIX_EPOCH)
570 .unwrap_or_default()
571 .as_secs();
572 Some(now + VALID_BEFORE_SECS)
573 };
574
575 let tx = mpp::client::tempo::charge::tx_builder::build_tempo_tx(
576 mpp::client::tempo::charge::tx_builder::TempoTxOptions {
577 calls,
578 chain_id: options.chain_id,
579 fee_token: options.currency,
580 nonce: 0,
581 nonce_key: EXPIRING_NONCE_KEY,
582 gas_limit: if options.fee_payer {
583 SESSION_OPEN_FEE_PAYER_GAS_LIMIT
584 } else {
585 SESSION_OPEN_GAS_LIMIT
586 },
587 max_fee_per_gas: MAX_FEE_PER_GAS,
588 max_priority_fee_per_gas: if options.fee_payer {
589 MAX_PRIORITY_FEE_PER_GAS_FEE_PAYER
590 } else {
591 MAX_PRIORITY_FEE_PER_GAS
592 },
593 fee_payer: options.fee_payer,
594 valid_before,
595 key_authorization: (!*self.key_provisioned.lock().unwrap())
596 .then(|| self.signing_mode.key_authorization().cloned())
597 .flatten(),
598 },
599 );
600
601 let expiring_nonce_hash = if options.fee_payer {
603 Self::compute_fee_payer_expiring_nonce_hash(&tx, payer)
604 } else {
605 compute_expiring_nonce_hash(&tx, payer)
606 };
607 let descriptor = ChannelDescriptor {
608 payer: payer.to_string(),
609 payee: options.payee.to_string(),
610 operator: operator.to_string(),
611 token: options.currency.to_string(),
612 salt: salt.to_string(),
613 authorized_signer: authorized_signer.to_string(),
614 expiring_nonce_hash: expiring_nonce_hash.to_string(),
615 };
616 let channel_id = compute_precompile_channel_id(
617 payer,
618 options.payee,
619 operator,
620 options.currency,
621 salt,
622 authorized_signer,
623 expiring_nonce_hash,
624 options.chain_id,
625 );
626
627 let signed_tx = if options.fee_payer {
628 sign_and_encode_fee_payer_request_async(tx, &self.signer, &self.signing_mode).await?
629 } else {
630 sign_and_encode_async(tx, &self.signer, &self.signing_mode).await?
631 };
632
633 let voucher = sign_precompile_voucher(
634 &self.signer,
635 channel_id,
636 options.initial_amount,
637 options.chain_id,
638 )
639 .await?;
640
641 let entry = ChannelEntry {
642 channel_id,
643 salt,
644 cumulative_amount: options.initial_amount,
645 escrow_contract: TIP20_CHANNEL_RESERVE_ADDRESS,
646 chain_id: options.chain_id,
647 opened: true,
648 };
649
650 Ok((
651 entry,
652 SessionCredentialPayload::Open {
653 payload_type: "transaction".to_string(),
654 channel_id: channel_id.to_string(),
655 transaction: alloy_primitives::hex::encode_prefixed(&signed_tx),
656 descriptor: Some(descriptor),
657 authorized_signer: Some(format!("{authorized_signer}")),
658 cumulative_amount: options.initial_amount.to_string(),
659 signature: alloy_primitives::hex::encode_prefixed(&voucher),
660 },
661 ))
662 }
663
664 async fn create_topup_tx(
665 &self,
666 entry: &ChannelEntry,
667 additional_deposit: u128,
668 currency: Address,
669 fee_payer: bool,
670 ) -> Result<SessionCredentialPayload, MppError> {
671 if is_precompile_escrow(entry.escrow_contract) {
674 return Err(MppError::InvalidConfig(
675 "T5 precompile escrow top-up is not supported. Close the channel (or wait \
676 for expiry) and re-run with a larger MPP_DEPOSIT. T5 cumulative_amount is \
677 capped at uint96."
678 .to_string(),
679 ));
680 }
681
682 alloy_sol_types::sol! {
683 interface ITIP20 {
684 function approve(address spender, uint256 amount) external returns (bool);
685 }
686 interface IEscrow {
687 function topUp(bytes32 channelId, uint256 additionalDeposit) external;
688 }
689 }
690
691 let approve_data =
692 ITIP20::approveCall::new((entry.escrow_contract, U256::from(additional_deposit)))
693 .abi_encode();
694 let topup_data =
695 IEscrow::topUpCall::new((entry.channel_id, U256::from(additional_deposit)))
696 .abi_encode();
697
698 let calls = vec![
699 Call {
700 to: TxKind::Call(currency),
701 value: U256::ZERO,
702 input: Bytes::from(approve_data),
703 },
704 Call {
705 to: TxKind::Call(entry.escrow_contract),
706 value: U256::ZERO,
707 input: Bytes::from(topup_data),
708 },
709 ];
710
711 let valid_before = {
712 let now = std::time::SystemTime::now()
713 .duration_since(std::time::UNIX_EPOCH)
714 .unwrap_or_default()
715 .as_secs();
716 Some(now + VALID_BEFORE_SECS)
717 };
718
719 let tx = mpp::client::tempo::charge::tx_builder::build_tempo_tx(
720 mpp::client::tempo::charge::tx_builder::TempoTxOptions {
721 calls,
722 chain_id: entry.chain_id,
723 fee_token: currency,
724 nonce: 0,
725 nonce_key: EXPIRING_NONCE_KEY,
726 gas_limit: if fee_payer {
727 SESSION_OPEN_FEE_PAYER_GAS_LIMIT
728 } else {
729 SESSION_OPEN_GAS_LIMIT
730 },
731 max_fee_per_gas: MAX_FEE_PER_GAS,
732 max_priority_fee_per_gas: if fee_payer {
733 MAX_PRIORITY_FEE_PER_GAS_FEE_PAYER
734 } else {
735 MAX_PRIORITY_FEE_PER_GAS
736 },
737 fee_payer,
738 valid_before,
739 key_authorization: None,
740 },
741 );
742
743 let signed_tx = if fee_payer {
744 sign_and_encode_fee_payer_envelope_async(tx, &self.signer, &self.signing_mode).await?
745 } else {
746 sign_and_encode_async(tx, &self.signer, &self.signing_mode).await?
747 };
748
749 Ok(SessionCredentialPayload::TopUp {
750 payload_type: "transaction".to_string(),
751 channel_id: entry.channel_id.to_string(),
752 transaction: alloy_primitives::hex::encode_prefixed(&signed_tx),
753 descriptor: None,
754 additional_deposit: additional_deposit.to_string(),
755 })
756 }
757}
758
759impl SessionProvider {
760 async fn pay_charge(
762 &self,
763 challenge: &PaymentChallenge,
764 ) -> Result<PaymentCredential, MppError> {
765 use mpp::client::tempo::charge::{SignOptions, TempoCharge};
766
767 let charge = TempoCharge::from_challenge(challenge)?;
768
769 let signing_mode = if *self.key_provisioned.lock().unwrap() {
773 match &self.signing_mode {
774 TempoSigningMode::Keychain { wallet, version, .. } => TempoSigningMode::Keychain {
775 wallet: *wallet,
776 key_authorization: None,
777 version: *version,
778 },
779 other => other.clone(),
780 }
781 } else {
782 self.signing_mode.clone()
783 };
784
785 let options = SignOptions { signing_mode: Some(signing_mode), ..Default::default() };
786 let signed = charge.sign_with_options(&self.signer, options).await?;
787 Ok(signed.into_credential())
788 }
789}
790
791impl PaymentProvider for SessionProvider {
792 fn supports(&self, method: &str, intent: &str) -> bool {
793 method == "tempo" && (intent == "session" || intent == "charge")
794 }
795
796 async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
797 if challenge.intent.as_str() == "charge" {
798 return self.pay_charge(challenge).await;
799 }
800 self.pay_session(challenge).await
801 }
802}
803
804impl SessionProvider {
805 async fn pay_session(
806 &self,
807 challenge: &PaymentChallenge,
808 ) -> Result<PaymentCredential, MppError> {
809 let session_req: SessionRequest = challenge.request.decode().map_err(|e| {
810 MppError::InvalidConfig(format!("failed to decode session request: {e}"))
811 })?;
812
813 let chain_id = resolve_chain_id(challenge);
814 let escrow_contract = resolve_escrow(challenge, chain_id, None)?;
815 let payee: Address = session_req
816 .recipient
817 .as_deref()
818 .ok_or_else(|| {
819 MppError::InvalidConfig("session challenge missing recipient".to_string())
820 })?
821 .parse()
822 .map_err(|_e| MppError::InvalidConfig("invalid recipient address".to_string()))?;
823 let currency: Address = session_req
824 .currency
825 .parse()
826 .map_err(|_e| MppError::InvalidConfig("invalid currency address".to_string()))?;
827 let amount: u128 = session_req.parse_amount()?;
828
829 let operator = if is_precompile_escrow(escrow_contract) {
832 match session_req.method_details.as_ref().and_then(|v| v.get("operator")) {
833 None => Address::ZERO,
834 Some(v) => {
835 let s = v.as_str().ok_or_else(|| {
836 MppError::InvalidConfig(
837 "methodDetails.operator must be a string".to_string(),
838 )
839 })?;
840 s.parse::<Address>().map_err(|_| {
841 MppError::InvalidConfig(format!("invalid operator address: {s}"))
842 })?
843 }
844 }
845 } else {
846 Address::ZERO
847 };
848
849 let payer = self.signing_mode.from_address(self.signer.address());
850
851 let key = Self::channel_key(
852 &self.origin,
853 &payer,
854 self.authorized_signer,
855 &payee,
856 ¤cy,
857 &escrow_contract,
858 chain_id,
859 operator,
860 );
861
862 let voucher_info = {
863 let mut channels = self.channels.lock().unwrap();
864 if let Some(entry) = channels.get_mut(&key)
865 && entry.opened
866 {
867 let deposit = self
868 .persisted
869 .lock()
870 .unwrap()
871 .get(&key)
872 .and_then(|p| p.deposit.parse::<u128>().ok())
873 .unwrap_or(u128::MAX);
874
875 let projected = entry.cumulative_amount.checked_add(amount).ok_or_else(|| {
877 MppError::InvalidConfig("voucher cumulative_amount overflow".to_string())
878 });
879 Some(match projected {
880 Err(e) => return Err(e),
881 Ok(p) if p > deposit => Err((entry.clone(), deposit)),
882 Ok(_) => Ok(entry.clone()),
883 })
884 } else {
885 None
886 }
887 };
888
889 if let Some(result) = voucher_info {
890 match result {
891 Err((entry, deposit)) => {
892 let additional =
893 self.resolve_deposit(session_req.suggested_deposit.as_deref())?;
894 tracing::debug!(
895 cumulative = entry.cumulative_amount,
896 amount,
897 deposit,
898 additional,
899 "channel deposit exhausted, topping up"
900 );
901
902 let payload = self
903 .create_topup_tx(&entry, additional, currency, session_req.fee_payer())
904 .await?;
905
906 let old_deposit = {
908 let mut persisted = self.persisted.lock().unwrap();
909 if let Some(p) = persisted.get_mut(&key) {
910 let old = p.deposit.clone();
911 let old_val: u128 = old.parse().unwrap_or(0);
912 let new_val = old_val.checked_add(additional).ok_or_else(|| {
913 MppError::InvalidConfig("top-up deposit overflow".to_string())
914 })?;
915 p.deposit = new_val.to_string();
916 old
917 } else {
918 "0".to_string()
919 }
920 };
921 *self.pending.lock().unwrap() =
922 Some(PendingAction::TopUp { key: key.clone(), old_deposit });
923
924 return Ok(build_credential(challenge, payload, chain_id, payer));
925 }
926 Ok(entry) => {
927 let old_cumulative = entry.cumulative_amount;
928 let new_cumulative = old_cumulative.checked_add(amount).ok_or_else(|| {
929 MppError::InvalidConfig("voucher cumulative_amount overflow".to_string())
930 })?;
931 let payload = if is_precompile_escrow(escrow_contract) {
932 if new_cumulative > PRECOMPILE_MAX_CUMULATIVE_AMOUNT {
933 return Err(MppError::InvalidConfig(
934 "precompile voucher cumulative_amount must fit uint96".to_string(),
935 ));
936 }
937 let descriptor = self
938 .precompile_descriptors
939 .lock()
940 .unwrap()
941 .get(&key)
942 .cloned()
943 .ok_or_else(|| {
944 MppError::InvalidConfig(
945 "missing TIP-1034 descriptor for cached precompile channel"
946 .to_string(),
947 )
948 })?;
949 let sig = sign_precompile_voucher(
950 &self.signer,
951 entry.channel_id,
952 new_cumulative,
953 chain_id,
954 )
955 .await?;
956 SessionCredentialPayload::Voucher {
957 channel_id: entry.channel_id.to_string(),
958 descriptor: Some(descriptor),
959 cumulative_amount: new_cumulative.to_string(),
960 signature: alloy_primitives::hex::encode_prefixed(&sig),
961 }
962 } else {
963 create_voucher_payload(
964 &self.signer,
965 entry.channel_id,
966 new_cumulative,
967 escrow_contract,
968 chain_id,
969 )
970 .await?
971 };
972
973 {
975 let mut channels = self.channels.lock().unwrap();
976 if let Some(e) = channels.get_mut(&key) {
977 e.cumulative_amount = new_cumulative;
978 }
979 }
980
981 let updated_entry = ChannelEntry { cumulative_amount: new_cumulative, ..entry };
985 let mut persisted = self.persisted.lock().unwrap();
986 persist::upsert_channel_in_memory(&mut persisted, &key, &updated_entry);
987 drop(persisted);
988
989 if self.pending.lock().unwrap().is_none() {
992 *self.pending.lock().unwrap() =
993 Some(PendingAction::Voucher { key, old_cumulative });
994 }
995
996 return Ok(build_credential(challenge, payload, chain_id, payer));
997 }
998 }
999 }
1000
1001 let deposit = self.resolve_deposit(session_req.suggested_deposit.as_deref())?;
1003
1004 let (entry, payload) = self
1005 .create_open_tx(
1006 payer,
1007 OpenPayloadOptions {
1008 authorized_signer: self.authorized_signer,
1009 escrow_contract,
1010 payee,
1011 currency,
1012 deposit,
1013 initial_amount: amount,
1014 chain_id,
1015 fee_payer: session_req.fee_payer(),
1016 },
1017 operator,
1018 )
1019 .await?;
1020
1021 self.channels.lock().unwrap().insert(key.clone(), entry.clone());
1023 if let SessionCredentialPayload::Open { descriptor: Some(descriptor), .. } = &payload {
1024 self.precompile_descriptors.lock().unwrap().insert(key.clone(), descriptor.clone());
1025 }
1026 let authorized_signer = self.authorized_signer.unwrap_or(payer);
1027 self.persisted.lock().unwrap().insert(
1028 key.clone(),
1029 persist::from_channel_entry(
1030 &entry,
1031 deposit,
1032 &self.origin,
1033 &payer,
1034 &payee,
1035 ¤cy,
1036 &authorized_signer,
1037 ),
1038 );
1039 *self.pending.lock().unwrap() = Some(PendingAction::Open { key });
1040 Ok(build_credential(challenge, payload, chain_id, payer))
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047 use mpp::client::tempo::signing::KeychainVersion;
1048 use tempo_alloy::contracts::precompiles::DEFAULT_FEE_TOKEN;
1049 use tempo_primitives::transaction::{
1050 KeyAuthorization, PrimitiveSignature, SignatureType, SignedKeyAuthorization,
1051 };
1052
1053 fn test_key_authorization() -> SignedKeyAuthorization {
1055 KeyAuthorization::unrestricted(4217, SignatureType::Secp256k1, Address::ZERO)
1056 .into_signed(PrimitiveSignature::default())
1057 }
1058
1059 fn strip_key_auth_if_provisioned(
1060 mode: &TempoSigningMode,
1061 provisioned: bool,
1062 ) -> TempoSigningMode {
1063 if provisioned {
1064 match mode {
1065 TempoSigningMode::Keychain { wallet, version, .. } => TempoSigningMode::Keychain {
1066 wallet: *wallet,
1067 key_authorization: None,
1068 version: *version,
1069 },
1070 other => other.clone(),
1071 }
1072 } else {
1073 mode.clone()
1074 }
1075 }
1076
1077 fn unique_origin() -> String {
1079 format!("https://rpc-{}.example.com", alloy_primitives::B256::random())
1080 }
1081
1082 #[test]
1083 fn test_key_provisioned_default_is_true() {
1084 let signer = mpp::PrivateKeySigner::random();
1085 let provider = SessionProvider::new(signer, unique_origin());
1086 assert!(*provider.key_provisioned.lock().unwrap());
1087 }
1088
1089 #[test]
1090 fn test_set_key_provisioned() {
1091 let signer = mpp::PrivateKeySigner::random();
1092 let provider = SessionProvider::new(signer, unique_origin());
1093 provider.set_key_provisioned(false);
1094 assert!(!*provider.key_provisioned.lock().unwrap());
1095 provider.set_key_provisioned(true);
1096 assert!(*provider.key_provisioned.lock().unwrap());
1097 }
1098
1099 #[test]
1100 fn test_pay_charge_strips_key_auth_when_provisioned() {
1101 let signer = mpp::PrivateKeySigner::random();
1102 let wallet = Address::repeat_byte(0xAA);
1103 let signing_mode = TempoSigningMode::Keychain {
1104 wallet,
1105 key_authorization: Some(Box::new(test_key_authorization())),
1106 version: KeychainVersion::V2,
1107 };
1108 let provider =
1109 SessionProvider::new(signer, unique_origin()).with_signing_mode(signing_mode);
1110
1111 let provisioned = *provider.key_provisioned.lock().unwrap();
1112 let result_mode = strip_key_auth_if_provisioned(&provider.signing_mode, provisioned);
1113
1114 assert!(
1115 result_mode.key_authorization().is_none(),
1116 "key_authorization should be stripped when key is provisioned"
1117 );
1118 }
1119
1120 #[test]
1121 fn test_pay_charge_keeps_key_auth_when_not_provisioned() {
1122 let signer = mpp::PrivateKeySigner::random();
1123 let wallet = Address::repeat_byte(0xAA);
1124 let signing_mode = TempoSigningMode::Keychain {
1125 wallet,
1126 key_authorization: Some(Box::new(test_key_authorization())),
1127 version: KeychainVersion::V2,
1128 };
1129 let provider =
1130 SessionProvider::new(signer, unique_origin()).with_signing_mode(signing_mode);
1131
1132 provider.set_key_provisioned(false);
1133
1134 let provisioned = *provider.key_provisioned.lock().unwrap();
1135 let result_mode = strip_key_auth_if_provisioned(&provider.signing_mode, provisioned);
1136
1137 assert!(
1138 result_mode.key_authorization().is_some(),
1139 "key_authorization should be preserved when key is NOT provisioned"
1140 );
1141 }
1142
1143 #[test]
1144 fn test_pay_charge_direct_mode_unaffected() {
1145 let signer = mpp::PrivateKeySigner::random();
1146 let provider = SessionProvider::new(signer, unique_origin())
1147 .with_signing_mode(TempoSigningMode::Direct);
1148
1149 let provisioned = *provider.key_provisioned.lock().unwrap();
1150 let result_mode = strip_key_auth_if_provisioned(&provider.signing_mode, provisioned);
1151
1152 assert!(
1153 matches!(result_mode, TempoSigningMode::Direct),
1154 "Direct mode should pass through unchanged"
1155 );
1156 }
1157
1158 fn payload_transaction_type(payload: &SessionCredentialPayload) -> u8 {
1159 let transaction = match payload {
1160 SessionCredentialPayload::Open { transaction, .. }
1161 | SessionCredentialPayload::TopUp { transaction, .. } => transaction,
1162 _ => panic!("expected transaction payload"),
1163 };
1164 let bytes =
1165 alloy_primitives::hex::decode(transaction.strip_prefix("0x").unwrap_or(transaction))
1166 .expect("valid transaction hex");
1167 bytes[0]
1168 }
1169
1170 #[tokio::test]
1174 async fn test_session_transactions_use_fee_payer_envelope_when_fee_payer_true() {
1175 let signer = mpp::PrivateKeySigner::random();
1176 let payer = signer.address();
1177 let provider = SessionProvider::new(signer, unique_origin())
1178 .with_signing_mode(TempoSigningMode::Direct);
1179 let escrow_contract = Address::repeat_byte(0x11);
1180 let payee = Address::repeat_byte(0x22);
1181 let chain_id = 4217;
1182
1183 let (entry, open_payload) = provider
1184 .create_open_tx(
1185 payer,
1186 OpenPayloadOptions {
1187 authorized_signer: None,
1188 escrow_contract,
1189 payee,
1190 currency: DEFAULT_FEE_TOKEN,
1191 deposit: 1_000_000,
1192 initial_amount: 10,
1193 chain_id,
1194 fee_payer: true,
1195 },
1196 Address::ZERO,
1197 )
1198 .await
1199 .unwrap();
1200 assert_eq!(payload_transaction_type(&open_payload), 0x78);
1201
1202 let topup_payload =
1203 provider.create_topup_tx(&entry, 1_000_000, DEFAULT_FEE_TOKEN, true).await.unwrap();
1204 assert_eq!(payload_transaction_type(&topup_payload), 0x78);
1205
1206 let (_, open_without_fee_payer) = provider
1207 .create_open_tx(
1208 payer,
1209 OpenPayloadOptions {
1210 authorized_signer: None,
1211 escrow_contract,
1212 payee,
1213 currency: DEFAULT_FEE_TOKEN,
1214 deposit: 1_000_000,
1215 initial_amount: 10,
1216 chain_id,
1217 fee_payer: false,
1218 },
1219 Address::ZERO,
1220 )
1221 .await
1222 .unwrap();
1223 assert_eq!(payload_transaction_type(&open_without_fee_payer), 0x76);
1224 }
1225
1226 #[tokio::test]
1228 async fn legacy_solidity_escrow_address_flows_through_open_payload() {
1229 let escrow: Address = "0x33b901018174DDabE4841042ab76ba85D4e24f25".parse().unwrap();
1230
1231 let signer = mpp::PrivateKeySigner::random();
1232 let provider = SessionProvider::new(signer, unique_origin());
1233 let payer = provider.funding_wallet_address();
1234 let opts = OpenPayloadOptions {
1235 authorized_signer: None,
1236 escrow_contract: escrow,
1237 payee: Address::repeat_byte(0x11),
1238 currency: Address::repeat_byte(0x22),
1239 deposit: 100_000,
1240 initial_amount: 1_000,
1241 chain_id: 4217,
1242 fee_payer: false,
1243 };
1244 let (payee, currency, chain_id) = (opts.payee, opts.currency, opts.chain_id);
1245
1246 let (entry, _payload) = provider
1247 .create_open_tx(payer, opts, Address::ZERO)
1248 .await
1249 .expect("create_open_tx with legacy Solidity escrow");
1250
1251 assert_eq!(entry.escrow_contract, escrow);
1252
1253 let recomputed =
1254 compute_channel_id(payer, payee, currency, entry.salt, payer, escrow, chain_id);
1255 assert_eq!(entry.channel_id, recomputed);
1256 }
1257
1258 #[tokio::test]
1260 async fn precompile_escrow_open_uses_precompile_channel_id() {
1261 let signer = mpp::PrivateKeySigner::random();
1262 let provider = SessionProvider::new(signer, unique_origin());
1263 let payer = provider.funding_wallet_address();
1264 let opts = OpenPayloadOptions {
1265 authorized_signer: None,
1266 escrow_contract: TIP20_CHANNEL_RESERVE_ADDRESS,
1267 payee: Address::repeat_byte(0x11),
1268 currency: Address::repeat_byte(0x22),
1269 deposit: 100_000,
1270 initial_amount: 1_000,
1271 chain_id: 4217,
1272 fee_payer: false,
1273 };
1274
1275 let (entry, _payload) =
1276 provider.create_open_tx(payer, opts, Address::ZERO).await.expect("precompile open");
1277
1278 assert_eq!(entry.escrow_contract, TIP20_CHANNEL_RESERVE_ADDRESS);
1279 let legacy = compute_channel_id(
1280 payer,
1281 Address::repeat_byte(0x11),
1282 Address::repeat_byte(0x22),
1283 entry.salt,
1284 payer,
1285 TIP20_CHANNEL_RESERVE_ADDRESS,
1286 4217,
1287 );
1288 assert_ne!(entry.channel_id, legacy);
1289 }
1290
1291 #[tokio::test]
1293 async fn precompile_escrow_open_rejects_uint96_overflow() {
1294 let signer = mpp::PrivateKeySigner::random();
1295 let provider = SessionProvider::new(signer, unique_origin());
1296 let payer = provider.funding_wallet_address();
1297 let opts = OpenPayloadOptions {
1298 authorized_signer: None,
1299 escrow_contract: TIP20_CHANNEL_RESERVE_ADDRESS,
1300 payee: Address::repeat_byte(0x11),
1301 currency: Address::repeat_byte(0x22),
1302 deposit: PRECOMPILE_MAX_CUMULATIVE_AMOUNT + 1,
1303 initial_amount: 1,
1304 chain_id: 4217,
1305 fee_payer: false,
1306 };
1307 let err = provider
1308 .create_open_tx(payer, opts, Address::ZERO)
1309 .await
1310 .expect_err("deposit > uint96 must reject");
1311 assert!(matches!(err, MppError::InvalidConfig(_)));
1312 }
1313
1314 #[tokio::test]
1316 async fn precompile_escrow_topup_is_rejected() {
1317 let signer = mpp::PrivateKeySigner::random();
1318 let provider = SessionProvider::new(signer, unique_origin());
1319 let entry = ChannelEntry {
1320 channel_id: B256::ZERO,
1321 salt: B256::ZERO,
1322 cumulative_amount: 0,
1323 escrow_contract: TIP20_CHANNEL_RESERVE_ADDRESS,
1324 chain_id: 4217,
1325 opened: true,
1326 };
1327 let err = provider
1328 .create_topup_tx(&entry, 1_000, Address::repeat_byte(0x22), false)
1329 .await
1330 .expect_err("precompile top-up must be rejected");
1331 assert!(matches!(err, MppError::InvalidConfig(_)));
1332 }
1333
1334 #[tokio::test]
1337 async fn precompile_escrow_open_binds_operator_into_calldata() {
1338 use alloy_eips::eip2718::Decodable2718;
1339 use tempo_primitives::transaction::TempoTxEnvelope;
1340
1341 let signer = mpp::PrivateKeySigner::random();
1342 let provider = SessionProvider::new(signer, unique_origin());
1343 let payer = provider.funding_wallet_address();
1344 let base_opts = || OpenPayloadOptions {
1345 authorized_signer: None,
1346 escrow_contract: TIP20_CHANNEL_RESERVE_ADDRESS,
1347 payee: Address::repeat_byte(0x11),
1348 currency: Address::repeat_byte(0x22),
1349 deposit: 100_000,
1350 initial_amount: 1_000,
1351 chain_id: 4217,
1352 fee_payer: false,
1353 };
1354 let expected_operator = Address::repeat_byte(0x99);
1355
1356 let (entry, payload) = provider
1357 .create_open_tx(payer, base_opts(), expected_operator)
1358 .await
1359 .expect("precompile open with explicit operator");
1360
1361 let SessionCredentialPayload::Open { transaction, .. } = payload else {
1362 panic!("expected Open payload");
1363 };
1364 let tx_bytes = alloy_primitives::hex::decode(&transaction).expect("hex tx");
1365 let envelope =
1366 TempoTxEnvelope::decode_2718(&mut tx_bytes.as_slice()).expect("decode envelope");
1367 let TempoTxEnvelope::AA(aa_signed) = envelope else {
1368 panic!("expected AA envelope (0x76)");
1369 };
1370 let unsigned = aa_signed.strip_signature();
1371 assert_eq!(unsigned.calls.len(), 1);
1372 let call = &unsigned.calls[0];
1373 assert_eq!(call.to, TxKind::Call(TIP20_CHANNEL_RESERVE_ADDRESS));
1374
1375 let decoded =
1376 ITIP20ChannelReserve::openCall::abi_decode(&call.input).expect("decode openCall");
1377 assert_eq!(decoded.operator, expected_operator);
1378
1379 let authorized_signer = payer;
1380 let expected_id = compute_precompile_channel_id(
1381 payer,
1382 base_opts().payee,
1383 expected_operator,
1384 base_opts().currency,
1385 entry.salt,
1386 authorized_signer,
1387 mpp::client::channel_ops::compute_expiring_nonce_hash(&unsigned, payer),
1388 base_opts().chain_id,
1389 );
1390 assert_eq!(entry.channel_id, expected_id);
1391 }
1392
1393 #[test]
1395 fn voucher_cumulative_overflow_is_rejected() {
1396 assert!((u128::MAX - 1).checked_add(5).is_none());
1397 }
1398
1399 #[tokio::test]
1402 async fn precompile_open_key_auth_tracks_provisioned_flag() {
1403 use alloy_eips::eip2718::Decodable2718;
1404 use tempo_primitives::transaction::TempoTxEnvelope;
1405
1406 async fn key_auth_present(provisioned: bool) -> bool {
1407 let signer = mpp::PrivateKeySigner::random();
1408 let signing_mode = TempoSigningMode::Keychain {
1409 wallet: Address::repeat_byte(0xAA),
1410 key_authorization: Some(Box::new(test_key_authorization())),
1411 version: KeychainVersion::V2,
1412 };
1413 let provider =
1414 SessionProvider::new(signer, unique_origin()).with_signing_mode(signing_mode);
1415 provider.set_key_provisioned(provisioned);
1416
1417 let payer = provider.funding_wallet_address();
1418 let opts = OpenPayloadOptions {
1419 authorized_signer: None,
1420 escrow_contract: TIP20_CHANNEL_RESERVE_ADDRESS,
1421 payee: Address::repeat_byte(0x11),
1422 currency: Address::repeat_byte(0x22),
1423 deposit: 100_000,
1424 initial_amount: 1_000,
1425 chain_id: 4217,
1426 fee_payer: false,
1427 };
1428 let (_entry, payload) =
1429 provider.create_open_tx(payer, opts, Address::ZERO).await.expect("precompile open");
1430 let SessionCredentialPayload::Open { transaction, .. } = payload else {
1431 panic!("expected Open payload");
1432 };
1433 let tx_bytes = alloy_primitives::hex::decode(&transaction).expect("hex tx");
1434 let envelope =
1435 TempoTxEnvelope::decode_2718(&mut tx_bytes.as_slice()).expect("decode envelope");
1436 let TempoTxEnvelope::AA(aa_signed) = envelope else {
1437 panic!("expected AA envelope (0x76)");
1438 };
1439 aa_signed.strip_signature().key_authorization.is_some()
1440 }
1441
1442 assert!(key_auth_present(false).await, "must include witness when not provisioned");
1443 assert!(!key_auth_present(true).await, "must omit witness once provisioned");
1444 }
1445
1446 #[tokio::test]
1449 #[ignore = "integration-only: requires FOUNDRY_MPP_T5_RPC_URL + FOUNDRY_MPP_T5_PRIVATE_KEY"]
1450 async fn integration_pay_t5_precompile_endpoint() {
1451 use mpp::protocol::core::parse_www_authenticate_all;
1452
1453 let Ok(url) = std::env::var("FOUNDRY_MPP_T5_RPC_URL") else {
1454 let _ = crate::sh_eprintln!("skip: set FOUNDRY_MPP_T5_RPC_URL");
1455 return;
1456 };
1457 let Ok(key_hex) = std::env::var("FOUNDRY_MPP_T5_PRIVATE_KEY") else {
1458 let _ = crate::sh_eprintln!("skip: set FOUNDRY_MPP_T5_PRIVATE_KEY");
1459 return;
1460 };
1461
1462 let signer: mpp::PrivateKeySigner = key_hex.parse().expect("invalid private key");
1463 let deposit: u128 =
1464 std::env::var("MPP_DEPOSIT").ok().and_then(|s| s.parse().ok()).unwrap_or(100_000);
1465 let provider = SessionProvider::new(signer, url.clone()).with_default_deposit(deposit);
1466
1467 let client = reqwest::Client::builder().no_proxy().build().unwrap();
1468 let body = serde_json::json!({
1469 "jsonrpc": "2.0",
1470 "id": 1,
1471 "method": "eth_blockNumber",
1472 "params": []
1473 });
1474 let resp = client
1475 .post(&url)
1476 .header("content-type", "application/json")
1477 .body(serde_json::to_vec(&body).unwrap())
1478 .send()
1479 .await
1480 .expect("request");
1481
1482 assert_eq!(
1483 resp.status(),
1484 reqwest::StatusCode::PAYMENT_REQUIRED,
1485 "endpoint did not return 402; not MPP-gated?"
1486 );
1487 let www: Vec<_> = resp
1488 .headers()
1489 .get_all("www-authenticate")
1490 .into_iter()
1491 .filter_map(|v| v.to_str().ok())
1492 .collect();
1493 let challenge = parse_www_authenticate_all(www)
1496 .into_iter()
1497 .filter_map(|r| r.ok())
1498 .find(|c| {
1499 if c.intent.as_str() != "session"
1500 || !provider.supports(c.method.as_str(), c.intent.as_str())
1501 {
1502 return false;
1503 }
1504 let Ok(req) = c.request.decode::<mpp::protocol::intents::SessionRequest>() else {
1505 return false;
1506 };
1507 req.escrow_contract().ok().and_then(|s| s.parse::<Address>().ok())
1508 == Some(TIP20_CHANNEL_RESERVE_ADDRESS)
1509 })
1510 .expect("no T5 precompile session challenge offered by endpoint");
1511
1512 let credential = provider.pay(&challenge).await.expect("pay must succeed");
1513 let payload: mpp::protocol::methods::tempo::session::SessionCredentialPayload =
1514 credential.payload_as().expect("session payload");
1515 match payload {
1516 mpp::protocol::methods::tempo::session::SessionCredentialPayload::Open {
1517 channel_id,
1518 ..
1519 }
1520 | mpp::protocol::methods::tempo::session::SessionCredentialPayload::Voucher {
1521 channel_id,
1522 ..
1523 } => {
1524 assert!(channel_id.starts_with("0x") && channel_id.len() == 66, "{channel_id}");
1525 }
1526 other => panic!("unexpected payload variant: {other:?}"),
1527 }
1528 }
1529
1530 #[test]
1533 fn channel_key_distinguishes_legacy_and_precompile_escrow() {
1534 let origin = "https://rpc.example.com";
1535 let payer = Address::repeat_byte(0xAA);
1536 let payee = Address::repeat_byte(0x11);
1537 let currency = Address::repeat_byte(0x22);
1538 let chain_id = 4217u64;
1539
1540 let legacy_escrow: Address = "0x33b901018174DDabE4841042ab76ba85D4e24f25".parse().unwrap();
1541 let t5_precompile: Address = "0x4D50500000000000000000000000000000000000".parse().unwrap();
1542
1543 let legacy_key = SessionProvider::channel_key(
1544 origin,
1545 &payer,
1546 None,
1547 &payee,
1548 ¤cy,
1549 &legacy_escrow,
1550 chain_id,
1551 Address::ZERO,
1552 );
1553 let precompile_key = SessionProvider::channel_key(
1554 origin,
1555 &payer,
1556 None,
1557 &payee,
1558 ¤cy,
1559 &t5_precompile,
1560 chain_id,
1561 Address::ZERO,
1562 );
1563
1564 assert_ne!(legacy_key, precompile_key);
1565
1566 let precompile_op_a = SessionProvider::channel_key(
1567 origin,
1568 &payer,
1569 None,
1570 &payee,
1571 ¤cy,
1572 &t5_precompile,
1573 chain_id,
1574 Address::repeat_byte(0xAA),
1575 );
1576 let precompile_op_b = SessionProvider::channel_key(
1577 origin,
1578 &payer,
1579 None,
1580 &payee,
1581 ¤cy,
1582 &t5_precompile,
1583 chain_id,
1584 Address::repeat_byte(0xBB),
1585 );
1586 assert_ne!(precompile_op_a, precompile_op_b);
1587 assert_ne!(precompile_key, precompile_op_a);
1588 }
1589
1590 #[tokio::test]
1594 async fn test_concurrent_voucher_increments_are_unique() {
1595 let channels: Arc<Mutex<HashMap<String, ChannelEntry>>> =
1596 Arc::new(Mutex::new(HashMap::new()));
1597 let key = "test-channel".to_string();
1598 channels.lock().unwrap().insert(
1599 key.clone(),
1600 ChannelEntry {
1601 channel_id: Default::default(),
1602 salt: Default::default(),
1603 cumulative_amount: 0,
1604 escrow_contract: Address::ZERO,
1605 chain_id: 42431,
1606 opened: true,
1607 },
1608 );
1609
1610 let pay_lock = std::sync::Arc::new(tokio::sync::Mutex::new(()));
1613 let amount: u128 = 1000;
1614 let num_tasks = 20;
1615 let results: Arc<Mutex<Vec<u128>>> = Arc::new(Mutex::new(Vec::new()));
1616
1617 let mut handles = Vec::new();
1618 for _ in 0..num_tasks {
1619 let channels = channels.clone();
1620 let key = key.clone();
1621 let results = results.clone();
1622 let pay_lock = pay_lock.clone();
1623 handles.push(tokio::spawn(async move {
1624 let _guard = pay_lock.lock().await;
1625 let cumulative = {
1626 let mut ch = channels.lock().unwrap();
1627 let entry = ch.get_mut(&key).unwrap();
1628 entry.cumulative_amount += amount;
1629 entry.cumulative_amount
1630 };
1631 results.lock().unwrap().push(cumulative);
1632 }));
1633 }
1634
1635 for h in handles {
1636 h.await.unwrap();
1637 }
1638
1639 let mut amounts = results.lock().unwrap().clone();
1640 amounts.sort();
1641 amounts.dedup();
1642 assert_eq!(
1643 amounts.len(),
1644 num_tasks,
1645 "each concurrent increment should produce a unique cumulative_amount"
1646 );
1647 assert_eq!(
1648 *amounts.last().unwrap(),
1649 amount * num_tasks as u128,
1650 "final cumulative_amount should equal amount × num_tasks"
1651 );
1652 }
1653}