1use 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
56pub const X402_VERSION: &str = "1.0";
62
63pub const X402_DOMAIN_SEPARATOR: &str = "X402_PAYMENT_V1";
65
66pub const X402_MAX_VALIDITY_SECONDS: u64 = 86400;
68
69pub const X402_DEFAULT_VALIDITY_SECONDS: u64 = 3600;
71
72pub const X402_DEFAULT_SIGNATURE_SCHEME: X402SignatureScheme = X402SignatureScheme::Ed25519MlDsa65;
74
75#[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 #[default]
89 #[strum(serialize = "set_chain", serialize = "set", serialize = "ssc")]
90 SetChain,
91 #[strum(serialize = "set_chain_testnet", serialize = "set_testnet")]
93 SetChainTestnet,
94 Base,
96 Arc,
98 #[strum(serialize = "arc_testnet", serialize = "arc-testnet")]
100 ArcTestnet,
101 BaseSepolia,
103 #[strum(serialize = "ethereum", serialize = "eth", serialize = "mainnet")]
105 Ethereum,
106 #[strum(serialize = "ethereum_sepolia", serialize = "sepolia")]
108 EthereumSepolia,
109 #[strum(serialize = "arbitrum", serialize = "arb")]
111 Arbitrum,
112 #[strum(serialize = "optimism", serialize = "op")]
114 Optimism,
115}
116
117impl X402Network {
118 #[must_use]
120 pub const fn chain_id(&self) -> u64 {
121 match self {
122 Self::SetChain => 84532001, Self::SetChainTestnet => 84532002, Self::Arc => 5042001, Self::ArcTestnet => 5042002, 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 #[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#[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 #[default]
155 #[strum(serialize = "USDC")]
156 Usdc,
157 #[strum(serialize = "USDT", serialize = "TETHER")]
159 Usdt,
160 #[serde(rename = "ssusd", alias = "ss_usd")]
162 #[strum(serialize = "ssUSD", serialize = "SSUSD", serialize = "SS_USD")]
163 SsUsd,
164 #[serde(rename = "wssusd", alias = "wss_usd")]
166 #[strum(serialize = "wssUSD", serialize = "WSSUSD", serialize = "WSS_USD")]
167 WssUsd,
168 #[strum(serialize = "DAI")]
170 Dai,
171 #[strum(serialize = "ETH", serialize = "ETHER")]
173 Eth,
174}
175
176impl X402Asset {
177 #[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 #[must_use]
188 pub const fn contract_address(&self, network: X402Network) -> Option<&'static str> {
189 match (self, network) {
190 (Self::Usdc, X402Network::SetChain) => {
192 Some("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
193 }
194 (Self::SsUsd, X402Network::SetChain) => {
195 Some("0x0000000000000000000000000000000000001001")
196 }
197 (Self::Usdc, X402Network::Arc | X402Network::ArcTestnet) => {
199 Some("0x3600000000000000000000000000000000000000")
200 }
201 (Self::Usdc, X402Network::Base) => Some("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"),
203 (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 (Self::Eth, _) => None,
213 _ => None,
214 }
215 }
216}
217
218#[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 #[default]
230 Created,
231 Signed,
233 Sequenced,
235 Batched,
237 Settled,
239 Expired,
241 Failed,
243 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#[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#[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 #[default]
287 Ed25519,
288 MlDsa65,
290 Ed25519MlDsa65,
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296pub struct X402SignatureBundle {
297 #[serde(with = "hex")]
299 pub ml_dsa_65_signature: Vec<u8>,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304pub struct X402PublicKeyBundle {
305 #[serde(with = "hex")]
307 pub ml_dsa_65_public_key: Vec<u8>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct X402PaymentIntent {
317 pub id: Uuid,
319
320 pub version: String,
322
323 pub status: X402IntentStatus,
325
326 pub payer_address: String,
331
332 pub payee_address: String,
334
335 pub amount: u64,
337
338 pub amount_decimal: Decimal,
340
341 pub asset: X402Asset,
343
344 pub network: X402Network,
346
347 pub chain_id: u64,
349
350 pub token_address: Option<String>,
352
353 pub created_at_unix: u64,
358
359 pub valid_until: u64,
361
362 pub nonce: u64,
364
365 pub idempotency_key: Option<String>,
367
368 pub resource_uri: Option<String>,
373
374 pub resource_method: Option<String>,
376
377 pub description: Option<String>,
379
380 pub cart_id: Option<Uuid>,
382
383 pub order_id: Option<Uuid>,
385
386 pub invoice_id: Option<Uuid>,
388
389 pub merchant_id: Option<String>,
391
392 pub signing_hash: Option<String>,
398
399 pub payer_signature_scheme: Option<X402SignatureScheme>,
401
402 pub payer_signature: Option<String>,
404
405 pub payer_public_key: Option<String>,
407
408 pub payer_signature_bundle: Option<X402SignatureBundle>,
410
411 pub payer_public_key_bundle: Option<X402PublicKeyBundle>,
413
414 pub sequence_number: Option<u64>,
419
420 pub sequenced_at: Option<DateTime<Utc>>,
422
423 pub batch_id: Option<Uuid>,
425
426 pub batch_merkle_root: Option<String>,
428
429 pub inclusion_proof: Option<Vec<String>>,
431
432 pub tx_hash: Option<String>,
437
438 pub block_number: Option<u64>,
440
441 pub gas_used: Option<u64>,
443
444 pub settled_at: Option<DateTime<Utc>>,
446
447 pub metadata: Option<String>,
452
453 pub created_at: DateTime<Utc>,
455
456 pub updated_at: DateTime<Utc>,
458}
459
460impl X402PaymentIntent {
461 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 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 #[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 #[must_use]
530 pub const fn with_nonce(mut self, nonce: u64) -> Self {
531 self.nonce = nonce;
532 self
533 }
534
535 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
544 self.description = Some(description.into());
545 self
546 }
547
548 #[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 pub fn with_idempotency_key(mut self, key: impl Into<String>) -> Self {
557 self.idempotency_key = Some(key.into());
558 self
559 }
560
561 #[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 #[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 #[must_use]
602 pub fn is_settled(&self) -> bool {
603 self.status == X402IntentStatus::Settled && self.tx_hash.is_some()
604 }
605
606 pub fn try_canonical_signing_data(&self) -> Result<String, X402CryptoError> {
608 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 pub fn canonical_signing_data(&self) -> Result<String, X402CryptoError> {
627 self.try_canonical_signing_data()
628 }
629
630 #[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 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 #[must_use]
671 pub fn signature_scheme(&self) -> X402SignatureScheme {
672 self.payer_signature_scheme.unwrap_or(X402SignatureScheme::Ed25519)
673 }
674
675 #[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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
833pub struct X402PaymentRequired {
834 pub version: String,
836
837 pub payee_address: String,
839
840 pub amount: u64,
842
843 pub amount_display: String,
845
846 pub asset: X402Asset,
848
849 pub networks: Vec<X402Network>,
851
852 pub resource_uri: String,
854
855 pub resource_method: String,
857
858 pub description: Option<String>,
860
861 pub validity_seconds: u64,
863
864 pub merchant_id: Option<String>,
866
867 pub merchant_name: Option<String>,
869
870 pub terms: Option<String>,
872
873 pub generated_at: DateTime<Utc>,
875}
876
877impl X402PaymentRequired {
878 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 #[must_use]
911 pub fn with_networks(mut self, networks: Vec<X402Network>) -> Self {
912 self.networks = networks;
913 self
914 }
915
916 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 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 pub fn to_header_value(&self) -> std::result::Result<String, serde_json::Error> {
932 self.try_to_header_value()
933 }
934
935 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#[derive(Debug, Clone, Serialize, Deserialize)]
953pub struct X402PaymentReceipt {
954 #[serde(alias = "id")]
956 pub receipt_id: Uuid,
957
958 pub intent_id: Uuid,
960
961 pub sequence_number: u64,
963
964 pub batch_id: Uuid,
966
967 pub merkle_root: String,
969
970 pub inclusion_proof: Vec<String>,
972
973 pub leaf_index: u32,
975
976 pub total_leaves: u32,
978
979 pub tx_hash: Option<String>,
981
982 pub block_number: Option<u64>,
984
985 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 pub signing_hash: String,
996 #[serde(default, skip_serializing_if = "Option::is_none")]
998 pub payer_signature_scheme: Option<X402SignatureScheme>,
999 pub payer_signature: String,
1001 #[serde(default, skip_serializing_if = "Option::is_none")]
1003 pub payer_signature_bundle: Option<X402SignatureBundle>,
1004
1005 pub created_at: DateTime<Utc>,
1007}
1008
1009impl X402PaymentReceipt {
1010 #[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#[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#[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#[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#[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#[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#[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 #[default]
1122 Pending,
1123 Committed,
1125 Settling,
1127 Settled,
1129 Failed,
1131}
1132
1133#[derive(Debug, Clone, Serialize, Deserialize)]
1138pub struct X402PaymentBatch {
1139 pub id: Uuid,
1141
1142 pub status: X402BatchStatus,
1144
1145 pub network: X402Network,
1147
1148 pub payment_count: u32,
1150
1151 pub total_amounts: Vec<(X402Asset, u64)>,
1153
1154 pub merkle_root: Option<String>,
1156
1157 pub prev_state_root: Option<String>,
1159
1160 pub new_state_root: Option<String>,
1162
1163 pub sequence_start: u64,
1165 pub sequence_end: u64,
1166
1167 pub tx_hash: Option<String>,
1169 pub block_number: Option<u64>,
1170 pub gas_used: Option<u64>,
1171
1172 pub created_at: DateTime<Utc>,
1174 pub committed_at: Option<DateTime<Utc>>,
1175 pub settled_at: Option<DateTime<Utc>>,
1176}
1177
1178impl X402PaymentBatch {
1179 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1209pub struct CreateX402PaymentIntent {
1210 pub payer_address: String,
1212 pub payee_address: String,
1214 pub amount: u64,
1216 pub asset: X402Asset,
1218 pub network: X402Network,
1220 pub nonce: Option<u64>,
1222 pub validity_seconds: Option<u64>,
1224 pub resource_uri: Option<String>,
1226 pub resource_method: Option<String>,
1228 pub description: Option<String>,
1230 pub cart_id: Option<Uuid>,
1232 pub order_id: Option<Uuid>,
1234 pub invoice_id: Option<Uuid>,
1236 pub merchant_id: Option<String>,
1238 pub idempotency_key: Option<String>,
1240 pub metadata: Option<String>,
1242 pub signature_scheme: Option<X402SignatureScheme>,
1244}
1245
1246#[derive(Debug, Clone, Serialize, Deserialize)]
1248pub struct SignX402PaymentIntent {
1249 pub intent_id: Uuid,
1251 pub signature_scheme: Option<X402SignatureScheme>,
1254 pub signature: String,
1256 pub public_key: String,
1258 pub signature_bundle: Option<X402SignatureBundle>,
1260 pub public_key_bundle: Option<X402PublicKeyBundle>,
1262}
1263
1264#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1266pub struct X402PaymentIntentFilter {
1267 pub payer_address: Option<String>,
1269 pub payee_address: Option<String>,
1271 pub status: Option<X402IntentStatus>,
1273 pub network: Option<X402Network>,
1275 pub asset: Option<X402Asset>,
1277 pub order_id: Option<Uuid>,
1279 pub batch_id: Option<Uuid>,
1281 pub from_date: Option<DateTime<Utc>>,
1283 pub to_date: Option<DateTime<Utc>>,
1284 pub limit: Option<u32>,
1286 pub offset: Option<u32>,
1287}
1288
1289#[must_use]
1295pub fn generate_x402_intent_id() -> Uuid {
1296 Uuid::new_v4()
1297}
1298
1299#[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 return scaled.ceil().to_u64().unwrap_or(u64::MAX);
1312 }
1313 scaled.to_u64().unwrap_or(0)
1314}
1315
1316#[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#[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, 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); 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); 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}