use alloy_primitives::{Address, B256, Bytes, address};
#[cfg(any(feature = "facilitator", feature = "client"))]
use alloy_sol_types::sol;
pub use r402_core::scheme::ExactScheme;
use r402_core::wire::UnixTimestamp;
use serde::{Deserialize, Serialize};
use crate::chain::TokenAmount;
pub const PERMIT2_ADDRESS: Address = address!("0x000000000022D473030F116dDEE9F6B43aC78BA3");
pub const X402_EXACT_PERMIT2_PROXY: Address =
address!("0x402085c248EeA27D92E8b30b2C58ed07f9E20001");
pub const X402_UPTO_PERMIT2_PROXY: Address = address!("0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002");
pub const VALIDATOR_ADDRESS: Address = address!("0xdAcD51A54883eb67D95FAEb2BBfdC4a9a6BD2a3B");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum AssetTransferMethod {
Eip3009,
Permit2,
}
impl<'de> Deserialize<'de> for AssetTransferMethod {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
match raw.as_str() {
"eip3009" => Ok(Self::Eip3009),
"permit2" => Ok(Self::Permit2),
other => Err(serde::de::Error::custom(format!(
"unsupported assetTransferMethod `{other}`: r402-evm implements \
eip3009 and permit2 only (ERC-7710 is not implemented)"
))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ExactPayload {
Permit2(Permit2Payload),
Eip3009(Eip3009Payload),
}
impl ExactPayload {
#[must_use]
pub const fn transfer_method(&self) -> AssetTransferMethod {
match self {
Self::Eip3009(_) => AssetTransferMethod::Eip3009,
Self::Permit2(_) => AssetTransferMethod::Permit2,
}
}
#[must_use]
pub const fn sender(&self) -> Address {
match self {
Self::Eip3009(p) => p.authorization.from,
Self::Permit2(p) => p.permit2_authorization.from,
}
}
pub const fn signature(&self) -> &Bytes {
match self {
Self::Eip3009(p) => &p.signature,
Self::Permit2(p) => &p.signature,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip3009Payload {
pub signature: Bytes,
pub authorization: Eip3009Authorization,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Permit2TokenPermissions {
pub token: Address,
pub amount: TokenAmount,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Permit2Witness {
pub to: Address,
pub valid_after: TokenAmount,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Permit2Authorization {
pub from: Address,
pub permitted: Permit2TokenPermissions,
pub spender: Address,
pub nonce: TokenAmount,
pub deadline: TokenAmount,
pub witness: Permit2Witness,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Permit2Payload {
pub signature: Bytes,
pub permit2_authorization: Permit2Authorization,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip3009Authorization {
pub from: Address,
pub to: Address,
pub value: TokenAmount,
pub valid_after: UnixTimestamp,
pub valid_before: UnixTimestamp,
pub nonce: B256,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequirementsExtra {
pub name: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asset_transfer_method: Option<AssetTransferMethod>,
}
impl PaymentRequirementsExtra {
#[must_use]
pub fn from_deployment(
eip712: Option<crate::chain::TokenDeploymentEip712>,
method: Option<AssetTransferMethod>,
) -> Option<serde_json::Value> {
let extra = match (eip712, method) {
(Some(eip712), method) => Self::from(eip712).with_transfer_method(method),
(None, Some(m)) => Self {
name: String::new(),
version: String::new(),
asset_transfer_method: Some(m),
},
(None, None) => return None,
};
serde_json::to_value(extra).ok()
}
#[must_use]
pub const fn with_transfer_method(mut self, method: Option<AssetTransferMethod>) -> Self {
self.asset_transfer_method = method;
self
}
}
impl From<crate::chain::TokenDeploymentEip712> for PaymentRequirementsExtra {
fn from(eip712: crate::chain::TokenDeploymentEip712) -> Self {
Self {
name: eip712.name,
version: eip712.version,
asset_transfer_method: None,
}
}
}
#[cfg(any(feature = "facilitator", feature = "client"))]
sol!(
#[derive(Serialize, Deserialize)]
struct TransferWithAuthorization {
address from;
address to;
uint256 value;
uint256 validAfter;
uint256 validBefore;
bytes32 nonce;
}
);
#[cfg(any(feature = "facilitator", feature = "client"))]
sol!(
#[derive(Serialize, Deserialize)]
struct TokenPermissions {
address token;
uint256 amount;
}
#[derive(Serialize, Deserialize)]
struct Witness {
address to;
uint256 validAfter;
}
#[derive(Serialize, Deserialize)]
struct PermitWitnessTransferFrom {
TokenPermissions permitted;
address spender;
uint256 nonce;
uint256 deadline;
Witness witness;
}
);
pub mod v2 {
use r402_core::wire as proto_v2;
use super::{ExactPayload, ExactScheme, PaymentRequirementsExtra};
use crate::chain::{ChecksummedAddress, TokenAmount};
pub type VerifyRequest = proto_v2::TypedVerifyRequest<2, PaymentPayload, PaymentRequirements>;
pub type SettleRequest = VerifyRequest;
pub type PaymentPayload = proto_v2::PaymentPayload<PaymentRequirements, ExactPayload>;
pub type PaymentRequirements = proto_v2::PaymentRequirements<
ExactScheme,
TokenAmount,
ChecksummedAddress,
PaymentRequirementsExtra,
>;
}
#[cfg(test)]
mod transfer_method_tests {
use super::AssetTransferMethod;
#[test]
fn serialises_as_camel_case_wire_strings() {
assert_eq!(
serde_json::to_string(&AssetTransferMethod::Eip3009).unwrap(),
"\"eip3009\""
);
assert_eq!(
serde_json::to_string(&AssetTransferMethod::Permit2).unwrap(),
"\"permit2\""
);
}
#[test]
fn deserialises_supported_methods() {
assert_eq!(
serde_json::from_str::<AssetTransferMethod>("\"eip3009\"").unwrap(),
AssetTransferMethod::Eip3009
);
assert_eq!(
serde_json::from_str::<AssetTransferMethod>("\"permit2\"").unwrap(),
AssetTransferMethod::Permit2
);
}
#[test]
fn rejects_erc7710_with_explicit_message() {
let err = serde_json::from_str::<AssetTransferMethod>("\"erc7710\"").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("unsupported assetTransferMethod `erc7710`"),
"unexpected error: {msg}"
);
assert!(
msg.contains("ERC-7710 is not implemented"),
"unexpected error: {msg}"
);
}
#[test]
fn rejects_unknown_method() {
let err = serde_json::from_str::<AssetTransferMethod>("\"somethingElse\"").unwrap_err();
assert!(err.to_string().contains("unsupported assetTransferMethod"));
}
}
#[cfg(test)]
#[cfg(any(feature = "facilitator", feature = "client"))]
mod tests {
use alloy_primitives::{U256, keccak256};
use alloy_sol_types::SolStruct;
use super::*;
#[test]
fn witness_typehash_equals_spec_byte_string() {
let witness = Witness {
to: Address::ZERO,
validAfter: U256::ZERO,
};
let expected = keccak256(b"Witness(address to,uint256 validAfter)");
assert_eq!(witness.eip712_type_hash(), expected);
}
#[test]
fn permit_witness_transfer_from_typehash_equals_spec_byte_string() {
let permit = PermitWitnessTransferFrom {
permitted: TokenPermissions {
token: Address::ZERO,
amount: U256::ZERO,
},
spender: Address::ZERO,
nonce: U256::ZERO,
deadline: U256::ZERO,
witness: Witness {
to: Address::ZERO,
validAfter: U256::ZERO,
},
};
let expected = keccak256(
b"PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,uint256 validAfter)",
);
assert_eq!(permit.eip712_type_hash(), expected);
}
}