use alloy_primitives::{Address, U256};
use alloy_provider::fillers::{FillProvider, TxFiller};
use alloy_provider::{Network, Provider, ProviderBuilder, RootProvider};
use alloy_sol_types::{SolStruct, eip712_domain};
use async_trait::async_trait;
use rand::{RngExt, rng};
use serde::{Deserialize, Serialize};
use url::Url;
use x402_types::proto::v2::{ExtensionsJson, ResourceInfo};
use x402_types::proto::{OriginalJson, PaymentRequired, v2};
use x402_types::scheme::X402SchemeId;
use x402_types::scheme::client::{
PaymentCandidate, PaymentCandidateSigner, X402Error, X402SchemeClient,
};
use x402_types::timestamp::UnixTimestamp;
use x402_types::util::Base64Bytes;
use crate::chain::erc20::IERC20;
use crate::chain::permit2::{
PERMIT2_ADDRESS, Permit2Authorization, Permit2AuthorizationPermitted,
UPTO_PERMIT2_PROXY_ADDRESS, UptoPermit2Payload, UptoPermit2Witness,
};
use crate::chain::{ChecksummedAddress, EOASignature, Eip155ChainReference};
use crate::eip2612_gas_sponsoring::{
Eip2612GasSponsoring, Eip2612GasSponsoringInfo, Eip2612GasSponsoringServer, Permit,
};
use crate::v1_eip155_exact::client::SignerLike;
use crate::v2_eip155_upto::types::{ISignatureTransfer, PermitWitnessTransferFrom};
use crate::v2_eip155_upto::{IERC20Permit, UptoSupportedExtra, V2Eip155Upto};
use crate::v2_eip155_upto::{types, x402UptoPermit2Proxy};
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct Permit2UptoSigningParams {
pub chain_id: u64,
pub asset_address: Address,
pub pay_to: Address,
pub max_amount: U256,
pub max_timeout_seconds: u64,
pub facilitator: Address,
}
#[allow(dead_code)] pub async fn sign_permit2_upto_authorization<S: SignerLike + Sync>(
signer: &S,
params: &Permit2UptoSigningParams,
) -> Result<UptoPermit2Payload, X402Error> {
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 valid_after = UnixTimestamp::from_secs(valid_after_secs);
let deadline = now + params.max_timeout_seconds;
let nonce: [u8; 32] = rng().random();
let nonce = U256::from_be_bytes(nonce);
let permit_witness_transfer_from = PermitWitnessTransferFrom {
permitted: ISignatureTransfer::TokenPermissions {
token: params.asset_address,
amount: params.max_amount,
},
spender: UPTO_PERMIT2_PROXY_ADDRESS,
nonce,
deadline: U256::from(deadline.as_secs()),
witness: x402UptoPermit2Proxy::Witness {
to: params.pay_to,
facilitator: params.facilitator,
validAfter: U256::from(valid_after.as_secs()),
},
};
let eip712_hash = permit_witness_transfer_from.eip712_signing_hash(&domain);
let signature = signer
.sign_hash(&eip712_hash)
.await
.map_err(|e| X402Error::SigningError(format!("{e:?}")))?;
let authorization = Permit2Authorization {
deadline,
from: signer.address().into(),
nonce,
permitted: Permit2AuthorizationPermitted {
amount: params.max_amount,
token: params.asset_address.into(),
},
spender: UPTO_PERMIT2_PROXY_ADDRESS.into(),
witness: UptoPermit2Witness {
to: params.pay_to.into(),
facilitator: params.facilitator.into(),
valid_after,
},
};
Ok(UptoPermit2Payload {
permit_2_authorization: authorization,
signature: signature.as_bytes().into(),
})
}
#[derive(Debug)]
#[allow(dead_code)] pub struct V2Eip155UptoClient<S, P> {
signer: S,
provider: P,
}
#[allow(dead_code)] impl<S> V2Eip155UptoClient<S, ()> {
pub fn new(signer: S) -> Self {
Self {
signer,
provider: (),
}
}
}
impl<S, P> V2Eip155UptoClient<S, P> {
pub fn with_provider<P2>(self, provider: P2) -> V2Eip155UptoClient<S, P2> {
V2Eip155UptoClient {
signer: self.signer,
provider,
}
}
}
impl<S, P> X402SchemeId for V2Eip155UptoClient<S, P> {
fn namespace(&self) -> &str {
V2Eip155Upto.namespace()
}
fn scheme(&self) -> &str {
V2Eip155Upto.scheme()
}
}
impl<S, P> X402SchemeClient for V2Eip155UptoClient<S, P>
where
S: SignerLike + Clone + Send + Sync + 'static,
P: Clone + EIP2612ProviderLike + Send + Sync + 'static,
{
fn accept(&self, payment_required: &PaymentRequired) -> Vec<PaymentCandidate> {
let payment_required = match payment_required {
PaymentRequired::V2(payment_required) => payment_required,
PaymentRequired::V1(_) => {
return vec![];
}
};
payment_required
.accepts
.iter()
.filter_map(|original_requirements_json| {
let requirements =
types::PaymentRequirements::try_from(original_requirements_json).ok()?;
if requirements.scheme != types::UptoScheme {
return None;
}
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,
scheme: self.scheme().to_string(),
x402_version: self.x402_version(),
pay_to: requirements.pay_to.to_string(),
signer: Box::new(PayloadSigner {
resource_info: payment_required.resource.clone(),
signer: self.signer.clone(),
provider: self.provider.clone(),
chain_reference,
requirements,
extensions: payment_required.extensions.clone(),
requirements_json: original_requirements_json.clone(),
}),
};
Some(candidate)
})
.collect::<Vec<_>>()
}
}
#[allow(dead_code)] struct PayloadSigner<S, P> {
signer: S,
provider: P,
resource_info: Option<ResourceInfo>,
extensions: ExtensionsJson,
chain_reference: Eip155ChainReference,
requirements: types::PaymentRequirements,
requirements_json: OriginalJson,
}
impl<S, P> PayloadSigner<S, P>
where
S: SignerLike,
P: EIP2612ProviderLike,
{
pub async fn try_eip2612_gas_sponsoring(
&self,
deadline: UnixTimestamp,
eip2612_gas_sponsoring: Eip2612GasSponsoringServer,
) -> Result<Option<Eip2612GasSponsoring>, X402Error> {
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TokenDomain {
pub name: String,
pub version: String,
}
let token_contract = self.requirements.asset;
let value = self.requirements.amount;
let owner = self.signer.address();
let allowance = self
.provider
.read_erc20_allowance(token_contract.into(), owner, PERMIT2_ADDRESS)
.await
.inspect_err(|e| {
tracing::warn!(
error=%e,
"failed to read erc20 allowance for eip2612GasSponsoring, assuming zero"
)
})
.ok()
.flatten()
.unwrap_or(U256::ZERO);
if allowance >= value {
return Ok(None);
}
let token_domain = self
.requirements
.extra
.as_ref()
.and_then(|v| TokenDomain::deserialize(v).ok())
.ok_or(X402Error::SigningError(
"extra should contain token name and version for eip2612GasSponsoring".to_string(),
))?;
let nonce = self
.provider
.read_eip2612_nonce(self.requirements.asset.into(), owner)
.await?;
let nonce = match nonce {
Some(nonce) => nonce,
None => {
return Ok(None);
}
};
let domain = eip712_domain! {
name: token_domain.name,
version: token_domain.version,
chain_id: self.chain_reference.inner(),
verifying_contract: token_contract.into(),
};
let permit = Permit {
owner,
spender: PERMIT2_ADDRESS,
value,
nonce,
deadline: U256::from(deadline.as_secs()),
};
let signature = self
.signer
.sign_hash(&permit.eip712_signing_hash(&domain))
.await
.map_err(|e| X402Error::SigningError(format!("{e:?}")))?;
let info = Eip2612GasSponsoringInfo {
from: ChecksummedAddress::from(owner),
asset: self.requirements.asset,
spender: ChecksummedAddress::from(PERMIT2_ADDRESS),
amount: value,
nonce,
deadline,
signature: EOASignature::from(signature),
version: eip2612_gas_sponsoring.info.version,
};
let eip2612_gas_sponsoring = Eip2612GasSponsoring { info };
Ok(Some(eip2612_gas_sponsoring))
}
}
#[async_trait]
impl<S, P> PaymentCandidateSigner for PayloadSigner<S, P>
where
S: Sync + SignerLike,
P: EIP2612ProviderLike + Send + Sync + 'static,
{
async fn sign_payment(&self) -> Result<String, X402Error> {
let facilitator_address = self
.requirements
.extra
.as_ref()
.and_then(|v| UptoSupportedExtra::deserialize(v).ok())
.map(|extra| extra.facilitator_address)
.ok_or(X402Error::SigningError(
"upto scheme requires facilitatorAddress in payment requirements extra".to_string(),
))?;
let params = Permit2UptoSigningParams {
chain_id: self.chain_reference.inner(),
asset_address: self.requirements.asset.0,
pay_to: self.requirements.pay_to.into(),
max_amount: self.requirements.amount,
max_timeout_seconds: self.requirements.max_timeout_seconds,
facilitator: facilitator_address,
};
let permit2_payload = sign_permit2_upto_authorization(&self.signer, ¶ms).await?;
let mut client_extensions = ExtensionsJson::new();
if let Some(eip2612_gas_sponsoring) = self.extensions.get::<Eip2612GasSponsoringServer>() {
let eip2612_gas_sponsoring_result = self
.try_eip2612_gas_sponsoring(
permit2_payload.permit_2_authorization.deadline,
eip2612_gas_sponsoring,
)
.await
.inspect_err(
|e| tracing::error!(error=%e, "failed to sign eip2612GasSponsoring extension"),
)
.ok()
.flatten();
if let Some(result) = eip2612_gas_sponsoring_result {
client_extensions.insert(result)?;
}
}
let payload = v2::PaymentPayload {
x402_version: v2::X402Version2,
accepted: self.requirements_json.clone(),
resource: self.resource_info.clone(),
payload: permit2_payload,
extensions: client_extensions,
};
let json = serde_json::to_vec(&payload)?;
let b64 = Base64Bytes::encode(&json);
Ok(b64.to_string())
}
}
pub trait EIP2612ProviderLike {
fn read_eip2612_nonce(
&self,
asset: Address,
owner: Address,
) -> impl Future<Output = Result<Option<U256>, X402Error>> + Send;
fn read_erc20_allowance(
&self,
asset: Address,
owner: Address,
spender: Address,
) -> impl Future<Output = Result<Option<U256>, X402Error>> + Send;
}
impl EIP2612ProviderLike for Url {
async fn read_eip2612_nonce(
&self,
asset: Address,
owner: Address,
) -> Result<Option<U256>, X402Error> {
let provider = ProviderBuilder::new().connect_http(self.clone());
provider.read_eip2612_nonce(asset, owner).await
}
async fn read_erc20_allowance(
&self,
asset: Address,
owner: Address,
spender: Address,
) -> Result<Option<U256>, X402Error> {
let provider = ProviderBuilder::new().connect_http(self.clone());
provider.read_erc20_allowance(asset, owner, spender).await
}
}
impl EIP2612ProviderLike for () {
async fn read_eip2612_nonce(
&self,
_asset: Address,
_owner: Address,
) -> Result<Option<U256>, X402Error> {
Ok(None)
}
async fn read_erc20_allowance(
&self,
_asset: Address,
_owner: Address,
_spender: Address,
) -> Result<Option<U256>, X402Error> {
Ok(None)
}
}
impl<N> EIP2612ProviderLike for RootProvider<N>
where
N: Network,
{
async fn read_eip2612_nonce(
&self,
asset: Address,
owner: Address,
) -> Result<Option<U256>, X402Error> {
let token = IERC20Permit::new(asset, self);
let nonce =
token.nonces(owner).call().await.map_err(|e| {
X402Error::SigningError(format!("failed to get permit nonce {e:?}"))
})?;
Ok(Some(nonce))
}
async fn read_erc20_allowance(
&self,
asset: Address,
owner: Address,
spender: Address,
) -> Result<Option<U256>, X402Error> {
let token = IERC20::new(asset, self);
let allowance =
token.allowance(owner, spender).call().await.map_err(|e| {
X402Error::SigningError(format!("failed to get erc20 allowance {e:?}"))
})?;
Ok(Some(allowance))
}
}
impl<F, P, N> EIP2612ProviderLike for FillProvider<F, P, N>
where
F: TxFiller<N>,
P: Provider<N>,
N: Network,
{
async fn read_eip2612_nonce(
&self,
asset: Address,
owner: Address,
) -> Result<Option<U256>, X402Error> {
let root_provider = self.root();
root_provider.read_eip2612_nonce(asset, owner).await
}
async fn read_erc20_allowance(
&self,
asset: Address,
owner: Address,
spender: Address,
) -> Result<Option<U256>, X402Error> {
let root_provider = self.root();
root_provider
.read_erc20_allowance(asset, owner, spender)
.await
}
}