use alloy_primitives::{Address, B256, U256};
use alloy_provider::Provider;
use alloy_sol_types::{Eip712Domain, SolStruct, eip712_domain};
use r402::chain::ChainId;
use r402::proto::{PaymentVerificationError, UnixTimestamp};
#[cfg(feature = "telemetry")]
use tracing::instrument;
use super::contract::{IEIP3009, IERC20, Validator6492};
use super::error::Eip155ExactError;
use super::settle::{TransferWithAuthorization0Call, TransferWithAuthorization1Call};
use super::signature::{SignedMessage, StructuredSignature};
use super::{Eip3009Payment, Permit2Payment, VALIDATOR_ADDRESS};
use crate::chain::Eip155ChainReference;
use crate::exact::types::TokenPermissions as SolTokenPermissions;
use crate::exact::types::Witness as SolWitness;
use crate::exact::{
Eip3009Payload, PERMIT2_ADDRESS, PaymentRequirementsExtra, PermitWitnessTransferFrom,
X402_EXACT_PERMIT2_PROXY, types,
};
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub(super) async fn assert_valid_payment<P: Provider>(
provider: P,
chain: &Eip155ChainReference,
eip3009: &Eip3009Payload,
payload: &types::v2::PaymentPayload,
requirements: &types::v2::PaymentRequirements,
clock_skew_tolerance: u64,
) -> Result<(IEIP3009::IEIP3009Instance<P>, Eip3009Payment, Eip712Domain), Eip155ExactError> {
let accepted = &payload.accepted;
assert_requirements_match(accepted, requirements)?;
let chain_id: ChainId = chain.into();
let payload_chain_id = &accepted.network;
if payload_chain_id != &chain_id {
return Err(PaymentVerificationError::ChainIdMismatch.into());
}
let authorization = &eip3009.authorization;
if authorization.to != accepted.pay_to {
return Err(PaymentVerificationError::RecipientMismatch.into());
}
let valid_after = authorization.valid_after;
let valid_before = authorization.valid_before;
assert_time(valid_after, valid_before, clock_skew_tolerance)?;
let asset_address = accepted.asset;
let contract = IEIP3009::new(asset_address.into(), provider);
let amount_required = accepted.amount;
let asset_addr: Address = asset_address.into();
let (domain, (), ()) = tokio::try_join!(
assert_domain(chain, &contract, &asset_addr, accepted.extra.as_ref()),
assert_nonce_unused(&contract, &authorization.from, &authorization.nonce),
assert_enough_balance(&contract, &authorization.from, amount_required.into()),
)?;
assert_enough_value(&authorization.value.into(), &amount_required.into())?;
let payment = Eip3009Payment {
from: authorization.from,
to: authorization.to,
value: authorization.value.into(),
valid_after: authorization.valid_after,
valid_before: authorization.valid_before,
nonce: authorization.nonce,
signature: eip3009.signature.clone(),
};
Ok((contract, payment, domain))
}
pub fn assert_requirements_match(
accepted: &types::v2::PaymentRequirements,
requirements: &types::v2::PaymentRequirements,
) -> Result<(), PaymentVerificationError> {
if accepted.scheme == requirements.scheme
&& accepted.network == requirements.network
&& accepted.amount == requirements.amount
&& accepted.asset == requirements.asset
&& accepted.pay_to == requirements.pay_to
{
Ok(())
} else {
Err(PaymentVerificationError::AcceptedRequirementsMismatch)
}
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err, fields(
from = %authorizer,
nonce = %nonce
)))]
pub async fn assert_nonce_unused<P: Provider>(
contract: &IEIP3009::IEIP3009Instance<P>,
authorizer: &Address,
nonce: &B256,
) -> Result<(), Eip155ExactError> {
let call = contract.authorizationState(*authorizer, *nonce);
let used_fut = call.call().into_future();
let used = traced!(
used_fut,
tracing::info_span!("check_authorization_state", otel.kind = "client")
)?;
if used {
return Err(PaymentVerificationError::NonceAlreadyUsed.into());
}
Ok(())
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub fn assert_time(
valid_after: UnixTimestamp,
valid_before: UnixTimestamp,
clock_skew_tolerance: u64,
) -> Result<(), PaymentVerificationError> {
let now = UnixTimestamp::now();
if valid_before < now + clock_skew_tolerance {
return Err(PaymentVerificationError::Expired);
}
if valid_after > now + clock_skew_tolerance {
return Err(PaymentVerificationError::Early);
}
Ok(())
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err, fields(
network = %chain.as_chain_id(),
asset = %asset_address
)))]
pub async fn assert_domain<P: Provider>(
chain: &Eip155ChainReference,
token_contract: &IEIP3009::IEIP3009Instance<P>,
asset_address: &Address,
extra: Option<&PaymentRequirementsExtra>,
) -> Result<Eip712Domain, Eip155ExactError> {
let name = extra.map(|extra| extra.name.clone());
let name = if let Some(name) = name {
name
} else {
let name_b = token_contract.name();
let name_fut = name_b.call().into_future();
traced!(
name_fut,
tracing::info_span!("fetch_eip712_name", otel.kind = "client")
)?
};
let version = extra.map(|extra| extra.version.clone());
let version = if let Some(version) = version {
version
} else {
let version_b = token_contract.version();
let version_fut = version_b.call().into_future();
traced!(
version_fut,
tracing::info_span!("fetch_eip712_version", otel.kind = "client")
)?
};
let domain = eip712_domain! {
name: name,
version: version,
chain_id: chain.inner(),
verifying_contract: *asset_address,
};
Ok(domain)
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err, fields(
sender = %sender,
max_required = %max_amount_required,
token_contract = %ieip3009_token_contract.address()
)))]
pub async fn assert_enough_balance<P: Provider>(
ieip3009_token_contract: &IEIP3009::IEIP3009Instance<P>,
sender: &Address,
max_amount_required: U256,
) -> Result<(), Eip155ExactError> {
let balance_of = ieip3009_token_contract.balanceOf(*sender);
let balance_fut = balance_of.call().into_future();
let balance = traced!(
balance_fut,
tracing::info_span!(
"fetch_token_balance",
token_contract = %ieip3009_token_contract.address(),
sender = %sender,
otel.kind = "client"
)
)?;
if balance < max_amount_required {
Err(PaymentVerificationError::InsufficientFunds.into())
} else {
Ok(())
}
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err, fields(
sent = %sent,
max_amount_required = %max_amount_required
)))]
pub fn assert_enough_value(
sent: &U256,
max_amount_required: &U256,
) -> Result<(), PaymentVerificationError> {
if sent < max_amount_required {
Err(PaymentVerificationError::InvalidPaymentAmount)
} else {
Ok(())
}
}
pub async fn verify_payment<P: Provider>(
provider: &P,
contract: &IEIP3009::IEIP3009Instance<&P>,
payment: &Eip3009Payment,
eip712_domain: &Eip712Domain,
) -> Result<Address, Eip155ExactError> {
let signed_message = SignedMessage::extract(payment, eip712_domain)?;
let payer = signed_message.address;
let hash = signed_message.hash;
match signed_message.signature {
StructuredSignature::EIP6492 {
factory: _,
factory_calldata: _,
inner,
original,
} => {
let validator6492 = Validator6492::new(VALIDATOR_ADDRESS, &provider);
let is_valid_signature_call =
validator6492.isValidSigWithSideEffects(payer, hash, original);
let transfer_call = TransferWithAuthorization0Call::new(contract, payment, inner);
let transfer_call = transfer_call.0;
let aggregate3 = provider
.multicall()
.add(is_valid_signature_call)
.add(transfer_call.tx);
let aggregate3_call = aggregate3.aggregate3();
let (is_valid_signature_result, transfer_result) = traced!(
aggregate3_call,
transfer_span!("call_transferWithAuthorization_0", transfer_call)
)?;
let is_valid_signature_result = is_valid_signature_result
.map_err(|e| PaymentVerificationError::InvalidSignature(e.to_string()))?;
if !is_valid_signature_result {
return Err(PaymentVerificationError::InvalidSignature(
"Chain reported signature to be invalid".to_string(),
)
.into());
}
transfer_result
.map_err(|e| PaymentVerificationError::TransactionSimulation(e.to_string()))?;
}
StructuredSignature::EIP1271(signature) => {
let transfer_call = TransferWithAuthorization0Call::new(contract, payment, signature);
let transfer_call = transfer_call.0;
let transfer_call_fut = transfer_call.tx.call().into_future();
traced!(
transfer_call_fut,
transfer_span!("call_transferWithAuthorization_0", transfer_call)
)?;
}
StructuredSignature::Eoa(signature) => {
let transfer_call = TransferWithAuthorization1Call::new(contract, payment, signature);
let transfer_call = transfer_call.0;
let transfer_call_fut = transfer_call.tx.call().into_future();
traced!(
transfer_call_fut,
transfer_span!("call_transferWithAuthorization_1", transfer_call)
)?;
}
}
Ok(payer)
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub(super) async fn assert_valid_permit2_payment<P: Provider>(
provider: P,
chain: &Eip155ChainReference,
permit2: &crate::exact::Permit2Payload,
payload: &types::v2::PaymentPayload,
requirements: &types::v2::PaymentRequirements,
clock_skew_tolerance: u64,
) -> Result<(IERC20::IERC20Instance<P>, Permit2Payment, Eip712Domain), Eip155ExactError> {
let accepted = &payload.accepted;
assert_requirements_match(accepted, requirements)?;
let chain_id: ChainId = chain.into();
if accepted.network != chain_id {
return Err(PaymentVerificationError::ChainIdMismatch.into());
}
let auth = &permit2.permit2_authorization;
if auth.spender != X402_EXACT_PERMIT2_PROXY {
return Err(PaymentVerificationError::InvalidSignature(
"invalid Permit2 spender: must be x402ExactPermit2Proxy".into(),
)
.into());
}
if auth.witness.to != Address::from(accepted.pay_to) {
return Err(PaymentVerificationError::RecipientMismatch.into());
}
let now = UnixTimestamp::now();
let deadline_u64: u64 = auth.deadline.0.try_into().unwrap_or(u64::MAX);
let deadline_threshold = now.as_secs() + clock_skew_tolerance;
if deadline_u64 < deadline_threshold {
return Err(PaymentVerificationError::Expired.into());
}
let valid_after_u64: u64 = auth.witness.valid_after.0.try_into().unwrap_or(u64::MAX);
if valid_after_u64 > now.as_secs() + clock_skew_tolerance {
return Err(PaymentVerificationError::Early.into());
}
let auth_amount: U256 = auth.permitted.amount.into();
let required_amount: U256 = accepted.amount.into();
assert_enough_value(&auth_amount, &required_amount)?;
if auth.permitted.token != Address::from(accepted.asset) {
return Err(PaymentVerificationError::AssetMismatch.into());
}
let token_address: Address = accepted.asset.into();
let erc20 = IERC20::new(token_address, provider);
let allowance_call = erc20.allowance(auth.from, PERMIT2_ADDRESS);
let balance_call = erc20.balanceOf(auth.from);
let (allowance_result, balance_result) =
tokio::join!(allowance_call.call(), balance_call.call(),);
if let Ok(allowance) = allowance_result
&& allowance < required_amount
{
return Err(PaymentVerificationError::Permit2AllowanceInsufficient.into());
}
if let Ok(balance) = balance_result
&& balance < required_amount
{
return Err(PaymentVerificationError::InsufficientFunds.into());
}
let domain = eip712_domain! {
name: "Permit2",
chain_id: chain.inner(),
verifying_contract: PERMIT2_ADDRESS,
};
let payment = Permit2Payment {
from: auth.from,
to: auth.witness.to,
token: auth.permitted.token,
amount: auth_amount,
spender: auth.spender,
nonce: auth.nonce.into(),
deadline: auth.deadline.into(),
valid_after: auth.witness.valid_after.into(),
extra: auth.witness.extra.clone(),
signature: permit2.signature.clone(),
};
Ok((erc20, payment, domain))
}
pub async fn verify_permit2_payment<P: Provider>(
provider: &P,
payment: &Permit2Payment,
eip712_domain: &Eip712Domain,
) -> Result<Address, Eip155ExactError> {
let permit_witness = PermitWitnessTransferFrom {
permitted: SolTokenPermissions {
token: payment.token,
amount: payment.amount,
},
spender: payment.spender,
nonce: payment.nonce,
deadline: payment.deadline,
witness: SolWitness {
to: payment.to,
validAfter: payment.valid_after,
extra: payment.extra.clone(),
},
};
let eip712_hash = permit_witness.eip712_signing_hash(eip712_domain);
let payer = payment.from;
let signature_bytes = payment.signature.clone();
let validator6492 = Validator6492::new(VALIDATOR_ADDRESS, provider);
let is_valid_call =
validator6492.isValidSigWithSideEffects(payer, eip712_hash, signature_bytes);
let is_valid_fut = is_valid_call.call().into_future();
let is_valid = traced!(
is_valid_fut,
tracing::info_span!("verify_permit2_signature",
from = %payer,
token = %payment.token,
amount = %payment.amount,
otel.kind = "client",
)
)
.map_err(|e| PaymentVerificationError::InvalidSignature(e.to_string()))?;
if !is_valid {
return Err(
PaymentVerificationError::InvalidSignature("invalid Permit2 signature".into()).into(),
);
}
Ok(payer)
}