Rust SDK 参考#
Rust SDK 参考(适用于 exact、aggr_deferred)#
Crate#
| Crate | 描述 |
|---|---|
x402-core | 核心:服务端、facilitator 客户端、类型、HTTP 工具、HMAC 认证 |
x402-axum | Axum 中间件(Tower Layer/Service) |
x402-evm | EVM 机制:exact、aggr_deferred |
Rust SDK 目前提供服务端(卖方)和facilitator 客户端功能。买方侧的支付签名功能正在计划中。
核心类型#
Network / Money / Price#
pub type Network = String;
// CAIP-2 format, e.g., "eip155:196"
pub type Money = String;
// User-friendly amount, e.g., "$0.01", "0.01"
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Price {
Money(Money),
Asset(AssetAmount),
}
AssetAmount#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetAmount {
pub asset: String, // Token contract address
pub amount: String, // Amount in token's smallest unit
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra: Option<HashMap<String, serde_json::Value>>,
}
ResourceInfo#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceInfo {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
PaymentRequirements#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequirements {
pub scheme: String, // "exact" | "aggr_deferred"
pub network: Network, // CAIP-2 identifier
pub asset: String, // Token contract address
pub amount: String, // Price in token's smallest unit
pub pay_to: String, // Recipient wallet address
pub max_timeout_seconds: u64, // Authorization validity window
#[serde(default)]
pub extra: HashMap<String, serde_json::Value>, // Scheme-specific data
}
PaymentRequired#
402 响应体。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequired {
#[serde(rename = "x402Version")]
pub x402_version: u32, // Protocol version (2)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub resource: ResourceInfo,
pub accepts: Vec<PaymentRequirements>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
PaymentPayload#
客户端的签名支付。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentPayload {
#[serde(rename = "x402Version")]
pub x402_version: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource: Option<ResourceInfo>,
pub accepted: PaymentRequirements,
pub payload: HashMap<String, serde_json::Value>, // Scheme-specific signed data
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
Facilitator 类型#
VerifyRequest / VerifyResponse#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyRequest {
#[serde(rename = "x402Version")]
pub x402_version: u32,
pub payment_payload: PaymentPayload,
pub payment_requirements: PaymentRequirements,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyResponse {
pub is_valid: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invalid_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invalid_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
SettleRequest / SettleResponse#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SettleRequest {
#[serde(rename = "x402Version")]
pub x402_version: u32,
pub payment_payload: PaymentPayload,
pub payment_requirements: PaymentRequirements,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sync_settle: Option<bool>, // OKX extension: wait for on-chain confirmation
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SettleResponse {
pub success: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payer: Option<String>,
pub transaction: String, // Tx hash (empty for aggr_deferred)
pub network: Network,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub amount: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>, // OKX: "pending" | "success" | "timeout"
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
SupportedKind / SupportedResponse#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SupportedKind {
#[serde(rename = "x402Version")]
pub x402_version: u32,
pub scheme: String,
pub network: Network,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupportedResponse {
pub kinds: Vec<SupportedKind>,
pub extensions: Vec<String>,
pub signers: HashMap<String, Vec<String>>,
}
SettleStatusResponse#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SettleStatusResponse {
pub success: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transaction: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network: Option<Network>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>, // "pending" | "success" | "failed"
}
Traits#
SchemeNetworkServer#
服务端 scheme 实现。
#[async_trait]
pub trait SchemeNetworkServer: Send + Sync {
fn scheme(&self) -> &str;
async fn parse_price(
&self,
price: &Price,
network: &Network,
) -> Result<AssetAmount, X402Error>;
async fn enhance_payment_requirements(
&self,
payment_requirements: PaymentRequirements,
supported_kind: &SupportedKind,
facilitator_extensions: &[String],
) -> Result<PaymentRequirements, X402Error>;
}
FacilitatorClient#
与远程 facilitator 通信的网络边界。
#[async_trait]
pub trait FacilitatorClient: Send + Sync {
async fn get_supported(&self) -> Result<SupportedResponse, X402Error>;
async fn verify(&self, request: &VerifyRequest) -> Result<VerifyResponse, X402Error>;
async fn settle(&self, request: &SettleRequest) -> Result<SettleResponse, X402Error>;
async fn get_settle_status(&self, tx_hash: &str) -> Result<SettleStatusResponse, X402Error>;
}
ResourceServerExtension#
#[async_trait]
pub trait ResourceServerExtension: Send + Sync {
fn key(&self) -> &str;
async fn enrich_payment_required(
&self,
payment_required: PaymentRequired,
context: &PaymentRequiredContext,
) -> PaymentRequired;
async fn enrich_verify_extensions(
&self,
extensions: HashMap<String, serde_json::Value>,
payment_payload: &PaymentPayload,
payment_requirements: &PaymentRequirements,
) -> HashMap<String, serde_json::Value>;
async fn enrich_settle_extensions(
&self,
extensions: HashMap<String, serde_json::Value>,
payment_payload: &PaymentPayload,
payment_requirements: &PaymentRequirements,
) -> HashMap<String, serde_json::Value>;
}
pub struct PaymentRequiredContext {
pub url: String,
pub method: String,
}
pub struct SettleResultContext {
pub url: String,
pub method: String,
pub payment_payload: PaymentPayload,
pub payment_requirements: PaymentRequirements,
pub settle_response: SettleResponse,
}
FacilitatorExtension#
#[async_trait]
pub trait FacilitatorExtension: Send + Sync {
fn key(&self) -> &str;
fn supported_networks(&self) -> Vec<Network>;
}
服务端 API(X402ResourceServer)#
构造函数与注册#
use x402_core::server::X402ResourceServer;
use x402_evm::{ExactEvmScheme, AggrDeferredEvmScheme};
// register() uses builder pattern (consumes self, returns Self)
let mut server = X402ResourceServer::new(facilitator)
.register("eip155:196", ExactEvmScheme::new())
.register("eip155:196", AggrDeferredEvmScheme::new());
方法#
use std::collections::HashMap;
use std::time::Duration;
use x402_core::error::X402Error;
use x402_core::facilitator::FacilitatorClient;
use x402_core::http::{PollResult, SettlementOverrides};
use x402_core::server::X402ResourceServer;
use x402_core::types::{
PaymentPayload, PaymentRequirements, ResourceInfo, SchemeNetworkServer,
SettleResponse, SupportedResponse, VerifyResponse,
};
impl X402ResourceServer {
pub fn new(facilitator: impl FacilitatorClient + 'static) -> Self;
pub fn register(
mut self,
network: &str,
scheme: impl SchemeNetworkServer + 'static,
) -> Self;
pub async fn initialize(&mut self) -> Result<(), X402Error>;
pub fn supported(&self) -> Option<&SupportedResponse>;
pub fn facilitator(&self) -> &dyn FacilitatorClient;
pub async fn build_payment_requirements(
&self,
scheme: &str, // "exact"
network: &str, // "eip155:196"
price: &str, // "$0.01"
pay_to: &str, // "0xSeller"
max_timeout_seconds: u64,
resource: &ResourceInfo,
config_extra: Option<&HashMap<String, serde_json::Value>>,
) -> Result<PaymentRequirements, X402Error>;
pub async fn verify_payment(
&self,
payment_payload: &PaymentPayload,
payment_requirements: &PaymentRequirements,
) -> Result<VerifyResponse, X402Error>;
pub async fn settle_payment(
&self,
payment_payload: &PaymentPayload,
payment_requirements: &PaymentRequirements,
sync_settle: Option<bool>,
settlement_overrides: Option<&SettlementOverrides>,
) -> Result<SettleResponse, X402Error>;
pub async fn poll_settle_status(
&self,
tx_hash: &str,
poll_interval: Duration, // DEFAULT_POLL_INTERVAL = 1s
poll_deadline: Duration, // DEFAULT_POLL_DEADLINE = 5s
) -> PollResult;
}
PollResult#
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PollResult {
Success, // Transaction confirmed on-chain
Failed, // Transaction failed on-chain
Timeout, // Polling deadline exceeded
}
OKX Facilitator 客户端(OkxHttpFacilitatorClient)#
use x402_core::http::OkxHttpFacilitatorClient;
// Default URL (https://web3.okx.com)
let client = OkxHttpFacilitatorClient::new(
"your-api-key",
"your-secret-key",
"your-passphrase",
)?;
实现了 FacilitatorClient trait。所有请求均包含 HMAC-SHA256 认证。
调用的端点:#
| 方法 | OKX 路径 |
|---|---|
get_supported() | GET /api/v6/pay/x402/supported |
verify(request) | POST /api/v6/pay/x402/verify |
settle(request) | POST /api/v6/pay/x402/settle |
get_settle_status(tx_hash) | GET /api/v6/pay/x402/settle/status?txHash=... |
OKX 的响应被包装在 {"code": 0, "data": {...}, "msg": ""} 中 -- 客户端会自动解包。
HMAC 认证#
use x402_core::http::hmac::{sign_request, build_auth_headers};
// Sign a request
let signature = sign_request(secret_key, timestamp, method, request_path, body);
// Message = timestamp + METHOD + requestPath + body
// Signature = Base64(HMAC-SHA256(secretKey, message))
// Build all auth headers
let headers = build_auth_headers(api_key, secret_key, passphrase, method, request_path, body);
// Returns: OK-ACCESS-KEY, OK-ACCESS-SIGN, OK-ACCESS-TIMESTAMP, OK-ACCESS-PASSPHRASE
HTTP 工具#
请求头编码/解码#
use x402_core::http::{
encode_payment_signature_header,
decode_payment_signature_header,
encode_payment_required_header,
decode_payment_required_header,
encode_payment_response_header,
decode_payment_response_header,
};
let encoded = encode_payment_required_header(&payment_required)?; // → base64 string
let decoded = decode_payment_required_header(&header_value)?; // → PaymentRequired
常量#
pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(1);
pub const DEFAULT_POLL_DEADLINE: Duration = Duration::from_secs(5);
pub const PAYMENT_SIGNATURE_HEADER: &str = "PAYMENT-SIGNATURE";
pub const PAYMENT_REQUIRED_HEADER: &str = "PAYMENT-REQUIRED";
pub const PAYMENT_RESPONSE_HEADER: &str = "PAYMENT-RESPONSE";
路由配置#
use x402_core::http::{RoutesConfig, RoutePaymentConfig, AcceptConfig};
use std::collections::HashMap;
pub type RoutesConfig = HashMap<String, RoutePaymentConfig>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoutePaymentConfig {
pub accepts: Vec<AcceptConfig>, // Accepted payment options
pub description: String, // Resource description
pub mime_type: String, // Response MIME type
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sync_settle: Option<bool>, // Enable sync settlement
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AcceptConfig {
pub scheme: String, // "exact" | "aggr_deferred"
pub price: String, // "$0.01" or token amount
pub network: String, // "eip155:196"
pub pay_to: String, // Recipient wallet
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_timeout_seconds: Option<u64>, // Default: 300s (5 min)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra: Option<HashMap<String, serde_json::Value>>, // Scheme-specific metadata
}
示例:#
use std::collections::HashMap;
use x402_axum::{AcceptConfig, RoutePaymentConfig};
let mut routes = HashMap::new();
routes.insert("GET /api/data".to_string(), RoutePaymentConfig {
accepts: vec![
AcceptConfig {
scheme: "exact".to_string(),
price: "$0.01".to_string(),
network: "eip155:196".to_string(),
pay_to: "0xSeller".to_string(),
max_timeout_seconds: None,
extra: None,
},
AcceptConfig {
scheme: "aggr_deferred".to_string(),
price: "$0.001".to_string(),
network: "eip155:196".to_string(),
pay_to: "0xSeller".to_string(),
max_timeout_seconds: None,
extra: None,
},
],
description: "Premium data".to_string(),
mime_type: "application/json".to_string(),
sync_settle: Some(true),
});
Axum 中间件(x402-axum)#
基本用法#
use std::time::Duration;
use axum::{Router, routing::get};
use x402_axum::{
payment_middleware, payment_middleware_with_poll_deadline,
payment_middleware_with_timeout_hook, payment_middleware_with_timeout_hook_and_deadline,
OnSettlementTimeoutHook, RoutesConfig,
};
use x402_core::server::X402ResourceServer;
let app = Router::new()
.route("/api/data", get(handler))
.layer(payment_middleware(routes, server));
构造函数#
use std::time::Duration;
use x402_axum::{
OnSettlementTimeoutHook, PaymentLayer, RoutesConfig,
};
use x402_core::server::X402ResourceServer;
// Basic middleware
pub fn payment_middleware(
routes: RoutesConfig,
server: X402ResourceServer,
) -> PaymentLayer;
// With custom poll deadline
pub fn payment_middleware_with_poll_deadline(
routes: RoutesConfig,
server: X402ResourceServer,
poll_deadline: Duration,
) -> PaymentLayer;
// With settlement timeout recovery hook
pub fn payment_middleware_with_timeout_hook(
routes: RoutesConfig,
server: X402ResourceServer,
timeout_hook: OnSettlementTimeoutHook,
) -> PaymentLayer;
// With both
pub fn payment_middleware_with_timeout_hook_and_deadline(
routes: RoutesConfig,
server: X402ResourceServer,
timeout_hook: OnSettlementTimeoutHook,
poll_deadline: Duration,
) -> PaymentLayer;
OnSettlementTimeoutHook#
use std::pin::Pin;
use std::future::Future;
use x402_axum::{OnSettlementTimeoutHook, SettlementTimeoutResult};
pub struct SettlementTimeoutResult {
pub confirmed: bool,
}
pub type OnSettlementTimeoutHook = Box<
dyn Fn(String, String) -> Pin<Box<dyn Future<Output = SettlementTimeoutResult> + Send>>
+ Send + Sync,
>;
// Usage: arguments are (tx_hash, network), not (route, tx_hash)
let hook: OnSettlementTimeoutHook = Box::new(|tx_hash, network| {
Box::pin(async move {
tracing::warn!("Timeout for tx={} network={}", tx_hash, network);
SettlementTimeoutResult { confirmed: false }
})
});
中间件流程#
-
检查路由是否需要支付(
find_route_config) -
如果没有
payment-signature请求头 -> 返回 402 并附带PAYMENT-REQUIRED请求头 -
解码并验证支付 payload
-
将 payload 与路由接受的 requirements 进行匹配
-
通过 facilitator 验证(
POST /verify) -
调用内部处理器并缓冲响应
-
通过 facilitator 结算(
POST /settle) -
如果异步(
status: "pending") -> 在截止时间内轮询 -
如果超时 -> 调用超时回调(如果已配置)
-
向响应添加
PAYMENT-RESPONSE请求头
重新导出#
pub use x402_core::http::{
AcceptConfig,
BeforeHookResult,
OnAfterSettleHook,
OnAfterVerifyHook,
OnBeforeSettleHook,
OnBeforeVerifyHook,
OnProtectedRequestHook,
OnSettleFailureHook,
OnSettlementTimeoutHook,
OnVerifyFailureHook,
PaymentResolverFn,
PollResult,
ProtectedRequestResult,
RequestContext,
ResolvedAccept,
RoutePaymentConfig,
RoutesConfig,
SettleContext,
SettleRecoveryResult,
SettleResultContext,
SettlementOverrides,
SettlementTimeoutResult,
VerifyContext,
VerifyRecoveryResult,
VerifyResultContext,
DEFAULT_POLL_DEADLINE,
DEFAULT_POLL_INTERVAL,
SETTLEMENT_OVERRIDES_HEADER,
};
pub use x402_core::server::X402ResourceServer;
EVM 机制(x402-evm)#
ExactEvmScheme#
use x402_evm::ExactEvmScheme;
let scheme = ExactEvmScheme::new();
scheme.scheme(); // "exact"
处理以下功能:
-
价格解析:
"$0.01"、"0.01"、AssetAmount -
使用正确小数位的代币金额转换
-
按网络查找默认资产(Base 上的 USDC,X Layer 上的 USDT)
-
为 EIP-3009 代币注入 EIP-712 域信息到
extra中
DeferredEvmScheme#
use x402_evm::AggrDeferredEvmScheme;
let scheme = AggrDeferredEvmScheme::new();
scheme.scheme(); // "aggr_deferred"
所有价格/需求逻辑均委托给 ExactEvmScheme。从卖方角度来看完全相同。
EVM Payload 类型#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AssetTransferMethod {
#[serde(rename = "eip3009")]
Eip3009,
Permit2,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EIP3009Authorization {
pub from: String,
pub to: String,
pub value: String,
pub valid_after: String,
pub valid_before: String,
pub nonce: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExactEIP3009Payload {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
pub authorization: EIP3009Authorization,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExactPermit2Payload {
pub signature: String,
pub permit2_authorization: Permit2Authorization,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ExactEvmPayloadV2 {
EIP3009(ExactEIP3009Payload),
Permit2(ExactPermit2Payload),
}
impl ExactEvmPayloadV2 {
pub fn is_permit2(&self) -> bool;
pub fn is_eip3009(&self) -> bool;
}
资产配置#
#[derive(Debug, Clone)]
pub struct DefaultAssetInfo {
pub address: &'static str, // Token contract address
pub name: &'static str, // EIP-712 domain name
pub version: &'static str, // EIP-712 domain version
pub decimals: u8, // Token decimal places
pub asset_transfer_method: Option<&'static str>, // "permit2" override
pub supports_eip2612: bool, // EIP-2612 permit() support
}
#[derive(Debug, Clone)]
pub struct ChainConfig {
pub network: &'static str, // CAIP-2 identifier
pub chain_id: u64,
}
// Pre-registered assets:
// XLAYER_MAINNET_USDT: eip155:196, 0x779ded..., 6 decimals, name: "USD₮0"
pub fn get_default_asset(network: &str) -> Option<DefaultAssetInfo>;
错误类型#
#[derive(Debug, thiserror::Error)]
pub enum X402Error {
#[error(transparent)]
Verify(#[from] VerifyError),
#[error(transparent)]
Settle(#[from] SettleError),
#[error(transparent)]
FacilitatorResponse(#[from] FacilitatorResponseError),
#[error("configuration error: {0}")]
Config(String),
#[error("route configuration error: {0}")]
RouteConfig(String),
#[error("unsupported scheme: {0}")]
UnsupportedScheme(String),
#[error("unsupported network: {0}")]
UnsupportedNetwork(String),
#[error("price parse error: {0}")]
PriceParse(String),
#[error("not initialized: {0}")]
NotInitialized(String),
#[error("http error: {0}")]
Http(#[from] reqwest::Error),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("base64 decode error: {0}")]
Base64Decode(#[from] base64::DecodeError),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone, thiserror::Error)]
pub struct VerifyError {
pub status_code: u16,
pub invalid_reason: Option<String>,
pub invalid_message: Option<String>,
pub payer: Option<String>,
}
#[derive(Debug, Clone, thiserror::Error)]
pub struct SettleError {
pub status_code: u16,
pub error_reason: Option<String>,
pub error_message: Option<String>,
pub payer: Option<String>,
pub transaction: String,
pub network: Network,
}
#[derive(Debug, Clone, thiserror::Error)]
pub struct FacilitatorResponseError(pub String);
工具函数#
pub fn safe_base64_encode(data: &str) -> String;
pub fn safe_base64_decode(data: &str) -> Result<String, X402Error>;
// Network pattern matching: "eip155:*" matches "eip155:196"
pub fn network_matches_pattern(network: &str, pattern: &str) -> bool;
pub fn find_schemes_by_network<'a, T>(
map: &'a HashMap<String, HashMap<String, T>>,
network: &str,
) -> Option<&'a HashMap<String, T>>;
pub fn find_by_network_and_scheme<'a, T>(
map: &'a HashMap<String, HashMap<String, T>>,
scheme: &str,
network: &str,
) -> Option<&'a T>;
pub fn deep_equal(obj1: &serde_json::Value, obj2: &serde_json::Value) -> bool;
Schema 验证#
pub fn validate_payment_requirements(req: &PaymentRequirements) -> Result<(), X402Error>;
pub fn validate_payment_payload(payload: &PaymentPayload) -> Result<(), X402Error>;
pub fn validate_payment_required(required: &PaymentRequired) -> Result<(), X402Error>;
验证所有必填字段均不为空。
Rust SDK 参考(适用于 charge、session )#
Crate#
| Crate | 描述 |
|---|---|
mpp-evm | OKX MPP EVM Seller SDK:EvmChargeMethod / EvmSessionMethod / EvmChargeChallenger、SA-API client、本地 store、EIP-712 签名、Axum handlers |
mpp | 上游 MPP 协议层(tempoxyz/mpp-rs):PaymentCredential / PaymentChallenge / ChargeMethod / SessionMethod traits、Axum extractor MppCharge<C> / WithReceipt<T>、challenge codec、HMAC、PaymentErrorDetails |
payment-router-axum | 双协议(MPP + x402)路由 Tower Layer,以 adapter pattern 让一个 axum app 同时接两种协议 |
Rust MPP SDK 当前提供卖家(server-side)能力。买家侧(在 axum 里发起付费请求)规划中。
核心类型(mpp-evm)#
Chain ID 默认值#
/// Default X Layer chain ID.
pub const DEFAULT_CHAIN_ID: u64 = 196;
SaApiResponse#
SA-API 统一响应包装,客户端会自动解包 data。
#[derive(Debug, Clone, Deserialize)]
pub struct SaApiResponse<T> {
pub code: i64,
pub data: Option<T>,
#[serde(default)]
pub msg: String,
}
ChargeMethodDetails / ChargeSplit#
Charge challenge 的 methodDetails(放在 base64url-encoded request 字段内)。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChargeMethodDetails {
pub chain_id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub fee_payer: Option<bool>, // server pays gas (transaction mode)
#[serde(skip_serializing_if = "Option::is_none")]
pub permit2_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub splits: Option<Vec<ChargeSplit>>,
}
/// Constraints: sum(splits[].amount) < request.amount; primary recipient
/// must retain a non-zero remainder.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChargeSplit {
pub amount: String, // base-units integer string
pub recipient: String, // 40-hex EIP-55 address
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,
}
SessionMethodDetails / SessionSplit#
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionMethodDetails {
pub chain_id: u64,
pub escrow_contract: String, // 40-hex escrow address
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_voucher_delta: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fee_payer: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub splits: Option<Vec<SessionSplit>>,
}
/// Constraints: bps in [1, 9999]; sum(splits[].bps) < 10000.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSplit {
pub recipient: String,
pub bps: u32, // basis points
#[serde(skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,
}
Eip3009Authorization / Eip3009Split#
Charge payload.authorization 形状(client 签 EIP-3009 后填进去)。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip3009Authorization {
#[serde(rename = "type")]
pub auth_type: String, // always "eip-3009"
pub from: String,
pub to: String,
pub value: String,
pub valid_after: String,
pub valid_before: String,
pub nonce: String,
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub splits: Option<Vec<Eip3009Split>>, // independent signature per split
}
impl Eip3009Authorization {
pub const TYPE: &'static str = "eip-3009";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip3009Split {
pub from: String,
pub to: String,
pub value: String,
pub valid_after: String,
pub valid_before: String,
pub nonce: String,
pub signature: String,
}
ChargeReceipt / SessionReceipt / ChannelStatus#
SA-API 返回体。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChargeReceipt {
pub method: String, // "evm"
pub reference: String, // on-chain tx hash
pub status: String,
pub timestamp: String,
pub chain_id: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub confirmations: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub challenge_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionReceipt {
pub method: String,
pub intent: String,
pub status: String,
pub timestamp: String,
pub chain_id: u64,
pub channel_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deposit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub challenge_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accepted_cumulative: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confirmations: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub units: Option<u64>,
}
/// Response from GET /session/status.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelStatus {
pub channel_id: String,
pub payer: String,
pub payee: String,
pub token: String,
pub deposit: String,
pub settled_on_chain: String,
pub session_status: String,
pub remaining_balance: String
}
SettleRequestPayload / CloseRequestPayload#
SDK 主动调 /session/settle / /session/close 的请求 body(扁平结构,不带 challenge wrapper)。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SettleRequestPayload {
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<String>, // "settle"
pub channel_id: String,
pub cumulative_amount: String, // uint128 decimal string
pub voucher_signature: String, // 65-byte r‖s‖v hex (payer)
pub payee_signature: String, // 65-byte r‖s‖v hex (payee)
pub nonce: String, // uint256 decimal string
pub deadline: String, // uint256 decimal string
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseRequestPayload {
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<String>, // "close"
pub channel_id: String,
pub cumulative_amount: String,
/// Normal branch: 65-byte r‖s‖v hex.
/// Waiver branch (cumulative ≤ settledOnChain or no local voucher): "".
pub voucher_signature: String,
pub payee_signature: String,
pub nonce: String,
pub deadline: String,
}
ServerAccountingState#
/// Server-side per-session accounting maintained by the SDK.
/// Invariants:
/// accepted_cumulative is monotonically non-decreasing
/// spent is monotonically non-decreasing
/// available = accepted_cumulative - spent
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerAccountingState {
pub accepted_cumulative: u128,
pub spent: u128,
pub settled_on_chain: u128,
}
SaApiClient trait#
可插拔 SA-API client 接口,默认实现 [OkxSaApiClient]。
#[async_trait]
pub trait SaApiClient: Send + Sync {
// Charge
async fn charge_settle(
&self,
credential: &serde_json::Value,
) -> Result<ChargeReceipt, SaApiError>;
async fn charge_verify_hash(
&self,
credential: &serde_json::Value,
) -> Result<ChargeReceipt, SaApiError>;
async fn session_open(
&self,
credential: &serde_json::Value,
) -> Result<SessionReceipt, SaApiError>;
async fn session_top_up(
&self,
credential: &serde_json::Value,
) -> Result<SessionReceipt, SaApiError>;
async fn session_settle(
&self,
payload: &SettleRequestPayload,
) -> Result<SessionReceipt, SaApiError>;
async fn session_close(
&self,
payload: &CloseRequestPayload,
) -> Result<SessionReceipt, SaApiError>;
async fn session_status(
&self,
channel_id: &str,
) -> Result<ChannelStatus, SaApiError>;
}
OKX SA-API 客户端(OkxSaApiClient)#
#[derive(Debug, Clone)]
pub struct OkxSaApiClient { /* private */ }
impl OkxSaApiClient {
/// Default production base URL (https://web3.okx.com).
pub fn new(api_key: String, secret_key: String, passphrase: String) -> Self;
/// Custom base URL (sandbox / staging).
pub fn with_base_url(
base_url: String,
api_key: String,
secret_key: String,
passphrase: String,
) -> Self;
}
实现 SaApiClient trait;每个请求自动加 HMAC-SHA256 认证头。HTTP 超时 30 秒。
调用的端点#
| trait 方法 | OKX 路径 |
|---|---|
charge_settle() | POST /api/v6/pay/mpp/charge/settle |
charge_verify_hash() | POST /api/v6/pay/mpp/charge/verifyHash |
session_open() | POST /api/v6/pay/mpp/session/open |
session_top_up() | POST /api/v6/pay/mpp/session/topUp |
session_settle() | POST /api/v6/pay/mpp/session/settle |
session_close() | POST /api/v6/pay/mpp/session/close |
session_status(channel_id) | GET /api/v6/pay/mpp/session/status?channelId=... |
OKX 响应被包装在 {"code": 0, "data": {...}, "msg": ""} 中,客户端会自动解包。
HMAC 认证头常量#
pub const HEADER_API_KEY: &str = "OK-ACCESS-KEY";
pub const HEADER_SIGN: &str = "OK-ACCESS-SIGN";
pub const HEADER_TIMESTAMP: &str = "OK-ACCESS-TIMESTAMP";
pub const HEADER_PASSPHRASE: &str = "OK-ACCESS-PASSPHRASE";
签名算法:Base64(HMAC-SHA256(secret_key, timestamp + METHOD + request_path + body)),跟 OKX REST API v5 一致。
Charge — EvmChargeMethod#
实现 mpp::protocol::traits::ChargeMethod,把 credential 透传给 SA-API。
#[derive(Clone)]
pub struct EvmChargeMethod { /* private */ }
impl EvmChargeMethod {
pub fn new(sa_client: Arc<dyn SaApiClient>) -> Self;
}
payload.type 路由:
"transaction"→charge_settle(SA-API 链上 broadcasttransferWithAuthorization)"hash"→charge_verify_hash(client 已自行 broadcast,SA-API 验证 tx hash)
splits 透传 payload.authorization.splits[],SA-API 拥有 split 校验权。
Charge — EvmChargeChallenger#
实现 upstream mpp::server::axum::ChargeChallenger,可挂到 MppCharge<C> extractor。
配置#
pub struct EvmChargeChallengerConfig {
pub charge_method: EvmChargeMethod,
pub currency: String, // ERC-20 contract, 40-hex
pub recipient: String, // payee address
pub chain_id: u64, // 196 = X Layer
pub fee_payer: Option<bool>, // Some(true) = transaction mode
pub realm: String, // for WWW-Authenticate header
pub secret_key: String, // HMAC for challenge signing
pub splits: Option<Vec<ChargeSplit>>,
}
构造#
#[derive(Clone)]
pub struct EvmChargeChallenger { /* private */ }
impl EvmChargeChallenger {
pub fn new(cfg: EvmChargeChallengerConfig) -> Self;
pub fn builder(
charge_method: EvmChargeMethod,
realm: impl Into<String>,
secret_key: impl Into<String>,
) -> EvmChargeChallengerBuilder;
}
pub struct EvmChargeChallengerBuilder { /* private */ }
impl EvmChargeChallengerBuilder {
pub fn currency(mut self, v: impl Into<String>) -> Self;
pub fn recipient(mut self, v: impl Into<String>) -> Self;
pub fn chain_id(mut self, v: u64) -> Self;
pub fn fee_payer(mut self, v: bool) -> Self;
pub fn splits(mut self, v: Vec<ChargeSplit>) -> Self;
pub fn build(self) -> EvmChargeChallenger;
}
EvmChargeChallenger 实现 mpp::server::axum::ChargeChallenger,提供 challenge(amount, options) 和 verify_payment(authorization_header) 两个方法 —— 跟 upstream MppCharge<C> extractor / WithReceipt<T> 响应包装一起组成最简
charge handler。
Session — EvmSessionMethod#
实现 mpp::protocol::traits::SessionMethod。维护本地 channel state、支持 voucher 本地验签 + 累计扣费、商户主动 settle/close。
构造与配置#
#[derive(Clone)]
pub struct EvmSessionMethod { /* private */ }
impl EvmSessionMethod {
/// Default in-memory store.
pub fn new(sa_client: Arc<dyn SaApiClient>) -> Self;
/// Inject a custom [`SessionStore`].
pub fn with_store(
sa_client: Arc<dyn SaApiClient>,
store: Arc<dyn SessionStore>,
) -> Self;
/// Inject the payee signer. Accepts any
/// `alloy::signers::Signer + Send + Sync + 'static`
/// (PrivateKeySigner / AwsSigner / LedgerSigner / custom remote-signer wrapper).
pub fn with_signer<S: Signer + Send + Sync + 'static>(mut self, signer: S) -> Self;
/// Startup fast-fail check: `signer.address() == expected`.
pub fn verify_payee(self, expected: Address) -> Result<Self, SaApiError>;
/// Custom nonce allocator (default: [`UuidNonceProvider`]).
pub fn with_nonce_provider(mut self, p: Arc<dyn NonceProvider>) -> Self;
/// Custom EIP-712 domain `name` / `version` (default: OKX canonical values).
pub fn with_domain_meta(
mut self,
name: impl Into<Cow<'static, str>>,
version: impl Into<Cow<'static, str>>,
) -> Self;
/// Custom signature deadline (default: `U256::MAX`, never expires).
pub fn with_deadline(mut self, d: U256) -> Self;
/// Set challenge `methodDetails` from a raw JSON value.
pub fn with_method_details(mut self, details: serde_json::Value) -> Self;
/// Set challenge `methodDetails` from a typed struct.
pub fn with_typed_method_details(
mut self,
details: SessionMethodDetails,
) -> Result<Self, serde_json::Error>;
/// Minimal builder: only sets escrow; `chain_id` defaults to X Layer (196).
pub fn with_escrow(self, escrow_contract: impl Into<String>) -> Self;
/// Startup check that the local EIP-712 domain matches the contract's
/// on-chain `domainSeparator()`; mismatch → 8000. Run once at startup —
/// otherwise every voucher / settle / close signature will be rejected
/// by the contract.
pub fn assert_domain_matches(&self, on_chain: B256) -> Result<(), SaApiError>;
}
业务方法#
impl EvmSessionMethod {
/// Local store handle.
pub fn store(&self) -> Arc<dyn SessionStore>;
/// On-chain channel status (passthrough to SA-API).
pub async fn status(&self, channel_id: &str) -> Result<ChannelStatus, SaApiError>;
/// Local voucher: guard + EIP-712 verify + bump `highest_voucher`.
/// Byte-level idempotent (same cum + same sig): verify and highest
/// update are skipped, but deduct still runs.
pub async fn submit_voucher(
&self,
channel_id: &str,
cumulative_amount: u128,
signature: Bytes,
) -> Result<(), SaApiError>;
/// Atomic deduct: `available = highest_voucher_amount - spent`;
/// insufficient → 70015.
pub async fn deduct_from_channel(
&self,
channel_id: &str,
amount: u128,
) -> Result<ChannelRecord, SaApiError>;
/// Take local highest voucher → sign SettleAuth → call `/session/settle`.
pub async fn settle_with_authorization(
&self,
channel_id: &str,
) -> Result<SessionReceipt, SaApiError>;
/// Sign CloseAuth → call `/session/close` → remove from store on success.
/// If `cumulative_amount` / `provided_voucher_sig` are `None`, fall back
/// to local highest; if both are `None` and the store has no voucher,
/// take the waiver path (empty string).
pub async fn close_with_authorization(
&self,
channel_id: &str,
cumulative_amount: Option<u128>,
provided_voucher_sig: Option<Bytes>,
) -> Result<SessionReceipt, SaApiError>;
}
Session action 路由(SessionMethod::verify_session 内部)#
payload.action | 行为 |
|---|---|
"open" | payee 校验 → 调 SA session/open → 写本地 store |
"voucher" | submit_voucher(本地验签 + 升 highest) → deduct_from_channel(扣费) |
"topUp" | 调 SA session/topUp → 累加本地 deposit |
"close" | 取 payer 提供的 voucher → 本地 close 流程 |
Session — SessionStore trait#
/// Closure-based atomic update. `Err` aborts the whole update; the store
/// keeps the old value (transaction semantics).
pub type ChannelUpdater =
Box<dyn FnOnce(&mut ChannelRecord) -> Result<(), SaApiError> + Send>;
#[async_trait]
pub trait SessionStore: Send + Sync {
async fn get(&self, channel_id: &str) -> Option<ChannelRecord>;
async fn put(&self, record: ChannelRecord);
async fn remove(&self, channel_id: &str);
/// Atomic read-modify-write. Channel absent → 70010 channel_not_found;
/// updater returns Err → no write happens, the error propagates.
async fn update(
&self,
channel_id: &str,
updater: ChannelUpdater,
) -> Result<ChannelRecord, SaApiError>;
}
ChannelRecord#
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelRecord {
pub channel_id: String,
pub chain_id: u64,
pub escrow_contract: Address,
pub payer: Address,
pub payee: Address,
pub authorized_signer: Address, // address(0) 在 open 时已解析为 payer
pub deposit: u128,
pub highest_voucher_amount: u128,
pub highest_voucher_signature: Option<Bytes>,
pub min_voucher_delta: Option<u128>, // 节流:voucher 最小递增量
#[serde(default)]
pub spent: u128, // 已扣费(invariant: spent ≤ highest)
#[serde(default)]
pub units: u64, // deduct 调用次数
}
impl ChannelRecord {
pub fn voucher_signer(&self) -> Address; // 返回 authorized_signer
}
默认实现 — InMemorySessionStore#
#[derive(Debug, Default, Clone)]
pub struct InMemorySessionStore { /* private */ }
impl InMemorySessionStore {
pub fn new() -> Self;
}
进程内 HashMap,适合大多数 single-process 部署(操作短、锁竞争小)。两个 caveat:
- 重启即丢:进程重启 / crash 丢失所有 channel state。如果业务不能接受这个 loss(长期 channel、多实例 HA、热重载),自实现持久化 store(SQLite / Redis / Postgres / DynamoDB / ...)接
with_store(...)注入。 - abandoned channel 累积:payer 不调 close 时记录会一直留着 — 这是 session lifecycle 通用问题,不是 in-memory 特有(SQL / Redis store 同样会满)。商户应有 cleanup 策略,或按业务 TTL 清理。
NonceProvider trait#
#[async_trait]
pub trait NonceProvider: Send + Sync {
async fn allocate(
&self,
payee: Address,
channel_id: B256,
) -> Result<U256, SaApiError>;
}
/// Default impl: UUID v4 → U256 (128-bit random, stateless, safe across
/// multi-instance / restart).
#[derive(Debug, Default, Clone)]
pub struct UuidNonceProvider;
合约层 nonce 已用集 key = (payee, channelId, nonce),重复使用以 NonceAlreadyUsed revert。SDK 只负责分配「大概率没用过」的 nonce,不追踪已用集。
EIP-712 签名(mpp_evm::eip712)#
Domain#
pub const VOUCHER_DOMAIN_NAME: &str = "EVM Payment Channel";
pub const VOUCHER_DOMAIN_VERSION: &str = "1";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DomainMeta {
pub name: Cow<'static, str>,
pub version: Cow<'static, str>,
}
impl DomainMeta {
pub fn new(
name: impl Into<Cow<'static, str>>,
version: impl Into<Cow<'static, str>>,
) -> Self;
}
impl Default for DomainMeta { /* uses VOUCHER_DOMAIN_* constants */ }
pub fn build_domain(
meta: &DomainMeta,
chain_id: u64,
escrow_contract: Address,
) -> alloy_sol_types::Eip712Domain;
Voucher 验签#
sol! {
/// EIP-712 typed struct; 1:1 with the contract's `Voucher`.
struct Voucher {
bytes32 channelId;
uint128 cumulativeAmount;
}
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum VerifyError {
#[error("signature must be 65 bytes, got {0}")]
BadLength(usize),
#[error("non-canonical signature: s exceeds secp256k1 half-order (high-s)")]
HighS,
#[error("signature parse failed (cannot construct Signature from bytes)")]
SignatureParse,
#[error("ecrecover failed (cannot recover signer from prehash)")]
Recover,
#[error("signer mismatch: recovered {recovered}, expected {expected}")]
AddressMismatch { recovered: Address, expected: Address },
}
/// 1) signature.len() == 65 2) low-s precheck 3) EIP-712 digest
/// 4) ecrecover + strict address comparison
pub fn verify_voucher(
meta: &DomainMeta,
escrow_contract: Address,
chain_id: u64,
channel_id: B256,
cumulative_amount: u128,
signature: &[u8],
expected_signer: Address,
) -> Result<(), VerifyError>;
SettleAuthorization / CloseAuthorization 签名#
sol! {
struct SettleAuthorization {
bytes32 channelId;
uint128 cumulativeAmount;
uint256 nonce;
uint256 deadline;
}
struct CloseAuthorization {
bytes32 channelId;
uint128 cumulativeAmount;
uint256 nonce;
uint256 deadline;
}
}
#[derive(Debug, Clone)]
pub struct SignedAuthorization {
pub channel_id: B256,
pub cumulative_amount: u128,
pub nonce: U256,
pub deadline: U256,
pub signature: Bytes, // 65-byte (r, s, v)
}
pub async fn sign_settle_authorization(
meta: &DomainMeta,
signer: &(impl Signer + ?Sized),
escrow_contract: Address,
chain_id: u64,
channel_id: B256,
cumulative_amount: u128,
nonce: U256,
deadline: U256,
) -> Result<SignedAuthorization, SaApiError>;
pub async fn sign_close_authorization(
meta: &DomainMeta,
signer: &(impl Signer + ?Sized),
escrow_contract: Address,
chain_id: u64,
channel_id: B256,
cumulative_amount: u128,
nonce: U256,
deadline: U256,
) -> Result<SignedAuthorization, SaApiError>;
Challenge 构造器(mpp_evm::charge::challenge)#
pub const METHOD_NAME: &str = "evm";
pub const INTENT_CHARGE: &str = "charge";
pub const INTENT_SESSION: &str = "session";
pub const DEFAULT_EXPIRES_MINUTES: i64 = 5;
/// Build a method="evm" charge challenge with HMAC-protected `id`.
pub fn build_charge_challenge(
secret_key: &str,
realm: &str,
request: &mpp::protocol::intents::ChargeRequest,
expires: Option<&str>,
description: Option<&str>,
) -> Result<mpp::protocol::core::PaymentChallenge, String>;
pub fn build_session_challenge(
secret_key: &str,
realm: &str,
request: &mpp::protocol::intents::SessionRequest,
expires: Option<&str>,
description: Option<&str>,
) -> Result<mpp::protocol::core::PaymentChallenge, String>;
/// Assemble a request body from a base-units amount + typed method details.
pub fn charge_request_with(
amount_base_units: impl Into<String>,
currency: impl Into<String>,
recipient: impl Into<String>,
details: ChargeMethodDetails,
) -> Result<mpp::protocol::intents::ChargeRequest, String>;
pub fn session_request_with(
amount_per_unit_base: impl Into<String>,
currency: impl Into<String>,
recipient: impl Into<String>,
details: SessionMethodDetails,
) -> Result<mpp::protocol::intents::SessionRequest, String>;
CredentialExt — 解码 challenge.request#
PaymentCredential.challenge.request 是 Base64UrlJson<T>,.decode() 返通用 error。本扩展把它归一到 SaApiError,可与 SDK 其他调用一起 ?。
pub trait CredentialExt {
fn decode_request<R: DeserializeOwned>(&self) -> Result<R, SaApiError>;
}
impl CredentialExt for mpp::protocol::core::PaymentCredential { /* ... */ }
// Usage:
use mpp_evm::CredentialExt;
use mpp::protocol::intents::SessionRequest;
let request: SessionRequest = credential.decode_request()?;
Axum drop-in handlers(feature = "handlers")#
use mpp_evm::handlers;
#[derive(Debug, Clone, Deserialize)]
pub struct SettleBody {
#[serde(rename = "channelId")]
pub channel_id: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct StatusQuery {
#[serde(rename = "channelId")]
pub channel_id: String,
}
/// POST /session/settle — body { "channelId": "0x..." }.
pub async fn session_settle(
State(method): State<Arc<EvmSessionMethod>>,
Json(body): Json<SettleBody>,
) -> Response;
/// GET /session/status?channelId=0x...
pub async fn session_status(
State(method): State<Arc<EvmSessionMethod>>,
Query(q): Query<StatusQuery>,
) -> Response;
错误自动按 SaApiError::to_problem_details(...) 映射到正确的 HTTP 状态码。
错误类型#
#[derive(Debug, Clone, thiserror::Error)]
#[error("SA API error {code}: {msg}")]
pub struct SaApiError {
pub code: u32,
pub msg: String,
}
impl SaApiError {
pub fn new(code: u32, msg: impl Into<String>) -> Self;
/// Map to mpp-rs `PaymentErrorDetails` (RFC 9457 ProblemDetails).
pub fn to_problem_details(
&self,
challenge_id: Option<&str>,
) -> mpp::PaymentErrorDetails;
}
错误码映射表#
| code | 含义 |
|---|---|
| 8000 | API 服务内部错误 |
| 70000 | 缺少必填字段或格式错误 |
| 70001 | 链不在支持列表 |
| 70002 | 付款方在黑名单 |
| 70003 | source 缺失、feePayer=true 不支持 hash 模式、或 txHash 已被使用 |
| 70004 | 签名验证失败 |
| 70005 | 分账总额 ≥ 主金额 |
| 70006 | 分账数量 > 10 |
| 70007 | 交易未在链上确认 |
| 70008 | 链上合约 channel 状态已关闭 |
| 70009 | challenge 不存在或已过期 |
| 70010 | channelId 不存在 |
| 70011 | Escrow 合约 grace period < 10 分钟,拒绝开通 channel |
| 70012 | cumulativeAmount 超过 channel 存款余额 |
| 70013 | voucher 递增量低于 minVoucherDelta |
| 70014 | channel 处于 CLOSING 状态,不接受新 Voucher |
双协议路由(payment-router-axum)#
让一个 axum app 同时接 MPP + x402,业务 handler 协议无关。
Adapter trait#
pub trait ProtocolAdapter: Send + Sync + 'static {
fn name(&self) -> &str; // "mpp" | "x402" | 自定义
fn priority(&self) -> u32; // 10 = MPP, 20 = x402, 100+ = 自定义
fn detect(&self, parts: &http::request::Parts) -> bool; // 看请求头是否属于本协议
fn get_challenge<'a>(
&'a self,
parts: &'a http::request::Parts,
route_cfg: &'a serde_json::Value,
) -> ChallengeFuture<'a>; // 生成 402 challenge 行
fn make_service(&self, inner: InnerService) -> InnerService; // 用本协议原生中间件包 inner
}
内置 adapter#
use payment_router_axum::adapters::{MppAdapter, X402Adapter};
let mpp_adapter = Arc::new(MppAdapter::new(mpp_charge_challenger));
// X402Adapter::new 接受 owned (routes, server),不是 Arc 包装。
let x402_adapter = Arc::new(X402Adapter::new(x402_routes, x402_server));
MppAdapter 内部用 mpp::server::axum::ChargeChallenger 完整流程(HMAC 校验 + EIP-3009 验签 + SA-API 结算)。X402Adapter 内部走 x402-axum 原生 PaymentMiddleware。
Router 配置#
pub struct UnifiedRouteConfig {
/// 可选的人类可读描述。
pub description: Option<String>,
/// adapter.name() → 该 adapter 在本 route 上的 JSON 配置。
/// Map 里没列的 adapter 在本 route 上不启用。
pub adapter_configs: HashMap<String, serde_json::Value>,
}
pub struct PaymentRouterConfig {
/// `Vec<(pattern, route_cfg)>`。Vec 而非 HashMap —— 声明顺序敏感
/// (spec §9 first-match-wins)。pattern 形如 "GET /path" 或 "/path"
/// (任意方法)。
pub routes: Vec<(String, UnifiedRouteConfig)>,
/// 协议 adapter 列表(MPP / x402 / 自定义)。
pub protocols: Vec<Arc<dyn ProtocolAdapter>>,
/// 可选的错误观察器。
pub on_error: Option<Arc<ErrorHandler>>,
}
/// `ErrorHandler` 类型别名:
/// `dyn Fn(&(dyn Error + Send + Sync), ErrorContext) + Send + Sync + 'static`
pub type ErrorHandler = /* see payment-router-axum::types */;
pub struct PaymentRouterLayer { /* tower::Layer */ }
impl PaymentRouterLayer {
pub fn new(cfg: PaymentRouterConfig) -> Result<Self, BuildError>;
}
- Rust SDK 参考(适用于 exact、aggr_deferred)Crate核心类型Facilitator 类型Traits服务端 API(X402ResourceServer)OKX Facilitator 客户端(OkxHttpFacilitatorClient)HMAC 认证HTTP 工具路由配置Axum 中间件(x402-axum)EVM 机制(x402-evm)错误类型工具函数Schema 验证Rust SDK 参考(适用于 charge、session )Crate核心类型(mpp-evm)SaApiClient traitOKX SA-API 客户端(OkxSaApiClient)Charge — EvmChargeMethodCharge — EvmChargeChallengerSession — EvmSessionMethodSession — SessionStore traitNonceProvider traitEIP-712 签名(mpp_evm::eip712)Challenge 构造器(mpp_evm::charge::challenge)CredentialExt — 解码 challenge.requestAxum drop-in handlers(feature = "handlers")错误类型双协议路由(payment-router-axum)