1use crate::{
2 cmd::{
3 keychain::ensure_tempo_precompile_active,
4 tip20::{resolve_tip20_signer, send_tip20_transaction},
5 },
6 tempo::print_payload,
7 tx::{SendTxOpts, TxParams},
8};
9use alloy_ens::NameOrAddress;
10use alloy_primitives::{Address, Bytes, U256, keccak256};
11use alloy_provider::Provider;
12use alloy_sol_types::{SolCall, SolValue};
13use clap::{Parser, Subcommand};
14use eyre::{Result, WrapErr, ensure};
15use foundry_cli::{
16 json::print_json_success,
17 opts::RpcOpts,
18 utils::{LoadConfig, get_provider},
19};
20use foundry_common::{provider::ProviderBuilder, shell};
21use foundry_evm::hardfork::TempoHardfork;
22use foundry_evm_networks::TEMPO_PRECOMPILE_ADDRESSES;
23use serde_json::{Value, json};
24use std::str::FromStr;
25use tempo_alloy::TempoNetwork;
26use tempo_contracts::precompiles::{
27 ADDRESS_REGISTRY_ADDRESS, IAddressRegistry, IReceivePolicyGuard, ITIP403Registry,
28 RECEIVE_POLICY_GUARD_ADDRESS, TIP403_REGISTRY_ADDRESS,
29};
30use tempo_primitives::TempoAddressExt;
31
32#[derive(Debug, Parser, Clone)]
34pub enum ReceivePolicySubcommand {
35 Set {
39 sender_policy_id: u64,
41
42 token_filter_id: u64,
44
45 #[arg(long, value_name = "ADDRESS", default_value_t = Address::ZERO)]
47 recovery_authority: Address,
48
49 #[arg(long, visible_alias = "dry-run")]
51 preview: bool,
52
53 #[arg(long)]
55 force: bool,
56
57 #[command(flatten)]
58 send_tx: SendTxOpts,
59
60 #[command(flatten)]
61 tx: TxParams,
62 },
63
64 Get {
66 #[arg(value_parser = NameOrAddress::from_str)]
68 account: NameOrAddress,
69
70 #[command(flatten)]
71 rpc: RpcOpts,
72 },
73
74 Validate {
76 #[arg(value_parser = NameOrAddress::from_str)]
78 token: NameOrAddress,
79
80 #[arg(value_parser = NameOrAddress::from_str)]
82 sender: NameOrAddress,
83
84 #[arg(value_parser = NameOrAddress::from_str)]
86 receiver: NameOrAddress,
87
88 #[command(flatten)]
89 rpc: RpcOpts,
90 },
91
92 Receipt {
94 #[command(subcommand)]
95 command: ReceivePolicyReceiptSubcommand,
96 },
97
98 Claim {
100 #[arg(value_parser = NameOrAddress::from_str)]
102 to: NameOrAddress,
103
104 receipt: Bytes,
106
107 #[command(flatten)]
108 send_tx: SendTxOpts,
109
110 #[command(flatten)]
111 tx: TxParams,
112 },
113}
114
115#[derive(Debug, Subcommand, Clone)]
116pub enum ReceivePolicyReceiptSubcommand {
117 Decode {
119 receipt: Bytes,
121 },
122
123 Balance {
125 receipt: Bytes,
127
128 #[command(flatten)]
129 rpc: RpcOpts,
130 },
131
132 Burn {
134 receipt: Bytes,
136
137 #[command(flatten)]
138 send_tx: Box<SendTxOpts>,
139
140 #[command(flatten)]
141 tx: Box<TxParams>,
142 },
143}
144
145impl ReceivePolicySubcommand {
146 pub async fn run(self) -> Result<()> {
147 match self {
148 Self::Set {
149 sender_policy_id,
150 token_filter_id,
151 recovery_authority,
152 preview,
153 force,
154 send_tx,
155 tx,
156 } => {
157 set(
158 sender_policy_id,
159 token_filter_id,
160 recovery_authority,
161 preview,
162 force,
163 send_tx,
164 tx,
165 )
166 .await?
167 }
168 Self::Get { account, rpc } => get(account, rpc).await?,
169 Self::Validate { token, sender, receiver, rpc } => {
170 validate(token, sender, receiver, rpc).await?
171 }
172 Self::Receipt { command } => match command {
173 ReceivePolicyReceiptSubcommand::Decode { receipt } => decode_receipt(receipt)?,
174 ReceivePolicyReceiptSubcommand::Balance { receipt, rpc } => {
175 receipt_balance(receipt, rpc).await?
176 }
177 ReceivePolicyReceiptSubcommand::Burn { receipt, send_tx, tx } => {
178 burn_receipt(receipt, *send_tx, *tx).await?
179 }
180 },
181 Self::Claim { to, receipt, send_tx, tx } => claim(to, receipt, send_tx, tx).await?,
182 }
183
184 Ok(())
185 }
186}
187
188async fn set(
189 sender_policy_id: u64,
190 token_filter_id: u64,
191 recovery_authority: Address,
192 preview: bool,
193 force: bool,
194 send_tx: SendTxOpts,
195 tx: TxParams,
196) -> Result<()> {
197 if let Some(message) = invalid_recovery_authority_message(recovery_authority) {
200 eyre::bail!("{message}");
201 }
202
203 let warning = if force {
204 None
205 } else {
206 recovery_warning(sender_policy_id, recovery_authority, &send_tx.eth.rpc).await?
207 };
208
209 let call = ITIP403Registry::setReceivePolicyCall {
210 senderPolicyId: sender_policy_id,
211 tokenFilterId: token_filter_id,
212 recoveryAuthority: recovery_authority,
213 };
214 let calldata = Bytes::from(call.abi_encode());
215
216 if preview {
217 let payload = json!({
218 "action": "set_receive_policy",
219 "registry": format!("{TIP403_REGISTRY_ADDRESS}"),
220 "sender_policy_id": sender_policy_id,
221 "token_filter_id": token_filter_id,
222 "recovery_authority": format!("{recovery_authority}"),
223 "recovery_mode": recovery_mode(recovery_authority),
224 "calldata": format!("{calldata}"),
225 "warning": warning,
226 });
227 if shell::is_json() {
228 print_json_success(payload)?;
229 } else {
230 sh_println!(
231 "Registry: {TIP403_REGISTRY_ADDRESS}\n\
232 Sender policy ID: {sender_policy_id}\n\
233 Token filter ID: {token_filter_id}\n\
234 Recovery authority: {recovery_authority}\n\
235 Recovery mode: {}\n\
236 Calldata: {calldata}",
237 recovery_mode(recovery_authority)
238 )?;
239 if let Some(warning) = warning.as_deref() {
240 sh_warn!("{warning}")?;
241 }
242 }
243 return Ok(());
244 }
245
246 if let Some(warning) = warning.as_deref() {
247 sh_warn!("{warning}")?;
248 }
249
250 let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
251 send_tip20_transaction(
252 NameOrAddress::Address(TIP403_REGISTRY_ADDRESS),
253 "setReceivePolicy(uint64,uint64,address)",
254 vec![
255 sender_policy_id.to_string(),
256 token_filter_id.to_string(),
257 recovery_authority.to_string(),
258 ],
259 send_tx,
260 tx,
261 signer,
262 access_key,
263 )
264 .await
265}
266
267async fn get(account: NameOrAddress, rpc: RpcOpts) -> Result<()> {
268 let config = rpc.load_config()?;
269 let provider = get_provider(&config)?;
270 let account = account.resolve(&provider).await?;
271 let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
272 let policy = registry.receivePolicy(account).call().await?;
273
274 let payload = json!({
275 "account": format!("{account}"),
276 "has_receive_policy": policy.hasReceivePolicy,
277 "sender_policy_id": policy.senderPolicyId,
278 "sender_policy_type": policy_type(policy.senderPolicyType),
279 "token_filter_id": policy.tokenFilterId,
280 "token_filter_type": policy_type(policy.tokenFilterType),
281 "recovery_authority": format!("{}", policy.recoveryAuthority),
282 "recovery_mode": recovery_mode(policy.recoveryAuthority),
283 });
284 print_payload(payload, |payload| {
285 sh_println!(
286 "Account: {}\n\
287 Has receive policy: {}\n\
288 Sender policy ID: {}\n\
289 Sender policy type: {}\n\
290 Token filter ID: {}\n\
291 Token filter type: {}\n\
292 Recovery authority: {}\n\
293 Recovery mode: {}",
294 payload["account"].as_str().unwrap_or_default(),
295 payload["has_receive_policy"].as_bool().unwrap_or_default(),
296 payload["sender_policy_id"],
297 payload["sender_policy_type"].as_str().unwrap_or_default(),
298 payload["token_filter_id"],
299 payload["token_filter_type"].as_str().unwrap_or_default(),
300 payload["recovery_authority"].as_str().unwrap_or_default(),
301 payload["recovery_mode"].as_str().unwrap_or_default(),
302 )
303 })
304}
305
306async fn validate(
307 token: NameOrAddress,
308 sender: NameOrAddress,
309 receiver: NameOrAddress,
310 rpc: RpcOpts,
311) -> Result<()> {
312 let config = rpc.load_config()?;
313 let provider = get_provider(&config)?;
314 let token = token.resolve(&provider).await?;
315 let sender = sender.resolve(&provider).await?;
316 let receiver = receiver.resolve(&provider).await?;
317 let effective_receiver = IAddressRegistry::new(ADDRESS_REGISTRY_ADDRESS, &provider)
318 .resolveRecipient(receiver)
319 .call()
320 .await?;
321 let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
322 let result = registry.validateReceivePolicy(token, sender, effective_receiver).call().await?;
323 let delivery_state = if result.authorized { "credited" } else { "held" };
324
325 let payload = validate_payload(
326 token,
327 sender,
328 receiver,
329 effective_receiver,
330 result.authorized,
331 result.blockedReason,
332 delivery_state,
333 );
334 print_payload(payload, |payload| {
335 sh_println!(
336 "Token: {}\n\
337 Sender: {}\n\
338 Receiver: {}\n\
339 Effective recv: {}\n\
340 Authorized: {}\n\
341 Blocked reason: {}\n\
342 Delivery state: {}",
343 payload["token"].as_str().unwrap_or_default(),
344 payload["sender"].as_str().unwrap_or_default(),
345 payload["receiver"].as_str().unwrap_or_default(),
346 payload["effective_receiver"].as_str().unwrap_or_default(),
347 payload["authorized"].as_bool().unwrap_or_default(),
348 payload["blocked_reason"].as_str().unwrap_or_default(),
349 payload["delivery_state"].as_str().unwrap_or_default(),
350 )
351 })
352}
353
354fn validate_payload(
355 token: Address,
356 sender: Address,
357 receiver: Address,
358 effective_receiver: Address,
359 authorized: bool,
360 blocked_reason_value: ITIP403Registry::BlockedReason,
361 delivery_state: &str,
362) -> Value {
363 json!({
364 "token": format!("{token}"),
365 "sender": format!("{sender}"),
366 "receiver": format!("{receiver}"),
367 "effective_receiver": format!("{effective_receiver}"),
368 "receiver_was_resolved": receiver != effective_receiver,
369 "authorized": authorized,
370 "blocked_reason": blocked_reason(blocked_reason_value),
371 "delivery_state": delivery_state,
372 })
373}
374
375fn decode_receipt(receipt: Bytes) -> Result<()> {
376 let decoded = decode_claim_receipt(&receipt)?;
377 let payload = receipt_payload(&receipt, &decoded, None);
378 print_payload(payload, |payload| {
379 print_decoded_receipt(payload)?;
380 print_claim_hint(payload)
381 })
382}
383
384async fn receipt_balance(receipt: Bytes, rpc: RpcOpts) -> Result<()> {
385 let config = rpc.load_config()?;
386 let provider = get_provider(&config)?;
387 let guard = IReceivePolicyGuard::new(RECEIVE_POLICY_GUARD_ADDRESS, provider);
388 let amount = guard.balanceOf(receipt.clone()).call().await?;
389 let decoded = decode_claim_receipt(&receipt)?;
390 let payload = receipt_payload(&receipt, &decoded, Some(amount));
391 print_payload(payload, |payload| {
392 print_decoded_receipt(payload)?;
393 sh_println!("Held balance: {}", payload["held_balance"].as_str().unwrap_or_default())
394 })
395}
396
397async fn ensure_receive_policy_t6<P>(provider: &P, command: &str) -> Result<()>
400where
401 P: Provider<TempoNetwork>,
402{
403 ensure_tempo_precompile_active(
404 provider,
405 TempoHardfork::T6,
406 RECEIVE_POLICY_GUARD_ADDRESS,
407 &format!("{command} requires a Tempo T6-capable ReceivePolicy RPC"),
408 )
409 .await
410}
411
412async fn burn_receipt(receipt: Bytes, send_tx: SendTxOpts, tx: TxParams) -> Result<()> {
413 decode_claim_receipt(&receipt)?;
414 let config = send_tx.eth.rpc.load_config()?;
415 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
416 ensure_receive_policy_t6(&provider, "cast receive-policy receipt burn").await?;
417 let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
418 send_tip20_transaction(
419 NameOrAddress::Address(RECEIVE_POLICY_GUARD_ADDRESS),
420 "burnBlockedReceipt(bytes)",
421 vec![format!("{receipt}")],
422 send_tx,
423 tx,
424 signer,
425 access_key,
426 )
427 .await
428}
429
430async fn claim(to: NameOrAddress, receipt: Bytes, send_tx: SendTxOpts, tx: TxParams) -> Result<()> {
431 decode_claim_receipt(&receipt)?;
432 let config = send_tx.eth.rpc.load_config()?;
433 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
434 ensure_receive_policy_t6(&provider, "cast receive-policy claim").await?;
435 let to = to.resolve(&provider).await?;
436 let (signer, access_key) = resolve_tip20_signer(&send_tx, &tx).await?;
437 send_tip20_transaction(
438 NameOrAddress::Address(RECEIVE_POLICY_GUARD_ADDRESS),
439 "claim(address,bytes)",
440 vec![to.to_string(), format!("{receipt}")],
441 send_tx,
442 tx,
443 signer,
444 access_key,
445 )
446 .await
447}
448
449async fn recovery_warning(
450 sender_policy_id: u64,
451 recovery_authority: Address,
452 rpc: &RpcOpts,
453) -> Result<Option<String>> {
454 if recovery_authority != Address::ZERO {
455 return Ok(None);
456 }
457
458 let config = rpc.load_config()?;
459 let provider = get_provider(&config)?;
460 let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, provider);
461 let mut blocked = Vec::new();
462 for address in TEMPO_PRECOMPILE_ADDRESSES {
463 if !registry.isAuthorizedSender(sender_policy_id, *address).call().await.unwrap_or(true) {
464 blocked.push(*address);
465 }
466 }
467
468 if blocked.is_empty() {
469 return Ok(None);
470 }
471
472 Ok(Some(format!(
473 "originator recovery is enabled because recovery authority is 0x0, but sender policy \
474 {sender_policy_id} blocks {} Tempo system/precompile sender(s): {}. Receipts created \
475 for those senders may not be claimable by a user. Choose receiver or third-party \
476 recovery authority when blocking system senders, or pass --force if this is intentional.",
477 blocked.len(),
478 blocked.iter().map(Address::to_string).collect::<Vec<_>>().join(", ")
479 )))
480}
481
482fn invalid_recovery_authority_message(recovery_authority: Address) -> Option<String> {
491 if recovery_authority == Address::ZERO {
492 return None;
493 }
494 if recovery_authority.is_virtual() {
495 return Some("recovery authority cannot be a TIP-1022 virtual address".to_string());
496 }
497 if recovery_authority.is_tip20() {
498 return Some("recovery authority cannot be a TIP-20 token address".to_string());
499 }
500 if TEMPO_PRECOMPILE_ADDRESSES.contains(&recovery_authority) {
501 return Some(format!(
502 "recovery authority cannot be a fixed Tempo system precompile: {recovery_authority}"
503 ));
504 }
505 None
506}
507
508fn decode_claim_receipt(receipt: &Bytes) -> Result<IReceivePolicyGuard::ClaimReceiptV1> {
509 let decoded = IReceivePolicyGuard::ClaimReceiptV1::abi_decode(receipt)
510 .wrap_err("invalid ReceivePolicyGuard claim receipt")?;
511
512 ensure!(
513 decoded.version == 1,
514 "unsupported ReceivePolicyGuard claim receipt version {}",
515 decoded.version
516 );
517 ensure!(decoded.token != Address::ZERO, "ReceivePolicyGuard claim receipt token is zero");
518 ensure!(
519 decoded.recipient != RECEIVE_POLICY_GUARD_ADDRESS,
520 "ReceivePolicyGuard claim receipt recipient cannot be the guard precompile"
521 );
522 ensure!(
523 matches!(
524 decoded.blockedReason,
525 reason if reason == ITIP403Registry::BlockedReason::TOKEN_FILTER as u8 ||
526 reason == ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8
527 ),
528 "ReceivePolicyGuard claim receipt blocked reason is not claimable"
529 );
530 ensure!(
531 matches!(
532 decoded.kind,
533 IReceivePolicyGuard::InboundKind::TRANSFER | IReceivePolicyGuard::InboundKind::MINT
534 ),
535 "ReceivePolicyGuard claim receipt inbound kind is unknown"
536 );
537
538 Ok(decoded)
539}
540
541fn receipt_payload(
542 receipt: &Bytes,
543 decoded: &IReceivePolicyGuard::ClaimReceiptV1,
544 amount: Option<U256>,
545) -> Value {
546 let receipt_key = keccak256(receipt);
547 let delivery_state = match amount {
548 Some(amount) if amount > U256::ZERO => "held",
549 Some(_) => "not_held",
550 None => "unknown",
551 };
552 let mut payload = json!({
553 "receipt": format!("{receipt}"),
554 "receipt_key": format!("{receipt_key}"),
555 "version": decoded.version,
556 "token": format!("{}", decoded.token),
557 "recovery_authority": format!("{}", decoded.recoveryAuthority),
558 "recovery_mode": recovery_mode(decoded.recoveryAuthority),
559 "originator": format!("{}", decoded.originator),
560 "recipient": format!("{}", decoded.recipient),
561 "recipient_is_virtual": decoded.recipient.is_virtual(),
562 "claim_target": if decoded.recipient.is_virtual() || decoded.recoveryAuthority == Address::ZERO {
563 Value::Null
564 } else {
565 json!(format!("{}", decoded.recipient))
566 },
567 "blocked_at": decoded.blockedAt,
568 "blocked_nonce": decoded.blockedNonce,
569 "blocked_reason": blocked_reason_u8(decoded.blockedReason),
570 "kind": inbound_kind(decoded.kind),
571 "memo": format!("{}", decoded.memo),
572 "delivery_state": delivery_state,
573 });
574 if let Some(amount) = amount {
575 payload["held_balance"] = json!(amount.to_string());
576 }
577 payload
578}
579
580fn print_decoded_receipt(payload: &Value) -> Result<()> {
581 sh_println!(
582 "Receipt key: {}\n\
583 Token: {}\n\
584 Recovery authority: {}\n\
585 Recovery mode: {}\n\
586 Originator: {}\n\
587 Recipient: {}\n\
588 Blocked at: {}\n\
589 Blocked nonce: {}\n\
590 Blocked reason: {}\n\
591 Kind: {}\n\
592 Memo: {}\n\
593 Delivery state: {}",
594 payload["receipt_key"].as_str().unwrap_or_default(),
595 payload["token"].as_str().unwrap_or_default(),
596 payload["recovery_authority"].as_str().unwrap_or_default(),
597 payload["recovery_mode"].as_str().unwrap_or_default(),
598 payload["originator"].as_str().unwrap_or_default(),
599 payload["recipient"].as_str().unwrap_or_default(),
600 payload["blocked_at"],
601 payload["blocked_nonce"],
602 payload["blocked_reason"].as_str().unwrap_or_default(),
603 payload["kind"].as_str().unwrap_or_default(),
604 payload["memo"].as_str().unwrap_or_default(),
605 payload["delivery_state"].as_str().unwrap_or_default(),
606 )
607}
608
609fn print_claim_hint(payload: &Value) -> Result<()> {
610 let recipient = payload["recipient"].as_str().unwrap_or_default();
611 let receipt = payload["receipt"].as_str().unwrap_or_default();
612 if payload["recovery_mode"].as_str() == Some("originator") {
613 sh_println!(
614 "\nClaim target: originator recovery reroutes funds, so do not default to the blocked recipient. Claim to an address that can receive the token:\n cast receive-policy claim <target-address> {receipt}"
615 )
616 } else if payload["recipient_is_virtual"].as_bool().unwrap_or_default() {
617 sh_println!(
618 "\nClaim target: recipient is a virtual address; resolve it first with:\n cast vaddr resolve {recipient}\nThen claim to the registered master address:\n cast receive-policy claim <master-address> {receipt}"
619 )
620 } else {
621 sh_println!("\nClaim path: cast receive-policy claim {recipient} {receipt}")
622 }
623}
624
625fn recovery_mode(recovery_authority: Address) -> &'static str {
626 if recovery_authority == Address::ZERO { "originator" } else { "authority" }
627}
628
629const fn policy_type(policy_type: ITIP403Registry::PolicyType) -> &'static str {
630 match policy_type {
631 ITIP403Registry::PolicyType::WHITELIST => "whitelist",
632 ITIP403Registry::PolicyType::BLACKLIST => "blacklist",
633 ITIP403Registry::PolicyType::COMPOUND => "compound",
634 _ => "unknown",
635 }
636}
637
638const fn blocked_reason(reason: ITIP403Registry::BlockedReason) -> &'static str {
639 match reason {
640 ITIP403Registry::BlockedReason::NONE => "none",
641 ITIP403Registry::BlockedReason::TOKEN_FILTER => "token_filter",
642 ITIP403Registry::BlockedReason::RECEIVE_POLICY => "receive_policy",
643 _ => "unknown",
644 }
645}
646
647const fn blocked_reason_u8(reason: u8) -> &'static str {
648 match reason {
649 0 => "none",
650 1 => "token_filter",
651 2 => "receive_policy",
652 _ => "unknown",
653 }
654}
655
656const fn inbound_kind(kind: IReceivePolicyGuard::InboundKind) -> &'static str {
657 match kind {
658 IReceivePolicyGuard::InboundKind::TRANSFER => "transfer",
659 IReceivePolicyGuard::InboundKind::MINT => "mint",
660 _ => "unknown",
661 }
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use alloy_primitives::{address, b256};
668 use tempo_primitives::{MasterId, UserTag};
669
670 fn sample_receipt() -> Bytes {
671 IReceivePolicyGuard::ClaimReceiptV1::new(
672 address!("0000000000000000000000000000000000000010"),
673 address!("0000000000000000000000000000000000000020"),
674 address!("0000000000000000000000000000000000000030"),
675 address!("0000000000000000000000000000000000000040"),
676 1_780_000_000,
677 7,
678 ITIP403Registry::BlockedReason::RECEIVE_POLICY as u8,
679 IReceivePolicyGuard::InboundKind::TRANSFER,
680 b256!("0000000000000000000000000000000000000000000000000000000000000042"),
681 )
682 .abi_encode()
683 .into()
684 }
685
686 #[test]
687 fn decodes_guard_claim_receipt() {
688 let receipt = sample_receipt();
689 let decoded = decode_claim_receipt(&receipt).unwrap();
690 assert_eq!(decoded.version, 1);
691 assert_eq!(decoded.token, address!("0000000000000000000000000000000000000010"));
692 assert_eq!(decoded.recoveryAuthority, address!("0000000000000000000000000000000000000020"));
693 assert_eq!(decoded.originator, address!("0000000000000000000000000000000000000030"));
694 assert_eq!(decoded.recipient, address!("0000000000000000000000000000000000000040"));
695 assert_eq!(decoded.blockedNonce, 7);
696 assert_eq!(decoded.kind, IReceivePolicyGuard::InboundKind::TRANSFER);
697 }
698
699 #[test]
700 fn rejects_invalid_guard_claim_receipt() {
701 let err = decode_claim_receipt(&Bytes::from_static(&[0xde, 0xad])).unwrap_err();
702 assert!(err.to_string().contains("invalid ReceivePolicyGuard claim receipt"));
703 }
704
705 #[test]
706 fn rejects_semantically_invalid_guard_claim_receipts() {
707 let receipt = sample_receipt();
708 let decoded = IReceivePolicyGuard::ClaimReceiptV1::abi_decode(&receipt).unwrap();
709
710 let mut bad_version = decoded.clone();
711 bad_version.version = 2;
712 let err = decode_claim_receipt(&bad_version.abi_encode().into()).unwrap_err();
713 assert!(err.to_string().contains("unsupported ReceivePolicyGuard claim receipt version"));
714
715 let mut bad_token = decoded.clone();
716 bad_token.token = Address::ZERO;
717 let err = decode_claim_receipt(&bad_token.abi_encode().into()).unwrap_err();
718 assert!(err.to_string().contains("token is zero"));
719
720 let mut bad_recipient = decoded.clone();
721 bad_recipient.recipient = RECEIVE_POLICY_GUARD_ADDRESS;
722 let err = decode_claim_receipt(&bad_recipient.abi_encode().into()).unwrap_err();
723 assert!(err.to_string().contains("recipient cannot be the guard precompile"));
724
725 let mut bad_reason = decoded;
726 bad_reason.blockedReason = ITIP403Registry::BlockedReason::NONE as u8;
727 let err = decode_claim_receipt(&bad_reason.abi_encode().into()).unwrap_err();
728 assert!(err.to_string().contains("blocked reason is not claimable"));
729 }
730
731 #[test]
732 fn validate_payload_records_effective_receiver() {
733 let receiver = Address::new_virtual(
734 MasterId::from([0x12, 0x34, 0x56, 0x78]),
735 UserTag::from([0xab, 0xcd, 0xef, 0x01, 0x23, 0x45]),
736 );
737 let effective_receiver = address!("0000000000000000000000000000000000000040");
738
739 let payload = validate_payload(
740 address!("0000000000000000000000000000000000000010"),
741 address!("0000000000000000000000000000000000000030"),
742 receiver,
743 effective_receiver,
744 false,
745 ITIP403Registry::BlockedReason::RECEIVE_POLICY,
746 "held",
747 );
748
749 assert_eq!(payload["receiver"], format!("{receiver}"));
750 assert_eq!(payload["effective_receiver"], format!("{effective_receiver}"));
751 assert_eq!(payload["receiver_was_resolved"], true);
752 assert_eq!(payload["authorized"], false);
753 assert_eq!(payload["blocked_reason"], "receive_policy");
754 assert_eq!(payload["delivery_state"], "held");
755 }
756
757 #[test]
758 fn receipt_payload_preserves_delivery_state_confidence() {
759 let receipt = sample_receipt();
760 let decoded = decode_claim_receipt(&receipt).unwrap();
761
762 let unknown = receipt_payload(&receipt, &decoded, None);
763 assert_eq!(unknown["delivery_state"], "unknown");
764
765 let held = receipt_payload(&receipt, &decoded, Some(U256::from(1)));
766 assert_eq!(held["delivery_state"], "held");
767 assert_eq!(held["blocked_reason"], "receive_policy");
768 assert_eq!(held["kind"], "transfer");
769 assert_eq!(held["held_balance"], "1");
770 assert_eq!(held["recipient_is_virtual"], false);
771 assert_eq!(held["claim_target"], format!("{}", decoded.recipient));
772
773 let not_held = receipt_payload(&receipt, &decoded, Some(U256::ZERO));
774 assert_eq!(not_held["delivery_state"], "not_held");
775 assert_eq!(not_held["held_balance"], "0");
776 }
777
778 #[test]
779 fn virtual_receipt_recipient_requires_resolved_claim_target() {
780 let receipt = sample_receipt();
781 let mut decoded = decode_claim_receipt(&receipt).unwrap();
782 decoded.recipient = Address::new_virtual(
783 MasterId::from([0x12, 0x34, 0x56, 0x78]),
784 UserTag::from([0xab, 0xcd, 0xef, 0x01, 0x23, 0x45]),
785 );
786
787 let payload = receipt_payload(&receipt, &decoded, None);
788 assert_eq!(payload["recipient"], format!("{}", decoded.recipient));
789 assert_eq!(payload["recipient_is_virtual"], true);
790 assert_eq!(payload["claim_target"], Value::Null);
791 }
792
793 #[test]
794 fn originator_recovery_receipt_requires_explicit_claim_target() {
795 let receipt = sample_receipt();
796 let mut decoded = decode_claim_receipt(&receipt).unwrap();
797 decoded.recoveryAuthority = Address::ZERO;
798
799 let payload = receipt_payload(&receipt, &decoded, None);
800 assert_eq!(payload["recovery_mode"], "originator");
801 assert_eq!(payload["recipient_is_virtual"], false);
802 assert_eq!(payload["claim_target"], Value::Null);
803 }
804
805 #[test]
806 fn originator_recovery_takes_precedence_over_virtual_recipient() {
807 let receipt = sample_receipt();
808 let mut decoded = decode_claim_receipt(&receipt).unwrap();
809 decoded.recoveryAuthority = Address::ZERO;
810 decoded.recipient = Address::new_virtual(
811 MasterId::from([0x12, 0x34, 0x56, 0x78]),
812 UserTag::from([0xab, 0xcd, 0xef, 0x01, 0x23, 0x45]),
813 );
814
815 let payload = receipt_payload(&receipt, &decoded, None);
816 assert_eq!(payload["recovery_mode"], "originator");
817 assert_eq!(payload["recipient_is_virtual"], true);
818 assert_eq!(payload["claim_target"], Value::Null);
819 }
820
821 #[test]
822 fn rejects_unclaimable_recovery_authorities() {
823 assert_eq!(invalid_recovery_authority_message(Address::ZERO), None);
825 assert_eq!(
826 invalid_recovery_authority_message(address!(
827 "1111111111111111111111111111111111111111"
828 )),
829 None
830 );
831
832 for authority in TEMPO_PRECOMPILE_ADDRESSES {
834 let err = invalid_recovery_authority_message(*authority).unwrap();
835 assert!(err.contains("fixed Tempo system precompile"));
836 }
837
838 let err = invalid_recovery_authority_message(address!(
840 "20c0000000000000000000000000000000000001"
841 ))
842 .unwrap();
843 assert!(err.contains("TIP-20 token address"));
844
845 let virtual_address = Address::new_virtual(
846 MasterId::from([0x12, 0x34, 0x56, 0x78]),
847 UserTag::from([0xab, 0xcd, 0xef, 0x01, 0x23, 0x45]),
848 );
849 let err = invalid_recovery_authority_message(virtual_address).unwrap();
850 assert!(err.contains("TIP-1022 virtual address"));
851 }
852
853 #[test]
854 fn preview_calldata_uses_set_receive_policy_selector() {
855 let call = ITIP403Registry::setReceivePolicyCall {
856 senderPolicyId: 0,
857 tokenFilterId: 1,
858 recoveryAuthority: Address::ZERO,
859 };
860 let calldata = call.abi_encode();
861 assert_eq!(&calldata[..4], ITIP403Registry::setReceivePolicyCall::SELECTOR);
862 }
863}