diff --git a/Cargo.toml b/Cargo.toml index 31c614b..b7e4d0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ resolver = "2" [workspace.dependencies] scanner = { path = "scanner", version = "0.1" } x402 = { path = "x402", version = "0.1" } +eip8004 = { git = "https://github.com/zpaynow/8004" } alloy = "1.0" anyhow = "1.0" async-trait = "0.1" diff --git a/api/src/main.rs b/api/src/main.rs index 244c0a1..6e6863b 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -26,7 +26,7 @@ use std::{net::SocketAddr, sync::Arc}; use tokio::{net::TcpListener, sync::mpsc::UnboundedSender}; use tower_http::cors::{Any, CorsLayer}; use tracing::level_filters::LevelFilter; -use x402::{EvmScheme, Facilitator}; +use x402::{Evm8004Registry, EvmScheme, Facilitator}; #[derive(Parser)] #[command(version, about, long_about = None)] @@ -62,6 +62,14 @@ struct Command { /// Scanner chains configure file path #[arg(long, env = "SCANNER_CONFIG", default_value = "config.toml")] scanner_config: String, + + /// EIP-8004 registry agent id + #[arg(long, env = "AGENT_ID")] + agent_id: Option, + + /// EIP-8004 registry agent identity contract + #[arg(long, env = "AGENT_IDENTITY")] + agent_identity: Option, } #[derive(Clone)] @@ -136,11 +144,17 @@ async fn main() { .unwrap(); // building x402 facilitator + let agent = match (args.agent_id, args.agent_identity) { + (Some(agent_id), Some(identity)) => Some(Evm8004Registry { agent_id, identity }), + _ => None, + }; let mut facilitator = Facilitator::new(); for c in x402_assets { match c.ctype { ChainType::Evm => { - let mut scheme = EvmScheme::new(&c.rpc, &c.network, &c.signer).unwrap(); + let mut scheme = EvmScheme::new(&c.rpc, &c.network, &c.signer, agent.clone()) + .await + .unwrap(); for asset in c.assets { scheme .asset(&asset.address, &asset.name, &asset.version) diff --git a/x402/Cargo.toml b/x402/Cargo.toml index cf94246..0b33896 100644 --- a/x402/Cargo.toml +++ b/x402/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +eip8004.workspace = true alloy.workspace = true anyhow.workspace = true async-trait.workspace = true diff --git a/x402/src/client.rs b/x402/src/client.rs index 4b44edb..a17f5b0 100644 --- a/x402/src/client.rs +++ b/x402/src/client.rs @@ -97,11 +97,12 @@ impl ClientFacilitator { pub fn build<'a>( &self, prs: &'a [PaymentRequirements], + feedback_index: Option, ) -> Result<(PaymentPayload, &'a PaymentRequirements)> { for pr in prs.iter() { let identity = format!("{}-{}", pr.scheme, pr.network); if self.infos.contains_key(&identity) { - let payload = self.build_with_scheme(pr)?; + let payload = self.build_with_scheme(pr, feedback_index)?; return Ok((payload, pr)); } } @@ -110,7 +111,11 @@ impl ClientFacilitator { } /// Build the payment payload by a paymentRequirements - pub fn build_with_scheme(&self, pr: &PaymentRequirements) -> Result { + pub fn build_with_scheme( + &self, + pr: &PaymentRequirements, + feedback_index: Option, + ) -> Result { let identity = format!("{}-{}", pr.scheme, pr.network); if let Some(info) = self.infos.get(&identity) { @@ -125,6 +130,7 @@ impl ClientFacilitator { payload: SchemePayload { signature, authorization, + feedback_index, }, }) } else { diff --git a/x402/src/facilitator.rs b/x402/src/facilitator.rs index ee2ebf2..9445bb7 100644 --- a/x402/src/facilitator.rs +++ b/x402/src/facilitator.rs @@ -76,6 +76,7 @@ impl Facilitator { transaction: "".to_owned(), network: req.payment_payload.network.clone(), payer: req.payment_payload.payload.authorization.from.clone(), + feedback_auth: None, } } } diff --git a/x402/src/lib.rs b/x402/src/lib.rs index 42f7814..f1c981d 100644 --- a/x402/src/lib.rs +++ b/x402/src/lib.rs @@ -1,5 +1,5 @@ mod scheme; -pub use scheme::evm::EvmScheme; +pub use scheme::evm::{Evm8004Registry, EvmAsset, EvmScheme}; pub use scheme::sol::SolScheme; pub mod client; @@ -7,6 +7,7 @@ pub mod facilitator; pub use facilitator::Facilitator; use async_trait::async_trait; +use eip8004::FeedbackAuth; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -75,6 +76,10 @@ pub struct SchemePayload { pub signature: String, /// EIP-3009 authorization parameters pub authorization: Authorization, + /// The index in client feedback for 8004 Reputation, + /// If has this field, and service has register agent info, + /// The `SettlementResponse` will has `feedback_auth` field. + pub feedback_index: Option, } /// EIP-3009 authorization parameters @@ -126,6 +131,7 @@ impl VerifyResponse { transaction: tx.to_owned(), network: network.to_owned(), payer: self.payer, + feedback_auth: None, } } } @@ -144,6 +150,8 @@ pub struct SettlementResponse { pub network: String, /// Address of the payer's wallet pub payer: String, + /// The feedback authorized signature for 8004 Reputation + pub feedback_auth: Option, } /// List supported payment schemes. @@ -338,6 +346,7 @@ impl Error { transaction: "".to_owned(), network: req.network.clone(), payer: req.payload.authorization.from.clone(), + feedback_auth: None, } } } diff --git a/x402/src/scheme/evm.rs b/x402/src/scheme/evm.rs index 29edef0..f14f0bd 100644 --- a/x402/src/scheme/evm.rs +++ b/x402/src/scheme/evm.rs @@ -3,7 +3,7 @@ use crate::{ VerifyRequest, VerifyResponse, }; use alloy::{ - primitives::{Address, B256, U256}, + primitives::{Address, B256, Bytes, U256}, providers::{Provider, ProviderBuilder}, signers::{Signature, SignerSync, local::PrivateKeySigner}, sol, @@ -12,6 +12,7 @@ use alloy::{ }; use anyhow::Result; use async_trait::async_trait; +use eip8004::{FeedbackAuth, FeedbackOnchainAuth}; use serde_json::{Value, json}; use std::collections::HashMap; use std::str::FromStr; @@ -60,6 +61,7 @@ impl TransferWithAuthorization { } } +/// EIP-3009 based assets/tokens pub struct EvmAsset { name: String, version: String, @@ -68,21 +70,58 @@ pub struct EvmAsset { extra: Value, } +/// EIP-8004 agent registry infomation +#[derive(Clone, Debug)] +pub struct Evm8004Registry { + pub agent_id: i64, + pub identity: String, +} + +struct InnerEvm8004Registry { + pub agent_id: U256, + pub identity_registry: Address, +} + +/// Evm-based scheme pub struct EvmScheme { + chain_id: u64, scheme: String, network: String, rpc: Url, signer: PrivateKeySigner, assets: HashMap, + agent: Option, } impl EvmScheme { - pub fn new(url: &str, network: &str, signer: &str) -> Result { - let rpc = url.parse()?; + /// Build the evm scheme with EIP-8004 agent + pub async fn new( + url: &str, + network: &str, + signer: &str, + agent: Option, + ) -> Result { + let rpc: Url = url.parse()?; let signer = signer.parse()?; + + let provider = ProviderBuilder::new().connect_http(rpc.clone()); + let chain_id = provider.get_chain_id().await?; + + let agent = if let Some(agent) = agent { + let identity_registry: Address = agent.identity.parse()?; + Some(InnerEvm8004Registry { + agent_id: U256::from(agent.agent_id), + identity_registry, + }) + } else { + None + }; + Ok(Self { + chain_id, rpc, signer, + agent, scheme: SCHEME.to_owned(), network: network.to_owned(), assets: HashMap::new(), @@ -105,9 +144,6 @@ impl EvmScheme { // Create provider and contract instance let provider = ProviderBuilder::new().connect_http(self.rpc.clone()); - // Get chain ID for EIP-712 domain - let chain_id = provider.get_chain_id().await?; - // Verify the contract has the required EIP-3009 functions by calling view functions let contract = Eip3009Token::new(token_address, &provider); let decimal = contract.decimals().call().await?; @@ -123,7 +159,7 @@ impl EvmScheme { let domain = create_eip712_domain( name.to_string(), version.to_string(), - chain_id, + self.chain_id, token_address, ); @@ -251,7 +287,10 @@ impl EvmScheme { Ok(()) } - async fn handle_settle(&self, req: &VerifyRequest) -> Result { + async fn handle_settle( + &self, + req: &VerifyRequest, + ) -> Result<(String, Option), Error> { // Get the token address and parse authorization let token: Address = req .payment_requirements @@ -322,8 +361,33 @@ impl EvmScheme { .await .map_err(|_| Error::InvalidTransactionState)?; + let feedback_auth = match (&self.agent, req.payment_payload.payload.feedback_index) { + (Some(agent), Some(index)) => { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| Error::UnexpectedVerifyError)? + .as_secs(); + let expiry = now + 86400; // default is 1 Day + + let mut feedback = FeedbackOnchainAuth { + agentId: agent.agent_id, + identityRegistry: agent.identity_registry, + clientAddress: from, + indexLimit: index, + expiry: U256::from(expiry), + chainId: U256::from(self.chain_id), + signerAddress: self.signer.address(), + signature: Bytes::default(), + }; + let _ = feedback.sign(&self.signer).await; + + Some(feedback.to_feedback_auth()) + } + _ => None, + }; + // Return the transaction hash - Ok(format!("{:?}", receipt.transaction_hash)) + Ok((format!("{:?}", receipt.transaction_hash), feedback_auth)) } } @@ -410,12 +474,13 @@ impl PaymentScheme for EvmScheme { /// parameters provided in the payment payload. async fn settle(&self, req: &VerifyRequest) -> SettlementResponse { match self.handle_settle(req).await { - Ok(tx_hash) => SettlementResponse { + Ok((tx_hash, feedback_auth)) => SettlementResponse { success: true, error_reason: None, transaction: tx_hash, network: req.payment_payload.network.clone(), payer: req.payment_payload.payload.authorization.from.clone(), + feedback_auth, }, Err(error) => error.settle(&req.payment_payload), }