mod permit2;
use std::future::Future;
use std::sync::Arc;
use alloy_primitives::{Address, Bytes, FixedBytes, Signature, U256};
use alloy_signer_local::PrivateKeySigner;
use alloy_sol_types::{SolStruct, eip712_domain};
#[cfg(feature = "client-provider")]
use permit2::BuiltinPermit2Approver;
pub use permit2::{Permit2Approver, permit2_allowance_calldata, permit2_approval_calldata};
use r402::proto::Base64Bytes;
use r402::proto::PaymentRequired;
use r402::proto::UnixTimestamp;
use r402::proto::v2::{self, ResourceInfo};
use r402::scheme::SchemeId;
use r402::scheme::{ClientError, PaymentCandidate, PaymentCandidateSigner, SchemeClient};
use rand::RngExt;
use rand::rng;
use crate::chain::Eip155ChainReference;
use crate::chain::TokenAmount;
use crate::exact::types;
use crate::exact::types::{TokenPermissions as SolTokenPermissions, Witness as SolWitness};
use crate::exact::{
AssetTransferMethod, Eip155Exact, Eip3009Authorization, Eip3009Payload, ExactPayload,
PERMIT2_ADDRESS, PaymentRequirementsExtra, Permit2Authorization, Permit2Payload,
Permit2TokenPermissions, Permit2Witness, PermitWitnessTransferFrom, TransferWithAuthorization,
X402_EXACT_PERMIT2_PROXY,
};
pub trait SignerLike: Send + Sync {
fn address(&self) -> Address;
fn sign_hash(
&self,
hash: &FixedBytes<32>,
) -> impl Future<Output = Result<Signature, alloy_signer::Error>> + Send;
}
impl SignerLike for PrivateKeySigner {
fn address(&self) -> Address {
Self::address(self)
}
async fn sign_hash(&self, hash: &FixedBytes<32>) -> Result<Signature, alloy_signer::Error> {
alloy_signer::Signer::sign_hash(self, hash).await
}
}
impl<T: SignerLike + Send + Sync> SignerLike for Arc<T> {
fn address(&self) -> Address {
(**self).address()
}
async fn sign_hash(&self, hash: &FixedBytes<32>) -> Result<Signature, alloy_signer::Error> {
(**self).sign_hash(hash).await
}
}
#[derive(Debug, Clone)]
pub struct Eip3009SigningParams {
pub chain_id: u64,
pub asset_address: Address,
pub pay_to: Address,
pub amount: U256,
pub max_timeout_seconds: u64,
pub extra: Option<PaymentRequirementsExtra>,
}
pub async fn sign_erc3009_authorization<S: SignerLike + Sync>(
signer: &S,
params: &Eip3009SigningParams,
) -> Result<Eip3009Payload, ClientError> {
let (name, version) = params.extra.as_ref().map_or_else(
|| (String::new(), String::new()),
|extra| (extra.name.clone(), extra.version.clone()),
);
let domain = eip712_domain! {
name: name,
version: version,
chain_id: params.chain_id,
verifying_contract: params.asset_address,
};
let now = UnixTimestamp::now();
let valid_after_secs = now.as_secs().saturating_sub(10 * 60);
let valid_after = UnixTimestamp::from_secs(valid_after_secs);
let valid_before = now + params.max_timeout_seconds;
let nonce: [u8; 32] = rng().random();
let nonce = FixedBytes(nonce);
let authorization = Eip3009Authorization {
from: signer.address(),
to: params.pay_to,
value: params.amount.into(),
valid_after,
valid_before,
nonce,
};
let transfer_with_authorization = TransferWithAuthorization {
from: authorization.from,
to: authorization.to,
value: authorization.value.into(),
validAfter: U256::from(authorization.valid_after.as_secs()),
validBefore: U256::from(authorization.valid_before.as_secs()),
nonce: authorization.nonce,
};
let eip712_hash = transfer_with_authorization.eip712_signing_hash(&domain);
let signature = signer
.sign_hash(&eip712_hash)
.await
.map_err(|e| ClientError::SigningError(format!("{e:?}")))?;
Ok(Eip3009Payload {
signature: signature.as_bytes().into(),
authorization,
})
}
#[derive(Debug, Clone, Copy)]
pub struct Permit2SigningParams {
pub chain_id: u64,
pub asset_address: Address,
pub pay_to: Address,
pub amount: U256,
pub max_timeout_seconds: u64,
}
pub async fn sign_permit2_authorization<S: SignerLike + Sync>(
signer: &S,
params: &Permit2SigningParams,
) -> Result<Permit2Payload, ClientError> {
let domain = eip712_domain! {
name: "Permit2",
chain_id: params.chain_id,
verifying_contract: PERMIT2_ADDRESS,
};
let now = UnixTimestamp::now();
let valid_after_secs = now.as_secs().saturating_sub(10 * 60);
let deadline_secs = now.as_secs() + params.max_timeout_seconds;
let nonce_bytes: [u8; 32] = rng().random();
let nonce = U256::from_be_bytes(nonce_bytes);
let permit_witness = PermitWitnessTransferFrom {
permitted: SolTokenPermissions {
token: params.asset_address,
amount: params.amount,
},
spender: X402_EXACT_PERMIT2_PROXY,
nonce,
deadline: U256::from(deadline_secs),
witness: SolWitness {
to: params.pay_to,
validAfter: U256::from(valid_after_secs),
extra: Bytes::new(),
},
};
let eip712_hash = permit_witness.eip712_signing_hash(&domain);
let signature = signer
.sign_hash(&eip712_hash)
.await
.map_err(|e| ClientError::SigningError(format!("{e:?}")))?;
let authorization = Permit2Authorization {
from: signer.address(),
permitted: Permit2TokenPermissions {
token: params.asset_address,
amount: TokenAmount::from(params.amount),
},
spender: X402_EXACT_PERMIT2_PROXY,
nonce: TokenAmount::from(nonce),
deadline: TokenAmount::from(U256::from(deadline_secs)),
witness: Permit2Witness {
to: params.pay_to,
valid_after: TokenAmount::from(U256::from(valid_after_secs)),
extra: Bytes::new(),
},
};
Ok(Permit2Payload {
signature: signature.as_bytes().into(),
permit2_authorization: authorization,
})
}
pub struct Eip155ExactClient<S> {
signer: S,
approver: Option<Arc<dyn Permit2Approver>>,
auto_approve: bool,
}
impl<S: std::fmt::Debug> std::fmt::Debug for Eip155ExactClient<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Eip155ExactClient")
.field("signer", &self.signer)
.field("has_approver", &self.approver.is_some())
.field("auto_approve", &self.auto_approve)
.finish()
}
}
impl<S> Eip155ExactClient<S> {
pub const fn new(signer: S) -> Self {
Self {
signer,
approver: None,
auto_approve: false,
}
}
pub fn builder(signer: S) -> Eip155ExactClientBuilder<S> {
Eip155ExactClientBuilder {
signer,
approver: None,
auto_approve: true,
}
}
}
pub struct Eip155ExactClientBuilder<S> {
signer: S,
approver: Option<Arc<dyn Permit2Approver>>,
auto_approve: bool,
}
impl<S: std::fmt::Debug> std::fmt::Debug for Eip155ExactClientBuilder<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Eip155ExactClientBuilder")
.field("signer", &self.signer)
.field("has_approver", &self.approver.is_some())
.field("auto_approve", &self.auto_approve)
.finish()
}
}
impl<S> Eip155ExactClientBuilder<S> {
#[must_use]
pub fn approver<A: Permit2Approver + 'static>(mut self, approver: A) -> Self {
self.approver = Some(Arc::new(approver));
self
}
#[must_use]
pub const fn auto_approve(mut self, auto_approve: bool) -> Self {
self.auto_approve = auto_approve;
self
}
#[cfg(feature = "client-provider")]
#[must_use]
pub fn provider<P: alloy_provider::Provider + Send + Sync + 'static>(
self,
provider: P,
) -> Self {
self.approver(BuiltinPermit2Approver { provider })
}
pub fn build(self) -> Eip155ExactClient<S> {
Eip155ExactClient {
signer: self.signer,
approver: self.approver,
auto_approve: self.auto_approve,
}
}
}
impl<S> SchemeId for Eip155ExactClient<S> {
fn namespace(&self) -> &str {
Eip155Exact.namespace()
}
fn scheme(&self) -> &str {
Eip155Exact.scheme()
}
}
impl<S> SchemeClient for Eip155ExactClient<S>
where
S: SignerLike + Clone + Send + Sync + 'static,
{
fn accept(&self, payment_required: &PaymentRequired) -> Vec<PaymentCandidate> {
payment_required
.accepts
.iter()
.filter_map(|v| {
let requirements: types::v2::PaymentRequirements = v.as_concrete()?;
let chain_reference = Eip155ChainReference::try_from(&requirements.network).ok()?;
let candidate = PaymentCandidate {
chain_id: requirements.network.clone(),
asset: requirements.asset.to_string(),
amount: requirements.amount.0.to_string(),
scheme: self.scheme().to_string(),
pay_to: requirements.pay_to.to_string(),
signer: Box::new(V2PayloadSigner {
resource_info: Some(payment_required.resource.clone()),
signer: self.signer.clone(),
chain_reference,
requirements,
approver: self.approver.clone(),
auto_approve: self.auto_approve,
}),
};
Some(candidate)
})
.collect::<Vec<_>>()
}
}
struct V2PayloadSigner<S> {
signer: S,
resource_info: Option<ResourceInfo>,
chain_reference: Eip155ChainReference,
requirements: types::v2::PaymentRequirements,
approver: Option<Arc<dyn Permit2Approver>>,
auto_approve: bool,
}
impl<S> PaymentCandidateSigner for V2PayloadSigner<S>
where
S: Sync + SignerLike,
{
fn sign_payment(&self) -> r402::facilitator::BoxFuture<'_, Result<String, ClientError>> {
Box::pin(async move {
let use_permit2 = self
.requirements
.extra
.as_ref()
.and_then(|e| e.asset_transfer_method)
== Some(AssetTransferMethod::Permit2);
let exact_payload = if use_permit2 {
if let Some(approver) = &self.approver {
let token = self.requirements.asset.0;
let owner = self.signer.address();
let required: U256 = self.requirements.amount.into();
let allowance = approver.check_permit2_allowance(token, owner).await?;
if allowance < required {
if self.auto_approve {
approver.approve_permit2(token, owner).await?;
} else {
return Err(ClientError::PreConditionFailed(format!(
"Permit2 allowance insufficient for token {token}: \
have {allowance}, need {required}. \
Call approve({PERMIT2_ADDRESS}, MAX) on the token contract, \
or enable auto_approve in the client builder."
)));
}
}
}
let params = Permit2SigningParams {
chain_id: self.chain_reference.inner(),
asset_address: self.requirements.asset.0,
pay_to: self.requirements.pay_to.into(),
amount: self.requirements.amount.into(),
max_timeout_seconds: self.requirements.max_timeout_seconds,
};
let permit2_payload = sign_permit2_authorization(&self.signer, ¶ms).await?;
ExactPayload::Permit2(permit2_payload)
} else {
let params = Eip3009SigningParams {
chain_id: self.chain_reference.inner(),
asset_address: self.requirements.asset.0,
pay_to: self.requirements.pay_to.into(),
amount: self.requirements.amount.into(),
max_timeout_seconds: self.requirements.max_timeout_seconds,
extra: self.requirements.extra.clone(),
};
let eip3009_payload = sign_erc3009_authorization(&self.signer, ¶ms).await?;
ExactPayload::Eip3009(eip3009_payload)
};
let payload = types::v2::PaymentPayload {
x402_version: v2::V2,
accepted: self.requirements.clone(),
resource: self.resource_info.clone(),
payload: exact_payload,
extensions: None,
};
let json = serde_json::to_vec(&payload)?;
let b64 = Base64Bytes::encode(&json);
Ok(b64.to_string())
})
}
}