Skip to main content

stateset_core/models/
x402.rs

1//! x402 Protocol Payment Types
2//!
3//! Implementation of the x402 HTTP-native payment protocol for AI agents.
4//! Enables off-chain payment signing with on-chain settlement via Set Chain L2.
5//!
6//! ## x402 Protocol Overview
7//!
8//! The x402 protocol uses HTTP 402 (Payment Required) status codes to enable
9//! instant stablecoin micropayments. When a server requires payment, it returns
10//! a 402 response with payment details. The client signs a payment intent off-chain
11//! and includes it in the retry request.
12//!
13//! ## Flow
14//!
15//! 1. Client requests resource
16//! 2. Server returns HTTP 402 with `X402PaymentRequired` header
17//! 3. Client creates `X402PaymentIntent`, signs it with the intent's configured scheme
18//! 4. Client syncs intent to sequencer for batching
19//! 5. Batched payments are settled on Set Chain L2
20//! 6. Server verifies payment via inclusion proof
21//!
22//! ## Example
23//!
24//! ```rust
25//! use stateset_core::models::x402::{
26//!     X402PaymentIntent, X402Network, X402Asset, X402_DEFAULT_SIGNATURE_SCHEME,
27//! };
28//!
29//! let intent = X402PaymentIntent::new(
30//!     "0x1234abcd1234abcd1234abcd1234abcd1234abcd",
31//!     "0x5678efab5678efab5678efab5678efab5678efab",
32//!     1_000_000, // 1 USDC (6 decimals)
33//!     X402Asset::Usdc,
34//!     X402Network::SetChain,
35//! );
36//! assert_eq!(intent.amount, 1_000_000);
37//! assert_eq!(intent.signature_scheme(), X402_DEFAULT_SIGNATURE_SCHEME);
38//! ```
39
40use chrono::{DateTime, Utc};
41use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
42use rs_merkle::{MerkleProof, algorithms::Sha256 as MerkleSha256};
43use rust_decimal::Decimal;
44use rust_decimal::prelude::ToPrimitive;
45use serde::{Deserialize, Serialize};
46use sha2::{Digest, Sha256};
47use stateset_crypto::pqc::{
48    HybridSignatureBundle as PqcHybridSignatureBundle, HybridSigningKeypair,
49    HybridSigningPublicKey, StrictSigningKeypair, StrictSigningPublicKey, hybrid_sign_event_hash,
50    hybrid_verify_event_signature, strict_sign_event_hash, strict_verify_event_signature,
51};
52use strum::{Display, EnumString};
53use thiserror::Error;
54use uuid::Uuid;
55
56// =============================================================================
57// x402 Protocol Constants
58// =============================================================================
59
60/// x402 protocol version
61pub const X402_VERSION: &str = "1.0";
62
63/// Domain separator for x402 payment signing (per EIP-712 style)
64pub const X402_DOMAIN_SEPARATOR: &str = "X402_PAYMENT_V1";
65
66/// Maximum payment validity window (24 hours in seconds)
67pub const X402_MAX_VALIDITY_SECONDS: u64 = 86400;
68
69/// Default payment validity window (1 hour in seconds)
70pub const X402_DEFAULT_VALIDITY_SECONDS: u64 = 3600;
71
72/// Default signature scheme for newly created x402 payment intents.
73pub const X402_DEFAULT_SIGNATURE_SCHEME: X402SignatureScheme = X402SignatureScheme::Ed25519MlDsa65;
74
75// =============================================================================
76// x402 Network & Asset Types
77// =============================================================================
78
79/// Supported blockchain networks for x402 payments
80#[derive(
81    Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize, Deserialize, Default,
82)]
83#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
84#[serde(rename_all = "snake_case")]
85#[non_exhaustive]
86pub enum X402Network {
87    /// Set Chain L2 (StateSet native) - primary network
88    #[default]
89    #[strum(serialize = "set_chain", serialize = "set", serialize = "ssc")]
90    SetChain,
91    /// Set Chain testnet
92    #[strum(serialize = "set_chain_testnet", serialize = "set_testnet")]
93    SetChainTestnet,
94    /// Base L2 (Coinbase)
95    Base,
96    /// Arc L2 (Circle stablecoin-native)
97    Arc,
98    /// Arc testnet
99    #[strum(serialize = "arc_testnet", serialize = "arc-testnet")]
100    ArcTestnet,
101    /// Base Sepolia testnet
102    BaseSepolia,
103    /// Ethereum mainnet
104    #[strum(serialize = "ethereum", serialize = "eth", serialize = "mainnet")]
105    Ethereum,
106    /// Ethereum Sepolia testnet
107    #[strum(serialize = "ethereum_sepolia", serialize = "sepolia")]
108    EthereumSepolia,
109    /// Arbitrum One
110    #[strum(serialize = "arbitrum", serialize = "arb")]
111    Arbitrum,
112    /// Optimism
113    #[strum(serialize = "optimism", serialize = "op")]
114    Optimism,
115}
116
117impl X402Network {
118    /// Get the chain ID for this network
119    #[must_use]
120    pub const fn chain_id(&self) -> u64 {
121        match self {
122            Self::SetChain => 84532001,        // Set Chain mainnet
123            Self::SetChainTestnet => 84532002, // Set Chain testnet
124            Self::Arc => 5042001,              // Arc mainnet
125            Self::ArcTestnet => 5042002,       // Arc testnet
126            Self::Base => 8453,
127            Self::BaseSepolia => 84532,
128            Self::Ethereum => 1,
129            Self::EthereumSepolia => 11155111,
130            Self::Arbitrum => 42161,
131            Self::Optimism => 10,
132        }
133    }
134
135    /// Check if this is a testnet
136    #[must_use]
137    pub const fn is_testnet(&self) -> bool {
138        matches!(
139            self,
140            Self::SetChainTestnet | Self::ArcTestnet | Self::BaseSepolia | Self::EthereumSepolia
141        )
142    }
143}
144
145/// Supported payment assets for x402
146#[derive(
147    Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize, Deserialize, Default,
148)]
149#[strum(ascii_case_insensitive)]
150#[serde(rename_all = "snake_case")]
151#[non_exhaustive]
152pub enum X402Asset {
153    /// USD Coin (USDC) - primary stablecoin
154    #[default]
155    #[strum(serialize = "USDC")]
156    Usdc,
157    /// Tether (USDT)
158    #[strum(serialize = "USDT", serialize = "TETHER")]
159    Usdt,
160    /// StateSet USD (ssUSD) - yield-bearing stablecoin
161    #[serde(rename = "ssusd", alias = "ss_usd")]
162    #[strum(serialize = "ssUSD", serialize = "SSUSD", serialize = "SS_USD")]
163    SsUsd,
164    /// Wrapped StateSet USD (ERC-4626)
165    #[serde(rename = "wssusd", alias = "wss_usd")]
166    #[strum(serialize = "wssUSD", serialize = "WSSUSD", serialize = "WSS_USD")]
167    WssUsd,
168    /// DAI stablecoin
169    #[strum(serialize = "DAI")]
170    Dai,
171    /// Native ETH (for gas)
172    #[strum(serialize = "ETH", serialize = "ETHER")]
173    Eth,
174}
175
176impl X402Asset {
177    /// Get the number of decimals for this asset
178    #[must_use]
179    pub const fn decimals(&self) -> u8 {
180        match self {
181            Self::Usdc | Self::Usdt | Self::SsUsd | Self::WssUsd => 6,
182            Self::Dai | Self::Eth => 18,
183        }
184    }
185
186    /// Get the token contract address for a given network
187    #[must_use]
188    pub const fn contract_address(&self, network: X402Network) -> Option<&'static str> {
189        match (self, network) {
190            // Set Chain addresses
191            (Self::Usdc, X402Network::SetChain) => {
192                Some("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
193            }
194            (Self::SsUsd, X402Network::SetChain) => {
195                Some("0x0000000000000000000000000000000000001001")
196            }
197            // Arc addresses
198            (Self::Usdc, X402Network::Arc | X402Network::ArcTestnet) => {
199                Some("0x3600000000000000000000000000000000000000")
200            }
201            // Base addresses
202            (Self::Usdc, X402Network::Base) => Some("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"),
203            // Ethereum addresses
204            (Self::Usdc, X402Network::Ethereum) => {
205                Some("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
206            }
207            (Self::Usdt, X402Network::Ethereum) => {
208                Some("0xdAC17F958D2ee523a2206206994597C13D831ec7")
209            }
210            (Self::Dai, X402Network::Ethereum) => Some("0x6B175474E89094C44Da98b954Ee4606eB48"),
211            // Native ETH has no contract
212            (Self::Eth, _) => None,
213            _ => None,
214        }
215    }
216}
217
218// =============================================================================
219// x402 Payment Intent (Off-Chain Signed Payment Request)
220// =============================================================================
221
222/// Status of an x402 payment intent
223#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
224#[strum(serialize_all = "snake_case")]
225#[serde(rename_all = "snake_case")]
226#[non_exhaustive]
227pub enum X402IntentStatus {
228    /// Intent created, not yet signed
229    #[default]
230    Created,
231    /// Intent signed by payer
232    Signed,
233    /// Intent submitted to sequencer
234    Sequenced,
235    /// Intent included in batch commitment
236    Batched,
237    /// Intent settled on-chain
238    Settled,
239    /// Intent expired (validity window passed)
240    Expired,
241    /// Intent failed to settle
242    Failed,
243    /// Intent cancelled by payer
244    Cancelled,
245}
246
247impl std::str::FromStr for X402IntentStatus {
248    type Err = String;
249
250    fn from_str(s: &str) -> Result<Self, Self::Err> {
251        match s.to_lowercase().as_str() {
252            "created" => Ok(Self::Created),
253            "signed" => Ok(Self::Signed),
254            "sequenced" => Ok(Self::Sequenced),
255            "batched" => Ok(Self::Batched),
256            "settled" => Ok(Self::Settled),
257            "expired" => Ok(Self::Expired),
258            "failed" => Ok(Self::Failed),
259            "cancelled" | "canceled" => Ok(Self::Cancelled),
260            _ => Err(format!("Unknown x402 intent status: {s}")),
261        }
262    }
263}
264
265/// Direction of x402 credit ledger entries
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize, Deserialize)]
267#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
268#[serde(rename_all = "snake_case")]
269#[non_exhaustive]
270pub enum X402CreditDirection {
271    #[strum(serialize = "credit", serialize = "cr")]
272    Credit,
273    #[strum(serialize = "debit", serialize = "dr")]
274    Debit,
275}
276
277/// Supported x402 signature schemes for off-chain payment intents.
278#[derive(
279    Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString, Serialize, Deserialize, Default,
280)]
281#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
282#[serde(rename_all = "snake_case")]
283#[non_exhaustive]
284pub enum X402SignatureScheme {
285    /// Legacy Ed25519 only.
286    #[default]
287    Ed25519,
288    /// PQC-strict ML-DSA-65 only.
289    MlDsa65,
290    /// Hybrid Ed25519 + ML-DSA-65.
291    Ed25519MlDsa65,
292}
293
294/// Additional PQC signature material for hybrid and strict x402 signatures.
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296pub struct X402SignatureBundle {
297    /// ML-DSA-65 signature bytes.
298    #[serde(with = "hex")]
299    pub ml_dsa_65_signature: Vec<u8>,
300}
301
302/// Additional PQC public-key material for hybrid and strict x402 signatures.
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304pub struct X402PublicKeyBundle {
305    /// ML-DSA-65 public key bytes.
306    #[serde(with = "hex")]
307    pub ml_dsa_65_public_key: Vec<u8>,
308}
309
310/// x402 Payment Intent - A signed off-chain payment request
311///
312/// This is the core data structure for x402 payments. It contains all the
313/// information needed to authorize a payment, signed by the payer's key.
314/// Intents are batched by the sequencer and settled on Set Chain L2.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct X402PaymentIntent {
317    /// Unique intent ID (UUID v4)
318    pub id: Uuid,
319
320    /// x402 protocol version
321    pub version: String,
322
323    /// Current status
324    pub status: X402IntentStatus,
325
326    // =========================================================================
327    // Payment Parameters (signed fields)
328    // =========================================================================
329    /// Payer wallet address (sender)
330    pub payer_address: String,
331
332    /// Payee wallet address (recipient)
333    pub payee_address: String,
334
335    /// Payment amount in smallest unit (e.g., 1000000 = 1 USDC)
336    pub amount: u64,
337
338    /// Human-readable amount for display
339    pub amount_decimal: Decimal,
340
341    /// Payment asset (USDC, ssUSD, etc.)
342    pub asset: X402Asset,
343
344    /// Target blockchain network
345    pub network: X402Network,
346
347    /// Chain ID for EIP-712 domain
348    pub chain_id: u64,
349
350    /// Token contract address (None for native ETH)
351    pub token_address: Option<String>,
352
353    // =========================================================================
354    // Validity & Replay Protection
355    // =========================================================================
356    /// Unix timestamp when intent was created
357    pub created_at_unix: u64,
358
359    /// Unix timestamp when intent expires (validity window)
360    pub valid_until: u64,
361
362    /// Unique nonce for replay protection (per payer)
363    pub nonce: u64,
364
365    /// Idempotency key for deduplication
366    pub idempotency_key: Option<String>,
367
368    // =========================================================================
369    // Resource & Context
370    // =========================================================================
371    /// Resource URI this payment unlocks (e.g., API endpoint)
372    pub resource_uri: Option<String>,
373
374    /// HTTP method for resource (GET, POST, etc.)
375    pub resource_method: Option<String>,
376
377    /// Description of what the payment is for
378    pub description: Option<String>,
379
380    /// Associated cart ID (if applicable)
381    pub cart_id: Option<Uuid>,
382
383    /// Associated order ID (if applicable)
384    pub order_id: Option<Uuid>,
385
386    /// Associated invoice ID (if applicable)
387    pub invoice_id: Option<Uuid>,
388
389    /// Merchant/payee identifier
390    pub merchant_id: Option<String>,
391
392    // =========================================================================
393    // Cryptographic Fields
394    // =========================================================================
395    /// Signing hash (SHA-256 of canonical payment data)
396    /// Format: `SHA256(X402_DOMAIN_SEPARATOR` || `canonical_json`)
397    pub signing_hash: Option<String>,
398
399    /// Signature scheme used to authorize this intent.
400    pub payer_signature_scheme: Option<X402SignatureScheme>,
401
402    /// Payer's Ed25519 signature over `signing_hash` (hex-encoded)
403    pub payer_signature: Option<String>,
404
405    /// Payer's public key (hex-encoded, 32 bytes)
406    pub payer_public_key: Option<String>,
407
408    /// Additional PQC signature material for hybrid or strict schemes.
409    pub payer_signature_bundle: Option<X402SignatureBundle>,
410
411    /// Additional PQC public-key material for hybrid or strict schemes.
412    pub payer_public_key_bundle: Option<X402PublicKeyBundle>,
413
414    // =========================================================================
415    // Sequencer Fields (set after submission)
416    // =========================================================================
417    /// Sequence number assigned by sequencer
418    pub sequence_number: Option<u64>,
419
420    /// Timestamp when sequenced
421    pub sequenced_at: Option<DateTime<Utc>>,
422
423    /// Batch ID containing this intent
424    pub batch_id: Option<Uuid>,
425
426    /// Merkle root of the batch
427    pub batch_merkle_root: Option<String>,
428
429    /// Merkle inclusion proof (for verification)
430    pub inclusion_proof: Option<Vec<String>>,
431
432    // =========================================================================
433    // Settlement Fields (set after on-chain execution)
434    // =========================================================================
435    /// On-chain transaction hash
436    pub tx_hash: Option<String>,
437
438    /// Block number where settled
439    pub block_number: Option<u64>,
440
441    /// Gas used for settlement
442    pub gas_used: Option<u64>,
443
444    /// Timestamp when settled on-chain
445    pub settled_at: Option<DateTime<Utc>>,
446
447    // =========================================================================
448    // Metadata
449    // =========================================================================
450    /// Additional metadata (JSON)
451    pub metadata: Option<String>,
452
453    /// When the intent record was created
454    pub created_at: DateTime<Utc>,
455
456    /// When the intent was last updated
457    pub updated_at: DateTime<Utc>,
458}
459
460impl X402PaymentIntent {
461    /// Create a new payment intent
462    pub fn new(
463        payer_address: impl Into<String>,
464        payee_address: impl Into<String>,
465        amount: u64,
466        asset: X402Asset,
467        network: X402Network,
468    ) -> Self {
469        let now = Utc::now();
470        let now_unix = now.timestamp() as u64;
471
472        // Calculate decimal amount
473        let decimals = asset.decimals();
474        let divisor = 10u64.pow(u32::from(decimals));
475        let amount_decimal = Decimal::from(amount) / Decimal::from(divisor);
476
477        Self {
478            id: Uuid::new_v4(),
479            version: X402_VERSION.to_string(),
480            status: X402IntentStatus::Created,
481            payer_address: payer_address.into(),
482            payee_address: payee_address.into(),
483            amount,
484            amount_decimal,
485            asset,
486            network,
487            chain_id: network.chain_id(),
488            token_address: asset.contract_address(network).map(String::from),
489            created_at_unix: now_unix,
490            valid_until: now_unix + X402_DEFAULT_VALIDITY_SECONDS,
491            nonce: 0,
492            idempotency_key: None,
493            resource_uri: None,
494            resource_method: None,
495            description: None,
496            cart_id: None,
497            order_id: None,
498            invoice_id: None,
499            merchant_id: None,
500            signing_hash: None,
501            payer_signature_scheme: Some(X402_DEFAULT_SIGNATURE_SCHEME),
502            payer_signature: None,
503            payer_public_key: None,
504            payer_signature_bundle: None,
505            payer_public_key_bundle: None,
506            sequence_number: None,
507            sequenced_at: None,
508            batch_id: None,
509            batch_merkle_root: None,
510            inclusion_proof: None,
511            tx_hash: None,
512            block_number: None,
513            gas_used: None,
514            settled_at: None,
515            metadata: None,
516            created_at: now,
517            updated_at: now,
518        }
519    }
520
521    /// Set the validity window in seconds
522    #[must_use]
523    pub fn with_validity(mut self, seconds: u64) -> Self {
524        self.valid_until = self.created_at_unix + seconds.min(X402_MAX_VALIDITY_SECONDS);
525        self
526    }
527
528    /// Set the nonce for replay protection
529    #[must_use]
530    pub const fn with_nonce(mut self, nonce: u64) -> Self {
531        self.nonce = nonce;
532        self
533    }
534
535    /// Set the resource URI this payment unlocks
536    pub fn with_resource(mut self, uri: impl Into<String>, method: impl Into<String>) -> Self {
537        self.resource_uri = Some(uri.into());
538        self.resource_method = Some(method.into());
539        self
540    }
541
542    /// Set the description
543    pub fn with_description(mut self, description: impl Into<String>) -> Self {
544        self.description = Some(description.into());
545        self
546    }
547
548    /// Set the associated order ID
549    #[must_use]
550    pub const fn with_order(mut self, order_id: Uuid) -> Self {
551        self.order_id = Some(order_id);
552        self
553    }
554
555    /// Set the idempotency key
556    pub fn with_idempotency_key(mut self, key: impl Into<String>) -> Self {
557        self.idempotency_key = Some(key.into());
558        self
559    }
560
561    /// Check if the intent has expired
562    #[must_use]
563    pub fn is_expired(&self) -> bool {
564        let now = Utc::now().timestamp() as u64;
565        now > self.valid_until
566    }
567
568    /// Check if the intent is signed
569    #[must_use]
570    pub fn is_signed(&self) -> bool {
571        let has_signing_hash =
572            self.signing_hash.as_ref().is_some_and(|signing_hash| !signing_hash.is_empty());
573        if !has_signing_hash {
574            return false;
575        }
576
577        match self.signature_scheme() {
578            X402SignatureScheme::Ed25519 => {
579                self.payer_signature.as_ref().is_some_and(|signature| !signature.is_empty())
580                    && self
581                        .payer_public_key
582                        .as_ref()
583                        .is_some_and(|public_key| !public_key.is_empty())
584            }
585            X402SignatureScheme::MlDsa65 => {
586                self.payer_signature_bundle.is_some() && self.payer_public_key_bundle.is_some()
587            }
588            X402SignatureScheme::Ed25519MlDsa65 => {
589                self.payer_signature.as_ref().is_some_and(|signature| !signature.is_empty())
590                    && self
591                        .payer_public_key
592                        .as_ref()
593                        .is_some_and(|public_key| !public_key.is_empty())
594                    && self.payer_signature_bundle.is_some()
595                    && self.payer_public_key_bundle.is_some()
596            }
597        }
598    }
599
600    /// Check if the intent is settled
601    #[must_use]
602    pub fn is_settled(&self) -> bool {
603        self.status == X402IntentStatus::Settled && self.tx_hash.is_some()
604    }
605
606    /// Try to get canonical JSON for signing (JCS - RFC 8785).
607    pub fn try_canonical_signing_data(&self) -> Result<String, X402CryptoError> {
608        // Per x402 spec, only signed fields are included
609        let payload = serde_json::json!({
610            "version": self.version,
611            "payer": self.payer_address,
612            "payee": self.payee_address,
613            "amount": self.amount.to_string(),
614            "asset": self.asset.to_string(),
615            "chainId": self.chain_id,
616            "tokenAddress": self.token_address,
617            "nonce": self.nonce,
618            "validUntil": self.valid_until,
619            "resourceUri": self.resource_uri,
620            "resourceMethod": self.resource_method,
621        });
622        serde_jcs::to_string(&payload).map_err(|e| X402CryptoError::Serialization(e.to_string()))
623    }
624
625    /// Get canonical JSON for signing (JCS - RFC 8785).
626    pub fn canonical_signing_data(&self) -> Result<String, X402CryptoError> {
627        self.try_canonical_signing_data()
628    }
629
630    /// Compute sequencer-compatible signing hash (`X402_PAYMENT_V1`)
631    #[must_use]
632    pub fn sequencer_signing_hash(&self) -> [u8; 32] {
633        let mut hasher = Sha256::new();
634
635        hasher.update(X402_DOMAIN_SEPARATOR.as_bytes());
636        hasher.update(self.payer_address.as_bytes());
637        hasher.update(self.payee_address.as_bytes());
638        hasher.update(self.amount.to_be_bytes());
639        hasher.update(format!("{:?}", self.asset).to_lowercase().as_bytes());
640        hasher.update(self.network.to_string().as_bytes());
641        hasher.update(self.chain_id.to_be_bytes());
642        hasher.update(self.valid_until.to_be_bytes());
643        hasher.update(self.nonce.to_be_bytes());
644        // Bind signatures to the protected resource and method to prevent replay
645        // across endpoints with identical payment parameters.
646        match &self.resource_uri {
647            Some(uri) => {
648                hasher.update([1u8]);
649                hasher.update((uri.len() as u64).to_be_bytes());
650                hasher.update(uri.as_bytes());
651            }
652            None => hasher.update([0u8]),
653        }
654        match &self.resource_method {
655            Some(method) => {
656                hasher.update([1u8]);
657                hasher.update((method.len() as u64).to_be_bytes());
658                hasher.update(method.as_bytes());
659            }
660            None => hasher.update([0u8]),
661        }
662
663        let result = hasher.finalize();
664        let mut hash = [0u8; 32];
665        hash.copy_from_slice(&result);
666        hash
667    }
668
669    /// Return the effective signature scheme, defaulting legacy rows to Ed25519.
670    #[must_use]
671    pub fn signature_scheme(&self) -> X402SignatureScheme {
672        self.payer_signature_scheme.unwrap_or(X402SignatureScheme::Ed25519)
673    }
674
675    /// Check whether a signing request matches the persisted intent policy.
676    ///
677    /// Legacy rows created before signature schemes were persisted return `true`
678    /// for any requested scheme to preserve compatibility during migration.
679    #[must_use]
680    pub fn allows_signing_scheme(&self, requested: X402SignatureScheme) -> bool {
681        match self.payer_signature_scheme {
682            Some(configured) => configured == requested,
683            None => true,
684        }
685    }
686
687    /// Sign the intent using Ed25519 (sequencer-compatible hash)
688    pub fn sign_with_ed25519(&mut self, private_key: &[u8; 32]) -> Result<(), X402CryptoError> {
689        let signing_hash = self.sequencer_signing_hash();
690        let signing_key = SigningKey::from_bytes(private_key);
691        let signature = signing_key.sign(&signing_hash);
692        let public_key = signing_key.verifying_key();
693
694        self.signing_hash = Some(hex0x(signing_hash));
695        self.payer_signature_scheme = Some(X402SignatureScheme::Ed25519);
696        self.payer_signature = Some(hex0x(signature.to_bytes()));
697        self.payer_public_key = Some(hex0x(public_key.to_bytes()));
698        self.payer_signature_bundle = None;
699        self.payer_public_key_bundle = None;
700        self.status = X402IntentStatus::Signed;
701        Ok(())
702    }
703
704    /// Sign the intent using hybrid Ed25519 + ML-DSA-65.
705    pub fn sign_with_hybrid(
706        &mut self,
707        keypair: &HybridSigningKeypair,
708    ) -> Result<(), X402CryptoError> {
709        let signing_hash = self.sequencer_signing_hash();
710        let signature = hybrid_sign_event_hash(&signing_hash, &keypair.private)
711            .map_err(|e| X402CryptoError::InvalidKey(e.to_string()))?;
712
713        self.signing_hash = Some(hex0x(signing_hash));
714        self.payer_signature_scheme = Some(X402SignatureScheme::Ed25519MlDsa65);
715        self.payer_signature = Some(hex0x(signature.ed25519_signature));
716        self.payer_public_key = Some(hex0x(keypair.public.ed25519_public_key));
717        self.payer_signature_bundle =
718            Some(X402SignatureBundle { ml_dsa_65_signature: signature.ml_dsa_65_signature });
719        self.payer_public_key_bundle = Some(X402PublicKeyBundle {
720            ml_dsa_65_public_key: keypair.public.ml_dsa_65_public_key.clone(),
721        });
722        self.status = X402IntentStatus::Signed;
723        Ok(())
724    }
725
726    /// Sign the intent using PQC-strict ML-DSA-65.
727    pub fn sign_with_strict(
728        &mut self,
729        keypair: &StrictSigningKeypair,
730    ) -> Result<(), X402CryptoError> {
731        let signing_hash = self.sequencer_signing_hash();
732        let signature = strict_sign_event_hash(&signing_hash, &keypair.private)
733            .map_err(|e| X402CryptoError::InvalidKey(e.to_string()))?;
734
735        self.signing_hash = Some(hex0x(signing_hash));
736        self.payer_signature_scheme = Some(X402SignatureScheme::MlDsa65);
737        self.payer_signature = None;
738        self.payer_public_key = None;
739        self.payer_signature_bundle = Some(X402SignatureBundle { ml_dsa_65_signature: signature });
740        self.payer_public_key_bundle = Some(X402PublicKeyBundle {
741            ml_dsa_65_public_key: keypair.public.ml_dsa_65_public_key.clone(),
742        });
743        self.status = X402IntentStatus::Signed;
744        Ok(())
745    }
746
747    /// Verify the configured x402 signature against the sequencer-compatible hash.
748    pub fn verify_signature(&self) -> Result<bool, X402CryptoError> {
749        let signing_hash = self.sequencer_signing_hash();
750
751        let stored_hash =
752            self.signing_hash.as_deref().ok_or(X402CryptoError::MissingField("signing_hash"))?;
753        if decode_hex_array::<32>(stored_hash)? != signing_hash {
754            return Ok(false);
755        }
756
757        match self.signature_scheme() {
758            X402SignatureScheme::Ed25519 => {
759                let signature_hex = self
760                    .payer_signature
761                    .as_deref()
762                    .ok_or(X402CryptoError::MissingField("payer_signature"))?;
763                let public_key_hex = self
764                    .payer_public_key
765                    .as_deref()
766                    .ok_or(X402CryptoError::MissingField("payer_public_key"))?;
767
768                let signature = Signature::from_bytes(&decode_hex_array::<64>(signature_hex)?);
769                let public_key = VerifyingKey::from_bytes(&decode_hex_array::<32>(public_key_hex)?)
770                    .map_err(|e| X402CryptoError::InvalidKey(e.to_string()))?;
771
772                Ok(public_key.verify(&signing_hash, &signature).is_ok())
773            }
774            X402SignatureScheme::MlDsa65 => {
775                let signature_bundle = self
776                    .payer_signature_bundle
777                    .as_ref()
778                    .ok_or(X402CryptoError::MissingField("payer_signature_bundle"))?;
779                let public_key_bundle = self
780                    .payer_public_key_bundle
781                    .as_ref()
782                    .ok_or(X402CryptoError::MissingField("payer_public_key_bundle"))?;
783                let public_key = StrictSigningPublicKey {
784                    ml_dsa_65_public_key: public_key_bundle.ml_dsa_65_public_key.clone(),
785                };
786                Ok(strict_verify_event_signature(
787                    &signing_hash,
788                    &signature_bundle.ml_dsa_65_signature,
789                    &public_key,
790                ))
791            }
792            X402SignatureScheme::Ed25519MlDsa65 => {
793                let signature_hex = self
794                    .payer_signature
795                    .as_deref()
796                    .ok_or(X402CryptoError::MissingField("payer_signature"))?;
797                let public_key_hex = self
798                    .payer_public_key
799                    .as_deref()
800                    .ok_or(X402CryptoError::MissingField("payer_public_key"))?;
801                let signature_bundle = self
802                    .payer_signature_bundle
803                    .as_ref()
804                    .ok_or(X402CryptoError::MissingField("payer_signature_bundle"))?;
805                let public_key_bundle = self
806                    .payer_public_key_bundle
807                    .as_ref()
808                    .ok_or(X402CryptoError::MissingField("payer_public_key_bundle"))?;
809                let signature = PqcHybridSignatureBundle {
810                    ed25519_signature: decode_hex_array::<64>(signature_hex)?,
811                    ml_dsa_65_signature: signature_bundle.ml_dsa_65_signature.clone(),
812                };
813                let public_key = HybridSigningPublicKey {
814                    ed25519_public_key: decode_hex_array::<32>(public_key_hex)?,
815                    ml_dsa_65_public_key: public_key_bundle.ml_dsa_65_public_key.clone(),
816                };
817
818                Ok(hybrid_verify_event_signature(&signing_hash, &signature, &public_key))
819            }
820        }
821    }
822}
823
824// =============================================================================
825// x402 Payment Response (Server's 402 Response)
826// =============================================================================
827
828/// x402 Payment Required Response
829///
830/// This is returned by servers in the `X-Payment-Required` header when
831/// payment is needed to access a resource.
832#[derive(Debug, Clone, Serialize, Deserialize)]
833pub struct X402PaymentRequired {
834    /// x402 protocol version
835    pub version: String,
836
837    /// Payee (merchant) wallet address
838    pub payee_address: String,
839
840    /// Required payment amount in smallest unit
841    pub amount: u64,
842
843    /// Human-readable amount
844    pub amount_display: String,
845
846    /// Required payment asset
847    pub asset: X402Asset,
848
849    /// Accepted networks (in order of preference)
850    pub networks: Vec<X402Network>,
851
852    /// Resource being accessed
853    pub resource_uri: String,
854
855    /// HTTP method for resource
856    pub resource_method: String,
857
858    /// Human-readable description of what payment unlocks
859    pub description: Option<String>,
860
861    /// Validity window for payment (seconds from now)
862    pub validity_seconds: u64,
863
864    /// Merchant identifier
865    pub merchant_id: Option<String>,
866
867    /// Merchant name for display
868    pub merchant_name: Option<String>,
869
870    /// Additional terms or conditions
871    pub terms: Option<String>,
872
873    /// Timestamp when this response was generated
874    pub generated_at: DateTime<Utc>,
875}
876
877impl X402PaymentRequired {
878    /// Create a new payment required response
879    pub fn new(
880        payee_address: impl Into<String>,
881        amount: u64,
882        asset: X402Asset,
883        resource_uri: impl Into<String>,
884        resource_method: impl Into<String>,
885    ) -> Self {
886        let decimals = asset.decimals();
887        let divisor = 10u64.pow(u32::from(decimals));
888        let decimal_amount = Decimal::from(amount) / Decimal::from(divisor);
889        let amount_display = format!("{decimal_amount:.6} {asset}");
890
891        Self {
892            version: X402_VERSION.to_string(),
893            payee_address: payee_address.into(),
894            amount,
895            amount_display,
896            asset,
897            networks: vec![X402Network::SetChain, X402Network::Base],
898            resource_uri: resource_uri.into(),
899            resource_method: resource_method.into(),
900            description: None,
901            validity_seconds: X402_DEFAULT_VALIDITY_SECONDS,
902            merchant_id: None,
903            merchant_name: None,
904            terms: None,
905            generated_at: Utc::now(),
906        }
907    }
908
909    /// Set accepted networks
910    #[must_use]
911    pub fn with_networks(mut self, networks: Vec<X402Network>) -> Self {
912        self.networks = networks;
913        self
914    }
915
916    /// Set merchant info
917    pub fn with_merchant(mut self, id: impl Into<String>, name: impl Into<String>) -> Self {
918        self.merchant_id = Some(id.into());
919        self.merchant_name = Some(name.into());
920        self
921    }
922
923    /// Try encoding as base64 for HTTP header.
924    pub fn try_to_header_value(&self) -> std::result::Result<String, serde_json::Error> {
925        use base64::{Engine, engine::general_purpose::STANDARD};
926        let json = serde_json::to_string(self)?;
927        Ok(STANDARD.encode(json.as_bytes()))
928    }
929
930    /// Encode as base64 for HTTP header.
931    pub fn to_header_value(&self) -> std::result::Result<String, serde_json::Error> {
932        self.try_to_header_value()
933    }
934
935    /// Decode from HTTP header value
936    pub fn from_header_value(value: &str) -> Result<Self, String> {
937        use base64::{Engine, engine::general_purpose::STANDARD};
938        let bytes = STANDARD.decode(value).map_err(|e| format!("Invalid base64: {e}"))?;
939        let json = String::from_utf8(bytes).map_err(|e| format!("Invalid UTF-8: {e}"))?;
940        serde_json::from_str(&json).map_err(|e| format!("Invalid JSON: {e}"))
941    }
942}
943
944// =============================================================================
945// x402 Payment Receipt (Proof of Payment)
946// =============================================================================
947
948/// x402 Payment Receipt - Proof that payment was made
949///
950/// This is returned after successful payment and can be used to verify
951/// the payment via Merkle inclusion proofs.
952#[derive(Debug, Clone, Serialize, Deserialize)]
953pub struct X402PaymentReceipt {
954    /// Receipt ID
955    #[serde(alias = "id")]
956    pub receipt_id: Uuid,
957
958    /// Original payment intent ID
959    pub intent_id: Uuid,
960
961    /// Sequence number from sequencer
962    pub sequence_number: u64,
963
964    /// Batch ID containing this payment
965    pub batch_id: Uuid,
966
967    /// Merkle root of the batch
968    pub merkle_root: String,
969
970    /// Merkle inclusion proof (list of sibling hashes)
971    pub inclusion_proof: Vec<String>,
972
973    /// Leaf index in the Merkle tree
974    pub leaf_index: u32,
975
976    /// Total leaves in the Merkle tree
977    pub total_leaves: u32,
978
979    /// On-chain transaction hash (if settled)
980    pub tx_hash: Option<String>,
981
982    /// Block number (if settled)
983    pub block_number: Option<u64>,
984
985    /// Payment details for verification
986    pub payer_address: String,
987    pub payee_address: String,
988    pub amount: u64,
989    pub asset: X402Asset,
990    pub network: X402Network,
991    pub chain_id: u64,
992    pub nonce: u64,
993    pub valid_until: u64,
994    /// Sequencer signing hash (hex-encoded, 32 bytes)
995    pub signing_hash: String,
996    /// Signature scheme used for the original payer authorization.
997    #[serde(default, skip_serializing_if = "Option::is_none")]
998    pub payer_signature_scheme: Option<X402SignatureScheme>,
999    /// Legacy Ed25519 signature (hex-encoded, 64 bytes).
1000    pub payer_signature: String,
1001    /// Additional PQC signature material for hybrid or strict signatures.
1002    #[serde(default, skip_serializing_if = "Option::is_none")]
1003    pub payer_signature_bundle: Option<X402SignatureBundle>,
1004
1005    /// Timestamp
1006    pub created_at: DateTime<Utc>,
1007}
1008
1009impl X402PaymentReceipt {
1010    /// Verify the inclusion proof against the merkle root
1011    #[must_use]
1012    pub fn verify_inclusion(&self) -> bool {
1013        if self.merkle_root.is_empty() {
1014            return false;
1015        }
1016
1017        if self.total_leaves == 0 || self.leaf_index >= self.total_leaves {
1018            return false;
1019        }
1020
1021        let root = match decode_hex_array::<32>(&self.merkle_root) {
1022            Ok(bytes) => bytes,
1023            Err(_) => return false,
1024        };
1025
1026        let leaf = match payment_leaf_hash(self) {
1027            Ok(hash) => hash,
1028            Err(_) => return false,
1029        };
1030
1031        let mut proof_hashes = Vec::with_capacity(self.inclusion_proof.len());
1032        for proof_hash in &self.inclusion_proof {
1033            match decode_hex_array::<32>(proof_hash) {
1034                Ok(hash) => proof_hashes.push(hash),
1035                Err(_) => return false,
1036            }
1037        }
1038
1039        let proof = MerkleProof::<MerkleSha256>::new(proof_hashes);
1040        proof.verify(root, &[self.leaf_index as usize], &[leaf], self.total_leaves as usize)
1041    }
1042}
1043
1044// =============================================================================
1045// x402 Credit Ledger (Metered Billing)
1046// =============================================================================
1047
1048/// x402 Credit Account - tracks prepaid balances for metered usage
1049#[derive(Debug, Clone, Serialize, Deserialize)]
1050pub struct X402CreditAccount {
1051    pub id: Uuid,
1052    pub payer_address: String,
1053    pub asset: X402Asset,
1054    pub network: X402Network,
1055    pub balance: u64,
1056    pub created_at: DateTime<Utc>,
1057    pub updated_at: DateTime<Utc>,
1058}
1059
1060/// Create a new x402 credit account
1061#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1062pub struct CreateX402CreditAccount {
1063    pub payer_address: String,
1064    pub asset: X402Asset,
1065    pub network: X402Network,
1066    pub initial_balance: Option<u64>,
1067}
1068
1069/// Credit ledger adjustment (credit or debit)
1070#[derive(Debug, Clone, Serialize, Deserialize)]
1071pub struct X402CreditAdjustment {
1072    pub payer_address: String,
1073    pub asset: X402Asset,
1074    pub network: X402Network,
1075    pub direction: X402CreditDirection,
1076    pub amount: u64,
1077    pub reason: Option<String>,
1078    pub reference_id: Option<String>,
1079    pub metadata: Option<String>,
1080}
1081
1082/// x402 credit transaction (ledger entry)
1083#[derive(Debug, Clone, Serialize, Deserialize)]
1084pub struct X402CreditTransaction {
1085    pub id: Uuid,
1086    pub account_id: Uuid,
1087    pub payer_address: String,
1088    pub asset: X402Asset,
1089    pub network: X402Network,
1090    pub direction: X402CreditDirection,
1091    pub amount: u64,
1092    pub balance_after: u64,
1093    pub reason: Option<String>,
1094    pub reference_id: Option<String>,
1095    pub metadata: Option<String>,
1096    pub created_at: DateTime<Utc>,
1097}
1098
1099/// Filter for listing credit ledger transactions
1100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1101pub struct X402CreditTransactionFilter {
1102    pub payer_address: Option<String>,
1103    pub asset: Option<X402Asset>,
1104    pub network: Option<X402Network>,
1105    pub direction: Option<X402CreditDirection>,
1106    pub limit: Option<u32>,
1107    pub offset: Option<u32>,
1108}
1109
1110// =============================================================================
1111// x402 Payment Batch (For Sequencer)
1112// =============================================================================
1113
1114/// Status of a payment batch
1115#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, Serialize, Deserialize, Default)]
1116#[strum(serialize_all = "snake_case")]
1117#[serde(rename_all = "snake_case")]
1118#[non_exhaustive]
1119pub enum X402BatchStatus {
1120    /// Batch is being assembled
1121    #[default]
1122    Pending,
1123    /// Batch is committed (Merkle root computed)
1124    Committed,
1125    /// Batch is being settled on-chain
1126    Settling,
1127    /// Batch is fully settled
1128    Settled,
1129    /// Batch settlement failed
1130    Failed,
1131}
1132
1133/// x402 Payment Batch - A collection of payment intents for batch settlement
1134///
1135/// Multiple payment intents are batched together for gas-efficient
1136/// settlement on Set Chain L2.
1137#[derive(Debug, Clone, Serialize, Deserialize)]
1138pub struct X402PaymentBatch {
1139    /// Batch ID
1140    pub id: Uuid,
1141
1142    /// Batch status
1143    pub status: X402BatchStatus,
1144
1145    /// Target network for settlement
1146    pub network: X402Network,
1147
1148    /// Number of payments in batch
1149    pub payment_count: u32,
1150
1151    /// Total amount across all payments (by asset)
1152    pub total_amounts: Vec<(X402Asset, u64)>,
1153
1154    /// Merkle root of payment hashes
1155    pub merkle_root: Option<String>,
1156
1157    /// Previous state root (for chaining)
1158    pub prev_state_root: Option<String>,
1159
1160    /// New state root after this batch
1161    pub new_state_root: Option<String>,
1162
1163    /// Sequence range [start, end]
1164    pub sequence_start: u64,
1165    pub sequence_end: u64,
1166
1167    /// On-chain settlement details
1168    pub tx_hash: Option<String>,
1169    pub block_number: Option<u64>,
1170    pub gas_used: Option<u64>,
1171
1172    /// Timestamps
1173    pub created_at: DateTime<Utc>,
1174    pub committed_at: Option<DateTime<Utc>>,
1175    pub settled_at: Option<DateTime<Utc>>,
1176}
1177
1178impl X402PaymentBatch {
1179    /// Create a new empty batch
1180    #[must_use]
1181    pub fn new(network: X402Network) -> Self {
1182        Self {
1183            id: Uuid::new_v4(),
1184            status: X402BatchStatus::Pending,
1185            network,
1186            payment_count: 0,
1187            total_amounts: Vec::new(),
1188            merkle_root: None,
1189            prev_state_root: None,
1190            new_state_root: None,
1191            sequence_start: 0,
1192            sequence_end: 0,
1193            tx_hash: None,
1194            block_number: None,
1195            gas_used: None,
1196            created_at: Utc::now(),
1197            committed_at: None,
1198            settled_at: None,
1199        }
1200    }
1201}
1202
1203// =============================================================================
1204// Input/Filter Types for API
1205// =============================================================================
1206
1207/// Input for creating an x402 payment intent
1208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1209pub struct CreateX402PaymentIntent {
1210    /// Payer wallet address
1211    pub payer_address: String,
1212    /// Payee wallet address
1213    pub payee_address: String,
1214    /// Amount in smallest unit
1215    pub amount: u64,
1216    /// Payment asset
1217    pub asset: X402Asset,
1218    /// Target network
1219    pub network: X402Network,
1220    /// Nonce for replay protection
1221    pub nonce: Option<u64>,
1222    /// Validity window in seconds
1223    pub validity_seconds: Option<u64>,
1224    /// Resource URI this payment unlocks
1225    pub resource_uri: Option<String>,
1226    /// HTTP method for resource
1227    pub resource_method: Option<String>,
1228    /// Description
1229    pub description: Option<String>,
1230    /// Associated cart ID (for checkout flows)
1231    pub cart_id: Option<Uuid>,
1232    /// Associated order ID
1233    pub order_id: Option<Uuid>,
1234    /// Associated invoice ID
1235    pub invoice_id: Option<Uuid>,
1236    /// Merchant ID
1237    pub merchant_id: Option<String>,
1238    /// Idempotency key
1239    pub idempotency_key: Option<String>,
1240    /// Additional metadata
1241    pub metadata: Option<String>,
1242    /// Preferred signature scheme for this intent. Absent = hybrid default.
1243    pub signature_scheme: Option<X402SignatureScheme>,
1244}
1245
1246/// Input for signing an x402 payment intent
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1248pub struct SignX402PaymentIntent {
1249    /// Intent ID to sign
1250    pub intent_id: Uuid,
1251    /// Signature scheme used to authorize the intent. Absent = the intent's stored preference,
1252    /// with legacy rows falling back to Ed25519.
1253    pub signature_scheme: Option<X402SignatureScheme>,
1254    /// Ed25519 signature material (hex-encoded, 64 bytes) for legacy or hybrid flows.
1255    pub signature: String,
1256    /// Payer's Ed25519 public key (hex-encoded, 32 bytes) for legacy or hybrid flows.
1257    pub public_key: String,
1258    /// Additional PQC signature material for hybrid or strict signatures.
1259    pub signature_bundle: Option<X402SignatureBundle>,
1260    /// Additional PQC public-key material for hybrid or strict signatures.
1261    pub public_key_bundle: Option<X402PublicKeyBundle>,
1262}
1263
1264/// Filter for listing x402 payment intents
1265#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1266pub struct X402PaymentIntentFilter {
1267    /// Filter by payer address
1268    pub payer_address: Option<String>,
1269    /// Filter by payee address
1270    pub payee_address: Option<String>,
1271    /// Filter by status
1272    pub status: Option<X402IntentStatus>,
1273    /// Filter by network
1274    pub network: Option<X402Network>,
1275    /// Filter by asset
1276    pub asset: Option<X402Asset>,
1277    /// Filter by order ID
1278    pub order_id: Option<Uuid>,
1279    /// Filter by batch ID
1280    pub batch_id: Option<Uuid>,
1281    /// Filter by date range
1282    pub from_date: Option<DateTime<Utc>>,
1283    pub to_date: Option<DateTime<Utc>>,
1284    /// Pagination
1285    pub limit: Option<u32>,
1286    pub offset: Option<u32>,
1287}
1288
1289// =============================================================================
1290// Helper Functions
1291// =============================================================================
1292
1293/// Generate a unique x402 intent ID
1294#[must_use]
1295pub fn generate_x402_intent_id() -> Uuid {
1296    Uuid::new_v4()
1297}
1298
1299/// Calculate amount in smallest unit from decimal
1300#[must_use]
1301pub fn to_smallest_unit(amount: Decimal, asset: X402Asset) -> u64 {
1302    if amount <= Decimal::ZERO {
1303        return 0;
1304    }
1305
1306    let decimals = asset.decimals();
1307    let multiplier = Decimal::from(10u64.pow(u32::from(decimals)));
1308    let scaled = amount * multiplier;
1309    if scaled.fract() != Decimal::ZERO {
1310        // Prevent positive sub-unit values from silently turning into zero.
1311        return scaled.ceil().to_u64().unwrap_or(u64::MAX);
1312    }
1313    scaled.to_u64().unwrap_or(0)
1314}
1315
1316/// Calculate decimal amount from smallest unit
1317#[must_use]
1318pub fn from_smallest_unit(amount: u64, asset: X402Asset) -> Decimal {
1319    let decimals = asset.decimals();
1320    let divisor = Decimal::from(10u64.pow(u32::from(decimals)));
1321    Decimal::from(amount) / divisor
1322}
1323
1324// =============================================================================
1325// x402 Crypto Helpers
1326// =============================================================================
1327
1328#[derive(Debug, Error)]
1329#[non_exhaustive]
1330pub enum X402CryptoError {
1331    #[error("missing field: {0}")]
1332    MissingField(&'static str),
1333    #[error("serialization error: {0}")]
1334    Serialization(String),
1335    #[error("invalid hex: {0}")]
1336    InvalidHex(String),
1337    #[error("invalid length: expected {expected}, got {got}")]
1338    InvalidLength { expected: usize, got: usize },
1339    #[error("invalid key: {0}")]
1340    InvalidKey(String),
1341}
1342
1343fn hex0x(bytes: impl AsRef<[u8]>) -> String {
1344    format!("0x{}", hex::encode(bytes))
1345}
1346
1347fn decode_hex_array<const N: usize>(value: &str) -> Result<[u8; N], X402CryptoError> {
1348    let trimmed = value.strip_prefix("0x").unwrap_or(value);
1349    let bytes = hex::decode(trimmed).map_err(|e| X402CryptoError::InvalidHex(e.to_string()))?;
1350    if bytes.len() != N {
1351        return Err(X402CryptoError::InvalidLength { expected: N, got: bytes.len() });
1352    }
1353    let mut arr = [0u8; N];
1354    arr.copy_from_slice(&bytes);
1355    Ok(arr)
1356}
1357
1358fn decode_hex_bytes(value: &str) -> Result<Vec<u8>, X402CryptoError> {
1359    let trimmed = value.strip_prefix("0x").unwrap_or(value);
1360    hex::decode(trimmed).map_err(|e| X402CryptoError::InvalidHex(e.to_string()))
1361}
1362
1363fn normalize_optional_string(value: &str) -> Option<String> {
1364    let trimmed = value.trim();
1365    (!trimmed.is_empty()).then(|| trimmed.to_string())
1366}
1367
1368fn update_optional_leaf_bytes(
1369    hasher: &mut Sha256,
1370    bytes: Option<&[u8]>,
1371) -> Result<(), X402CryptoError> {
1372    match bytes {
1373        Some(bytes) => {
1374            let len = u64::try_from(bytes.len())
1375                .map_err(|_| X402CryptoError::Serialization("byte slice too large".to_string()))?;
1376            hasher.update([1u8]);
1377            hasher.update(len.to_be_bytes());
1378            hasher.update(bytes);
1379        }
1380        None => hasher.update([0u8]),
1381    }
1382    Ok(())
1383}
1384
1385fn payment_leaf_hash(receipt: &X402PaymentReceipt) -> Result<[u8; 32], X402CryptoError> {
1386    let mut hasher = Sha256::new();
1387
1388    hasher.update(receipt.intent_id.as_bytes());
1389    hasher.update(receipt.sequence_number.to_be_bytes());
1390
1391    hasher.update(receipt.payer_address.as_bytes());
1392    hasher.update(receipt.payee_address.as_bytes());
1393    hasher.update(receipt.amount.to_be_bytes());
1394    hasher.update(receipt.asset.to_string().to_lowercase().as_bytes());
1395    hasher.update(receipt.network.to_string().as_bytes());
1396    hasher.update(receipt.chain_id.to_be_bytes());
1397    hasher.update(receipt.nonce.to_be_bytes());
1398    hasher.update(receipt.valid_until.to_be_bytes());
1399
1400    let signing_hash = decode_hex_array::<32>(&receipt.signing_hash)?;
1401    let legacy_signature =
1402        normalize_optional_string(&receipt.payer_signature).map(|sig| decode_hex_bytes(&sig));
1403    let legacy_signature = legacy_signature.transpose()?;
1404    hasher.update(signing_hash);
1405    hasher.update(
1406        receipt
1407            .payer_signature_scheme
1408            .unwrap_or(X402SignatureScheme::Ed25519)
1409            .to_string()
1410            .as_bytes(),
1411    );
1412    update_optional_leaf_bytes(&mut hasher, legacy_signature.as_deref())?;
1413    update_optional_leaf_bytes(
1414        &mut hasher,
1415        receipt.payer_signature_bundle.as_ref().map(|bundle| bundle.ml_dsa_65_signature.as_slice()),
1416    )?;
1417
1418    let result = hasher.finalize();
1419    let mut hash = [0u8; 32];
1420    hash.copy_from_slice(&result);
1421    Ok(hash)
1422}
1423
1424#[cfg(test)]
1425mod tests {
1426    use super::*;
1427    use stateset_crypto::pqc::{generate_hybrid_signing_keypair, generate_strict_signing_keypair};
1428
1429    #[test]
1430    fn test_x402_payment_intent_creation() {
1431        let intent = X402PaymentIntent::new(
1432            "0x1234567890abcdef1234567890abcdef12345678",
1433            "0xabcdef1234567890abcdef1234567890abcdef12",
1434            1_000_000, // 1 USDC
1435            X402Asset::Usdc,
1436            X402Network::SetChain,
1437        );
1438
1439        assert_eq!(intent.amount, 1_000_000);
1440        assert_eq!(intent.amount_decimal, Decimal::from(1));
1441        assert_eq!(intent.asset, X402Asset::Usdc);
1442        assert_eq!(intent.network, X402Network::SetChain);
1443        assert_eq!(intent.chain_id, 84532001);
1444        assert_eq!(intent.payer_signature_scheme, Some(X402_DEFAULT_SIGNATURE_SCHEME));
1445        assert_eq!(intent.signature_scheme(), X402_DEFAULT_SIGNATURE_SCHEME);
1446        assert!(!intent.is_expired());
1447        assert!(!intent.is_signed());
1448    }
1449
1450    #[test]
1451    fn test_x402_legacy_rows_fall_back_to_ed25519() {
1452        let mut intent = X402PaymentIntent::new(
1453            "0x1234567890abcdef1234567890abcdef12345678",
1454            "0xabcdef1234567890abcdef1234567890abcdef12",
1455            1_000_000,
1456            X402Asset::Usdc,
1457            X402Network::SetChain,
1458        );
1459
1460        intent.payer_signature_scheme = None;
1461
1462        assert_eq!(intent.signature_scheme(), X402SignatureScheme::Ed25519);
1463    }
1464
1465    #[test]
1466    fn test_x402_new_intents_require_configured_signature_scheme() {
1467        let intent = X402PaymentIntent::new(
1468            "0x1234567890abcdef1234567890abcdef12345678",
1469            "0xabcdef1234567890abcdef1234567890abcdef12",
1470            1_000_000,
1471            X402Asset::Usdc,
1472            X402Network::SetChain,
1473        );
1474
1475        assert!(intent.allows_signing_scheme(X402_DEFAULT_SIGNATURE_SCHEME));
1476        assert!(!intent.allows_signing_scheme(X402SignatureScheme::Ed25519));
1477    }
1478
1479    #[test]
1480    fn test_x402_network_chain_ids() {
1481        assert_eq!(X402Network::SetChain.chain_id(), 84532001);
1482        assert_eq!(X402Network::Base.chain_id(), 8453);
1483        assert_eq!(X402Network::Ethereum.chain_id(), 1);
1484    }
1485
1486    #[test]
1487    fn test_x402_asset_decimals() {
1488        assert_eq!(X402Asset::Usdc.decimals(), 6);
1489        assert_eq!(X402Asset::Dai.decimals(), 18);
1490        assert_eq!(X402Asset::Eth.decimals(), 18);
1491    }
1492
1493    #[test]
1494    fn test_amount_conversion() {
1495        let decimal = Decimal::from(100);
1496        let smallest = to_smallest_unit(decimal, X402Asset::Usdc);
1497        assert_eq!(smallest, 100_000_000); // 100 USDC = 100,000,000 (6 decimals)
1498
1499        let back = from_smallest_unit(smallest, X402Asset::Usdc);
1500        assert_eq!(back, decimal);
1501    }
1502
1503    #[test]
1504    fn test_amount_conversion_rounds_up_sub_precision() {
1505        let decimal = Decimal::new(1, 7); // 0.0000001
1506        let smallest = to_smallest_unit(decimal, X402Asset::Usdc);
1507        assert_eq!(smallest, 1);
1508    }
1509
1510    #[test]
1511    fn test_amount_conversion_non_positive_maps_to_zero() {
1512        assert_eq!(to_smallest_unit(Decimal::ZERO, X402Asset::Usdc), 0);
1513        assert_eq!(to_smallest_unit(Decimal::new(-1, 0), X402Asset::Usdc), 0);
1514    }
1515
1516    #[test]
1517    fn test_signature_fails_when_resource_binding_changes() {
1518        let mut intent = X402PaymentIntent::new(
1519            "0x1234567890abcdef1234567890abcdef12345678",
1520            "0xabcdef1234567890abcdef1234567890abcdef12",
1521            1_000_000,
1522            X402Asset::Usdc,
1523            X402Network::SetChain,
1524        )
1525        .with_resource("/a", "GET")
1526        .with_nonce(42);
1527
1528        intent.sign_with_ed25519(&[7u8; 32]).unwrap();
1529        assert!(intent.verify_signature().unwrap());
1530
1531        let mut replayed = intent.clone();
1532        replayed.resource_uri = Some("/premium".to_string());
1533        assert!(!replayed.verify_signature().unwrap());
1534
1535        let mut method_changed = intent;
1536        method_changed.resource_method = Some("POST".to_string());
1537        assert!(!method_changed.verify_signature().unwrap());
1538    }
1539
1540    #[test]
1541    #[cfg_attr(miri, ignore = "hybrid PQC signature verification is too slow under Miri")]
1542    fn test_hybrid_signature_verifies() {
1543        let mut intent = X402PaymentIntent::new(
1544            "0x1234567890abcdef1234567890abcdef12345678",
1545            "0xabcdef1234567890abcdef1234567890abcdef12",
1546            1_000_000,
1547            X402Asset::Usdc,
1548            X402Network::SetChain,
1549        )
1550        .with_resource("/hybrid", "POST")
1551        .with_nonce(7);
1552        let keypair = generate_hybrid_signing_keypair().unwrap();
1553
1554        intent.sign_with_hybrid(&keypair).unwrap();
1555
1556        assert_eq!(intent.payer_signature_scheme, Some(X402SignatureScheme::Ed25519MlDsa65));
1557        assert!(intent.payer_signature_bundle.is_some());
1558        assert!(intent.payer_public_key_bundle.is_some());
1559        assert!(intent.verify_signature().unwrap());
1560    }
1561
1562    #[test]
1563    #[cfg_attr(miri, ignore = "strict PQC signature verification is too slow under Miri")]
1564    fn test_strict_signature_verifies() {
1565        let mut intent = X402PaymentIntent::new(
1566            "0x1234567890abcdef1234567890abcdef12345678",
1567            "0xabcdef1234567890abcdef1234567890abcdef12",
1568            1_000_000,
1569            X402Asset::Usdc,
1570            X402Network::SetChain,
1571        )
1572        .with_resource("/strict", "POST")
1573        .with_nonce(9);
1574        let keypair = generate_strict_signing_keypair().unwrap();
1575
1576        intent.sign_with_strict(&keypair).unwrap();
1577
1578        assert_eq!(intent.payer_signature_scheme, Some(X402SignatureScheme::MlDsa65));
1579        assert!(intent.payer_signature.is_none());
1580        assert!(intent.payer_public_key.is_none());
1581        assert!(intent.verify_signature().unwrap());
1582    }
1583
1584    #[test]
1585    fn test_x402_payment_required_header() {
1586        let req =
1587            X402PaymentRequired::new("0xpayee", 1_000_000, X402Asset::Usdc, "/api/resource", "GET");
1588
1589        let header = req.to_header_value().unwrap();
1590        let decoded = X402PaymentRequired::from_header_value(&header).unwrap();
1591
1592        assert_eq!(decoded.payee_address, "0xpayee");
1593        assert_eq!(decoded.amount, 1_000_000);
1594    }
1595
1596    #[test]
1597    fn test_x402_merkle_inclusion_verification() {
1598        let mut receipt = X402PaymentReceipt {
1599            receipt_id: Uuid::new_v4(),
1600            intent_id: Uuid::new_v4(),
1601            sequence_number: 42,
1602            batch_id: Uuid::new_v4(),
1603            merkle_root: String::new(),
1604            inclusion_proof: vec![],
1605            leaf_index: 0,
1606            total_leaves: 0,
1607            tx_hash: None,
1608            block_number: None,
1609            payer_address: "0xpayer".to_string(),
1610            payee_address: "0xpayee".to_string(),
1611            amount: 1_000_000,
1612            asset: X402Asset::Usdc,
1613            network: X402Network::SetChain,
1614            chain_id: X402Network::SetChain.chain_id(),
1615            nonce: 7,
1616            valid_until: 1_705_320_000,
1617            signing_hash: format!("0x{}", "11".repeat(32)),
1618            payer_signature_scheme: Some(X402SignatureScheme::Ed25519),
1619            payer_signature: format!("0x{}", "22".repeat(64)),
1620            payer_signature_bundle: None,
1621            created_at: Utc::now(),
1622        };
1623
1624        let mut other = receipt.clone();
1625        other.intent_id = Uuid::new_v4();
1626        other.sequence_number = 43;
1627        other.nonce = 8;
1628        other.signing_hash = format!("0x{}", "33".repeat(32));
1629        other.payer_signature = format!("0x{}", "44".repeat(64));
1630
1631        let leaf = payment_leaf_hash(&receipt).unwrap();
1632        let other_leaf = payment_leaf_hash(&other).unwrap();
1633
1634        let leaves = vec![leaf, other_leaf];
1635        let tree = rs_merkle::MerkleTree::<MerkleSha256>::from_leaves(&leaves);
1636        let root = tree.root().expect("merkle root");
1637        let proof = tree.proof(&[0]);
1638
1639        receipt.inclusion_proof =
1640            proof.proof_hashes().iter().map(|h| format!("0x{}", hex::encode(h))).collect();
1641        receipt.merkle_root = format!("0x{}", hex::encode(root));
1642        receipt.total_leaves = leaves.len() as u32;
1643        receipt.leaf_index = 0;
1644
1645        assert!(receipt.verify_inclusion());
1646    }
1647}