From 2cfbefa2921dc9a703bda2f82fbda2e6ddd7e8d5 Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 13:24:29 +0800 Subject: [PATCH 01/13] feat: implement x402 signers --- Cargo.lock | 347 +++++++++++++++++++++++- Cargo.toml | 2 +- x402-client/Cargo.toml | 6 + x402-client/src/lib.rs | 14 + x402-kit/Cargo.toml | 9 +- x402-kit/src/networks/evm.rs | 272 +------------------ x402-kit/src/networks/svm.rs | 189 +------------ x402-kit/src/schemes/exact_evm.rs | 215 +-------------- x402-kit/src/schemes/exact_svm.rs | 80 +----- x402-networks/Cargo.toml | 31 +++ x402-networks/src/evm/exact.rs | 215 +++++++++++++++ x402-networks/src/evm/mod.rs | 277 +++++++++++++++++++ x402-networks/src/lib.rs | 5 + x402-networks/src/svm/exact.rs | 79 ++++++ x402-networks/src/svm/mod.rs | 194 ++++++++++++++ x402-signer/Cargo.toml | 66 +++++ x402-signer/src/client.rs | 46 ++++ x402-signer/src/errors.rs | 34 +++ x402-signer/src/evm/constants.rs | 13 + x402-signer/src/evm/eip3009.rs | 84 ++++++ x402-signer/src/evm/mod.rs | 9 + x402-signer/src/evm/permit2.rs | 105 ++++++++ x402-signer/src/evm/signer.rs | 146 ++++++++++ x402-signer/src/evm/types.rs | 61 +++++ x402-signer/src/evm/wallet.rs | 31 +++ x402-signer/src/lib.rs | 15 ++ x402-signer/src/selector.rs | 11 + x402-signer/src/signer.rs | 49 ++++ x402-signer/src/svm/constants.rs | 23 ++ x402-signer/src/svm/mod.rs | 10 + x402-signer/src/svm/rpc.rs | 21 ++ x402-signer/src/svm/signer.rs | 159 +++++++++++ x402-signer/src/svm/transaction.rs | 143 ++++++++++ x402-signer/src/svm/types.rs | 9 + x402-signer/src/svm/wallet.rs | 29 ++ x402-signer/tests/signing.rs | 409 +++++++++++++++++++++++++++++ 36 files changed, 2654 insertions(+), 754 deletions(-) create mode 100644 x402-client/Cargo.toml create mode 100644 x402-client/src/lib.rs create mode 100644 x402-networks/Cargo.toml create mode 100644 x402-networks/src/evm/exact.rs create mode 100644 x402-networks/src/evm/mod.rs create mode 100644 x402-networks/src/lib.rs create mode 100644 x402-networks/src/svm/exact.rs create mode 100644 x402-networks/src/svm/mod.rs create mode 100644 x402-signer/Cargo.toml create mode 100644 x402-signer/src/client.rs create mode 100644 x402-signer/src/errors.rs create mode 100644 x402-signer/src/evm/constants.rs create mode 100644 x402-signer/src/evm/eip3009.rs create mode 100644 x402-signer/src/evm/mod.rs create mode 100644 x402-signer/src/evm/permit2.rs create mode 100644 x402-signer/src/evm/signer.rs create mode 100644 x402-signer/src/evm/types.rs create mode 100644 x402-signer/src/evm/wallet.rs create mode 100644 x402-signer/src/lib.rs create mode 100644 x402-signer/src/selector.rs create mode 100644 x402-signer/src/signer.rs create mode 100644 x402-signer/src/svm/constants.rs create mode 100644 x402-signer/src/svm/mod.rs create mode 100644 x402-signer/src/svm/rpc.rs create mode 100644 x402-signer/src/svm/signer.rs create mode 100644 x402-signer/src/svm/transaction.rs create mode 100644 x402-signer/src/svm/types.rs create mode 100644 x402-signer/src/svm/wallet.rs create mode 100644 x402-signer/tests/signing.rs diff --git a/Cargo.lock b/Cargo.lock index 9f94d70..9604cc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,6 +1179,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bincode" version = "2.0.1" @@ -1556,6 +1565,34 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling" version = "0.21.3" @@ -1753,6 +1790,31 @@ dependencies = [ "spki", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "educe" version = "0.6.0" @@ -1877,6 +1939,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -2918,6 +2986,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3648,6 +3725,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -3779,6 +3865,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + [[package]] name = "sha3" version = "0.10.8" @@ -3875,6 +3967,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "solana-address" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" +dependencies = [ + "solana-address 2.2.0", +] + [[package]] name = "solana-address" version = "2.2.0" @@ -3882,13 +3983,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68c5d02824391b072dc5cd0aaa85fb0af9784a21d23286a767994d1e8a322131" dependencies = [ "borsh", + "curve25519-dalek", "five8", "five8_const", "serde", + "serde_derive", + "sha2-const-stable", "solana-atomic-u64", - "solana-define-syscall", + "solana-define-syscall 5.0.0", "solana-program-error", "solana-sanitize", + "solana-sha256-hasher", "wincode", ] @@ -3901,25 +4006,124 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + [[package]] name = "solana-define-syscall" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03aacdd7a61e2109887a7a7f046caebafce97ddf1150f33722eeac04f9039c73" +[[package]] +name = "solana-hash" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" +dependencies = [ + "solana-hash 4.2.0", +] + +[[package]] +name = "solana-hash" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8064ea1d591ec791be95245058ca40f4f5345d390c200069d0f79bbf55bfae55" +dependencies = [ + "borsh", + "five8", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wincode", +] + +[[package]] +name = "solana-instruction" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a6d22d0a6fdf345be294bb9afdcd40c296cdc095e64e7ceaa3bb3c2f608c1c" +dependencies = [ + "borsh", + "serde", + "solana-define-syscall 5.0.0", + "solana-instruction-error", + "solana-pubkey 4.1.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3d048edaaeef5a3dc8c01853e585539a74417e4c2d43a9e2c161270045b838" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-program-error", +] + +[[package]] +name = "solana-keypair" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263d614c12aa267a3278703175fd6440552ca61bc960b5a02a4482720c53438b" +dependencies = [ + "ed25519-dalek", + "five8", + "five8_core", + "rand 0.9.2", + "solana-address 2.2.0", + "solana-seed-phrase", + "solana-signature", + "solana-signer", +] + +[[package]] +name = "solana-message" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0448b1fd891c5f46491e5dc7d9986385ba3c852c340db2911dd29faa01d2b08d" +dependencies = [ + "bincode 1.3.3", + "lazy_static", + "serde", + "serde_derive", + "solana-address 2.2.0", + "solana-hash 4.2.0", + "solana-instruction", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-transaction-error", +] + [[package]] name = "solana-program-error" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" +[[package]] +name = "solana-pubkey" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" +dependencies = [ + "solana-address 1.1.0", +] + [[package]] name = "solana-pubkey" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b06bd918d60111ee1f97de817113e2040ca0cedb740099ee8d646233f6b906c" dependencies = [ - "solana-address", + "solana-address 2.2.0", ] [[package]] @@ -3928,18 +4132,106 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" +[[package]] +name = "solana-sdk-ids" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" +dependencies = [ + "solana-address 2.2.0", +] + +[[package]] +name = "solana-seed-phrase" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" +dependencies = [ + "hmac", + "pbkdf2", + "sha2", +] + +[[package]] +name = "solana-sha256-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +dependencies = [ + "sha2", + "solana-define-syscall 4.0.1", + "solana-hash 4.2.0", +] + +[[package]] +name = "solana-short-vec" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3bd991c2cc415291c86bb0b6b4d53e93d13bb40344e4c5a2884e0e4f5fa93f" +dependencies = [ + "serde_core", +] + [[package]] name = "solana-signature" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "132a93134f1262aa832f1849b83bec6c9945669b866da18661a427943b9e801e" dependencies = [ + "ed25519-dalek", "five8", "serde", + "serde-big-array", + "serde_derive", "solana-sanitize", "wincode", ] +[[package]] +name = "solana-signer" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +dependencies = [ + "solana-pubkey 3.0.0", + "solana-signature", + "solana-transaction-error", +] + +[[package]] +name = "solana-transaction" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96697cff5075a028265324255efed226099f6d761ca67342b230d09f72cc48d2" +dependencies = [ + "bincode 1.3.3", + "serde", + "serde_derive", + "solana-address 2.2.0", + "solana-hash 4.2.0", + "solana-instruction", + "solana-instruction-error", + "solana-message", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-signature", + "solana-signer", + "solana-transaction-error", +] + +[[package]] +name = "solana-transaction-error" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8396904805b0b385b9de115a652fe80fd01e5b98ce0513f4fcd8184ada9bb792" +dependencies = [ + "serde", + "serde_derive", + "solana-instruction-error", + "solana-sanitize", +] + [[package]] name = "spki" version = "0.7.3" @@ -5022,6 +5314,10 @@ dependencies = [ "tap", ] +[[package]] +name = "x402-client" +version = "0.1.0" + [[package]] name = "x402-core" version = "2.3.0" @@ -5055,7 +5351,7 @@ dependencies = [ "alloy-primitives", "alloy-signer", "axum", - "bincode", + "bincode 2.0.1", "bon", "futures-util", "hex", @@ -5064,7 +5360,7 @@ dependencies = [ "reqwest-middleware", "serde", "serde_json", - "solana-pubkey", + "solana-pubkey 4.1.0", "solana-signature", "thiserror 2.0.18", "tokio", @@ -5075,9 +5371,24 @@ dependencies = [ "url-macro", "x402-core", "x402-extensions", + "x402-networks", "x402-paywall", ] +[[package]] +name = "x402-networks" +version = "2.3.0" +dependencies = [ + "alloy-primitives", + "bon", + "hex", + "serde", + "serde_json", + "solana-pubkey 4.1.0", + "solana-signature", + "x402-core", +] + [[package]] name = "x402-paywall" version = "2.3.0" @@ -5091,6 +5402,34 @@ dependencies = [ "x402-core", ] +[[package]] +name = "x402-signer" +version = "2.3.0" +dependencies = [ + "alloy", + "alloy-core", + "alloy-primitives", + "alloy-signer", + "base64", + "bincode 2.0.1", + "hex", + "rand 0.9.2", + "serde", + "serde_json", + "solana-hash 3.1.0", + "solana-instruction", + "solana-keypair", + "solana-message", + "solana-pubkey 4.1.0", + "solana-signature", + "solana-signer", + "solana-transaction", + "thiserror 2.0.18", + "tokio", + "x402-core", + "x402-networks", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 7f6ec4f..e62a9e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["x402-core", "x402-extensions", "x402-kit", "x402-paywall"] +members = ["x402-client","x402-core", "x402-extensions", "x402-kit", "x402-networks", "x402-paywall", "x402-signer"] diff --git a/x402-client/Cargo.toml b/x402-client/Cargo.toml new file mode 100644 index 0000000..55fcb20 --- /dev/null +++ b/x402-client/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "x402-client" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/x402-client/src/lib.rs b/x402-client/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/x402-client/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/x402-kit/Cargo.toml b/x402-kit/Cargo.toml index 2baed48..a080c41 100644 --- a/x402-kit/Cargo.toml +++ b/x402-kit/Cargo.toml @@ -8,7 +8,13 @@ license = "MIT" description = "(V2 Supported) A fully modular SDK for building complex X402 payment integrations." [features] -default = ["facilitator-client", "evm-signer", "svm-signer", "axum", "actix-web"] +default = [ + "facilitator-client", + "evm-signer", + "svm-signer", + "axum", + "actix-web", +] facilitator-client = ["dep:http", "dep:reqwest-middleware"] evm-signer = ["dep:alloy-core", "dep:alloy-signer", "dep:rand"] svm-signer = ["dep:bincode"] @@ -19,6 +25,7 @@ actix-web = ["paywall", "x402-paywall/actix-web"] [dependencies] # === Core Deps === x402-core = { version = "2.3.0", path = "../x402-core" } +x402-networks = { version = "2.3.0", path = "../x402-networks" } x402-extensions = { version = "0.2.0", path = "../x402-extensions" } hex = { version = "0.4" } alloy-primitives = { version = "1.4" } diff --git a/x402-kit/src/networks/evm.rs b/x402-kit/src/networks/evm.rs index 736949b..4523214 100644 --- a/x402-kit/src/networks/evm.rs +++ b/x402-kit/src/networks/evm.rs @@ -1,275 +1,9 @@ -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; - -use serde::{Deserialize, Serialize}; - -use crate::core::{Address, Asset, NetworkFamily}; - -#[derive(Debug, Clone, Copy)] -pub struct EvmNetwork { - pub name: &'static str, - pub chain_id: u64, - pub network_id: &'static str, -} - -impl NetworkFamily for EvmNetwork { - fn network_name(&self) -> &str { - self.name - } - fn network_id(&self) -> &str { - self.network_id - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct EvmAddress(pub alloy_primitives::Address); - -impl From for EvmAddress { - fn from(addr: alloy_primitives::Address) -> Self { - EvmAddress(addr) - } -} - -impl FromStr for EvmAddress { - type Err = alloy_primitives::AddressError; - - fn from_str(s: &str) -> Result { - let addr = alloy_primitives::Address::from_str(s)?; - Ok(EvmAddress(addr)) - } -} - -impl Display for EvmAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Debug for EvmAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "EvmAddress({})", self.0) - } -} - -impl Serialize for EvmAddress { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for EvmAddress { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - EvmAddress::from_str(&s).map_err(serde::de::Error::custom) - } -} - -impl Address for EvmAddress { - type Network = EvmNetwork; -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct EvmSignature(pub alloy_primitives::Signature); - -impl Display for EvmSignature { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Debug for EvmSignature { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "EvmSignature({})", self.0) - } -} - -impl FromStr for EvmSignature { - type Err = alloy_primitives::SignatureError; - - fn from_str(s: &str) -> Result { - let sig = alloy_primitives::Signature::from_str(s)?; - Ok(EvmSignature(sig)) - } -} - -impl Serialize for EvmSignature { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for EvmSignature { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - EvmSignature::from_str(&s).map_err(serde::de::Error::custom) - } -} - -impl From for EvmSignature { - fn from(sig: alloy_primitives::Signature) -> Self { - EvmSignature(sig) - } -} - -pub type EvmAsset = Asset; - -pub trait ExplicitEvmNetwork { - const NETWORK: EvmNetwork; -} - -#[derive(Debug, Clone, Copy, Serialize)] -pub struct Eip712Domain { - pub name: &'static str, - pub version: &'static str, -} - -pub trait ExplicitEvmAsset { - type Network: ExplicitEvmNetwork; - - const ASSET: EvmAsset; - const EIP712_DOMAIN: Option; -} - -impl From for EvmNetwork -where - T: ExplicitEvmNetwork, -{ - fn from(_: T) -> Self { - T::NETWORK - } -} +pub use x402_networks::evm::*; pub mod networks { - use super::*; - - macro_rules! define_explicit_evm_network { - ($struct_name:ident, $network_const:expr) => { - pub struct $struct_name; - - impl ExplicitEvmNetwork for $struct_name { - const NETWORK: EvmNetwork = $network_const; - } - }; - } - - define_explicit_evm_network!( - Ethereum, - EvmNetwork { - name: "ethereum", - chain_id: 1, - network_id: "eip155:1", - } - ); - define_explicit_evm_network!( - EthereumSepolia, - EvmNetwork { - name: "ethereum-sepolia", - chain_id: 11155111, - network_id: "eip155:11155111", - } - ); - define_explicit_evm_network!( - Base, - EvmNetwork { - name: "base", - chain_id: 8453, - network_id: "eip155:8453", - } - ); - define_explicit_evm_network!( - BaseSepolia, - EvmNetwork { - name: "base-sepolia", - chain_id: 84532, - network_id: "eip155:84532", - } - ); + pub use x402_networks::evm::networks::*; } pub mod assets { - use alloy_primitives::address; - - use super::*; - - macro_rules! define_explicit_evm_asset { - ( - $struct_name:ident, - $network_struct:ty, - $addr:expr, - $decimals:expr, - $name:expr, - $symbol:expr, - $eip712_domain:expr - ) => { - pub struct $struct_name; - - impl ExplicitEvmAsset for $struct_name { - type Network = $network_struct; - - const ASSET: EvmAsset = EvmAsset { - address: EvmAddress(address!($addr)), - decimals: $decimals, - name: $name, - symbol: $symbol, - }; - - const EIP712_DOMAIN: Option = $eip712_domain; - } - }; - } - - macro_rules! define_explicit_usdc { - ($struct_name:ident, $network_struct:ty, $addr:expr) => { - define_explicit_evm_asset!( - $struct_name, - $network_struct, - $addr, - 6, - "USD Coin", - "USDC", - Some(Eip712Domain { - name: "USD Coin", - version: "2", - }) - ); - }; - } - - define_explicit_usdc!( - UsdcEthereum, - networks::Ethereum, - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - ); - - define_explicit_usdc!( - UsdcEthereumSepolia, - networks::EthereumSepolia, - "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" - ); - - define_explicit_usdc!( - UsdcBase, - networks::Base, - "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" - ); - - define_explicit_usdc!( - UsdcBaseSepolia, - networks::BaseSepolia, - "0x036CbD53842c5426634e7929541eC2318f3dCF7e" - ); + pub use x402_networks::evm::assets::*; } diff --git a/x402-kit/src/networks/svm.rs b/x402-kit/src/networks/svm.rs index 453570f..2a1591e 100644 --- a/x402-kit/src/networks/svm.rs +++ b/x402-kit/src/networks/svm.rs @@ -1,192 +1,9 @@ -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; - -use serde::{Deserialize, Serialize}; -use solana_pubkey::{ParsePubkeyError, Pubkey}; - -use crate::core::{Address, NetworkFamily}; - -pub struct SvmNetwork { - pub name: &'static str, - pub caip_2_id: &'static str, -} - -impl NetworkFamily for SvmNetwork { - fn network_name(&self) -> &str { - self.name - } - - fn network_id(&self) -> &str { - self.caip_2_id - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct SvmAddress(pub Pubkey); - -impl From for SvmAddress { - fn from(pk: Pubkey) -> Self { - SvmAddress(pk) - } -} - -impl FromStr for SvmAddress { - type Err = ParsePubkeyError; - - fn from_str(s: &str) -> Result { - let pk = Pubkey::from_str(s)?; - Ok(SvmAddress(pk)) - } -} - -impl Display for SvmAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Debug for SvmAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SvmAddress({})", self.0) - } -} - -impl Serialize for SvmAddress { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for SvmAddress { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let pk = Pubkey::from_str(&s).map_err(serde::de::Error::custom)?; - Ok(SvmAddress(pk)) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct SvmSignature(pub solana_signature::Signature); - -impl FromStr for SvmSignature { - type Err = solana_signature::ParseSignatureError; - - fn from_str(s: &str) -> Result { - let sig = solana_signature::Signature::from_str(s)?; - Ok(SvmSignature(sig)) - } -} - -impl Debug for SvmSignature { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SvmSignature({})", self.0) - } -} - -impl Display for SvmSignature { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Serialize for SvmSignature { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for SvmSignature { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let sig = solana_signature::Signature::from_str(&s).map_err(serde::de::Error::custom)?; - Ok(SvmSignature(sig)) - } -} - -impl Address for SvmAddress { - type Network = SvmNetwork; -} - -pub type SvmAsset = crate::core::Asset; - -pub trait ExplicitSvmNetwork { - const NETWORK: SvmNetwork; -} - -pub trait ExplicitSvmAsset { - type Network: ExplicitSvmNetwork; - const ASSET: SvmAsset; -} +pub use x402_networks::svm::*; pub mod networks { - use super::*; - - pub struct Solana; - impl ExplicitSvmNetwork for Solana { - const NETWORK: SvmNetwork = SvmNetwork { - name: "solana", - caip_2_id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", - }; - } - - pub struct SolanaDevnet; - impl ExplicitSvmNetwork for SolanaDevnet { - const NETWORK: SvmNetwork = SvmNetwork { - name: "solana-devnet", - caip_2_id: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", - }; - } - - pub struct SolanaTestnet; - impl ExplicitSvmNetwork for SolanaTestnet { - const NETWORK: SvmNetwork = SvmNetwork { - name: "solana-testnet", - caip_2_id: "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z", - }; - } + pub use x402_networks::svm::networks::*; } pub mod assets { - use solana_pubkey::pubkey; - - use super::*; - - macro_rules! create_usdc { - ($addr:expr) => { - SvmAsset { - address: SvmAddress($addr), - decimals: 6, - name: "USD Coin", - symbol: "USDC", - } - }; - } - - pub struct UsdcSolana; - impl ExplicitSvmAsset for UsdcSolana { - type Network = networks::Solana; - const ASSET: SvmAsset = - create_usdc!(pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")); - } - - pub struct UsdcSolanaDevnet; - impl ExplicitSvmAsset for UsdcSolanaDevnet { - type Network = networks::SolanaDevnet; - const ASSET: SvmAsset = - create_usdc!(pubkey!("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")); - } + pub use x402_networks::svm::assets::*; } diff --git a/x402-kit/src/schemes/exact_evm.rs b/x402-kit/src/schemes/exact_evm.rs index 9fbf5f9..5f5543e 100644 --- a/x402-kit/src/schemes/exact_evm.rs +++ b/x402-kit/src/schemes/exact_evm.rs @@ -1,214 +1 @@ -use bon::Builder; -use serde::{Deserialize, Serialize}; - -use crate::{ - core::{Payment, Scheme}, - networks::evm::{EvmAddress, EvmNetwork, EvmSignature, ExplicitEvmAsset, ExplicitEvmNetwork}, - transport::PaymentRequirements, - types::{AmountValue, AnyJson}, -}; - -use std::{ - fmt::{Debug, Display}, - str::FromStr, -}; - -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct Nonce(pub [u8; 32]); - -impl Debug for Nonce { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Nonce(0x{})", hex::encode(self.0)) - } -} - -impl Display for Nonce { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "0x{}", hex::encode(self.0)) - } -} - -impl FromStr for Nonce { - type Err = hex::FromHexError; - - fn from_str(s: &str) -> Result { - let s = s.strip_prefix("0x").unwrap_or(s); - let bytes = hex::decode(s)?; - if bytes.len() != 32 { - return Err(hex::FromHexError::InvalidStringLength); - } - let mut arr = [0u8; 32]; - arr.copy_from_slice(&bytes); - Ok(Nonce(arr)) - } -} - -impl Serialize for Nonce { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for Nonce { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let nonce = Nonce::from_str(&s).map_err(serde::de::Error::custom)?; - Ok(nonce) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct TimestampSeconds(pub u64); - -impl Display for TimestampSeconds { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Debug for TimestampSeconds { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "TimeSeconds({})", self.0) - } -} - -impl Serialize for TimestampSeconds { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for TimestampSeconds { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let seconds = s.parse::().map_err(serde::de::Error::custom)?; - Ok(TimestampSeconds(seconds)) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ExactEvmPayload { - pub signature: EvmSignature, - pub authorization: ExactEvmAuthorization, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ExactEvmAuthorization { - pub from: EvmAddress, - pub to: EvmAddress, - pub value: AmountValue, - pub valid_after: TimestampSeconds, - pub valid_before: TimestampSeconds, - pub nonce: Nonce, -} - -/// Exact EVM Scheme information holder -pub struct ExactEvmScheme(pub EvmNetwork); - -impl Scheme for ExactEvmScheme { - type Network = EvmNetwork; - type Payload = ExactEvmPayload; - const SCHEME_NAME: &'static str = "exact"; - - fn network(&self) -> &Self::Network { - &self.0 - } -} - -#[derive(Builder, Debug, Clone)] -pub struct ExactEvm { - pub asset: A, - #[builder(into)] - pub pay_to: EvmAddress, - pub amount: u64, - pub max_timeout_seconds_override: Option, - pub extra_override: Option, -} - -impl From> for Payment { - fn from(scheme: ExactEvm) -> Self { - Payment { - scheme: ExactEvmScheme(A::Network::NETWORK), - pay_to: scheme.pay_to, - asset: A::ASSET, - amount: scheme.amount.into(), - max_timeout_seconds: scheme.max_timeout_seconds_override.unwrap_or(300), - extra: scheme - .extra_override - .or(A::EIP712_DOMAIN.and_then(|v| serde_json::to_value(v).ok())), - } - } -} - -impl From> for PaymentRequirements { - fn from(scheme: ExactEvm) -> Self { - PaymentRequirements::from(Payment::from(scheme)) - } -} - -#[cfg(test)] -mod tests { - use alloy_primitives::address; - use serde_json::json; - - use crate::networks::evm::assets::UsdcBaseSepolia; - - use super::*; - - #[test] - fn test_build_payment_requirements() { - let scheme = ExactEvm::builder() - .asset(UsdcBaseSepolia) - .amount(1000) - .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) - .build(); - let payment_requirements: PaymentRequirements = scheme.into(); - - assert_eq!(payment_requirements.scheme, "exact"); - assert_eq!( - payment_requirements.asset, - UsdcBaseSepolia::ASSET.address.to_string() - ); - assert_eq!(payment_requirements.amount, 1000u64.into()); - } - - #[test] - fn test_extra_override() { - let pr: PaymentRequirements = ExactEvm::builder() - .asset(UsdcBaseSepolia) - .amount(1000) - .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) - .build() - .into(); - - assert!(pr.extra.is_some()); - assert_eq!( - pr.extra, - serde_json::to_value(UsdcBaseSepolia::EIP712_DOMAIN).ok() - ); - - let pr: PaymentRequirements = ExactEvm::builder() - .asset(UsdcBaseSepolia) - .amount(1000) - .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) - .extra_override(json!({"foo": "bar"})) - .build() - .into(); - - assert_eq!(pr.extra, Some(json!({"foo": "bar"}))); - } -} +pub use x402_networks::evm::exact::*; diff --git a/x402-kit/src/schemes/exact_svm.rs b/x402-kit/src/schemes/exact_svm.rs index cd9022c..cd8f541 100644 --- a/x402-kit/src/schemes/exact_svm.rs +++ b/x402-kit/src/schemes/exact_svm.rs @@ -1,79 +1 @@ -use bon::Builder; -use serde::{Deserialize, Serialize}; - -use crate::{ - core::{Payment, Scheme}, - networks::svm::{ExplicitSvmAsset, ExplicitSvmNetwork, SvmAddress, SvmNetwork}, - transport::PaymentRequirements, -}; - -#[derive(Builder, Debug, Clone)] -pub struct ExactSvm { - pub asset: A, - #[builder(into)] - pub pay_to: SvmAddress, - pub amount: u64, - pub max_timeout_seconds_override: Option, -} - -impl From> for Payment { - fn from(scheme: ExactSvm) -> Self { - Payment { - scheme: ExactSvmScheme(A::Network::NETWORK), - pay_to: scheme.pay_to, - asset: A::ASSET, - amount: scheme.amount.into(), - max_timeout_seconds: scheme.max_timeout_seconds_override.unwrap_or(300), - extra: None, - } - } -} - -impl From> for PaymentRequirements { - fn from(scheme: ExactSvm) -> Self { - PaymentRequirements::from(Payment::from(scheme)) - } -} - -pub struct ExactSvmScheme(pub SvmNetwork); - -impl Scheme for ExactSvmScheme { - type Network = SvmNetwork; - type Payload = ExplicitSvmPayload; - const SCHEME_NAME: &'static str = "exact"; - - fn network(&self) -> &Self::Network { - &self.0 - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ExplicitSvmPayload { - pub transaction: String, -} - -#[cfg(test)] -mod tests { - use solana_pubkey::pubkey; - - use crate::{ - networks::svm::assets::UsdcSolanaDevnet, schemes::exact_svm::ExactSvm, - transport::PaymentRequirements, - }; - - #[test] - fn test_build_payment_requirements() { - let pr: PaymentRequirements = ExactSvm::builder() - .asset(UsdcSolanaDevnet) - .amount(1000) - .pay_to(pubkey!("Ge3jkza5KRfXvaq3GELNLh6V1pjjdEKNpEdGXJgjjKUR")) - .build() - .into(); - - assert_eq!(pr.scheme, "exact"); - assert_eq!(pr.network, "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"); - assert_eq!(pr.amount, 1000u64.into()); - assert!(pr.extra.is_none()); - } -} +pub use x402_networks::svm::exact::*; diff --git a/x402-networks/Cargo.toml b/x402-networks/Cargo.toml new file mode 100644 index 0000000..ff3c3ab --- /dev/null +++ b/x402-networks/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "x402-networks" +version = "2.3.0" +edition = "2024" +repository = "https://github.com/AIMOverse/x402-kit" +authors = ["Archer "] +license = "MIT" +description = "Concrete chain types and scheme definitions for the x402 payment protocol." + +[features] +default = ["evm", "svm"] +evm = ["dep:alloy-primitives", "dep:hex", "dep:bon"] +svm = ["dep:solana-pubkey", "dep:solana-signature", "dep:bon"] + +[dependencies] +x402-core = { version = "2.3.0", path = "../x402-core" } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["preserve_order"] } + +# === Feature "evm" === +alloy-primitives = { version = "1.4", optional = true } +hex = { version = "0.4", optional = true } +bon = { version = "3.8", optional = true } + +# === Feature "svm" === +solana-pubkey = { version = "4.0", optional = true } +solana-signature = { version = "3.1", optional = true } + +[dev-dependencies] +alloy-primitives = { version = "1.4" } +solana-pubkey = { version = "4.0" } diff --git a/x402-networks/src/evm/exact.rs b/x402-networks/src/evm/exact.rs new file mode 100644 index 0000000..a5a0b57 --- /dev/null +++ b/x402-networks/src/evm/exact.rs @@ -0,0 +1,215 @@ +use bon::Builder; +use serde::{Deserialize, Serialize}; + +use x402_core::{ + core::{Payment, Scheme}, + transport::PaymentRequirements, + types::{AmountValue, AnyJson}, +}; + +use crate::evm::{EvmAddress, EvmNetwork, EvmSignature, ExplicitEvmAsset, ExplicitEvmNetwork}; + +use std::{ + fmt::{Debug, Display}, + str::FromStr, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct Nonce(pub [u8; 32]); + +impl Debug for Nonce { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Nonce(0x{})", hex::encode(self.0)) + } +} + +impl Display for Nonce { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "0x{}", hex::encode(self.0)) + } +} + +impl FromStr for Nonce { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result { + let s = s.strip_prefix("0x").unwrap_or(s); + let bytes = hex::decode(s)?; + if bytes.len() != 32 { + return Err(hex::FromHexError::InvalidStringLength); + } + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + Ok(Nonce(arr)) + } +} + +impl Serialize for Nonce { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Nonce { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let nonce = Nonce::from_str(&s).map_err(serde::de::Error::custom)?; + Ok(nonce) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct TimestampSeconds(pub u64); + +impl Display for TimestampSeconds { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Debug for TimestampSeconds { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "TimeSeconds({})", self.0) + } +} + +impl Serialize for TimestampSeconds { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for TimestampSeconds { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let seconds = s.parse::().map_err(serde::de::Error::custom)?; + Ok(TimestampSeconds(seconds)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExactEvmPayload { + pub signature: EvmSignature, + pub authorization: ExactEvmAuthorization, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExactEvmAuthorization { + pub from: EvmAddress, + pub to: EvmAddress, + pub value: AmountValue, + pub valid_after: TimestampSeconds, + pub valid_before: TimestampSeconds, + pub nonce: Nonce, +} + +/// Exact EVM Scheme information holder +pub struct ExactEvmScheme(pub EvmNetwork); + +impl Scheme for ExactEvmScheme { + type Network = EvmNetwork; + type Payload = ExactEvmPayload; + const SCHEME_NAME: &'static str = "exact"; + + fn network(&self) -> &Self::Network { + &self.0 + } +} + +#[derive(Builder, Debug, Clone)] +pub struct ExactEvm { + pub asset: A, + #[builder(into)] + pub pay_to: EvmAddress, + pub amount: u64, + pub max_timeout_seconds_override: Option, + pub extra_override: Option, +} + +impl From> for Payment { + fn from(scheme: ExactEvm) -> Self { + Payment { + scheme: ExactEvmScheme(A::Network::NETWORK), + pay_to: scheme.pay_to, + asset: A::ASSET, + amount: scheme.amount.into(), + max_timeout_seconds: scheme.max_timeout_seconds_override.unwrap_or(300), + extra: scheme + .extra_override + .or(A::EIP712_DOMAIN.and_then(|v| serde_json::to_value(v).ok())), + } + } +} + +impl From> for PaymentRequirements { + fn from(scheme: ExactEvm) -> Self { + PaymentRequirements::from(Payment::from(scheme)) + } +} + +#[cfg(test)] +mod tests { + use alloy_primitives::address; + use serde_json::json; + + use crate::evm::assets::UsdcBaseSepolia; + + use super::*; + + #[test] + fn test_build_payment_requirements() { + let scheme = ExactEvm::builder() + .asset(UsdcBaseSepolia) + .amount(1000) + .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) + .build(); + let payment_requirements: PaymentRequirements = scheme.into(); + + assert_eq!(payment_requirements.scheme, "exact"); + assert_eq!( + payment_requirements.asset, + UsdcBaseSepolia::ASSET.address.to_string() + ); + assert_eq!(payment_requirements.amount, 1000u64.into()); + } + + #[test] + fn test_extra_override() { + let pr: PaymentRequirements = ExactEvm::builder() + .asset(UsdcBaseSepolia) + .amount(1000) + .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) + .build() + .into(); + + assert!(pr.extra.is_some()); + assert_eq!( + pr.extra, + serde_json::to_value(UsdcBaseSepolia::EIP712_DOMAIN).ok() + ); + + let pr: PaymentRequirements = ExactEvm::builder() + .asset(UsdcBaseSepolia) + .amount(1000) + .pay_to(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")) + .extra_override(json!({"foo": "bar"})) + .build() + .into(); + + assert_eq!(pr.extra, Some(json!({"foo": "bar"}))); + } +} diff --git a/x402-networks/src/evm/mod.rs b/x402-networks/src/evm/mod.rs new file mode 100644 index 0000000..b7b4ee3 --- /dev/null +++ b/x402-networks/src/evm/mod.rs @@ -0,0 +1,277 @@ +use std::{ + fmt::{Debug, Display}, + str::FromStr, +}; + +use serde::{Deserialize, Serialize}; + +use x402_core::core::{Address, Asset, NetworkFamily}; + +pub mod exact; + +#[derive(Debug, Clone, Copy)] +pub struct EvmNetwork { + pub name: &'static str, + pub chain_id: u64, + pub network_id: &'static str, +} + +impl NetworkFamily for EvmNetwork { + fn network_name(&self) -> &str { + self.name + } + fn network_id(&self) -> &str { + self.network_id + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct EvmAddress(pub alloy_primitives::Address); + +impl From for EvmAddress { + fn from(addr: alloy_primitives::Address) -> Self { + EvmAddress(addr) + } +} + +impl FromStr for EvmAddress { + type Err = alloy_primitives::AddressError; + + fn from_str(s: &str) -> Result { + let addr = alloy_primitives::Address::from_str(s)?; + Ok(EvmAddress(addr)) + } +} + +impl Display for EvmAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Debug for EvmAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "EvmAddress({})", self.0) + } +} + +impl Serialize for EvmAddress { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for EvmAddress { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + EvmAddress::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl Address for EvmAddress { + type Network = EvmNetwork; +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct EvmSignature(pub alloy_primitives::Signature); + +impl Display for EvmSignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Debug for EvmSignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "EvmSignature({})", self.0) + } +} + +impl FromStr for EvmSignature { + type Err = alloy_primitives::SignatureError; + + fn from_str(s: &str) -> Result { + let sig = alloy_primitives::Signature::from_str(s)?; + Ok(EvmSignature(sig)) + } +} + +impl Serialize for EvmSignature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for EvmSignature { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + EvmSignature::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl From for EvmSignature { + fn from(sig: alloy_primitives::Signature) -> Self { + EvmSignature(sig) + } +} + +pub type EvmAsset = Asset; + +pub trait ExplicitEvmNetwork { + const NETWORK: EvmNetwork; +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub struct Eip712Domain { + pub name: &'static str, + pub version: &'static str, +} + +pub trait ExplicitEvmAsset { + type Network: ExplicitEvmNetwork; + + const ASSET: EvmAsset; + const EIP712_DOMAIN: Option; +} + +impl From for EvmNetwork +where + T: ExplicitEvmNetwork, +{ + fn from(_: T) -> Self { + T::NETWORK + } +} + +pub mod networks { + use super::*; + + macro_rules! define_explicit_evm_network { + ($struct_name:ident, $network_const:expr) => { + pub struct $struct_name; + + impl ExplicitEvmNetwork for $struct_name { + const NETWORK: EvmNetwork = $network_const; + } + }; + } + + define_explicit_evm_network!( + Ethereum, + EvmNetwork { + name: "ethereum", + chain_id: 1, + network_id: "eip155:1", + } + ); + define_explicit_evm_network!( + EthereumSepolia, + EvmNetwork { + name: "ethereum-sepolia", + chain_id: 11155111, + network_id: "eip155:11155111", + } + ); + define_explicit_evm_network!( + Base, + EvmNetwork { + name: "base", + chain_id: 8453, + network_id: "eip155:8453", + } + ); + define_explicit_evm_network!( + BaseSepolia, + EvmNetwork { + name: "base-sepolia", + chain_id: 84532, + network_id: "eip155:84532", + } + ); +} + +pub mod assets { + use alloy_primitives::address; + + use super::*; + + macro_rules! define_explicit_evm_asset { + ( + $struct_name:ident, + $network_struct:ty, + $addr:expr, + $decimals:expr, + $name:expr, + $symbol:expr, + $eip712_domain:expr + ) => { + pub struct $struct_name; + + impl ExplicitEvmAsset for $struct_name { + type Network = $network_struct; + + const ASSET: EvmAsset = EvmAsset { + address: EvmAddress(address!($addr)), + decimals: $decimals, + name: $name, + symbol: $symbol, + }; + + const EIP712_DOMAIN: Option = $eip712_domain; + } + }; + } + + macro_rules! define_explicit_usdc { + ($struct_name:ident, $network_struct:ty, $addr:expr) => { + define_explicit_evm_asset!( + $struct_name, + $network_struct, + $addr, + 6, + "USD Coin", + "USDC", + Some(Eip712Domain { + name: "USD Coin", + version: "2", + }) + ); + }; + } + + define_explicit_usdc!( + UsdcEthereum, + networks::Ethereum, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ); + + define_explicit_usdc!( + UsdcEthereumSepolia, + networks::EthereumSepolia, + "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" + ); + + define_explicit_usdc!( + UsdcBase, + networks::Base, + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + ); + + define_explicit_usdc!( + UsdcBaseSepolia, + networks::BaseSepolia, + "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + ); +} diff --git a/x402-networks/src/lib.rs b/x402-networks/src/lib.rs new file mode 100644 index 0000000..6fa25cf --- /dev/null +++ b/x402-networks/src/lib.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "evm")] +pub mod evm; + +#[cfg(feature = "svm")] +pub mod svm; diff --git a/x402-networks/src/svm/exact.rs b/x402-networks/src/svm/exact.rs new file mode 100644 index 0000000..4327056 --- /dev/null +++ b/x402-networks/src/svm/exact.rs @@ -0,0 +1,79 @@ +use bon::Builder; +use serde::{Deserialize, Serialize}; + +use x402_core::{ + core::{Payment, Scheme}, + transport::PaymentRequirements, +}; + +use crate::svm::{ExplicitSvmAsset, ExplicitSvmNetwork, SvmAddress, SvmNetwork}; + +#[derive(Builder, Debug, Clone)] +pub struct ExactSvm { + pub asset: A, + #[builder(into)] + pub pay_to: SvmAddress, + pub amount: u64, + pub max_timeout_seconds_override: Option, +} + +impl From> for Payment { + fn from(scheme: ExactSvm) -> Self { + Payment { + scheme: ExactSvmScheme(A::Network::NETWORK), + pay_to: scheme.pay_to, + asset: A::ASSET, + amount: scheme.amount.into(), + max_timeout_seconds: scheme.max_timeout_seconds_override.unwrap_or(300), + extra: None, + } + } +} + +impl From> for PaymentRequirements { + fn from(scheme: ExactSvm) -> Self { + PaymentRequirements::from(Payment::from(scheme)) + } +} + +pub struct ExactSvmScheme(pub SvmNetwork); + +impl Scheme for ExactSvmScheme { + type Network = SvmNetwork; + type Payload = ExplicitSvmPayload; + const SCHEME_NAME: &'static str = "exact"; + + fn network(&self) -> &Self::Network { + &self.0 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExplicitSvmPayload { + pub transaction: String, +} + +#[cfg(test)] +mod tests { + use solana_pubkey::pubkey; + + use crate::svm::assets::UsdcSolanaDevnet; + + use super::*; + + #[test] + fn test_build_payment_requirements() { + let pr: PaymentRequirements = ExactSvm::builder() + .asset(UsdcSolanaDevnet) + .amount(1000) + .pay_to(pubkey!("Ge3jkza5KRfXvaq3GELNLh6V1pjjdEKNpEdGXJgjjKUR")) + .build() + .into(); + + assert_eq!(pr.scheme, "exact"); + assert_eq!(pr.network, "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"); + assert_eq!(pr.amount, 1000u64.into()); + assert!(pr.extra.is_none()); + } +} diff --git a/x402-networks/src/svm/mod.rs b/x402-networks/src/svm/mod.rs new file mode 100644 index 0000000..bedcdfd --- /dev/null +++ b/x402-networks/src/svm/mod.rs @@ -0,0 +1,194 @@ +use std::{ + fmt::{Debug, Display}, + str::FromStr, +}; + +use serde::{Deserialize, Serialize}; +use solana_pubkey::{ParsePubkeyError, Pubkey}; + +use x402_core::core::{Address, NetworkFamily}; + +pub mod exact; + +pub struct SvmNetwork { + pub name: &'static str, + pub caip_2_id: &'static str, +} + +impl NetworkFamily for SvmNetwork { + fn network_name(&self) -> &str { + self.name + } + + fn network_id(&self) -> &str { + self.caip_2_id + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct SvmAddress(pub Pubkey); + +impl From for SvmAddress { + fn from(pk: Pubkey) -> Self { + SvmAddress(pk) + } +} + +impl FromStr for SvmAddress { + type Err = ParsePubkeyError; + + fn from_str(s: &str) -> Result { + let pk = Pubkey::from_str(s)?; + Ok(SvmAddress(pk)) + } +} + +impl Display for SvmAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Debug for SvmAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SvmAddress({})", self.0) + } +} + +impl Serialize for SvmAddress { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for SvmAddress { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let pk = Pubkey::from_str(&s).map_err(serde::de::Error::custom)?; + Ok(SvmAddress(pk)) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct SvmSignature(pub solana_signature::Signature); + +impl FromStr for SvmSignature { + type Err = solana_signature::ParseSignatureError; + + fn from_str(s: &str) -> Result { + let sig = solana_signature::Signature::from_str(s)?; + Ok(SvmSignature(sig)) + } +} + +impl Debug for SvmSignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SvmSignature({})", self.0) + } +} + +impl Display for SvmSignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Serialize for SvmSignature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for SvmSignature { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let sig = solana_signature::Signature::from_str(&s).map_err(serde::de::Error::custom)?; + Ok(SvmSignature(sig)) + } +} + +impl Address for SvmAddress { + type Network = SvmNetwork; +} + +pub type SvmAsset = x402_core::core::Asset; + +pub trait ExplicitSvmNetwork { + const NETWORK: SvmNetwork; +} + +pub trait ExplicitSvmAsset { + type Network: ExplicitSvmNetwork; + const ASSET: SvmAsset; +} + +pub mod networks { + use super::*; + + pub struct Solana; + impl ExplicitSvmNetwork for Solana { + const NETWORK: SvmNetwork = SvmNetwork { + name: "solana", + caip_2_id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + }; + } + + pub struct SolanaDevnet; + impl ExplicitSvmNetwork for SolanaDevnet { + const NETWORK: SvmNetwork = SvmNetwork { + name: "solana-devnet", + caip_2_id: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + }; + } + + pub struct SolanaTestnet; + impl ExplicitSvmNetwork for SolanaTestnet { + const NETWORK: SvmNetwork = SvmNetwork { + name: "solana-testnet", + caip_2_id: "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z", + }; + } +} + +pub mod assets { + use solana_pubkey::pubkey; + + use super::*; + + macro_rules! create_usdc { + ($addr:expr) => { + SvmAsset { + address: SvmAddress($addr), + decimals: 6, + name: "USD Coin", + symbol: "USDC", + } + }; + } + + pub struct UsdcSolana; + impl ExplicitSvmAsset for UsdcSolana { + type Network = networks::Solana; + const ASSET: SvmAsset = + create_usdc!(pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")); + } + + pub struct UsdcSolanaDevnet; + impl ExplicitSvmAsset for UsdcSolanaDevnet { + type Network = networks::SolanaDevnet; + const ASSET: SvmAsset = + create_usdc!(pubkey!("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")); + } +} diff --git a/x402-signer/Cargo.toml b/x402-signer/Cargo.toml new file mode 100644 index 0000000..b8845c3 --- /dev/null +++ b/x402-signer/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "x402-signer" +version = "2.3.0" +edition = "2024" +repository = "https://github.com/AIMOverse/x402-kit" +authors = ["Archer "] +license = "MIT" +description = "Buyer-side signing SDK for the x402 payment protocol." + +[features] +default = ["evm", "svm"] +evm = [ + "x402-networks/evm", + "dep:alloy-core", + "dep:alloy-signer", + "dep:alloy-primitives", + "dep:rand", +] +svm = [ + "x402-networks/svm", + "dep:solana-keypair", + "dep:solana-pubkey", + "dep:solana-instruction", + "dep:solana-transaction", + "dep:solana-message", + "dep:solana-hash", + "dep:solana-signer", + "dep:solana-signature", + "dep:bincode", + "dep:base64", + "dep:rand", + "dep:hex", +] + +[dependencies] +# === Core === +x402-core = { version = "2.3.0", path = "../x402-core" } +x402-networks = { version = "2.3.0", path = "../x402-networks", default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", features = ["preserve_order"] } +thiserror = { version = "2.0" } + +# === Feature "evm" === +alloy-core = { version = "1.4", features = ["sol-types"], optional = true } +alloy-signer = { version = "1.1", optional = true } +alloy-primitives = { version = "1.4", optional = true } +rand = { version = "0.9", optional = true } + +# === Feature "svm" === +solana-pubkey = { version = "4", features = ["curve25519"], optional = true } +solana-keypair = { version = "3", optional = true } +solana-instruction = { version = "3", optional = true } +solana-transaction = { version = "3", features = ["bincode"], optional = true } +solana-message = { version = "3", optional = true } +solana-hash = { version = "3", optional = true } +solana-signer = { version = "3", optional = true } +solana-signature = { version = "3", optional = true } +bincode = { version = "2.0", features = ["serde"], optional = true } +base64 = { version = "0.22", optional = true } +hex = { version = "0.4", optional = true } + +[dev-dependencies] +alloy = { version = "1" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +solana-pubkey = { version = "4" } +solana-keypair = { version = "3" } diff --git a/x402-signer/src/client.rs b/x402-signer/src/client.rs new file mode 100644 index 0000000..b824c1e --- /dev/null +++ b/x402-signer/src/client.rs @@ -0,0 +1,46 @@ +use x402_core::transport::{PaymentPayload, PaymentRequired}; + +use crate::{ + errors::SigningError, + selector::select_requirements, + signer::PaymentSigner, +}; + +/// High-level signing client that wraps a `PaymentSigner` and handles requirement selection. +pub struct X402Client

{ + signer: P, +} + +impl X402Client

{ + pub fn new(signer: P) -> Self { + Self { signer } + } + + /// Given a `PaymentRequired` (HTTP 402) response, select a compatible + /// payment method, sign it, and return the `PaymentPayload`. + pub async fn create_payment( + &self, + payment_required: &PaymentRequired, + ) -> Result { + let accepts = payment_required.accepts.as_ref(); + let requirements = select_requirements(accepts, &self.signer) + .ok_or(SigningError::NoMatchingRequirements)?; + + self.signer + .sign_payment( + requirements, + &payment_required.resource, + &payment_required.extensions, + ) + .await + .map_err(|_| SigningError::SchemeNotSupported { + scheme: requirements.scheme.clone(), + network: requirements.network.clone(), + }) + } + + /// Access the inner signer. + pub fn signer(&self) -> &P { + &self.signer + } +} diff --git a/x402-signer/src/errors.rs b/x402-signer/src/errors.rs new file mode 100644 index 0000000..a7361d2 --- /dev/null +++ b/x402-signer/src/errors.rs @@ -0,0 +1,34 @@ +use x402_core::transport::PaymentRequirements; + +/// Errors that can occur during payment signing. +#[derive(Debug, thiserror::Error)] +pub enum SigningError { + #[error("no matching payment requirements found for any configured signer")] + NoMatchingRequirements, + + #[error("payload serialization failed: {0}")] + PayloadSerialization(#[from] serde_json::Error), + + #[error("address parse error: {0}")] + AddressParse(String), + + #[error("unsupported scheme '{scheme}' on network '{network}'")] + SchemeNotSupported { scheme: String, network: String }, + + #[cfg(feature = "evm")] + #[error("EVM signing error: {0}")] + Evm(#[from] crate::evm::EvmSigningError), + + #[cfg(feature = "svm")] + #[error("SVM signing error: {0}")] + Svm(#[from] crate::svm::SvmSigningError), +} + +/// Convenience type alias for signing results. +pub type SigningResult = Result; + +/// Determines which `PaymentRequirements` entry was selected. +pub struct SelectedRequirements<'a> { + pub requirements: &'a PaymentRequirements, + pub index: usize, +} diff --git a/x402-signer/src/evm/constants.rs b/x402-signer/src/evm/constants.rs new file mode 100644 index 0000000..bc4a153 --- /dev/null +++ b/x402-signer/src/evm/constants.rs @@ -0,0 +1,13 @@ +use alloy_primitives::{Address, address}; + +/// Canonical Permit2 contract address (same on all EVM chains via CREATE2). +pub const PERMIT2_ADDRESS: Address = address!("0x000000000022D473030F116dDEE9F6B43aC78BA3"); + +/// x402 Exact Permit2 Proxy contract address. +pub const X402_EXACT_PERMIT2_PROXY: Address = + address!("0x402085c248EeA27D92E8b30b2C58ed07f9E20001"); + +/// Parse EVM chain ID from CAIP-2 network string (e.g., "eip155:84532" → 84532). +pub fn parse_evm_chain_id(network: &str) -> Option { + network.strip_prefix("eip155:").and_then(|s| s.parse().ok()) +} diff --git a/x402-signer/src/evm/eip3009.rs b/x402-signer/src/evm/eip3009.rs new file mode 100644 index 0000000..a9e9e52 --- /dev/null +++ b/x402-signer/src/evm/eip3009.rs @@ -0,0 +1,84 @@ +use alloy_core::{ + sol, + sol_types::{SolStruct, eip712_domain}, +}; +use alloy_primitives::{Address, FixedBytes, U256}; + +use x402_networks::evm::EvmAddress; +use x402_networks::evm::exact::{ + ExactEvmAuthorization, ExactEvmPayload, Nonce, TimestampSeconds, +}; + +use super::wallet::EvmWalletSigner; + +sol! { + struct Eip3009Authorization { + address from; + address to; + uint256 value; + uint256 validAfter; + uint256 validBefore; + bytes32 nonce; + } +} + +impl From<&ExactEvmAuthorization> for Eip3009Authorization { + fn from(auth: &ExactEvmAuthorization) -> Self { + Eip3009Authorization { + from: auth.from.0, + to: auth.to.0, + value: U256::from(auth.value.0), + validAfter: U256::from(auth.valid_after.0), + validBefore: U256::from(auth.valid_before.0), + nonce: FixedBytes(auth.nonce.0), + } + } +} + +/// Parameters for EIP-3009 signing. +pub struct Eip3009Params { + pub from: EvmAddress, + pub pay_to: EvmAddress, + pub amount: u128, + pub max_timeout_seconds: u64, + pub chain_id: u64, + pub asset_address: Address, + pub eip712_name: String, + pub eip712_version: String, +} + +/// Sign an EIP-3009 TransferWithAuthorization. +pub async fn sign_eip3009( + signer: &S, + params: Eip3009Params, +) -> Result { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock before UNIX epoch") + .as_secs(); + + let authorization = ExactEvmAuthorization { + from: params.from, + to: params.pay_to, + value: params.amount.into(), + valid_after: TimestampSeconds(now.saturating_sub(600)), + valid_before: TimestampSeconds(now + params.max_timeout_seconds), + nonce: Nonce(rand::random()), + }; + + let domain = eip712_domain!( + name: params.eip712_name, + version: params.eip712_version, + chain_id: params.chain_id, + verifying_contract: params.asset_address, + ); + + let sol_auth = Eip3009Authorization::from(&authorization); + let hash = sol_auth.eip712_signing_hash(&domain); + let signature = signer.sign_hash(&hash).await?; + + Ok(ExactEvmPayload { + signature, + authorization, + }) +} diff --git a/x402-signer/src/evm/mod.rs b/x402-signer/src/evm/mod.rs new file mode 100644 index 0000000..1149929 --- /dev/null +++ b/x402-signer/src/evm/mod.rs @@ -0,0 +1,9 @@ +pub mod constants; +pub mod eip3009; +pub mod permit2; +pub mod signer; +pub mod types; +pub mod wallet; + +pub use signer::{EvmPaymentSigner, EvmSigningError}; +pub use wallet::EvmWalletSigner; diff --git a/x402-signer/src/evm/permit2.rs b/x402-signer/src/evm/permit2.rs new file mode 100644 index 0000000..fed594d --- /dev/null +++ b/x402-signer/src/evm/permit2.rs @@ -0,0 +1,105 @@ +use alloy_core::{ + sol_types::{SolStruct, eip712_domain}, +}; +use alloy_primitives::{Address, U256}; + +use x402_core::types::AmountValue; +use x402_networks::evm::EvmAddress; + +use super::constants::{PERMIT2_ADDRESS, X402_EXACT_PERMIT2_PROXY}; +use super::types::{ + Permit2Authorization, Permit2Payload, Permit2Witness, +}; +use super::wallet::EvmWalletSigner; + +mod sol_types { + use alloy_core::sol; + + sol! { + struct PermitWitnessTransferFrom { + TokenPermissions permitted; + address spender; + uint256 nonce; + uint256 deadline; + Witness witness; + } + + struct TokenPermissions { + address token; + uint256 amount; + } + + struct Witness { + address to; + uint256 validAfter; + } + } +} + +/// Parameters for Permit2 signing. +pub struct Permit2Params { + pub from: EvmAddress, + pub pay_to: EvmAddress, + pub amount: u128, + pub max_timeout_seconds: u64, + pub chain_id: u64, + pub asset_address: Address, +} + +/// Sign a Permit2 PermitWitnessTransferFrom authorization. +pub async fn sign_permit2( + signer: &S, + params: Permit2Params, +) -> Result { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock before UNIX epoch") + .as_secs(); + + let valid_after = now.saturating_sub(600); + let deadline = now + params.max_timeout_seconds; + let nonce: U256 = U256::from_be_bytes(rand::random::<[u8; 32]>()); + + let sol_msg = sol_types::PermitWitnessTransferFrom { + permitted: sol_types::TokenPermissions { + token: params.asset_address, + amount: U256::from(params.amount), + }, + spender: X402_EXACT_PERMIT2_PROXY, + nonce, + deadline: U256::from(deadline), + witness: sol_types::Witness { + to: params.pay_to.0, + validAfter: U256::from(valid_after), + }, + }; + + let domain = eip712_domain!( + name: "Permit2", + chain_id: params.chain_id, + verifying_contract: PERMIT2_ADDRESS, + ); + + let hash = sol_msg.eip712_signing_hash(&domain); + let signature = signer.sign_hash(&hash).await?; + + let authorization = Permit2Authorization { + from: params.from, + permitted: super::types::TokenPermissions { + token: EvmAddress(params.asset_address), + amount: AmountValue(params.amount), + }, + spender: EvmAddress(X402_EXACT_PERMIT2_PROXY), + nonce: nonce.to_string(), + deadline: deadline.to_string(), + witness: Permit2Witness { + to: params.pay_to, + valid_after: valid_after.to_string(), + }, + }; + + Ok(Permit2Payload { + signature, + permit2_authorization: authorization, + }) +} diff --git a/x402-signer/src/evm/signer.rs b/x402-signer/src/evm/signer.rs new file mode 100644 index 0000000..3508ae3 --- /dev/null +++ b/x402-signer/src/evm/signer.rs @@ -0,0 +1,146 @@ +use std::str::FromStr; + +use serde::Deserialize; + +use x402_core::{ + transport::{PaymentPayload, PaymentRequirements, PaymentResource}, + types::{Extension, Record, X402V2}, +}; +use x402_networks::evm::EvmAddress; + +use crate::signer::PaymentSigner; + +use super::{ + constants::parse_evm_chain_id, + eip3009::{self, Eip3009Params}, + permit2::{self, Permit2Params}, + types::{TransferMethod, detect_transfer_method}, + wallet::EvmWalletSigner, +}; + +/// High-level EVM payment signer implementing `PaymentSigner`. +pub struct EvmPaymentSigner { + signer: S, +} + +impl EvmPaymentSigner { + pub fn new(signer: S) -> Self { + Self { signer } + } +} + +/// EVM-specific signing errors. +#[derive(Debug, thiserror::Error)] +pub enum EvmSigningError { + #[error("wallet error: {0}")] + Wallet(String), + + #[error("cannot parse chain ID from network: {0}")] + InvalidNetwork(String), + + #[error("cannot parse address: {0}")] + AddressParse(String), + + #[error("EIP-712 domain info missing from requirements extra")] + MissingEip712Domain, + + #[error("payload serialization: {0}")] + Serialization(#[from] serde_json::Error), +} + +#[derive(Deserialize, Default)] +struct Eip712DomainExtra { + name: Option, + version: Option, + #[allow(dead_code)] + #[serde(rename = "transferMethod")] + transfer_method: Option, +} + +impl PaymentSigner for EvmPaymentSigner { + type Error = EvmSigningError; + + fn matches(&self, requirements: &PaymentRequirements) -> bool { + requirements.scheme == "exact" && requirements.network.starts_with("eip155:") + } + + async fn sign_payment( + &self, + requirements: &PaymentRequirements, + resource: &PaymentResource, + extensions: &Record, + ) -> Result { + let chain_id = parse_evm_chain_id(&requirements.network) + .ok_or_else(|| EvmSigningError::InvalidNetwork(requirements.network.clone()))?; + + let pay_to = EvmAddress::from_str(&requirements.pay_to) + .map_err(|e| EvmSigningError::AddressParse(e.to_string()))?; + + let asset_address = alloy_primitives::Address::from_str(&requirements.asset) + .map_err(|e| EvmSigningError::AddressParse(e.to_string()))?; + + let from = self.signer.address(); + let amount = requirements.amount.0; + let transfer_method = detect_transfer_method(&requirements.extra); + + let payload_json = match transfer_method { + TransferMethod::Eip3009 => { + let domain_extra: Eip712DomainExtra = requirements + .extra + .as_ref() + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_default(); + + let eip712_name = domain_extra + .name + .ok_or(EvmSigningError::MissingEip712Domain)?; + let eip712_version = domain_extra + .version + .ok_or(EvmSigningError::MissingEip712Domain)?; + + let payload = eip3009::sign_eip3009( + &self.signer, + Eip3009Params { + from, + pay_to, + amount, + max_timeout_seconds: requirements.max_timeout_seconds, + chain_id, + asset_address, + eip712_name, + eip712_version, + }, + ) + .await + .map_err(|e| EvmSigningError::Wallet(e.to_string()))?; + + serde_json::to_value(payload)? + } + TransferMethod::Permit2 => { + let payload = permit2::sign_permit2( + &self.signer, + Permit2Params { + from, + pay_to, + amount, + max_timeout_seconds: requirements.max_timeout_seconds, + chain_id, + asset_address, + }, + ) + .await + .map_err(|e| EvmSigningError::Wallet(e.to_string()))?; + + serde_json::to_value(payload)? + } + }; + + Ok(PaymentPayload { + x402_version: X402V2, + resource: resource.clone(), + accepted: requirements.clone(), + payload: payload_json, + extensions: extensions.clone(), + }) + } +} diff --git a/x402-signer/src/evm/types.rs b/x402-signer/src/evm/types.rs new file mode 100644 index 0000000..bba3b61 --- /dev/null +++ b/x402-signer/src/evm/types.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +use x402_core::types::AmountValue; +use x402_networks::evm::{EvmAddress, EvmSignature}; + +/// Permit2 authorization payload for the x402 protocol. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Permit2Payload { + pub signature: EvmSignature, + pub permit2_authorization: Permit2Authorization, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Permit2Authorization { + pub from: EvmAddress, + pub permitted: TokenPermissions, + pub spender: EvmAddress, + /// Random nonce as a decimal string (uint256). + pub nonce: String, + /// Deadline as a decimal string of unix timestamp (uint256). + pub deadline: String, + pub witness: Permit2Witness, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TokenPermissions { + pub token: EvmAddress, + pub amount: AmountValue, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Permit2Witness { + pub to: EvmAddress, + /// validAfter as a decimal string of unix timestamp (uint256). + pub valid_after: String, +} + +/// Transfer method detection from `extra.transferMethod`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum TransferMethod { + #[default] + Eip3009, + Permit2, +} + +/// Parse `transferMethod` from the extra field. +pub fn detect_transfer_method(extra: &Option) -> TransferMethod { + extra + .as_ref() + .and_then(|v| v.get("transferMethod")) + .and_then(|v| v.as_str()) + .map(|s| match s { + "permit2" => TransferMethod::Permit2, + _ => TransferMethod::Eip3009, + }) + .unwrap_or(TransferMethod::Eip3009) +} diff --git a/x402-signer/src/evm/wallet.rs b/x402-signer/src/evm/wallet.rs new file mode 100644 index 0000000..254ccd0 --- /dev/null +++ b/x402-signer/src/evm/wallet.rs @@ -0,0 +1,31 @@ +use alloy_primitives::B256; +use alloy_signer::{Error as AlloySignerError, Signer as AlloySigner}; + +use x402_networks::evm::{EvmAddress, EvmSignature}; + +/// Abstraction over an EVM wallet capable of signing EIP-712 hashes. +pub trait EvmWalletSigner { + type Error: std::error::Error + Send + Sync; + + /// The signer's address. + fn address(&self) -> EvmAddress; + + /// Sign a pre-computed EIP-712 hash. + fn sign_hash( + &self, + hash: &B256, + ) -> impl Future> + Send; +} + +impl EvmWalletSigner for T { + type Error = AlloySignerError; + + fn address(&self) -> EvmAddress { + EvmAddress(AlloySigner::address(self)) + } + + async fn sign_hash(&self, hash: &B256) -> Result { + let sig = AlloySigner::sign_hash(self, hash).await?; + Ok(EvmSignature(sig)) + } +} diff --git a/x402-signer/src/lib.rs b/x402-signer/src/lib.rs new file mode 100644 index 0000000..02c99c6 --- /dev/null +++ b/x402-signer/src/lib.rs @@ -0,0 +1,15 @@ +pub mod errors; +pub mod signer; +pub mod selector; +pub mod client; + +#[cfg(feature = "evm")] +pub mod evm; + +#[cfg(feature = "svm")] +pub mod svm; + +pub use client::X402Client; +pub use errors::SigningError; +pub use selector::select_requirements; +pub use signer::PaymentSigner; diff --git a/x402-signer/src/selector.rs b/x402-signer/src/selector.rs new file mode 100644 index 0000000..c79563c --- /dev/null +++ b/x402-signer/src/selector.rs @@ -0,0 +1,11 @@ +use x402_core::transport::PaymentRequirements; + +use crate::signer::PaymentSigner; + +/// Select the first `PaymentRequirements` that the signer can handle. +pub fn select_requirements<'a, S: PaymentSigner>( + accepts: &'a [PaymentRequirements], + signer: &S, +) -> Option<&'a PaymentRequirements> { + accepts.iter().find(|req| signer.matches(req)) +} diff --git a/x402-signer/src/signer.rs b/x402-signer/src/signer.rs new file mode 100644 index 0000000..2a2ed6f --- /dev/null +++ b/x402-signer/src/signer.rs @@ -0,0 +1,49 @@ +use x402_core::{ + transport::{PaymentPayload, PaymentRequirements, PaymentResource}, + types::{Extension, Record}, +}; + +/// High-level trait for signing x402 payments. +/// +/// Implementors can match against `PaymentRequirements` and produce a signed `PaymentPayload`. +/// Use tuple composition `(A, B)` to combine multiple signers without dynamic dispatch. +pub trait PaymentSigner { + type Error: std::error::Error; + + /// Returns `true` if this signer can handle the given payment requirements. + fn matches(&self, requirements: &PaymentRequirements) -> bool; + + /// Sign a payment, producing a complete `PaymentPayload`. + fn sign_payment( + &self, + requirements: &PaymentRequirements, + resource: &PaymentResource, + extensions: &Record, + ) -> impl Future>; +} + +/// Tuple composition: tries `A` first, falls back to `B`. +impl PaymentSigner for (A, B) +where + A: PaymentSigner, + B: PaymentSigner, +{ + type Error = A::Error; + + fn matches(&self, requirements: &PaymentRequirements) -> bool { + self.0.matches(requirements) || self.1.matches(requirements) + } + + async fn sign_payment( + &self, + requirements: &PaymentRequirements, + resource: &PaymentResource, + extensions: &Record, + ) -> Result { + if self.0.matches(requirements) { + self.0.sign_payment(requirements, resource, extensions).await + } else { + self.1.sign_payment(requirements, resource, extensions).await + } + } +} diff --git a/x402-signer/src/svm/constants.rs b/x402-signer/src/svm/constants.rs new file mode 100644 index 0000000..e71dd16 --- /dev/null +++ b/x402-signer/src/svm/constants.rs @@ -0,0 +1,23 @@ +use solana_pubkey::{Pubkey, pubkey}; + +/// SPL Token program address. +pub const TOKEN_PROGRAM: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + +/// SPL Token-2022 program address. +pub const TOKEN_2022_PROGRAM: Pubkey = pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); + +/// Compute Budget program address. +pub const COMPUTE_BUDGET_PROGRAM: Pubkey = pubkey!("ComputeBudget111111111111111111111111111111"); + +/// Memo program address. +pub const MEMO_PROGRAM: Pubkey = pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); + +/// Associated Token Account program address. +#[rustfmt::skip] +pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey = pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); + +/// Default compute unit price in microlamports. +pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 1; + +/// Default compute unit limit. +pub const DEFAULT_COMPUTE_UNIT_LIMIT: u32 = 20_000; diff --git a/x402-signer/src/svm/mod.rs b/x402-signer/src/svm/mod.rs new file mode 100644 index 0000000..cf77ace --- /dev/null +++ b/x402-signer/src/svm/mod.rs @@ -0,0 +1,10 @@ +pub mod constants; +pub mod rpc; +pub mod signer; +pub mod transaction; +pub mod types; +pub mod wallet; + +pub use rpc::SvmRpc; +pub use signer::{SvmPaymentSigner, SvmSigningError}; +pub use wallet::SvmWalletSigner; diff --git a/x402-signer/src/svm/rpc.rs b/x402-signer/src/svm/rpc.rs new file mode 100644 index 0000000..eb31d2e --- /dev/null +++ b/x402-signer/src/svm/rpc.rs @@ -0,0 +1,21 @@ +use x402_networks::svm::SvmAddress; + +use super::types::MintInfo; + +/// Async RPC operations needed for SVM transaction building. +/// +/// Users provide their own implementation backed by their preferred RPC client. +pub trait SvmRpc { + type Error: std::error::Error + Send + Sync; + + /// Fetch the latest blockhash from the cluster. + fn get_latest_blockhash( + &self, + ) -> impl Future> + Send; + + /// Fetch mint metadata (program address and decimals) for a given token mint. + fn fetch_mint_info( + &self, + mint: SvmAddress, + ) -> impl Future> + Send; +} diff --git a/x402-signer/src/svm/signer.rs b/x402-signer/src/svm/signer.rs new file mode 100644 index 0000000..f4cdbd9 --- /dev/null +++ b/x402-signer/src/svm/signer.rs @@ -0,0 +1,159 @@ +use std::str::FromStr; + +use base64::{Engine, prelude::BASE64_STANDARD}; +use serde::Deserialize; +use solana_pubkey::Pubkey; + +use x402_core::{ + transport::{PaymentPayload, PaymentRequirements, PaymentResource}, + types::{Extension, Record, X402V2}, +}; +use x402_networks::svm::SvmAddress; + +use crate::signer::PaymentSigner; + +use super::{ + rpc::SvmRpc, + transaction::{self, TransactionParams}, + wallet::SvmWalletSigner, +}; + +/// High-level SVM payment signer implementing `PaymentSigner`. +pub struct SvmPaymentSigner { + wallet: S, + rpc: R, +} + +impl SvmPaymentSigner { + pub fn new(wallet: S, rpc: R) -> Self { + Self { wallet, rpc } + } +} + +/// SVM-specific signing errors. +#[derive(Debug, thiserror::Error)] +pub enum SvmSigningError { + #[error("wallet error: {0}")] + Wallet(String), + + #[error("RPC error: {0}")] + Rpc(String), + + #[error("cannot parse address '{0}': {1}")] + AddressParse(String, String), + + #[error("feePayer missing from requirements extra")] + MissingFeePayer, + + #[error("payload serialization: {0}")] + Serialization(String), +} + +#[derive(Deserialize)] +struct SvmExtra { + #[serde(rename = "feePayer")] + fee_payer: Option, +} + +impl PaymentSigner for SvmPaymentSigner +where + S: SvmWalletSigner + Sync, + R: SvmRpc + Sync, +{ + type Error = SvmSigningError; + + fn matches(&self, requirements: &PaymentRequirements) -> bool { + requirements.scheme == "exact" && requirements.network.starts_with("solana:") + } + + async fn sign_payment( + &self, + requirements: &PaymentRequirements, + resource: &PaymentResource, + extensions: &Record, + ) -> Result { + // Extract feePayer from extra + let extra: SvmExtra = requirements + .extra + .as_ref() + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or(SvmExtra { fee_payer: None }); + + let fee_payer_str = extra.fee_payer.ok_or(SvmSigningError::MissingFeePayer)?; + let fee_payer = parse_pubkey(&fee_payer_str)?; + + let mint = parse_pubkey(&requirements.asset)?; + let destination_owner = parse_pubkey(&requirements.pay_to)?; + let payer = self.wallet.pubkey().0; + let amount = requirements.amount.0 as u64; + + // Fetch mint info from RPC + let mint_info = self + .rpc + .fetch_mint_info(SvmAddress(mint)) + .await + .map_err(|e| SvmSigningError::Rpc(e.to_string()))?; + + // Fetch latest blockhash + let recent_blockhash = self + .rpc + .get_latest_blockhash() + .await + .map_err(|e| SvmSigningError::Rpc(e.to_string()))?; + + // Build unsigned transaction + let mut tx = transaction::build_exact_svm_transaction(&TransactionParams { + fee_payer, + payer, + mint, + destination_owner, + amount, + decimals: mint_info.decimals, + token_program: mint_info.program_address.0, + recent_blockhash, + }); + + // Partially sign with the buyer's wallet + let message_bytes = tx.message_data(); + let signature = self + .wallet + .sign_message(&message_bytes) + .await + .map_err(|e| SvmSigningError::Wallet(e.to_string()))?; + + // Place signature at the correct index + let signer_index = tx + .message + .account_keys + .iter() + .position(|k| k == &payer) + .ok_or_else(|| { + SvmSigningError::Serialization("signer not found in account keys".into()) + })?; + tx.signatures[signer_index] = signature.0; + + // Serialize transaction to base64 + let tx_bytes = bincode::serde::encode_to_vec(&tx, bincode::config::legacy()) + .map_err(|e| SvmSigningError::Serialization(e.to_string()))?; + let transaction_b64 = BASE64_STANDARD.encode(&tx_bytes); + + let payload_json = serde_json::to_value( + x402_networks::svm::exact::ExplicitSvmPayload { + transaction: transaction_b64, + }, + ) + .map_err(|e| SvmSigningError::Serialization(e.to_string()))?; + + Ok(PaymentPayload { + x402_version: X402V2, + resource: resource.clone(), + accepted: requirements.clone(), + payload: payload_json, + extensions: extensions.clone(), + }) + } +} + +fn parse_pubkey(s: &str) -> Result { + Pubkey::from_str(s).map_err(|e| SvmSigningError::AddressParse(s.to_string(), e.to_string())) +} diff --git a/x402-signer/src/svm/transaction.rs b/x402-signer/src/svm/transaction.rs new file mode 100644 index 0000000..d1f0e9b --- /dev/null +++ b/x402-signer/src/svm/transaction.rs @@ -0,0 +1,143 @@ +use solana_hash::Hash; +use solana_instruction::{AccountMeta, Instruction}; +use solana_message::Message; +use solana_pubkey::Pubkey; +use solana_transaction::Transaction; + +use super::constants::*; + +/// Parameters for building an exact SVM payment transaction. +pub struct TransactionParams { + /// The facilitator's address (pays gas fees). + pub fee_payer: Pubkey, + /// The buyer's address (token authority). + pub payer: Pubkey, + /// The token mint address. + pub mint: Pubkey, + /// Destination wallet (pay_to). + pub destination_owner: Pubkey, + /// Amount in smallest token units. + pub amount: u64, + /// Token decimals. + pub decimals: u8, + /// Token program (SPL Token or Token-2022). + pub token_program: Pubkey, + /// Recent blockhash for the transaction. + pub recent_blockhash: Hash, +} + +/// Build an exact SVM payment transaction. +/// +/// Instruction layout: +/// 0. SetComputeUnitLimit +/// 1. SetComputeUnitPrice +/// 2. TransferChecked (source ATA → destination ATA) +/// 3. Memo (random nonce) +/// +/// The transaction is unsigned. Caller must partially sign with the buyer's key. +pub fn build_exact_svm_transaction(params: &TransactionParams) -> Transaction { + let source_ata = derive_ata(¶ms.payer, ¶ms.mint, ¶ms.token_program); + let destination_ata = + derive_ata(¶ms.destination_owner, ¶ms.mint, ¶ms.token_program); + + let instructions = vec![ + build_set_compute_unit_limit(DEFAULT_COMPUTE_UNIT_LIMIT), + build_set_compute_unit_price(DEFAULT_COMPUTE_UNIT_PRICE), + build_transfer_checked( + &source_ata, + ¶ms.mint, + &destination_ata, + ¶ms.payer, + params.amount, + params.decimals, + ¶ms.token_program, + ), + build_memo_instruction(), + ]; + + let message = + Message::new_with_blockhash(&instructions, Some(¶ms.fee_payer), ¶ms.recent_blockhash); + + Transaction::new_unsigned(message) +} + +/// Derive the Associated Token Account address. +pub fn derive_ata(owner: &Pubkey, mint: &Pubkey, token_program: &Pubkey) -> Pubkey { + let (ata, _bump) = Pubkey::find_program_address( + &[ + owner.as_ref(), + token_program.as_ref(), + mint.as_ref(), + ], + &ASSOCIATED_TOKEN_PROGRAM, + ); + ata +} + +/// ComputeBudget: SetComputeUnitLimit instruction. +fn build_set_compute_unit_limit(units: u32) -> Instruction { + // Discriminator 2 + u32 LE + let mut data = Vec::with_capacity(5); + data.push(2u8); + data.extend_from_slice(&units.to_le_bytes()); + + Instruction { + program_id: COMPUTE_BUDGET_PROGRAM, + accounts: vec![], + data, + } +} + +/// ComputeBudget: SetComputeUnitPrice instruction. +fn build_set_compute_unit_price(micro_lamports: u64) -> Instruction { + // Discriminator 3 + u64 LE + let mut data = Vec::with_capacity(9); + data.push(3u8); + data.extend_from_slice(µ_lamports.to_le_bytes()); + + Instruction { + program_id: COMPUTE_BUDGET_PROGRAM, + accounts: vec![], + data, + } +} + +/// SPL Token: TransferChecked instruction. +fn build_transfer_checked( + source: &Pubkey, + mint: &Pubkey, + destination: &Pubkey, + authority: &Pubkey, + amount: u64, + decimals: u8, + token_program: &Pubkey, +) -> Instruction { + // Discriminator 12 + u64 LE amount + u8 decimals = 10 bytes + let mut data = Vec::with_capacity(10); + data.push(12u8); + data.extend_from_slice(&amount.to_le_bytes()); + data.push(decimals); + + Instruction { + program_id: *token_program, + accounts: vec![ + AccountMeta::new(*source, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new(*destination, false), + AccountMeta::new_readonly(*authority, true), + ], + data, + } +} + +/// Memo program instruction with a random hex nonce. +fn build_memo_instruction() -> Instruction { + let nonce: [u8; 16] = rand::random(); + let hex_str = hex::encode(nonce); + + Instruction { + program_id: MEMO_PROGRAM, + accounts: vec![], + data: hex_str.into_bytes(), + } +} diff --git a/x402-signer/src/svm/types.rs b/x402-signer/src/svm/types.rs new file mode 100644 index 0000000..07dfa99 --- /dev/null +++ b/x402-signer/src/svm/types.rs @@ -0,0 +1,9 @@ +use x402_networks::svm::SvmAddress; + +/// Minimal mint metadata needed for building transactions. +pub struct MintInfo { + /// The token program that owns this mint (SPL Token or Token-2022). + pub program_address: SvmAddress, + /// Number of decimals for the token. + pub decimals: u8, +} diff --git a/x402-signer/src/svm/wallet.rs b/x402-signer/src/svm/wallet.rs new file mode 100644 index 0000000..72fc21e --- /dev/null +++ b/x402-signer/src/svm/wallet.rs @@ -0,0 +1,29 @@ +use x402_networks::svm::{SvmAddress, SvmSignature}; + +/// Abstraction over a Solana wallet capable of signing transaction messages. +pub trait SvmWalletSigner { + type Error: std::error::Error + Send + Sync; + + /// The signer's public key. + fn pubkey(&self) -> SvmAddress; + + /// Sign the serialized transaction message bytes. + fn sign_message( + &self, + message: &[u8], + ) -> impl Future> + Send; +} + +/// Blanket impl for any `solana_signer::Signer`. +impl SvmWalletSigner for T { + type Error = solana_signer::SignerError; + + fn pubkey(&self) -> SvmAddress { + SvmAddress(solana_signer::Signer::pubkey(self)) + } + + async fn sign_message(&self, message: &[u8]) -> Result { + let sig = solana_signer::Signer::try_sign_message(self, message)?; + Ok(SvmSignature(sig)) + } +} diff --git a/x402-signer/tests/signing.rs b/x402-signer/tests/signing.rs new file mode 100644 index 0000000..b1e8c35 --- /dev/null +++ b/x402-signer/tests/signing.rs @@ -0,0 +1,409 @@ +use serde_json::json; +use x402_core::{ + transport::{Accepts, PaymentRequired, PaymentRequirements, PaymentResource}, + types::{AmountValue, Record, X402V2}, +}; +use x402_signer::signer::PaymentSigner; + +fn test_resource() -> PaymentResource { + PaymentResource { + url: "https://example.com/api".parse().unwrap(), + description: "Test resource".into(), + mime_type: "application/json".into(), + } +} + +// ======================== EVM Tests ======================== + +#[cfg(feature = "evm")] +mod evm_tests { + use super::*; + use alloy::signers::local::PrivateKeySigner; + use x402_signer::evm::{EvmPaymentSigner, EvmSigningError}; + + fn evm_requirements_eip3009() -> PaymentRequirements { + PaymentRequirements { + scheme: "exact".into(), + network: "eip155:84532".into(), + amount: AmountValue(100_000), + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e".into(), + pay_to: "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF".into(), + max_timeout_seconds: 300, + extra: Some(json!({ + "name": "USD Coin", + "version": "2" + })), + } + } + + fn evm_requirements_permit2() -> PaymentRequirements { + PaymentRequirements { + scheme: "exact".into(), + network: "eip155:84532".into(), + amount: AmountValue(100_000), + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e".into(), + pay_to: "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF".into(), + max_timeout_seconds: 300, + extra: Some(json!({ + "transferMethod": "permit2" + })), + } + } + + #[test] + fn evm_signer_matches_eip155() { + let signer = PrivateKeySigner::random(); + let evm = EvmPaymentSigner::new(signer); + + let req = evm_requirements_eip3009(); + assert!(evm.matches(&req)); + } + + #[test] + fn evm_signer_rejects_svm() { + let signer = PrivateKeySigner::random(); + let evm = EvmPaymentSigner::new(signer); + + let req = PaymentRequirements { + scheme: "exact".into(), + network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp".into(), + amount: AmountValue(100_000), + asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".into(), + pay_to: "So11111111111111111111111111111111111111112".into(), + max_timeout_seconds: 300, + extra: None, + }; + assert!(!evm.matches(&req)); + } + + #[tokio::test] + async fn evm_sign_eip3009() { + let signer = PrivateKeySigner::random(); + let evm = EvmPaymentSigner::new(signer); + + let req = evm_requirements_eip3009(); + let resource = test_resource(); + let extensions = Record::default(); + + let result = evm.sign_payment(&req, &resource, &extensions).await; + assert!(result.is_ok(), "EIP-3009 signing failed: {:?}", result.err()); + + let payload = result.unwrap(); + assert_eq!(payload.x402_version, X402V2); + assert_eq!(payload.accepted, req); + + // Payload should contain signature and authorization fields + let p = &payload.payload; + assert!(p.get("signature").is_some(), "missing signature field"); + assert!( + p.get("authorization").is_some(), + "missing authorization field" + ); + } + + #[tokio::test] + async fn evm_sign_permit2() { + let signer = PrivateKeySigner::random(); + let evm = EvmPaymentSigner::new(signer); + + let req = evm_requirements_permit2(); + let resource = test_resource(); + let extensions = Record::default(); + + let result = evm.sign_payment(&req, &resource, &extensions).await; + assert!(result.is_ok(), "Permit2 signing failed: {:?}", result.err()); + + let payload = result.unwrap(); + let p = &payload.payload; + assert!(p.get("signature").is_some(), "missing signature field"); + assert!( + p.get("permit2Authorization").is_some(), + "missing permit2Authorization field" + ); + } + + #[tokio::test] + async fn evm_sign_missing_eip712_domain() { + let signer = PrivateKeySigner::random(); + let evm = EvmPaymentSigner::new(signer); + + // Missing name/version in extra → should fail for EIP-3009 + let req = PaymentRequirements { + scheme: "exact".into(), + network: "eip155:84532".into(), + amount: AmountValue(100_000), + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e".into(), + pay_to: "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF".into(), + max_timeout_seconds: 300, + extra: None, + }; + let resource = test_resource(); + let extensions = Record::default(); + + let result = evm.sign_payment(&req, &resource, &extensions).await; + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, EvmSigningError::MissingEip712Domain), + "expected MissingEip712Domain, got: {err:?}" + ); + } +} + +// ======================== SVM Tests ======================== + +#[cfg(feature = "svm")] +mod svm_tests { + use super::*; + use solana_pubkey::Pubkey; + use std::str::FromStr; + use x402_signer::svm::{ + SvmPaymentSigner, SvmRpc, + }; + use x402_signer::svm::transaction::{TransactionParams, build_exact_svm_transaction, derive_ata}; + use x402_networks::svm::SvmAddress; + + #[test] + fn derive_ata_deterministic() { + let owner = Pubkey::from_str("GVmFbJa2MWLBkk2s9Y4M5hFTdQ41QRNfzZWMg2F3udz").unwrap(); + let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); + let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); + + let ata1 = derive_ata(&owner, &mint, &token_program); + let ata2 = derive_ata(&owner, &mint, &token_program); + + assert_eq!(ata1, ata2, "ATA derivation should be deterministic"); + // ATA should be a valid off-curve address (PDA) + assert_ne!(ata1, owner, "ATA should differ from owner"); + } + + #[test] + fn build_transaction_has_4_instructions() { + let fee_payer = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); + let destination_owner = Pubkey::new_unique(); + let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); + let recent_blockhash = solana_hash::Hash::new_unique(); + + let tx = build_exact_svm_transaction(&TransactionParams { + fee_payer, + payer, + mint, + destination_owner, + amount: 1_000_000, + decimals: 6, + token_program, + recent_blockhash, + }); + + assert_eq!(tx.message.instructions.len(), 4, "expected 4 instructions"); + // Fee payer should be first account key + assert_eq!(tx.message.account_keys[0], fee_payer); + // Signatures should be pre-allocated + assert!(tx.signatures.len() >= 2, "expected at least 2 signature slots"); + } + + #[test] + fn build_transaction_instruction_data() { + let fee_payer = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); + let destination_owner = Pubkey::new_unique(); + let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); + + let tx = build_exact_svm_transaction(&TransactionParams { + fee_payer, + payer, + mint, + destination_owner, + amount: 1_000_000, + decimals: 6, + token_program, + recent_blockhash: solana_hash::Hash::new_unique(), + }); + + // Instruction 0: SetComputeUnitLimit — disc 2, u32 LE + let ix0_data = &tx.message.instructions[0].data; + assert_eq!(ix0_data[0], 2, "SetComputeUnitLimit discriminator"); + + // Instruction 1: SetComputeUnitPrice — disc 3, u64 LE + let ix1_data = &tx.message.instructions[1].data; + assert_eq!(ix1_data[0], 3, "SetComputeUnitPrice discriminator"); + + // Instruction 2: TransferChecked — disc 12, u64 amount, u8 decimals + let ix2_data = &tx.message.instructions[2].data; + assert_eq!(ix2_data[0], 12, "TransferChecked discriminator"); + let amount_bytes: [u8; 8] = ix2_data[1..9].try_into().unwrap(); + assert_eq!(u64::from_le_bytes(amount_bytes), 1_000_000); + assert_eq!(ix2_data[9], 6, "decimals"); + + // Instruction 3: Memo — random hex nonce + let ix3_data = &tx.message.instructions[3].data; + assert_eq!(ix3_data.len(), 32, "memo should be 32 hex chars (16 bytes → hex)"); + } + + // Mock RPC for testing + struct MockRpc; + + impl SvmRpc for MockRpc { + type Error = std::io::Error; + + async fn get_latest_blockhash(&self) -> Result { + Ok(solana_hash::Hash::new_unique()) + } + + async fn fetch_mint_info( + &self, + _mint: SvmAddress, + ) -> Result { + Ok(x402_signer::svm::types::MintInfo { + program_address: SvmAddress( + Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(), + ), + decimals: 6, + }) + } + } + + #[test] + fn svm_signer_matches_solana() { + let keypair = solana_keypair::Keypair::new(); + let signer = SvmPaymentSigner::new(keypair, MockRpc); + + let req = PaymentRequirements { + scheme: "exact".into(), + network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp".into(), + amount: AmountValue(1_000_000), + asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".into(), + pay_to: "GVmFbJa2MWLBkk2s9Y4M5hFTdQ41QRNfzZWMg2F3udz".into(), + max_timeout_seconds: 300, + extra: Some(json!({ + "feePayer": "GVmFbJa2MWLBkk2s9Y4M5hFTdQ41QRNfzZWMg2F3udz" + })), + }; + assert!(signer.matches(&req)); + } + + #[test] + fn svm_signer_rejects_evm() { + let keypair = solana_keypair::Keypair::new(); + let signer = SvmPaymentSigner::new(keypair, MockRpc); + + let req = PaymentRequirements { + scheme: "exact".into(), + network: "eip155:84532".into(), + amount: AmountValue(100_000), + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e".into(), + pay_to: "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF".into(), + max_timeout_seconds: 300, + extra: None, + }; + assert!(!signer.matches(&req)); + } + + #[tokio::test] + async fn svm_sign_payment() { + let keypair = solana_keypair::Keypair::new(); + let signer = SvmPaymentSigner::new(keypair, MockRpc); + + let req = PaymentRequirements { + scheme: "exact".into(), + network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp".into(), + amount: AmountValue(1_000_000), + asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".into(), + pay_to: "GVmFbJa2MWLBkk2s9Y4M5hFTdQ41QRNfzZWMg2F3udz".into(), + max_timeout_seconds: 300, + extra: Some(json!({ + "feePayer": "GVmFbJa2MWLBkk2s9Y4M5hFTdQ41QRNfzZWMg2F3udz" + })), + }; + let resource = test_resource(); + let extensions = Record::default(); + + let result = signer.sign_payment(&req, &resource, &extensions).await; + assert!(result.is_ok(), "SVM signing failed: {:?}", result.err()); + + let payload = result.unwrap(); + assert_eq!(payload.x402_version, X402V2); + // Payload should contain base64-encoded transaction + let tx_field = payload.payload.get("transaction"); + assert!(tx_field.is_some(), "missing transaction field"); + let tx_b64 = tx_field.unwrap().as_str().unwrap(); + assert!(!tx_b64.is_empty(), "transaction should not be empty"); + + // Should be valid base64 + use base64::{Engine, prelude::BASE64_STANDARD}; + let decoded = BASE64_STANDARD.decode(tx_b64); + assert!(decoded.is_ok(), "transaction should be valid base64"); + } +} + +// ======================== Client Tests ======================== + +#[cfg(feature = "evm")] +mod client_tests { + use super::*; + use alloy::signers::local::PrivateKeySigner; + use x402_signer::{X402Client, evm::EvmPaymentSigner}; + + #[tokio::test] + async fn client_selects_and_signs() { + let signer = PrivateKeySigner::random(); + let evm = EvmPaymentSigner::new(signer); + let client = X402Client::new(evm); + + let payment_required = PaymentRequired { + x402_version: X402V2, + error: "Payment Required".into(), + resource: test_resource(), + accepts: Accepts::from(PaymentRequirements { + scheme: "exact".into(), + network: "eip155:84532".into(), + amount: AmountValue(100_000), + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e".into(), + pay_to: "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF".into(), + max_timeout_seconds: 300, + extra: Some(json!({ + "name": "USD Coin", + "version": "2" + })), + }), + extensions: Record::default(), + }; + + let result = client.create_payment(&payment_required).await; + assert!(result.is_ok(), "client payment failed: {:?}", result.err()); + + let payload = result.unwrap(); + assert_eq!(payload.x402_version, X402V2); + assert!(payload.payload.get("signature").is_some()); + } + + #[tokio::test] + async fn client_rejects_unsupported_network() { + let signer = PrivateKeySigner::random(); + let evm = EvmPaymentSigner::new(signer); + let client = X402Client::new(evm); + + // Only SVM requirements — EVM signer can't handle + let payment_required = PaymentRequired { + x402_version: X402V2, + error: "Payment Required".into(), + resource: test_resource(), + accepts: Accepts::from(PaymentRequirements { + scheme: "exact".into(), + network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp".into(), + amount: AmountValue(1_000_000), + asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".into(), + pay_to: "GVmFbJa2MWLBkk2s9Y4M5hFTdQ41QRNfzZWMg2F3udz".into(), + max_timeout_seconds: 300, + extra: None, + }), + extensions: Record::default(), + }; + + let result = client.create_payment(&payment_required).await; + assert!(result.is_err(), "should reject unsupported network"); + } +} From cdfc7945deabea57439829bc5b51e4a7955b2b2a Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 14:06:11 +0800 Subject: [PATCH 02/13] chore: format code --- x402-signer/src/client.rs | 6 +----- x402-signer/src/evm/eip3009.rs | 4 +--- x402-signer/src/evm/permit2.rs | 8 ++----- x402-signer/src/lib.rs | 4 ++-- x402-signer/src/signer.rs | 8 +++++-- x402-signer/src/svm/signer.rs | 8 +++---- x402-signer/src/svm/transaction.rs | 20 ++++++++++-------- x402-signer/tests/signing.rs | 34 +++++++++++++++++++++--------- 8 files changed, 50 insertions(+), 42 deletions(-) diff --git a/x402-signer/src/client.rs b/x402-signer/src/client.rs index b824c1e..30661b5 100644 --- a/x402-signer/src/client.rs +++ b/x402-signer/src/client.rs @@ -1,10 +1,6 @@ use x402_core::transport::{PaymentPayload, PaymentRequired}; -use crate::{ - errors::SigningError, - selector::select_requirements, - signer::PaymentSigner, -}; +use crate::{errors::SigningError, selector::select_requirements, signer::PaymentSigner}; /// High-level signing client that wraps a `PaymentSigner` and handles requirement selection. pub struct X402Client

{ diff --git a/x402-signer/src/evm/eip3009.rs b/x402-signer/src/evm/eip3009.rs index a9e9e52..f82fe7c 100644 --- a/x402-signer/src/evm/eip3009.rs +++ b/x402-signer/src/evm/eip3009.rs @@ -5,9 +5,7 @@ use alloy_core::{ use alloy_primitives::{Address, FixedBytes, U256}; use x402_networks::evm::EvmAddress; -use x402_networks::evm::exact::{ - ExactEvmAuthorization, ExactEvmPayload, Nonce, TimestampSeconds, -}; +use x402_networks::evm::exact::{ExactEvmAuthorization, ExactEvmPayload, Nonce, TimestampSeconds}; use super::wallet::EvmWalletSigner; diff --git a/x402-signer/src/evm/permit2.rs b/x402-signer/src/evm/permit2.rs index fed594d..b0087c1 100644 --- a/x402-signer/src/evm/permit2.rs +++ b/x402-signer/src/evm/permit2.rs @@ -1,15 +1,11 @@ -use alloy_core::{ - sol_types::{SolStruct, eip712_domain}, -}; +use alloy_core::sol_types::{SolStruct, eip712_domain}; use alloy_primitives::{Address, U256}; use x402_core::types::AmountValue; use x402_networks::evm::EvmAddress; use super::constants::{PERMIT2_ADDRESS, X402_EXACT_PERMIT2_PROXY}; -use super::types::{ - Permit2Authorization, Permit2Payload, Permit2Witness, -}; +use super::types::{Permit2Authorization, Permit2Payload, Permit2Witness}; use super::wallet::EvmWalletSigner; mod sol_types { diff --git a/x402-signer/src/lib.rs b/x402-signer/src/lib.rs index 02c99c6..de83bf8 100644 --- a/x402-signer/src/lib.rs +++ b/x402-signer/src/lib.rs @@ -1,7 +1,7 @@ +pub mod client; pub mod errors; -pub mod signer; pub mod selector; -pub mod client; +pub mod signer; #[cfg(feature = "evm")] pub mod evm; diff --git a/x402-signer/src/signer.rs b/x402-signer/src/signer.rs index 2a2ed6f..f4a0a97 100644 --- a/x402-signer/src/signer.rs +++ b/x402-signer/src/signer.rs @@ -41,9 +41,13 @@ where extensions: &Record, ) -> Result { if self.0.matches(requirements) { - self.0.sign_payment(requirements, resource, extensions).await + self.0 + .sign_payment(requirements, resource, extensions) + .await } else { - self.1.sign_payment(requirements, resource, extensions).await + self.1 + .sign_payment(requirements, resource, extensions) + .await } } } diff --git a/x402-signer/src/svm/signer.rs b/x402-signer/src/svm/signer.rs index f4cdbd9..460b0c4 100644 --- a/x402-signer/src/svm/signer.rs +++ b/x402-signer/src/svm/signer.rs @@ -137,11 +137,9 @@ where .map_err(|e| SvmSigningError::Serialization(e.to_string()))?; let transaction_b64 = BASE64_STANDARD.encode(&tx_bytes); - let payload_json = serde_json::to_value( - x402_networks::svm::exact::ExplicitSvmPayload { - transaction: transaction_b64, - }, - ) + let payload_json = serde_json::to_value(x402_networks::svm::exact::ExplicitSvmPayload { + transaction: transaction_b64, + }) .map_err(|e| SvmSigningError::Serialization(e.to_string()))?; Ok(PaymentPayload { diff --git a/x402-signer/src/svm/transaction.rs b/x402-signer/src/svm/transaction.rs index d1f0e9b..dbe3879 100644 --- a/x402-signer/src/svm/transaction.rs +++ b/x402-signer/src/svm/transaction.rs @@ -37,8 +37,11 @@ pub struct TransactionParams { /// The transaction is unsigned. Caller must partially sign with the buyer's key. pub fn build_exact_svm_transaction(params: &TransactionParams) -> Transaction { let source_ata = derive_ata(¶ms.payer, ¶ms.mint, ¶ms.token_program); - let destination_ata = - derive_ata(¶ms.destination_owner, ¶ms.mint, ¶ms.token_program); + let destination_ata = derive_ata( + ¶ms.destination_owner, + ¶ms.mint, + ¶ms.token_program, + ); let instructions = vec![ build_set_compute_unit_limit(DEFAULT_COMPUTE_UNIT_LIMIT), @@ -55,8 +58,11 @@ pub fn build_exact_svm_transaction(params: &TransactionParams) -> Transaction { build_memo_instruction(), ]; - let message = - Message::new_with_blockhash(&instructions, Some(¶ms.fee_payer), ¶ms.recent_blockhash); + let message = Message::new_with_blockhash( + &instructions, + Some(¶ms.fee_payer), + ¶ms.recent_blockhash, + ); Transaction::new_unsigned(message) } @@ -64,11 +70,7 @@ pub fn build_exact_svm_transaction(params: &TransactionParams) -> Transaction { /// Derive the Associated Token Account address. pub fn derive_ata(owner: &Pubkey, mint: &Pubkey, token_program: &Pubkey) -> Pubkey { let (ata, _bump) = Pubkey::find_program_address( - &[ - owner.as_ref(), - token_program.as_ref(), - mint.as_ref(), - ], + &[owner.as_ref(), token_program.as_ref(), mint.as_ref()], &ASSOCIATED_TOKEN_PROGRAM, ); ata diff --git a/x402-signer/tests/signing.rs b/x402-signer/tests/signing.rs index b1e8c35..8f189e2 100644 --- a/x402-signer/tests/signing.rs +++ b/x402-signer/tests/signing.rs @@ -86,7 +86,11 @@ mod evm_tests { let extensions = Record::default(); let result = evm.sign_payment(&req, &resource, &extensions).await; - assert!(result.is_ok(), "EIP-3009 signing failed: {:?}", result.err()); + assert!( + result.is_ok(), + "EIP-3009 signing failed: {:?}", + result.err() + ); let payload = result.unwrap(); assert_eq!(payload.x402_version, X402V2); @@ -157,17 +161,18 @@ mod svm_tests { use super::*; use solana_pubkey::Pubkey; use std::str::FromStr; - use x402_signer::svm::{ - SvmPaymentSigner, SvmRpc, - }; - use x402_signer::svm::transaction::{TransactionParams, build_exact_svm_transaction, derive_ata}; use x402_networks::svm::SvmAddress; + use x402_signer::svm::transaction::{ + TransactionParams, build_exact_svm_transaction, derive_ata, + }; + use x402_signer::svm::{SvmPaymentSigner, SvmRpc}; #[test] fn derive_ata_deterministic() { let owner = Pubkey::from_str("GVmFbJa2MWLBkk2s9Y4M5hFTdQ41QRNfzZWMg2F3udz").unwrap(); let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); - let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); + let token_program = + Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); let ata1 = derive_ata(&owner, &mint, &token_program); let ata2 = derive_ata(&owner, &mint, &token_program); @@ -183,7 +188,8 @@ mod svm_tests { let payer = Pubkey::new_unique(); let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); let destination_owner = Pubkey::new_unique(); - let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); + let token_program = + Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); let recent_blockhash = solana_hash::Hash::new_unique(); let tx = build_exact_svm_transaction(&TransactionParams { @@ -201,7 +207,10 @@ mod svm_tests { // Fee payer should be first account key assert_eq!(tx.message.account_keys[0], fee_payer); // Signatures should be pre-allocated - assert!(tx.signatures.len() >= 2, "expected at least 2 signature slots"); + assert!( + tx.signatures.len() >= 2, + "expected at least 2 signature slots" + ); } #[test] @@ -210,7 +219,8 @@ mod svm_tests { let payer = Pubkey::new_unique(); let mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); let destination_owner = Pubkey::new_unique(); - let token_program = Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); + let token_program = + Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); let tx = build_exact_svm_transaction(&TransactionParams { fee_payer, @@ -240,7 +250,11 @@ mod svm_tests { // Instruction 3: Memo — random hex nonce let ix3_data = &tx.message.instructions[3].data; - assert_eq!(ix3_data.len(), 32, "memo should be 32 hex chars (16 bytes → hex)"); + assert_eq!( + ix3_data.len(), + 32, + "memo should be 32 hex chars (16 bytes → hex)" + ); } // Mock RPC for testing From 2706938e1e3819bd15b33f3ccd97d5f51ecfb27d Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 14:22:52 +0800 Subject: [PATCH 03/13] feat: implement SvmRpc for Solana RPC client --- Cargo.lock | 1649 ++++++++++++++++++++++++++--- x402-signer/Cargo.toml | 13 +- x402-signer/src/svm/mod.rs | 4 + x402-signer/src/svm/rpc_client.rs | 61 ++ 4 files changed, 1598 insertions(+), 129 deletions(-) create mode 100644 x402-signer/src/svm/rpc_client.rs diff --git a/Cargo.lock b/Cargo.lock index 9604cc0..653c042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "actix-codec" version = "0.5.2" @@ -191,6 +201,69 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "agave-feature-set" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a36f13a213d45f45f8ff87ea9fc6b0a792a7997c76b7c5d6d4a2ebe741d19d0" +dependencies = [ + "ahash", + "solana-epoch-schedule", + "solana-hash 3.1.0", + "solana-pubkey 3.0.0", + "solana-sha256-hasher", + "solana-svm-feature-set", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -1044,6 +1117,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -1053,6 +1132,24 @@ dependencies = [ "serde", ] +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1347,18 +1444,57 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + [[package]] name = "byte-slice-cast" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -1422,6 +1558,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cfg_eval" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "chrono" version = "0.4.44" @@ -1434,6 +1581,59 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "const-hex" version = "1.17.0" @@ -1562,9 +1762,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1578,6 +1788,7 @@ dependencies = [ "fiat-crypto", "rand_core 0.6.4", "rustc_version 0.4.1", + "serde", "subtle", "zeroize", ] @@ -1697,6 +1908,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derivative" version = "2.2.0" @@ -1856,6 +2073,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1929,6 +2152,12 @@ dependencies = [ "bytes", ] +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + [[package]] name = "ff" version = "0.13.1" @@ -2211,6 +2440,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2586,6 +2824,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2611,6 +2871,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -2655,6 +2924,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "k256" version = "0.13.4" @@ -2800,6 +3084,18 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "mime" version = "0.3.17" @@ -2853,6 +3149,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -2898,6 +3205,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.117", @@ -2923,6 +3231,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -3059,6 +3373,24 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -3163,6 +3495,15 @@ dependencies = [ "unarray", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3397,7 +3738,9 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http 1.4.0", "http-body", "http-body-util", @@ -3510,6 +3853,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3734,6 +4083,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -3968,12 +4327,98 @@ dependencies = [ ] [[package]] -name = "solana-address" -version = "1.1.0" +name = "solana-account" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" +checksum = "efc0ed36decb689413b9da5d57f2be49eea5bebb3cf7897015167b0c4336e731" dependencies = [ - "solana-address 2.2.0", + "bincode 1.3.3", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-instruction-error", + "solana-pubkey 4.1.0", + "solana-sdk-ids", + "solana-sysvar", +] + +[[package]] +name = "solana-account-decoder" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fcc1c9e2a2d82c9aa09e0d34940b6c5a43c2fd5eea38c7caed0f6b177e27a8" +dependencies = [ + "Inflector", + "base64", + "bincode 1.3.3", + "bs58", + "bv", + "serde", + "serde_json", + "solana-account", + "solana-account-decoder-client-types", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-config-interface", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-instruction", + "solana-loader-v3-interface", + "solana-nonce", + "solana-program-option", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-rent", + "solana-sdk-ids", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar", + "solana-vote-interface", + "spl-generic-token", + "spl-token-2022-interface", + "spl-token-group-interface", + "spl-token-interface", + "spl-token-metadata-interface", + "thiserror 2.0.18", + "zstd", +] + +[[package]] +name = "solana-account-decoder-client-types" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e62ffb8742a71d3e8b5263ba892cdc65b24b063fa5b5f414205350b031a730" +dependencies = [ + "base64", + "bs58", + "serde", + "serde_json", + "solana-account", + "solana-pubkey 3.0.0", + "zstd", +] + +[[package]] +name = "solana-account-info" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3397241392f5756925029acaa8515dc70fcbe3d8059d4885d7d6533baf64fd" +dependencies = [ + "solana-address 2.2.0", + "solana-program-error", + "solana-program-memory", +] + +[[package]] +name = "solana-address" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" +dependencies = [ + "solana-address 2.2.0", ] [[package]] @@ -3983,6 +4428,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68c5d02824391b072dc5cd0aaa85fb0af9784a21d23286a767994d1e8a322131" dependencies = [ "borsh", + "bytemuck", + "bytemuck_derive", "curve25519-dalek", "five8", "five8_const", @@ -3997,6 +4444,24 @@ dependencies = [ "wincode", ] +[[package]] +name = "solana-address-lookup-table-interface" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e8df0b083c10ce32490410f3795016b1b5d9b4d094658c0a5e496753645b7cd" +dependencies = [ + "bincode 1.3.3", + "bytemuck", + "serde", + "serde_derive", + "solana-clock", + "solana-instruction", + "solana-instruction-error", + "solana-pubkey 4.1.0", + "solana-sdk-ids", + "solana-slot-hashes", +] + [[package]] name = "solana-atomic-u64" version = "3.0.1" @@ -4006,6 +4471,89 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "solana-borsh" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4a37fc44f0633779a619840b5117c2a895996cec57eb3dc10597fac7867875" +dependencies = [ + "borsh", +] + +[[package]] +name = "solana-clock" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-commitment-config" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1517aa49dcfa9cb793ef90e7aac81346d62ca4a546bb1a754030a033e3972e1c" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-config-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e401ae56aed512821cc7a0adaa412ff97fecd2dff4602be7b1330d2daec0c4" +dependencies = [ + "bincode 1.3.3", + "serde", + "serde_derive", + "solana-account", + "solana-instruction", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-short-vec", + "solana-system-interface", +] + +[[package]] +name = "solana-cpi" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-instruction", + "solana-program-error", + "solana-pubkey 4.1.0", + "solana-stable-layout", +] + +[[package]] +name = "solana-curve25519" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d7e1177e6006823b91e0a930d94992ed74f8a6327d54ee50a9457ff72e625a" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "solana-define-syscall 3.0.0", + "subtle", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + [[package]] name = "solana-define-syscall" version = "4.0.1" @@ -4018,6 +4566,78 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03aacdd7a61e2109887a7a7f046caebafce97ddf1150f33722eeac04f9039c73" +[[package]] +name = "solana-derivation-path" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff71743072690fdbdfcdc37700ae1cb77485aaad49019473a81aee099b1e0b8c" +dependencies = [ + "derivation-path", + "qstring", + "uriparse", +] + +[[package]] +name = "solana-epoch-info" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e093c84f6ece620a6b10cd036574b0cd51944231ab32d81f80f76d54aba833e6" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-epoch-rewards" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 4.2.0", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5481e72cc4d52c169db73e4c0cd16de8bc943078aac587ec4817a75cc6388f" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ca9b5cbb6f500f7fd73db5bd95640f71a83f04d6121a0e59a43b202dca2731" +dependencies = [ + "serde", + "serde_derive", + "solana-program-error", + "solana-pubkey 4.1.0", + "solana-sdk-ids", +] + +[[package]] +name = "solana-fee-calculator" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2a5675b2cf8d407c672dc1776492b1f382337720ddf566645ae43237a3d8c3" +dependencies = [ + "log", + "serde", + "serde_derive", +] + [[package]] name = "solana-hash" version = "3.1.0" @@ -4028,218 +4648,936 @@ dependencies = [ ] [[package]] -name = "solana-hash" -version = "4.2.0" +name = "solana-hash" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8064ea1d591ec791be95245058ca40f4f5345d390c200069d0f79bbf55bfae55" +dependencies = [ + "borsh", + "bytemuck", + "bytemuck_derive", + "five8", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wincode", +] + +[[package]] +name = "solana-inflation" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e92f37a14e7c660628752833250dd3dcd8e95309876aee751d7f8769a27947c6" + +[[package]] +name = "solana-instruction" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a6d22d0a6fdf345be294bb9afdcd40c296cdc095e64e7ceaa3bb3c2f608c1c" +dependencies = [ + "bincode 1.3.3", + "borsh", + "serde", + "serde_derive", + "solana-define-syscall 5.0.0", + "solana-instruction-error", + "solana-pubkey 4.1.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3d048edaaeef5a3dc8c01853e585539a74417e4c2d43a9e2c161270045b838" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-program-error", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" +dependencies = [ + "bitflags", + "solana-account-info", + "solana-instruction", + "solana-instruction-error", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-keypair" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263d614c12aa267a3278703175fd6440552ca61bc960b5a02a4482720c53438b" +dependencies = [ + "ed25519-dalek", + "five8", + "five8_core", + "rand 0.9.2", + "solana-address 2.2.0", + "solana-seed-phrase", + "solana-signature", + "solana-signer", +] + +[[package]] +name = "solana-last-restart-slot" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee44c9b1328c5c712c68966fb8de07b47f3e7bac006e74ddd1bb053d3e46e5d" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey 3.0.0", + "solana-sdk-ids", +] + +[[package]] +name = "solana-message" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0448b1fd891c5f46491e5dc7d9986385ba3c852c340db2911dd29faa01d2b08d" +dependencies = [ + "bincode 1.3.3", + "lazy_static", + "serde", + "serde_derive", + "solana-address 2.2.0", + "solana-hash 4.2.0", + "solana-instruction", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-transaction-error", +] + +[[package]] +name = "solana-msg" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726b7cbbc6be6f1c6f29146ac824343b9415133eee8cce156452ad1db93f8008" +dependencies = [ + "solana-define-syscall 5.0.0", +] + +[[package]] +name = "solana-nonce" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc469152a63284ef959b80c59cda015262a021da55d3b8fe42171d89c4b64f8" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator", + "solana-hash 4.2.0", + "solana-pubkey 4.1.0", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-program-entrypoint" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997" +dependencies = [ + "solana-account-info", + "solana-define-syscall 4.0.1", + "solana-program-error", + "solana-pubkey 4.1.0", +] + +[[package]] +name = "solana-program-error" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" +dependencies = [ + "borsh", +] + +[[package]] +name = "solana-program-memory" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4068648649653c2c50546e9a7fb761791b5ab0cda054c771bb5808d3a4b9eb52" +dependencies = [ + "solana-define-syscall 4.0.1", +] + +[[package]] +name = "solana-program-option" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362279f6e8020e4cf11313233789bf619420ad8835ebc91963ee5cec91bb05da" + +[[package]] +name = "solana-program-pack" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7701cb15b90667ae1c89ef4ac35a59c61e66ce58ddee13d729472af7f41d59" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" +dependencies = [ + "solana-address 1.1.0", +] + +[[package]] +name = "solana-pubkey" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b06bd918d60111ee1f97de817113e2040ca0cedb740099ee8d646233f6b906c" +dependencies = [ + "solana-address 2.2.0", +] + +[[package]] +name = "solana-rent" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e860d5499a705369778647e97d760f7670adfb6fc8419dd3d568deccd46d5487" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-reward-info" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82be7946105c2ee6be9f9ee7bd18a068b558389221d29efa92b906476102bfcc" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "solana-rpc-client" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f7447f65aacd7ef752393bec2a9082d0313983b804f92d06fe72caf44e8cd6" +dependencies = [ + "async-trait", + "base64", + "bincode 1.3.3", + "bs58", + "futures", + "indicatif", + "log", + "reqwest", + "reqwest-middleware", + "semver 1.0.27", + "serde", + "serde_json", + "solana-account", + "solana-account-decoder", + "solana-account-decoder-client-types", + "solana-clock", + "solana-commitment-config", + "solana-epoch-info", + "solana-epoch-schedule", + "solana-feature-gate-interface", + "solana-hash 3.1.0", + "solana-instruction", + "solana-message", + "solana-pubkey 3.0.0", + "solana-rpc-client-api", + "solana-signature", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-version", + "solana-vote-interface", + "tokio", +] + +[[package]] +name = "solana-rpc-client-api" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d953ce18f99a07c8101e071dd54d2a99b80835ad2ea1566913c55d4bce2ef" +dependencies = [ + "anyhow", + "jsonrpc-core", + "reqwest", + "reqwest-middleware", + "serde", + "serde_json", + "solana-account-decoder-client-types", + "solana-clock", + "solana-rpc-client-types", + "solana-signer", + "solana-transaction-error", + "solana-transaction-status-client-types", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-rpc-client-types" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "095c430a4ff4ddb10b7399f8d3d26eafeaccb84f8802568594f6f5941c60dff5" +dependencies = [ + "base64", + "bs58", + "semver 1.0.27", + "serde", + "serde_json", + "solana-account", + "solana-account-decoder-client-types", + "solana-address 1.1.0", + "solana-clock", + "solana-commitment-config", + "solana-fee-calculator", + "solana-inflation", + "solana-reward-info", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-version", + "spl-generic-token", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-sanitize" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + +[[package]] +name = "solana-sbpf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15b079e08471a9dbfe1e48b2c7439c85aa2a055cbd54eddd8bd257b0a7dbb29" +dependencies = [ + "byteorder", + "combine", + "hash32", + "log", + "rustc-demangle", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-sdk-ids" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" +dependencies = [ + "solana-address 2.2.0", +] + +[[package]] +name = "solana-sdk-macro" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8765316242300c48242d84a41614cb3388229ec353ba464f6fe62a733e41806f" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "solana-seed-derivable" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7bdb72758e3bec33ed0e2658a920f1f35dfb9ed576b951d20d63cb61ecd95c" +dependencies = [ + "solana-derivation-path", +] + +[[package]] +name = "solana-seed-phrase" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" +dependencies = [ + "hmac", + "pbkdf2", + "sha2", +] + +[[package]] +name = "solana-serde-varint" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950e5b83e839dc0f92c66afc124bb8f40e89bc90f0579e8ec5499296d27f54e3" +dependencies = [ + "serde", +] + +[[package]] +name = "solana-serialize-utils" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" +dependencies = [ + "solana-instruction-error", + "solana-pubkey 4.1.0", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +dependencies = [ + "sha2", + "solana-define-syscall 4.0.1", + "solana-hash 4.2.0", +] + +[[package]] +name = "solana-short-vec" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3bd991c2cc415291c86bb0b6b4d53e93d13bb40344e4c5a2884e0e4f5fa93f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "solana-signature" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132a93134f1262aa832f1849b83bec6c9945669b866da18661a427943b9e801e" +dependencies = [ + "ed25519-dalek", + "five8", + "serde", + "serde-big-array", + "serde_derive", + "solana-sanitize", + "wincode", +] + +[[package]] +name = "solana-signer" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +dependencies = [ + "solana-pubkey 3.0.0", + "solana-signature", + "solana-transaction-error", +] + +[[package]] +name = "solana-slot-hashes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 4.2.0", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f914f6b108f5bba14a280b458d023e3621c9973f27f015a4d755b50e88d89e97" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9f6a291ba063a37780af29e7db14bdd3dc447584d8ba5b3fc4b88e2bbc982fa" +dependencies = [ + "solana-instruction", + "solana-pubkey 4.1.0", +] + +[[package]] +name = "solana-stake-interface" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bc26191b533f9a6e5a14cca05174119819ced680a80febff2f5051a713f0db" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-instruction", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", +] + +[[package]] +name = "solana-svm-feature-set" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62a20c4fc8d409780c4592c17ac3e01b6f3dc949e6ffd3acbda6d2a21e67b53a" + +[[package]] +name = "solana-system-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1790547bfc3061f1ee68ea9d8dc6c973c02a163697b24263a8e9f2e6d4afa2" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey 3.0.0", +] + +[[package]] +name = "solana-sysvar" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690d3dd88f15c21edff68eb391ef8800df7a1f5cec84ee3e8d1abf05affdf74" +dependencies = [ + "base64", + "bincode 1.3.3", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall 4.0.1", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash 4.2.0", + "solana-instruction", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey 4.1.0", + "solana-rent", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" +dependencies = [ + "solana-address 2.2.0", + "solana-sdk-ids", +] + +[[package]] +name = "solana-transaction" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96697cff5075a028265324255efed226099f6d761ca67342b230d09f72cc48d2" +dependencies = [ + "bincode 1.3.3", + "serde", + "serde_derive", + "solana-address 2.2.0", + "solana-hash 4.2.0", + "solana-instruction", + "solana-instruction-error", + "solana-message", + "solana-sanitize", + "solana-sdk-ids", + "solana-short-vec", + "solana-signature", + "solana-signer", + "solana-transaction-error", +] + +[[package]] +name = "solana-transaction-context" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be7c191d89fb883fef0b4bb4225121f7ad14eb5664d5dc9707b4af661e21924c" +dependencies = [ + "bincode 1.3.3", + "serde", + "solana-account", + "solana-instruction", + "solana-instructions-sysvar", + "solana-pubkey 3.0.0", + "solana-rent", + "solana-sbpf", + "solana-sdk-ids", +] + +[[package]] +name = "solana-transaction-error" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8396904805b0b385b9de115a652fe80fd01e5b98ce0513f4fcd8184ada9bb792" +dependencies = [ + "serde", + "serde_derive", + "solana-instruction-error", + "solana-sanitize", +] + +[[package]] +name = "solana-transaction-status-client-types" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a02265337e99bf3e446de1d8133b2d28556f06780e9ab516870317688f332e0" +dependencies = [ + "base64", + "bincode 1.3.3", + "bs58", + "serde", + "serde_json", + "solana-account-decoder-client-types", + "solana-commitment-config", + "solana-instruction", + "solana-message", + "solana-pubkey 3.0.0", + "solana-reward-info", + "solana-signature", + "solana-transaction", + "solana-transaction-context", + "solana-transaction-error", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-version" +version = "3.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8064ea1d591ec791be95245058ca40f4f5345d390c200069d0f79bbf55bfae55" +checksum = "17a9c5d23a31d8f34aac59812099c9d8d76203a447d04b65824f5c913ced9072" dependencies = [ - "borsh", - "five8", + "agave-feature-set", + "rand 0.8.5", + "semver 1.0.27", "serde", - "serde_derive", - "solana-atomic-u64", "solana-sanitize", - "wincode", + "solana-serde-varint", ] [[package]] -name = "solana-instruction" -version = "3.2.0" +name = "solana-vote-interface" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a6d22d0a6fdf345be294bb9afdcd40c296cdc095e64e7ceaa3bb3c2f608c1c" +checksum = "db6e123e16bfdd7a81d71b4c4699e0b29580b619f4cd2ef5b6aae1eb85e8979f" dependencies = [ - "borsh", + "bincode 1.3.3", + "cfg_eval", + "num-derive", + "num-traits", "serde", - "solana-define-syscall 5.0.0", + "serde_derive", + "serde_with", + "solana-clock", + "solana-hash 3.1.0", + "solana-instruction", "solana-instruction-error", - "solana-pubkey 4.1.0", + "solana-pubkey 3.0.0", + "solana-rent", + "solana-sdk-ids", + "solana-serde-varint", + "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface", ] [[package]] -name = "solana-instruction-error" -version = "2.2.0" +name = "solana-zk-sdk" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3d048edaaeef5a3dc8c01853e585539a74417e4c2d43a9e2c161270045b838" +checksum = "9602bcb1f7af15caef92b91132ec2347e1c51a72ecdbefdaefa3eac4b8711475" dependencies = [ + "aes-gcm-siv", + "base64", + "bincode 1.3.3", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "getrandom 0.2.17", + "itertools 0.12.1", + "js-sys", + "merlin", + "num-derive", "num-traits", + "rand 0.8.5", "serde", "serde_derive", - "solana-program-error", -] - -[[package]] -name = "solana-keypair" -version = "3.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "263d614c12aa267a3278703175fd6440552ca61bc960b5a02a4482720c53438b" -dependencies = [ - "ed25519-dalek", - "five8", - "five8_core", - "rand 0.9.2", - "solana-address 2.2.0", + "serde_json", + "sha3", + "solana-derivation-path", + "solana-instruction", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-seed-derivable", "solana-seed-phrase", "solana-signature", "solana-signer", + "subtle", + "thiserror 2.0.18", + "wasm-bindgen", + "zeroize", ] [[package]] -name = "solana-message" -version = "3.1.0" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0448b1fd891c5f46491e5dc7d9986385ba3c852c340db2911dd29faa01d2b08d" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "bincode 1.3.3", - "lazy_static", - "serde", - "serde_derive", - "solana-address 2.2.0", - "solana-hash 4.2.0", - "solana-instruction", - "solana-sanitize", - "solana-sdk-ids", - "solana-short-vec", - "solana-transaction-error", + "base64ct", + "der", ] [[package]] -name = "solana-program-error" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" - -[[package]] -name = "solana-pubkey" -version = "3.0.0" +name = "spl-discriminator" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8909d399deb0851aa524420beeb5646b115fd253ef446e35fe4504c904da3941" +checksum = "d48cc11459e265d5b501534144266620289720b4c44522a47bc6b63cd295d2f3" dependencies = [ - "solana-address 1.1.0", + "bytemuck", + "solana-program-error", + "solana-sha256-hasher", + "spl-discriminator-derive", ] [[package]] -name = "solana-pubkey" -version = "4.1.0" +name = "spl-discriminator-derive" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b06bd918d60111ee1f97de817113e2040ca0cedb740099ee8d646233f6b906c" +checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ - "solana-address 2.2.0", + "quote", + "spl-discriminator-syn", + "syn 2.0.117", ] [[package]] -name = "solana-sanitize" -version = "3.0.1" +name = "spl-discriminator-syn" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" +checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67" +dependencies = [ + "proc-macro2", + "quote", + "sha2", + "syn 2.0.117", + "thiserror 1.0.69", +] [[package]] -name = "solana-sdk-ids" -version = "3.1.0" +name = "spl-generic-token" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" +checksum = "233df81b75ab99b42f002b5cdd6e65a7505ffa930624f7096a7580a56765e9cf" dependencies = [ - "solana-address 2.2.0", + "bytemuck", + "solana-pubkey 3.0.0", ] [[package]] -name = "solana-seed-phrase" -version = "3.0.0" +name = "spl-pod" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" +checksum = "d6f3df240f67bea453d4bc5749761e45436d14b9457ed667e0300555d5c271f3" dependencies = [ - "hmac", - "pbkdf2", - "sha2", + "borsh", + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "num_enum", + "solana-program-error", + "solana-program-option", + "solana-pubkey 3.0.0", + "solana-zk-sdk", + "thiserror 2.0.18", ] [[package]] -name = "solana-sha256-hasher" -version = "3.1.0" +name = "spl-token-2022-interface" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" +checksum = "2fcd81188211f4b3c8a5eba7fd534c7142f9dd026123b3472492782cc72f4dc6" dependencies = [ - "sha2", - "solana-define-syscall 4.0.1", - "solana-hash 4.2.0", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-program-option", + "solana-program-pack", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-zk-sdk", + "spl-pod", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-type-length-value", + "thiserror 2.0.18", ] [[package]] -name = "solana-short-vec" -version = "3.2.0" +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3bd991c2cc415291c86bb0b6b4d53e93d13bb40344e4c5a2884e0e4f5fa93f" +checksum = "879a9ebad0d77383d3ea71e7de50503554961ff0f4ef6cbca39ad126e6f6da3a" dependencies = [ - "serde_core", + "bytemuck", + "solana-account-info", + "solana-curve25519", + "solana-instruction", + "solana-instructions-sysvar", + "solana-msg", + "solana-program-error", + "solana-pubkey 3.0.0", + "solana-sdk-ids", + "solana-zk-sdk", + "spl-pod", + "thiserror 2.0.18", ] [[package]] -name = "solana-signature" -version = "3.3.0" +name = "spl-token-confidential-transfer-proof-generation" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132a93134f1262aa832f1849b83bec6c9945669b866da18661a427943b9e801e" +checksum = "a0cd59fce3dc00f563c6fa364d67c3f200d278eae681f4dc250240afcfe044b1" dependencies = [ - "ed25519-dalek", - "five8", - "serde", - "serde-big-array", - "serde_derive", - "solana-sanitize", - "wincode", + "curve25519-dalek", + "solana-zk-sdk", + "thiserror 2.0.18", ] [[package]] -name = "solana-signer" -version = "3.0.0" +name = "spl-token-group-interface" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +checksum = "452d0f758af20caaa10d9a6f7608232e000d4c74462f248540b3d2ddfa419776" dependencies = [ + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-instruction", + "solana-program-error", "solana-pubkey 3.0.0", - "solana-signature", - "solana-transaction-error", + "spl-discriminator", + "spl-pod", + "thiserror 2.0.18", ] [[package]] -name = "solana-transaction" -version = "3.1.0" +name = "spl-token-interface" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96697cff5075a028265324255efed226099f6d761ca67342b230d09f72cc48d2" +checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" dependencies = [ - "bincode 1.3.3", - "serde", - "serde_derive", - "solana-address 2.2.0", - "solana-hash 4.2.0", + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", "solana-instruction", - "solana-instruction-error", - "solana-message", - "solana-sanitize", + "solana-program-error", + "solana-program-option", + "solana-program-pack", + "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-short-vec", - "solana-signature", - "solana-signer", - "solana-transaction-error", + "thiserror 2.0.18", ] [[package]] -name = "solana-transaction-error" -version = "3.1.0" +name = "spl-token-metadata-interface" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8396904805b0b385b9de115a652fe80fd01e5b98ce0513f4fcd8184ada9bb792" +checksum = "9c467c7c3bd056f8fe60119e7ec34ddd6f23052c2fa8f1f51999098063b72676" dependencies = [ - "serde", - "serde_derive", - "solana-instruction-error", - "solana-sanitize", + "borsh", + "num-derive", + "num-traits", + "solana-borsh", + "solana-instruction", + "solana-program-error", + "solana-pubkey 3.0.0", + "spl-discriminator", + "spl-pod", + "spl-type-length-value", + "thiserror 2.0.18", ] [[package]] -name = "spki" -version = "0.7.3" +name = "spl-type-length-value" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "ca20a1a19f4507a98ca4b28ff5ed54cac9b9d34ed27863e2bde50a3238f9a6ac" dependencies = [ - "base64ct", - "der", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info", + "solana-msg", + "solana-program-error", + "spl-discriminator", + "spl-pod", + "thiserror 2.0.18", ] [[package]] @@ -4589,13 +5927,18 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags", "bytes", + "futures-core", "futures-util", "http 1.4.0", "http-body", + "http-body-util", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -4720,12 +6063,43 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -4738,6 +6112,16 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + [[package]] name = "url" version = "2.5.8" @@ -4784,6 +6168,12 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -5416,11 +6806,14 @@ dependencies = [ "rand 0.9.2", "serde", "serde_json", + "solana-account", "solana-hash 3.1.0", "solana-instruction", "solana-keypair", "solana-message", "solana-pubkey 4.1.0", + "solana-rpc-client", + "solana-rpc-client-api", "solana-signature", "solana-signer", "solana-transaction", diff --git a/x402-signer/Cargo.toml b/x402-signer/Cargo.toml index b8845c3..42d0ecc 100644 --- a/x402-signer/Cargo.toml +++ b/x402-signer/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" description = "Buyer-side signing SDK for the x402 payment protocol." [features] -default = ["evm", "svm"] +default = ["evm", "svm", "solana-rpc"] evm = [ "x402-networks/evm", "dep:alloy-core", @@ -31,6 +31,12 @@ svm = [ "dep:rand", "dep:hex", ] +solana-rpc = [ + "svm", + "dep:solana-rpc-client", + "dep:solana-rpc-client-api", + "dep:solana-account", +] [dependencies] # === Core === @@ -59,6 +65,11 @@ bincode = { version = "2.0", features = ["serde"], optional = true } base64 = { version = "0.22", optional = true } hex = { version = "0.4", optional = true } +# === Feature "solana-rpc" === +solana-rpc-client = { version = "3", optional = true } +solana-rpc-client-api = { version = "3", optional = true } +solana-account = { version = "3", optional = true } + [dev-dependencies] alloy = { version = "1" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/x402-signer/src/svm/mod.rs b/x402-signer/src/svm/mod.rs index cf77ace..adc056b 100644 --- a/x402-signer/src/svm/mod.rs +++ b/x402-signer/src/svm/mod.rs @@ -1,10 +1,14 @@ pub mod constants; pub mod rpc; +#[cfg(feature = "solana-rpc")] +pub mod rpc_client; pub mod signer; pub mod transaction; pub mod types; pub mod wallet; pub use rpc::SvmRpc; +#[cfg(feature = "solana-rpc")] +pub use rpc_client::SolanaRpcError; pub use signer::{SvmPaymentSigner, SvmSigningError}; pub use wallet::SvmWalletSigner; diff --git a/x402-signer/src/svm/rpc_client.rs b/x402-signer/src/svm/rpc_client.rs new file mode 100644 index 0000000..d5d151e --- /dev/null +++ b/x402-signer/src/svm/rpc_client.rs @@ -0,0 +1,61 @@ +use solana_account::Account; +use solana_rpc_client_api::client_error::Error as ClientError; + +use x402_networks::svm::SvmAddress; + +use super::{ + constants::{TOKEN_2022_PROGRAM, TOKEN_PROGRAM}, + rpc::SvmRpc, + types::MintInfo, +}; + +/// Errors from the Solana RPC-backed [`SvmRpc`] implementation. +#[derive(Debug, thiserror::Error)] +pub enum SolanaRpcError { + #[error("RPC client error: {0}")] + Client(#[from] ClientError), + + #[error("mint account data too short (expected >= 45 bytes, got {0})")] + InvalidMintData(usize), + + #[error("mint account owner {0} is not a known SPL Token program")] + UnknownTokenProgram(String), +} + +/// Minimum byte length of an SPL Token Mint account (Mint layout). +const MIN_MINT_DATA_LEN: usize = 82; +/// Byte offset of the `decimals` field inside the SPL Token Mint layout. +const DECIMALS_OFFSET: usize = 44; + +impl SvmRpc for solana_rpc_client::nonblocking::rpc_client::RpcClient { + type Error = SolanaRpcError; + + async fn get_latest_blockhash(&self) -> Result { + Ok(self.get_latest_blockhash().await?) + } + + async fn fetch_mint_info(&self, mint: SvmAddress) -> Result { + let account: Account = self.get_account(&mint.0).await?; + + let program_address = if account.owner == TOKEN_PROGRAM { + SvmAddress(TOKEN_PROGRAM) + } else if account.owner == TOKEN_2022_PROGRAM { + SvmAddress(TOKEN_2022_PROGRAM) + } else { + return Err(SolanaRpcError::UnknownTokenProgram( + account.owner.to_string(), + )); + }; + + if account.data.len() < MIN_MINT_DATA_LEN { + return Err(SolanaRpcError::InvalidMintData(account.data.len())); + } + + let decimals = account.data[DECIMALS_OFFSET]; + + Ok(MintInfo { + program_address, + decimals, + }) + } +} From 8f1e058aea98759fb25fec039018a3fabdbe9369 Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 15:07:17 +0800 Subject: [PATCH 04/13] feat: reqwest middleware --- Cargo.lock | 65 +++++++++++++++++--- x402-signer/Cargo.toml | 14 ++++- x402-signer/src/lib.rs | 3 + x402-signer/src/middleware.rs | 108 ++++++++++++++++++++++++++++++++++ x402-signer/src/signer.rs | 6 +- 5 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 x402-signer/src/middleware.rs diff --git a/Cargo.lock b/Cargo.lock index 653c042..87e3c6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,7 +631,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.18", @@ -675,7 +675,7 @@ dependencies = [ "alloy-transport-http", "futures", "pin-project", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "tokio", @@ -877,7 +877,7 @@ dependencies = [ "alloy-json-rpc", "alloy-transport", "itertools 0.14.0", - "reqwest", + "reqwest 0.12.28", "serde_json", "tower", "tracing", @@ -3770,6 +3770,35 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reqwest-middleware" version = "0.4.2" @@ -3779,12 +3808,26 @@ dependencies = [ "anyhow", "async-trait", "http 1.4.0", - "reqwest", + "reqwest 0.12.28", "serde", "thiserror 1.0.69", "tower-service", ] +[[package]] +name = "reqwest-middleware" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199dda04a536b532d0cc04d7979e39b1c763ea749bf91507017069c00b96056f" +dependencies = [ + "anyhow", + "async-trait", + "http 1.4.0", + "reqwest 0.13.2", + "thiserror 2.0.18", + "tower-service", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -4899,8 +4942,8 @@ dependencies = [ "futures", "indicatif", "log", - "reqwest", - "reqwest-middleware", + "reqwest 0.12.28", + "reqwest-middleware 0.4.2", "semver 1.0.27", "serde", "serde_json", @@ -4934,8 +4977,8 @@ checksum = "841d953ce18f99a07c8101e071dd54d2a99b80835ad2ea1566913c55d4bce2ef" dependencies = [ "anyhow", "jsonrpc-core", - "reqwest", - "reqwest-middleware", + "reqwest 0.12.28", + "reqwest-middleware 0.4.2", "serde", "serde_json", "solana-account-decoder-client-types", @@ -6747,7 +6790,7 @@ dependencies = [ "hex", "http 1.4.0", "rand 0.9.2", - "reqwest-middleware", + "reqwest-middleware 0.4.2", "serde", "serde_json", "solana-pubkey 4.1.0", @@ -6800,10 +6843,14 @@ dependencies = [ "alloy-core", "alloy-primitives", "alloy-signer", + "async-trait", "base64", "bincode 2.0.1", "hex", + "http 1.4.0", "rand 0.9.2", + "reqwest 0.13.2", + "reqwest-middleware 0.5.1", "serde", "serde_json", "solana-account", diff --git a/x402-signer/Cargo.toml b/x402-signer/Cargo.toml index 42d0ecc..2412109 100644 --- a/x402-signer/Cargo.toml +++ b/x402-signer/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" description = "Buyer-side signing SDK for the x402 payment protocol." [features] -default = ["evm", "svm", "solana-rpc"] +default = ["evm", "svm", "solana-rpc", "reqwest"] evm = [ "x402-networks/evm", "dep:alloy-core", @@ -37,6 +37,12 @@ solana-rpc = [ "dep:solana-rpc-client-api", "dep:solana-account", ] +reqwest = [ + "dep:reqwest", + "dep:reqwest-middleware", + "dep:http", + "dep:async-trait", +] [dependencies] # === Core === @@ -70,6 +76,12 @@ solana-rpc-client = { version = "3", optional = true } solana-rpc-client-api = { version = "3", optional = true } solana-account = { version = "3", optional = true } +# === Feature "reqwest" === +reqwest = { version = "0.13", default-features = false, optional = true } +reqwest-middleware = { version = "0.5", optional = true } +http = { version = "1", optional = true } +async-trait = { version = "0.1", optional = true } + [dev-dependencies] alloy = { version = "1" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/x402-signer/src/lib.rs b/x402-signer/src/lib.rs index de83bf8..6969221 100644 --- a/x402-signer/src/lib.rs +++ b/x402-signer/src/lib.rs @@ -9,6 +9,9 @@ pub mod evm; #[cfg(feature = "svm")] pub mod svm; +#[cfg(feature = "reqwest")] +pub mod middleware; + pub use client::X402Client; pub use errors::SigningError; pub use selector::select_requirements; diff --git a/x402-signer/src/middleware.rs b/x402-signer/src/middleware.rs new file mode 100644 index 0000000..c98193d --- /dev/null +++ b/x402-signer/src/middleware.rs @@ -0,0 +1,108 @@ +//! Reqwest middleware for automatic x402 payment handling. +//! +//! Provides [`X402PaymentMiddleware`], a [`reqwest_middleware::Middleware`] implementation +//! that intercepts HTTP 402 responses, signs a payment using an [`X402Client`], +//! and retries the request with the `PAYMENT-SIGNATURE` header. + +use async_trait::async_trait; +use http::Extensions; +use reqwest::{Request, Response, StatusCode, header::HeaderValue}; +use reqwest_middleware::{Middleware, Next}; +use x402_core::{transport::PaymentRequired, types::Base64EncodedHeader}; + +use crate::{X402Client, signer::PaymentSigner}; + +/// Error type for X402 middleware operations. +#[derive(Debug, thiserror::Error)] +pub enum X402MiddlewareError { + #[error("missing or invalid PAYMENT-REQUIRED header in 402 response")] + MissingPaymentRequiredHeader, + + #[error("request body is not cloneable, cannot retry with payment")] + RequestNotCloneable, + + #[error("failed to encode/decode x402 header: {0}")] + HeaderCodec(#[from] x402_core::errors::Error), + + #[error("payment signing failed: {0}")] + Signing(#[from] crate::errors::SigningError), +} + +/// Reqwest middleware that automatically handles x402 payment flows. +/// +/// When a request returns HTTP 402 with a `PAYMENT-REQUIRED` header, +/// this middleware will: +/// 1. Decode the payment requirements from the header +/// 2. Sign a payment using the configured [`X402Client`] +/// 3. Retry the request with the `PAYMENT-SIGNATURE` header +pub struct X402PaymentMiddleware

{ + client: X402Client

, +} + +impl

X402PaymentMiddleware

{ + /// Create a new middleware wrapping the given signing client. + pub fn new(client: X402Client

) -> Self { + Self { client } + } +} + +#[async_trait] +impl Middleware for X402PaymentMiddleware

{ + async fn handle( + &self, + req: Request, + extensions: &mut Extensions, + next: Next<'_>, + ) -> reqwest_middleware::Result { + let retry_req = req.try_clone(); + let response = next.clone().run(req, extensions).await?; + + if response.status() != StatusCode::PAYMENT_REQUIRED { + return Ok(response); + } + + let header_str = response + .headers() + .get("PAYMENT-REQUIRED") + .ok_or_else(|| { + reqwest_middleware::Error::middleware( + X402MiddlewareError::MissingPaymentRequiredHeader, + ) + })? + .to_str() + .map_err(|_| { + reqwest_middleware::Error::middleware( + X402MiddlewareError::MissingPaymentRequiredHeader, + ) + })?; + + let payment_required = PaymentRequired::try_from(Base64EncodedHeader( + header_str.to_string(), + )) + .map_err(|e| reqwest_middleware::Error::middleware(X402MiddlewareError::HeaderCodec(e)))?; + + let payment_payload = self + .client + .create_payment(&payment_required) + .await + .map_err(|e| reqwest_middleware::Error::middleware(X402MiddlewareError::Signing(e)))?; + + let encoded: Base64EncodedHeader = payment_payload.try_into().map_err(|e| { + reqwest_middleware::Error::middleware(X402MiddlewareError::HeaderCodec(e)) + })?; + + let mut retry = retry_req.ok_or_else(|| { + reqwest_middleware::Error::middleware(X402MiddlewareError::RequestNotCloneable) + })?; + retry.headers_mut().insert( + "PAYMENT-SIGNATURE", + HeaderValue::from_str(&encoded.0).map_err(|_| { + reqwest_middleware::Error::middleware( + X402MiddlewareError::MissingPaymentRequiredHeader, + ) + })?, + ); + + next.run(retry, extensions).await + } +} diff --git a/x402-signer/src/signer.rs b/x402-signer/src/signer.rs index f4a0a97..1ebbdd4 100644 --- a/x402-signer/src/signer.rs +++ b/x402-signer/src/signer.rs @@ -19,14 +19,14 @@ pub trait PaymentSigner { requirements: &PaymentRequirements, resource: &PaymentResource, extensions: &Record, - ) -> impl Future>; + ) -> impl Future> + Send; } /// Tuple composition: tries `A` first, falls back to `B`. impl PaymentSigner for (A, B) where - A: PaymentSigner, - B: PaymentSigner, + A: PaymentSigner + Sync, + B: PaymentSigner + Sync, { type Error = A::Error; From 4f7e138eda7cc4db14214b409f7b7d298bb345e3 Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 15:26:36 +0800 Subject: [PATCH 05/13] chore: remove signer modules from x402-kit --- Cargo.lock | 5 - x402-kit/Cargo.toml | 41 ++--- x402-kit/examples/actix_web_seller.rs | 2 +- x402-kit/examples/axum_seller.rs | 2 +- x402-kit/src/lib.rs | 10 +- x402-kit/src/networks/mod.rs | 2 + x402-kit/src/schemes/exact_evm_signer.rs | 209 ----------------------- x402-kit/src/schemes/mod.rs | 5 +- 8 files changed, 29 insertions(+), 247 deletions(-) delete mode 100644 x402-kit/src/schemes/exact_evm_signer.rs diff --git a/Cargo.lock b/Cargo.lock index 87e3c6b..6457944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6779,17 +6779,12 @@ name = "x402-kit" version = "2.3.0" dependencies = [ "actix-web", - "alloy", - "alloy-core", "alloy-primitives", - "alloy-signer", "axum", - "bincode 2.0.1", "bon", "futures-util", "hex", "http 1.4.0", - "rand 0.9.2", "reqwest-middleware 0.4.2", "serde", "serde_json", diff --git a/x402-kit/Cargo.toml b/x402-kit/Cargo.toml index a080c41..a3611d7 100644 --- a/x402-kit/Cargo.toml +++ b/x402-kit/Cargo.toml @@ -8,16 +8,15 @@ license = "MIT" description = "(V2 Supported) A fully modular SDK for building complex X402 payment integrations." [features] -default = [ - "facilitator-client", - "evm-signer", - "svm-signer", - "axum", - "actix-web", -] +default = ["facilitator-client", "evm", "svm", "axum", "actix-web"] facilitator-client = ["dep:http", "dep:reqwest-middleware"] -evm-signer = ["dep:alloy-core", "dep:alloy-signer", "dep:rand"] -svm-signer = ["dep:bincode"] +evm = ["x402-networks/evm", "dep:alloy-primitives"] +svm = [ + "x402-networks/svm", + "dep:solana-pubkey", + "dep:solana-signature", + "dep:hex", +] paywall = ["dep:x402-paywall"] axum = ["paywall", "x402-paywall/axum"] actix-web = ["paywall", "x402-paywall/actix-web"] @@ -25,37 +24,31 @@ actix-web = ["paywall", "x402-paywall/actix-web"] [dependencies] # === Core Deps === x402-core = { version = "2.3.0", path = "../x402-core" } -x402-networks = { version = "2.3.0", path = "../x402-networks" } +x402-networks = { version = "2.3.0", path = "../x402-networks", default-features = false } x402-extensions = { version = "0.2.0", path = "../x402-extensions" } -hex = { version = "0.4" } -alloy-primitives = { version = "1.4" } bon = { version = "3.8" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } thiserror = { version = "2.0" } url = { version = "2.5" } -solana-pubkey = { version = "4.0" } -solana-signature = { version = "3.1" } - -# === Feature "facilitator-client" === -reqwest-middleware = { version = "0.4.2", optional = true, features = ["json"] } -# === Feature "evm-signer" === -alloy-core = { version = "1.4", features = ["sol-types"], optional = true } -alloy-signer = { version = "1.1", optional = true } -rand = { version = "0.9", optional = true } +# === Feature "evm" === +alloy-primitives = { version = "1.4", optional = true } -# === Feature "svm-signer" === -bincode = { version = "2.0", features = ["serde"], optional = true } +# === Feature "svm" === +solana-pubkey = { version = "4.0", optional = true } +solana-signature = { version = "3.1", optional = true } +hex = { version = "0.4", optional = true } # === Feature "facilitator-client" === +reqwest-middleware = { version = "0.4.2", optional = true, features = ["json"] } http = { version = "1.4", optional = true } # === Feature "paywall" === x402-paywall = { version = "2.3.0", path = "../x402-paywall", optional = true, default-features = false } [dev-dependencies] -alloy = { version = "1" } +alloy-primitives = { version = "1.4" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } url = { version = "2.5" } url-macro = { version = "0.2" } diff --git a/x402-kit/examples/actix_web_seller.rs b/x402-kit/examples/actix_web_seller.rs index e45e072..2c3323a 100644 --- a/x402-kit/examples/actix_web_seller.rs +++ b/x402-kit/examples/actix_web_seller.rs @@ -5,7 +5,7 @@ use actix_web::{ middleware::{self, Next}, web, }; -use alloy::primitives::address; +use alloy_primitives::address; use serde_json::json; use solana_pubkey::pubkey; use url::Url; diff --git a/x402-kit/examples/axum_seller.rs b/x402-kit/examples/axum_seller.rs index b75e61a..a3206d3 100644 --- a/x402-kit/examples/axum_seller.rs +++ b/x402-kit/examples/axum_seller.rs @@ -1,4 +1,4 @@ -use alloy::primitives::address; +use alloy_primitives::address; use axum::{ Extension, Json, Router, extract::{Request, State}, diff --git a/x402-kit/src/lib.rs b/x402-kit/src/lib.rs index 2c91a9f..e125df4 100644 --- a/x402-kit/src/lib.rs +++ b/x402-kit/src/lib.rs @@ -1,8 +1,9 @@ //! # X402 Kit //! -//! X402 Kit is a fully modular, framework-agnostic, easy-to-extend SDK for building complex X402 payment integrations. +//! X402 Kit is a fully modular, framework-agnostic, easy-to-extend SDK for building seller-side X402 payment integrations. //! -//! X402-kit is **not a facilitator** — it's a composable SDK for buyers (signers) and sellers (servers) to build custom business logic. +//! X402-kit is **not a facilitator** — it's a composable SDK for sellers (servers) to build custom payment-gated business logic. +//! For buyer-side signing, use the [`x402-signer`](https://docs.rs/x402-signer) crate. //! Future support for modular facilitator components is planned. //! //! ## Related Crates @@ -11,11 +12,12 @@ //! for the X402 protocol. This crate provides the foundational building blocks that `x402-kit` builds upon. //! - **[`x402_paywall`]**: A framework-agnostic HTTP paywall middleware //! built on top of `x402-kit`. Use it to protect HTTP resources with X402 payments. +//! - **[`x402-signer`](https://docs.rs/x402-signer)**: Buyer-side signing SDK for the x402 payment protocol. //! //! ## Quick Start //! //! ``` -//! use alloy::primitives::address; +//! use alloy_primitives::address; //! use axum::{ //! extract::{Request, State}, //! middleware::{from_fn_with_state, Next}, @@ -74,7 +76,7 @@ //! You can accept payments from multiple networks (e.g., EVM and SVM) using [`transport::Accepts`]: //! //! ``` -//! use alloy::primitives::address; +//! use alloy_primitives::address; //! use solana_pubkey::pubkey; //! use x402_kit::{ //! networks::{evm::assets::UsdcBaseSepolia, svm::assets::UsdcSolanaDevnet}, diff --git a/x402-kit/src/networks/mod.rs b/x402-kit/src/networks/mod.rs index 3737a32..0276eaa 100644 --- a/x402-kit/src/networks/mod.rs +++ b/x402-kit/src/networks/mod.rs @@ -1,2 +1,4 @@ +#[cfg(feature = "evm")] pub mod evm; +#[cfg(feature = "svm")] pub mod svm; diff --git a/x402-kit/src/schemes/exact_evm_signer.rs b/x402-kit/src/schemes/exact_evm_signer.rs deleted file mode 100644 index 43be902..0000000 --- a/x402-kit/src/schemes/exact_evm_signer.rs +++ /dev/null @@ -1,209 +0,0 @@ -use alloy_core::{ - sol, - sol_types::{Eip712Domain, SolStruct, eip712_domain}, -}; -use alloy_primitives::{FixedBytes, U256}; -use alloy_signer::{Error as AlloySignerError, Signer as AlloySigner}; -use serde::Deserialize; - -use crate::{ - core::{PaymentSelection, Scheme, SchemeSigner}, - networks::evm::{EvmAddress, EvmSignature, ExplicitEvmAsset, ExplicitEvmNetwork}, - schemes::exact_evm::*, -}; - -use std::{fmt::Debug, time::SystemTime}; - -pub trait AuthorizationSigner { - type Error: std::error::Error; - - fn sign_authorization( - &self, - authorization: &Eip3009Authorization, - asset_eip712_domain: &Eip712Domain, - ) -> impl Future>; -} - -sol!( - /// Represent EIP-3009 Authorization struct - /// - /// For generating the EIP-712 signing hash - struct Eip3009Authorization { - address from; - address to; - uint256 value; - uint256 validAfter; - uint256 validBefore; - bytes32 nonce; - } -); - -impl From for Eip3009Authorization { - fn from(authorization: ExactEvmAuthorization) -> Self { - Eip3009Authorization { - from: authorization.from.0, - to: authorization.to.0, - value: U256::from(authorization.value.0), - validAfter: U256::from(authorization.valid_after.0), - validBefore: U256::from(authorization.valid_before.0), - nonce: FixedBytes(authorization.nonce.0), - } - } -} - -impl AuthorizationSigner for S { - type Error = AlloySignerError; - - async fn sign_authorization( - &self, - authorization: &Eip3009Authorization, - domain: &Eip712Domain, - ) -> Result { - let eip712_hash = authorization.eip712_signing_hash(domain); - let signature = self.sign_hash(&eip712_hash).await?; - - Ok(EvmSignature(signature)) - } -} - -pub struct ExactEvmSigner { - pub signer: S, - pub asset: A, -} - -#[derive(Debug, thiserror::Error)] -pub enum ExactEvmSignError { - #[error("Signer error: {0}")] - SignerError(S::Error), - #[error("System time error: {0}")] - SystemTimeError(#[from] std::time::SystemTimeError), -} - -impl SchemeSigner for ExactEvmSigner -where - S: AuthorizationSigner + Debug, - A: ExplicitEvmAsset, -{ - type Scheme = ExactEvmScheme; - type Error = ExactEvmSignError; - - async fn sign( - &self, - selected: &PaymentSelection, - ) -> Result<::Payload, Self::Error> { - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs(); - - #[derive(Deserialize, Default)] - struct Eip712DomainExtra { - name: String, - version: String, - } - - let eip712_domain_info = selected - .extra - .as_ref() - .and_then(|extra| serde_json::from_value::(extra.clone()).ok()) - // Use empty string if not provided -- This doesn't work in many cases! - .unwrap_or_default(); - - let authorization = ExactEvmAuthorization { - from: selected.pay_to, - to: selected.pay_to, - value: selected.amount, - // Valid after: now - 5mins - valid_after: TimestampSeconds(now.saturating_sub(300)), - valid_before: TimestampSeconds(now + selected.max_timeout_seconds), - nonce: Nonce(rand::random()), - }; - - let signer = &self.signer; - let auth_clone = authorization.clone(); - let domain = eip712_domain!( - name: eip712_domain_info.name, - version: eip712_domain_info.version, - chain_id: A::Network::NETWORK.chain_id, - verifying_contract: A::ASSET.address.0, - ); - let signature = signer - .sign_authorization(&auth_clone.into(), &domain) - .await - .map_err(Self::Error::SignerError)?; - Ok(ExactEvmPayload { - signature, - authorization, - }) - } -} - -#[cfg(test)] -mod tests { - use alloy::signers::local::PrivateKeySigner; - use alloy_primitives::address; - use serde_json::json; - use url::Url; - - use crate::{ - core::Resource, - networks::evm::{assets::UsdcBaseSepolia, networks::BaseSepolia}, - types::{AmountValue, Record}, - }; - - use super::*; - - #[tokio::test] - async fn test_signing() { - let signer = PrivateKeySigner::random(); - - let evm_signer = ExactEvmSigner { - signer, - asset: UsdcBaseSepolia, - }; - - let resource = Resource::builder() - .url(Url::parse("https://example.com/payment").unwrap()) - .description("Payment for services".to_string()) - .mime_type("application/json".to_string()) - .build(); - - let payment = PaymentSelection { - amount: 1000u64.into(), - resource, - pay_to: EvmAddress(address!("0x3CB9B3bBfde8501f411bB69Ad3DC07908ED0dE20")), - max_timeout_seconds: 60, - asset: UsdcBaseSepolia::ASSET.address, - extra: Some(json!({ - "name": "USD Coin", - "version": "2" - })), - extensions: Record::new(), - }; - - let payload = evm_signer - .sign(&payment) - .await - .expect("Signing should succeed"); - - assert_eq!(payload.authorization.value, AmountValue(1000)); - - // Verify the signature - let domain = eip712_domain! { - name: "USD Coin".to_string(), - version: "2".to_string(), - chain_id: BaseSepolia::NETWORK.chain_id, - verifying_contract: UsdcBaseSepolia::ASSET.address.0, - }; - - let recovered_address = payload - .signature - .0 - .recover_address_from_prehash( - &Eip3009Authorization::from(payload.authorization.clone()) - .eip712_signing_hash(&domain.into()), - ) - .expect("Recovery should succeed"); - - assert_eq!(recovered_address, evm_signer.signer.address()); - } -} diff --git a/x402-kit/src/schemes/mod.rs b/x402-kit/src/schemes/mod.rs index 0120790..a63ec56 100644 --- a/x402-kit/src/schemes/mod.rs +++ b/x402-kit/src/schemes/mod.rs @@ -1,7 +1,6 @@ //! Schemes are defined here, for example, exact_evm, exact_svm, etc. +#[cfg(feature = "evm")] pub mod exact_evm; +#[cfg(feature = "svm")] pub mod exact_svm; - -#[cfg(feature = "evm-signer")] -pub mod exact_evm_signer; From 595538c5b03dfd4d8451b8a66ed97b0068bb4f7a Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 16:00:40 +0800 Subject: [PATCH 06/13] feat: add signer examples --- Cargo.lock | 345 ++++++++++++++++++++++++++++- x402-signer/Cargo.toml | 3 + x402-signer/examples/evm_client.rs | 57 +++++ x402-signer/examples/svm_client.rs | 68 ++++++ 4 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 x402-signer/examples/evm_client.rs create mode 100644 x402-signer/examples/svm_client.rs diff --git a/Cargo.lock b/Cargo.lock index 6457944..e85ce93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "flate2", "foldhash 0.1.5", "futures-core", - "h2", + "h2 0.3.27", "http 0.2.12", "httparse", "httpdate", @@ -1206,6 +1206,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.8" @@ -1546,6 +1568,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -1591,6 +1619,15 @@ dependencies = [ "inout", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "3.8.1" @@ -1604,6 +1641,16 @@ dependencies = [ "unreachable", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compression-codecs" version = "0.4.37" @@ -1692,6 +1739,26 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2253,6 +2320,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2440,6 +2513,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hash32" version = "0.3.1" @@ -2585,6 +2677,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2 0.4.13", "http 1.4.0", "http-body", "httparse", @@ -2632,9 +2725,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2904,6 +2999,28 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine 4.6.7", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -3237,6 +3354,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -3536,6 +3659,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -3778,18 +3902,27 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64", "bytes", + "encoding_rs", "futures-core", + "h2 0.4.13", "http 1.4.0", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "sync_wrapper", "tokio", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -3951,6 +4084,7 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -3959,6 +4093,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -3969,12 +4115,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4004,6 +4178,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.9.0" @@ -4083,6 +4275,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.11.0" @@ -5030,7 +5245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15b079e08471a9dbfe1e48b2c7439c85aa2a055cbd54eddd8bd257b0a7dbb29" dependencies = [ "byteorder", - "combine", + "combine 3.8.1", "hash32", "log", "rustc-demangle", @@ -5722,6 +5937,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -6226,6 +6462,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -6386,6 +6632,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.6" @@ -6395,6 +6650,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "wincode" version = "0.4.4" @@ -6461,6 +6725,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -6479,6 +6754,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6506,6 +6790,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6539,6 +6838,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6551,6 +6856,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6563,6 +6874,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6587,6 +6904,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6599,6 +6922,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6611,6 +6940,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6623,6 +6958,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/x402-signer/Cargo.toml b/x402-signer/Cargo.toml index 2412109..8c671bd 100644 --- a/x402-signer/Cargo.toml +++ b/x402-signer/Cargo.toml @@ -87,3 +87,6 @@ alloy = { version = "1" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } solana-pubkey = { version = "4" } solana-keypair = { version = "3" } +reqwest = { version = "0.13" } +reqwest-middleware = { version = "0.5" } +solana-rpc-client = { version = "3" } diff --git a/x402-signer/examples/evm_client.rs b/x402-signer/examples/evm_client.rs new file mode 100644 index 0000000..30a2654 --- /dev/null +++ b/x402-signer/examples/evm_client.rs @@ -0,0 +1,57 @@ +//! EVM client example using x402-signer reqwest middleware. +//! +//! Reads a hex private key from the `EVM_PRIVATE_KEY` env var (0x-prefixed), +//! then makes a POST request to an x402-protected resource server. +//! The middleware automatically handles the 402 → sign → retry flow. +//! +//! # Usage +//! +//! First, start the example seller server from x402-kit: +//! ```sh +//! FACILITATOR_URL=https://facilitator.example.com cargo run -p x402-kit --example axum_seller +//! ``` +//! +//! Then run this client: +//! ```sh +//! EVM_PRIVATE_KEY=0x... RESOURCE_URL=http://localhost:3000/resource/standard cargo run -p x402-signer --example evm_client +//! ``` + +use alloy::signers::local::PrivateKeySigner; +use reqwest_middleware::ClientBuilder; + +use x402_signer::{X402Client, evm::EvmPaymentSigner, middleware::X402PaymentMiddleware}; + +#[tokio::main] +async fn main() { + let private_key = std::env::var("EVM_PRIVATE_KEY") + .expect("Set EVM_PRIVATE_KEY to a 0x-prefixed hex private key"); + + let resource_url = std::env::var("RESOURCE_URL") + .unwrap_or_else(|_| "http://localhost:3000/resource/standard".to_string()); + + // Parse the hex private key into an alloy local signer + let wallet: PrivateKeySigner = private_key.parse().expect("Invalid EVM private key"); + println!("Signer address: {}", wallet.address()); + + // Build the x402 signing client with the EVM signer + let evm_signer = EvmPaymentSigner::new(wallet); + let x402_client = X402Client::new(evm_signer); + let middleware = X402PaymentMiddleware::new(x402_client); + + // Build a reqwest client with the x402 middleware + let client = ClientBuilder::new(reqwest::Client::new()) + .with(middleware) + .build(); + + // Make a request to the protected resource + println!("Requesting: {resource_url}"); + let response = client + .post(&resource_url) + .send() + .await + .expect("Request failed"); + + println!("Status: {}", response.status()); + let body = response.text().await.expect("Failed to read response body"); + println!("Body: {body}"); +} diff --git a/x402-signer/examples/svm_client.rs b/x402-signer/examples/svm_client.rs new file mode 100644 index 0000000..99bcd32 --- /dev/null +++ b/x402-signer/examples/svm_client.rs @@ -0,0 +1,68 @@ +//! SVM client example using x402-signer reqwest middleware. +//! +//! Reads a base58 Solana private key from the `SOLANA_PRIVATE_KEY` env var, +//! then makes a POST request to an x402-protected resource server. +//! The middleware automatically handles the 402 → sign → retry flow. +//! +//! The SVM signer requires an RPC connection to fetch blockhash and mint info. +//! Set `SOLANA_RPC_URL` to a Solana devnet RPC endpoint. +//! +//! # Usage +//! +//! First, start the example seller server from x402-kit: +//! ```sh +//! FACILITATOR_URL=https://facilitator.example.com cargo run -p x402-kit --example axum_seller +//! ``` +//! +//! Then run this client: +//! ```sh +//! SOLANA_PRIVATE_KEY= SOLANA_RPC_URL=https://api.devnet.solana.com RESOURCE_URL=http://localhost:3000/resource/multi_payments cargo run -p x402-signer --example svm_client +//! ``` + +use reqwest_middleware::ClientBuilder; +use solana_keypair::{Keypair, Signer}; +use solana_rpc_client::nonblocking::rpc_client::RpcClient; + +use x402_signer::{X402Client, middleware::X402PaymentMiddleware, svm::SvmPaymentSigner}; + +#[tokio::main] +async fn main() { + let private_key_b58 = std::env::var("SOLANA_PRIVATE_KEY") + .expect("Set SOLANA_PRIVATE_KEY to a base58-encoded Solana private key"); + + let rpc_url = std::env::var("SOLANA_RPC_URL") + .unwrap_or_else(|_| "https://api.devnet.solana.com".to_string()); + + let resource_url = std::env::var("RESOURCE_URL") + .unwrap_or_else(|_| "http://localhost:3000/resource/multi_payments".to_string()); + + // Parse base58 private key into a Keypair + let keypair = Keypair::from_base58_string(&private_key_b58); + println!("Signer pubkey: {}", keypair.pubkey()); + + // Create an RPC client for devnet + let rpc = RpcClient::new(rpc_url.clone()); + println!("Using RPC: {rpc_url}"); + + // Build the x402 signing client with the SVM signer + let svm_signer = SvmPaymentSigner::new(keypair, rpc); + let x402_client = X402Client::new(svm_signer); + let middleware = X402PaymentMiddleware::new(x402_client); + + // Build a reqwest client with the x402 middleware + let client = ClientBuilder::new(reqwest::Client::new()) + .with(middleware) + .build(); + + // Make a request to the protected resource + println!("Requesting: {resource_url}"); + let response = client + .post(&resource_url) + .send() + .await + .expect("Request failed"); + + println!("Status: {}", response.status()); + let body = response.text().await.expect("Failed to read response body"); + println!("Body: {body}"); +} From cc1e836eea763691de894216b126e214e3dab904 Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 16:26:09 +0800 Subject: [PATCH 07/13] fix: EVM invalid signature --- x402-networks/src/evm/mod.rs | 22 ++++++++++++++++++---- x402-signer/src/evm/eip3009.rs | 8 ++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/x402-networks/src/evm/mod.rs b/x402-networks/src/evm/mod.rs index b7b4ee3..5d7a7c3 100644 --- a/x402-networks/src/evm/mod.rs +++ b/x402-networks/src/evm/mod.rs @@ -257,10 +257,17 @@ pub mod assets { "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" ); - define_explicit_usdc!( + define_explicit_evm_asset!( UsdcEthereumSepolia, networks::EthereumSepolia, - "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" + "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + 6, + "USDC", + "USDC", + Some(Eip712Domain { + name: "USDC", + version: "2", + }) ); define_explicit_usdc!( @@ -269,9 +276,16 @@ pub mod assets { "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" ); - define_explicit_usdc!( + define_explicit_evm_asset!( UsdcBaseSepolia, networks::BaseSepolia, - "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + 6, + "USDC", + "USDC", + Some(Eip712Domain { + name: "USDC", + version: "2", + }) ); } diff --git a/x402-signer/src/evm/eip3009.rs b/x402-signer/src/evm/eip3009.rs index f82fe7c..8f49fd9 100644 --- a/x402-signer/src/evm/eip3009.rs +++ b/x402-signer/src/evm/eip3009.rs @@ -10,7 +10,7 @@ use x402_networks::evm::exact::{ExactEvmAuthorization, ExactEvmPayload, Nonce, T use super::wallet::EvmWalletSigner; sol! { - struct Eip3009Authorization { + struct TransferWithAuthorization { address from; address to; uint256 value; @@ -20,9 +20,9 @@ sol! { } } -impl From<&ExactEvmAuthorization> for Eip3009Authorization { +impl From<&ExactEvmAuthorization> for TransferWithAuthorization { fn from(auth: &ExactEvmAuthorization) -> Self { - Eip3009Authorization { + TransferWithAuthorization { from: auth.from.0, to: auth.to.0, value: U256::from(auth.value.0), @@ -71,7 +71,7 @@ pub async fn sign_eip3009( verifying_contract: params.asset_address, ); - let sol_auth = Eip3009Authorization::from(&authorization); + let sol_auth = TransferWithAuthorization::from(&authorization); let hash = sol_auth.eip712_signing_hash(&domain); let signature = signer.sign_hash(&hash).await?; From efaffb03518174d8f87829565561a20144f60161 Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 16:31:53 +0800 Subject: [PATCH 08/13] chore: signer snippets for README --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index be1be45..ba44574 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A fully modular, framework-agnostic, easy-to-extend SDK for building complex X40 | Crate | Description | | ------------------------------------------------------- | -------------------------------------------------------------------------- | | [`x402-kit`](https://crates.io/crates/x402-kit) | Main SDK with network definitions, payment schemes, and facilitator client | +| [`x402-signer`](https://crates.io/crates/x402-signer) | Buyer-side signing SDK with reqwest middleware for automatic x402 payments | | [`x402-core`](https://crates.io/crates/x402-core) | Core traits, types, and transport mechanisms for the X402 protocol | | [`x402-paywall`](https://crates.io/crates/x402-paywall) | Framework-agnostic HTTP paywall middleware | @@ -194,11 +195,65 @@ let facilitator = FacilitatorClient::from_url(facilitator_url) .with_settle_response_type::(); ``` +### Buyer-Side Signing with `x402-signer` + +The `x402-signer` crate provides a reqwest middleware that automatically handles the x402 payment flow: intercept HTTP 402 → sign payment → retry with payment header. + +#### EVM Client + +```bash +EVM_PRIVATE_KEY=0x... RESOURCE_URL=http://localhost:3000/resource/standard \ + cargo run -p x402-signer --example evm_client +``` + +```rust +use alloy::signers::local::PrivateKeySigner; +use reqwest_middleware::ClientBuilder; +use x402_signer::{X402Client, evm::EvmPaymentSigner, middleware::X402PaymentMiddleware}; + +let wallet: PrivateKeySigner = "0x...".parse().unwrap(); +let signer = EvmPaymentSigner::new(wallet); +let middleware = X402PaymentMiddleware::new(X402Client::new(signer)); + +let client = ClientBuilder::new(reqwest::Client::new()) + .with(middleware) + .build(); + +// Any 402 response is automatically signed and retried +let response = client.post("http://localhost:3000/resource/standard").send().await?; +``` + +#### SVM Client + +```bash +SOLANA_PRIVATE_KEY= SOLANA_RPC_URL=https://api.devnet.solana.com \ + RESOURCE_URL=http://localhost:3000/resource/multi_payments \ + cargo run -p x402-signer --example svm_client +``` + +```rust +use solana_keypair::Keypair; +use solana_rpc_client::nonblocking::rpc_client::RpcClient; +use reqwest_middleware::ClientBuilder; +use x402_signer::{X402Client, svm::SvmPaymentSigner, middleware::X402PaymentMiddleware}; + +let keypair = Keypair::from_base58_string(""); +let rpc = RpcClient::new("https://api.devnet.solana.com".to_string()); +let signer = SvmPaymentSigner::new(keypair, rpc); +let middleware = X402PaymentMiddleware::new(X402Client::new(signer)); + +let client = ClientBuilder::new(reqwest::Client::new()) + .with(middleware) + .build(); + +let response = client.post("http://localhost:3000/resource/multi_payments").send().await?; +``` + ## 🚀 Next Steps -- Full buyer-side signer support - More networks / assets / schemes - MCP / A2A transport support +- Facilitator components support ## 🤝 Contributing From 4eb99ada84fc078f3b92dc0c11897d3a981fa99e Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 16:34:54 +0800 Subject: [PATCH 09/13] chore: add faucet docs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ba44574..55853f6 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ let facilitator = FacilitatorClient::from_url(facilitator_url) The `x402-signer` crate provides a reqwest middleware that automatically handles the x402 payment flow: intercept HTTP 402 → sign payment → retry with payment header. +You need some testnet USDC on Solana devnet or Base Sepolia to run signer examples. You can get some at [Circle testnet faucet](https://faucet.circle.com/) + #### EVM Client ```bash From ae996039e9820c4ad1d0cd2605521986e84de08a Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 16:39:20 +0800 Subject: [PATCH 10/13] ci: update CI workflow --- .github/workflows/ci.yml | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 501195a..fb1b765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,12 +26,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # adjust to your feature list features: - "" # no extra features - "facilitator-client" - - "evm-signer" - - "svm-signer" + - "evm" + - "svm" - "paywall" - "axum" - "actix-web" @@ -52,11 +51,37 @@ jobs: cargo build -p x402-kit --release --no-default-features --features "${{ matrix.features }}" fi + build-x402-signer: + runs-on: ubuntu-latest + strategy: + matrix: + features: + - "" # no extra features + - "evm" + - "svm" + - "solana-rpc" + - "reqwest" + - "all" # marker for all features + steps: + - uses: actions/checkout@v6 + - name: Set up Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: Build with features = ${{ matrix.features }} + run: | + if [ "${{ matrix.features }}" = "all" ]; then + cargo build -p x402-signer --release --all-features + elif [ "${{ matrix.features }}" = "" ]; then + cargo build -p x402-signer --release --no-default-features + else + cargo build -p x402-signer --release --no-default-features --features "${{ matrix.features }}" + fi + build-x402-paywall: runs-on: ubuntu-latest strategy: matrix: - # adjust to your feature list features: - "" # no extra features - "tracing" @@ -148,7 +173,7 @@ jobs: RUSTDOCFLAGS: -D warnings test-docs: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable From 109e36ec24c8af4fc659d7c91c77ff466c98857c Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 16:41:44 +0800 Subject: [PATCH 11/13] ci: treat warnings as failures --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb1b765..0ff0704 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: env: CARGO_TERM_COLOR: always + RUSTFLAGS: "-D warnings" jobs: build-x402-core: From 1e1d6b9f96fbc26a9dafc3d47775c2629664c679 Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 16:43:01 +0800 Subject: [PATCH 12/13] chore: remove unused `x402-client` --- Cargo.lock | 4 ---- Cargo.toml | 2 +- x402-client/Cargo.toml | 6 ------ x402-client/src/lib.rs | 14 -------------- 4 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 x402-client/Cargo.toml delete mode 100644 x402-client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e85ce93..9afa398 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7088,10 +7088,6 @@ dependencies = [ "tap", ] -[[package]] -name = "x402-client" -version = "0.1.0" - [[package]] name = "x402-core" version = "2.3.0" diff --git a/Cargo.toml b/Cargo.toml index e62a9e4..7435af1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["x402-client","x402-core", "x402-extensions", "x402-kit", "x402-networks", "x402-paywall", "x402-signer"] +members = ["x402-*"] diff --git a/x402-client/Cargo.toml b/x402-client/Cargo.toml deleted file mode 100644 index 55fcb20..0000000 --- a/x402-client/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "x402-client" -version = "0.1.0" -edition = "2024" - -[dependencies] diff --git a/x402-client/src/lib.rs b/x402-client/src/lib.rs deleted file mode 100644 index b93cf3f..0000000 --- a/x402-client/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} From 5233325f27bd560a7c55a8db84416dd7e310265e Mon Sep 17 00:00:00 2001 From: archer Date: Tue, 17 Mar 2026 16:46:49 +0800 Subject: [PATCH 13/13] fix: unused variable --- x402-paywall/src/processor.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x402-paywall/src/processor.rs b/x402-paywall/src/processor.rs index aaaf0e1..bff8738 100644 --- a/x402-paywall/src/processor.rs +++ b/x402-paywall/src/processor.rs @@ -224,17 +224,17 @@ impl<'pw, F: Facilitator, Res: HttpResponse> ResponseProcessor<'pw, F, Res> { }; let header = Base64EncodedHeader::try_from(settlement_response) - .inspect_err(|err| { + .inspect_err(|_err| { #[cfg(feature = "tracing")] - tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {err}; skipping") + tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {_err}; skipping") }) .ok(); if let Some(header) = header { response .insert_header("payment-response", header.0.as_bytes()) - .inspect_err(|err| { + .inspect_err(|_err| { #[cfg(feature = "tracing")] - tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {err}; skipping") + tracing::warn!("Failed to encode PAYMENT-RESPONSE header: {_err}; skipping") }) .ok(); }