use alloy_primitives::{TxHash, U256};
use alloy_provider::bindings::IMulticall3;
use alloy_provider::{MULTICALL3_ADDRESS, MulticallItem, Provider};
use alloy_rpc_types_eth::TransactionReceipt;
use alloy_sol_types::{SolCall, SolStruct, eip712_domain};
use x402_types::chain::ChainProviderOps;
use x402_types::proto::{PaymentVerificationError, v2};
use x402_types::scheme::X402SchemeFacilitatorError;
#[cfg(feature = "telemetry")]
use tracing::Instrument;
#[cfg(feature = "telemetry")]
use tracing::instrument;
use crate::chain::erc20::IERC20;
use crate::chain::permit2::{PERMIT2_ADDRESS, UPTO_PERMIT2_PROXY_ADDRESS};
use crate::chain::{Eip155ChainReference, Eip155MetaTransactionProvider, MetaTransaction};
use crate::v1_eip155_exact::{
Eip155ExactError, StructuredSignature, VALIDATOR_ADDRESS, Validator6492, assert_time,
is_contract_deployed, tx_hash_from_receipt,
};
use crate::v2_eip155_exact::permit2::{assert_onchain_allowance, assert_onchain_balance};
use crate::v2_eip155_upto::types;
use crate::v2_eip155_upto::types::{
ISignatureTransfer, Permit2PaymentPayload, Permit2PaymentRequirements,
PermitWitnessTransferFrom, UptoSettleResponse, X402UptoPermit2Proxy, x402BasePermit2Proxy,
};
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub async fn verify_permit2_payment<P: Eip155MetaTransactionProvider + ChainProviderOps>(
provider: &P,
payment_payload: &Permit2PaymentPayload,
payment_requirements: &Permit2PaymentRequirements,
) -> Result<v2::VerifyResponse, Eip155ExactError> {
let required_amount = assert_offchain_valid_verify(payment_payload, payment_requirements)?;
let authorization = &payment_payload.payload.permit_2_authorization;
let payer = authorization.from;
assert_onchain_upto_permit2(
provider.inner(),
provider.chain(),
payment_payload,
required_amount,
)
.await?;
Ok(v2::VerifyResponse::valid(payer.to_string()))
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub async fn settle_permit2_payment<P, E>(
provider: &P,
payment_payload: &Permit2PaymentPayload,
payment_requirements: &Permit2PaymentRequirements,
) -> Result<UptoSettleResponse, X402SchemeFacilitatorError>
where
P: Eip155MetaTransactionProvider<Error = E> + ChainProviderOps,
Eip155ExactError: From<E>,
{
let required_amount = assert_offchain_valid_settle(payment_payload, payment_requirements)?;
let authorization = &payment_payload.payload.permit_2_authorization;
let payer = authorization.from;
if required_amount.is_zero() {
let network = &payment_payload.accepted.network;
return Ok(UptoSettleResponse::success(
payer.to_string(),
String::new(), network.to_string(),
"0".to_string(),
));
}
let tx_hash = settle_upto_permit2(provider, payment_payload, required_amount).await?;
let payer = authorization.from;
let network = &payment_payload.accepted.network;
Ok(UptoSettleResponse::success(
payer.to_string(),
tx_hash.to_string(),
network.to_string(),
required_amount.to_string(),
))
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub fn assert_offchain_valid_verify(
payment_payload: &Permit2PaymentPayload,
payment_requirements: &Permit2PaymentRequirements,
) -> Result<U256, PaymentVerificationError> {
assert_offchain_valid(payment_payload, payment_requirements)?;
let authorization = &payment_payload.payload.permit_2_authorization;
let accepted_amount = payment_payload.accepted.amount;
if authorization.permitted.amount != accepted_amount {
return Err(PaymentVerificationError::InvalidPaymentAmount);
}
Ok(accepted_amount)
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub fn assert_offchain_valid_settle(
payment_payload: &Permit2PaymentPayload,
payment_requirements: &Permit2PaymentRequirements,
) -> Result<U256, PaymentVerificationError> {
assert_offchain_valid(payment_payload, payment_requirements)?;
let authorization = &payment_payload.payload.permit_2_authorization;
let permitted_amount = authorization.permitted.amount;
let accepted = &payment_payload.accepted;
if permitted_amount != accepted.amount {
return Err(PaymentVerificationError::InvalidPaymentAmount);
}
let amount_to_settle = payment_requirements.amount;
if permitted_amount < amount_to_settle {
return Err(PaymentVerificationError::InvalidPaymentAmount);
}
Ok(amount_to_settle)
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub fn assert_offchain_valid(
payment_payload: &Permit2PaymentPayload,
payment_requirements: &Permit2PaymentRequirements,
) -> Result<(), PaymentVerificationError> {
let payload = &payment_payload.payload;
let accepted = &payment_payload.accepted;
if accepted.scheme != types::UptoScheme {
return Err(PaymentVerificationError::UnsupportedScheme);
}
if accepted.network != payment_requirements.network {
return Err(PaymentVerificationError::ChainIdMismatch);
}
if accepted.asset != payment_requirements.asset {
return Err(PaymentVerificationError::AssetMismatch);
}
let authorization = &payload.permit_2_authorization;
if authorization.spender.0 != UPTO_PERMIT2_PROXY_ADDRESS {
return Err(PaymentVerificationError::RecipientMismatch);
}
let witness = &authorization.witness;
if witness.to != accepted.pay_to {
return Err(PaymentVerificationError::RecipientMismatch);
}
let valid_after = witness.valid_after;
let valid_before = authorization.deadline;
assert_time(valid_after, valid_before)?;
if authorization.permitted.token != accepted.asset {
return Err(PaymentVerificationError::AssetMismatch);
}
Ok(())
}
#[cfg_attr(feature = "telemetry", instrument(skip_all, err))]
pub async fn assert_onchain_upto_permit2<P: Provider>(
provider: &P,
chain_reference: &Eip155ChainReference,
payment_payload: &Permit2PaymentPayload,
required_amount: U256,
) -> Result<(), Eip155ExactError> {
let authorization = &payment_payload.payload.permit_2_authorization;
let payer = authorization.from.0;
let asset_address = payment_payload.accepted.asset.0;
let token_contract = IERC20::new(asset_address, provider);
let onchain_allowance_fut = assert_onchain_allowance(&token_contract, payer, required_amount);
let onchain_balance_fut = assert_onchain_balance(&token_contract, payer, required_amount);
tokio::try_join!(onchain_allowance_fut, onchain_balance_fut)?;
let domain = eip712_domain! {
name: "Permit2",
chain_id: chain_reference.inner(),
verifying_contract: PERMIT2_ADDRESS,
};
let permit_witness_transfer_from = PermitWitnessTransferFrom {
permitted: ISignatureTransfer::TokenPermissions {
token: authorization.permitted.token.into(),
amount: authorization.permitted.amount,
},
spender: UPTO_PERMIT2_PROXY_ADDRESS,
nonce: authorization.nonce,
deadline: U256::from(authorization.deadline.as_secs()),
witness: x402BasePermit2Proxy::Witness {
to: authorization.witness.to.into(),
validAfter: U256::from(authorization.witness.valid_after.as_secs()),
extra: authorization.witness.extra.clone(),
},
};
let eip712_hash = permit_witness_transfer_from.eip712_signing_hash(&domain);
let structured_signature = StructuredSignature::try_from_bytes(
payment_payload.payload.signature.clone(),
payer,
&eip712_hash,
)?;
let upto_permit2_proxy = X402UptoPermit2Proxy::new(UPTO_PERMIT2_PROXY_ADDRESS, provider);
match structured_signature {
StructuredSignature::EIP6492 {
factory: _,
factory_calldata: _,
inner,
original,
} => {
let validator6492 = Validator6492::new(VALIDATOR_ADDRESS, provider);
let is_valid_signature_call =
validator6492.isValidSigWithSideEffects(payer, eip712_hash, original);
let permit_transfer_from = ISignatureTransfer::PermitTransferFrom {
permitted: permit_witness_transfer_from.permitted,
nonce: permit_witness_transfer_from.nonce,
deadline: permit_witness_transfer_from.deadline,
};
let witness = permit_witness_transfer_from.witness;
let settle_call = upto_permit2_proxy.settle(
permit_transfer_from,
authorization.permitted.amount,
payer,
witness,
inner,
);
let aggregate3 = provider
.multicall()
.add(is_valid_signature_call)
.add(settle_call);
let aggregate3_call = aggregate3.aggregate3();
#[cfg(feature = "telemetry")]
let (is_valid_signature_result, transfer_result) = aggregate3_call
.instrument(tracing::info_span!("multi_call_settle_upto_permit2",
from = %payer,
to = %authorization.witness.to,
value = %authorization.permitted.amount,
valid_after = %authorization.witness.valid_after,
valid_before = %authorization.deadline,
nonce = %authorization.nonce,
token_contract = %authorization.permitted.token,
otel.kind = "client",
))
.await?;
#[cfg(not(feature = "telemetry"))]
let (is_valid_signature_result, transfer_result) = aggregate3_call.await?;
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()))?;
Ok(())
}
StructuredSignature::EOA(signature) => {
let permit_transfer_from = ISignatureTransfer::PermitTransferFrom {
permitted: permit_witness_transfer_from.permitted,
nonce: permit_witness_transfer_from.nonce,
deadline: permit_witness_transfer_from.deadline,
};
let witness = permit_witness_transfer_from.witness;
let settle_call = upto_permit2_proxy.settle(
permit_transfer_from,
authorization.permitted.amount,
payer,
witness,
signature.as_bytes().into(),
);
let settle_call_fut = settle_call.call().into_future();
#[cfg(feature = "telemetry")]
settle_call_fut
.instrument(tracing::info_span!("call_settle_upto_permit2",
from = %payer,
to = %authorization.witness.to,
value = %authorization.permitted.amount,
valid_after = %authorization.witness.valid_after,
valid_before = %authorization.deadline,
nonce = %authorization.nonce,
token_contract = %authorization.permitted.token,
otel.kind = "client",
))
.await?;
#[cfg(not(feature = "telemetry"))]
settle_call_fut.await?;
Ok(())
}
StructuredSignature::EIP1271(signature) => {
let permit_transfer_from = ISignatureTransfer::PermitTransferFrom {
permitted: permit_witness_transfer_from.permitted,
nonce: permit_witness_transfer_from.nonce,
deadline: permit_witness_transfer_from.deadline,
};
let witness = permit_witness_transfer_from.witness;
let settle_call = upto_permit2_proxy.settle(
permit_transfer_from,
authorization.permitted.amount,
payer,
witness,
signature,
);
let settle_call_fut = settle_call.call().into_future();
#[cfg(feature = "telemetry")]
settle_call_fut
.instrument(tracing::info_span!("call_settle_upto_permit2",
from = %payer,
to = %authorization.witness.to,
value = %authorization.permitted.amount,
valid_after = %authorization.witness.valid_after,
valid_before = %authorization.deadline,
nonce = %authorization.nonce,
token_contract = %authorization.permitted.token,
otel.kind = "client",
))
.await?;
#[cfg(not(feature = "telemetry"))]
settle_call_fut.await?;
Ok(())
}
}
}
pub async fn settle_upto_permit2<P, E>(
provider: &P,
payment_payload: &Permit2PaymentPayload,
actual_amount: U256,
) -> Result<TxHash, Eip155ExactError>
where
P: Eip155MetaTransactionProvider<Error = E> + ChainProviderOps,
Eip155ExactError: From<E>,
{
let authorization = &payment_payload.payload.permit_2_authorization;
let payer = authorization.from.0;
let domain = eip712_domain! {
name: "Permit2",
chain_id: provider.chain().inner(),
verifying_contract: PERMIT2_ADDRESS,
};
let permit_witness_transfer_from = PermitWitnessTransferFrom {
permitted: ISignatureTransfer::TokenPermissions {
token: authorization.permitted.token.into(),
amount: authorization.permitted.amount,
},
spender: UPTO_PERMIT2_PROXY_ADDRESS,
nonce: authorization.nonce,
deadline: U256::from(authorization.deadline.as_secs()),
witness: x402BasePermit2Proxy::Witness {
to: authorization.witness.to.into(),
validAfter: U256::from(authorization.witness.valid_after.as_secs()),
extra: authorization.witness.extra.clone(),
},
};
let eip712_hash = permit_witness_transfer_from.eip712_signing_hash(&domain);
let structured_signature = StructuredSignature::try_from_bytes(
payment_payload.payload.signature.clone(),
payer,
&eip712_hash,
)?;
let upto_permit2_proxy =
X402UptoPermit2Proxy::new(UPTO_PERMIT2_PROXY_ADDRESS, provider.inner());
let permit_transfer_from = ISignatureTransfer::PermitTransferFrom {
permitted: permit_witness_transfer_from.permitted,
nonce: permit_witness_transfer_from.nonce,
deadline: permit_witness_transfer_from.deadline,
};
let witness = permit_witness_transfer_from.witness;
let receipt: TransactionReceipt = match structured_signature {
StructuredSignature::EIP6492 {
factory,
factory_calldata,
inner,
original: _,
} => {
let is_contract_deployed = is_contract_deployed(provider.inner(), &payer).await?;
let settle_call = upto_permit2_proxy.settle(
permit_transfer_from,
actual_amount,
payer,
witness,
inner.clone(),
);
if is_contract_deployed {
let tx_fut = Eip155MetaTransactionProvider::send_transaction(
provider,
MetaTransaction {
to: settle_call.target(),
calldata: settle_call.calldata().clone(),
confirmations: 1,
},
);
#[cfg(feature = "telemetry")]
let receipt = tx_fut
.instrument(
tracing::info_span!("call_upto_permit2_proxy_settle.EIP6492.deployed",
from = %payer,
to = %authorization.witness.to,
max_value = %authorization.permitted.amount,
actual_value = %actual_amount,
valid_after = %authorization.witness.valid_after,
valid_before = %authorization.deadline,
nonce = %authorization.nonce,
token_contract = %authorization.permitted.token,
signature = %inner,
sig_kind="EIP6492.deployed",
otel.kind = "client",
),
)
.await?;
#[cfg(not(feature = "telemetry"))]
let receipt = tx_fut.await?;
receipt
} else {
let deployment_call = IMulticall3::Call3 {
allowFailure: true,
target: factory,
callData: factory_calldata,
};
let transfer_with_authorization_call = IMulticall3::Call3 {
allowFailure: false,
target: settle_call.target(),
callData: settle_call.calldata().clone(),
};
let aggregate_call = IMulticall3::aggregate3Call {
calls: vec![deployment_call, transfer_with_authorization_call],
};
let tx_fut = Eip155MetaTransactionProvider::send_transaction(
provider,
MetaTransaction {
to: MULTICALL3_ADDRESS,
calldata: aggregate_call.abi_encode().into(),
confirmations: 1,
},
);
#[cfg(feature = "telemetry")]
let receipt = tx_fut
.instrument(
tracing::info_span!("call_upto_permit2_proxy_settle.EIP6492.counterfactual",
from = %payer,
to = %authorization.witness.to,
max_value = %authorization.permitted.amount,
actual_value = %actual_amount,
valid_after = %authorization.witness.valid_after,
valid_before = %authorization.deadline,
nonce = %authorization.nonce,
token_contract = %authorization.permitted.token,
signature = %inner,
sig_kind="EIP6492.counterfactual",
otel.kind = "client",
),
)
.await?;
#[cfg(not(feature = "telemetry"))]
let receipt = tx_fut.await?;
receipt
}
}
StructuredSignature::EOA(signature) => {
let settle_call = upto_permit2_proxy.settle(
permit_transfer_from,
actual_amount,
payer,
witness,
signature.as_bytes().into(),
);
let tx_fut = Eip155MetaTransactionProvider::send_transaction(
provider,
MetaTransaction {
to: settle_call.target(),
calldata: settle_call.calldata().clone(),
confirmations: 1,
},
);
#[cfg(feature = "telemetry")]
let receipt = tx_fut
.instrument(tracing::info_span!("call_upto_permit2_proxy_settle.EOA",
from = %payer,
to = %authorization.witness.to,
max_value = %authorization.permitted.amount,
actual_value = %actual_amount,
valid_after = %authorization.witness.valid_after,
valid_before = %authorization.deadline,
nonce = %authorization.nonce,
token_contract = %authorization.permitted.token,
signature = %signature,
sig_kind="EOA",
otel.kind = "client",
))
.await?;
#[cfg(not(feature = "telemetry"))]
let receipt = tx_fut.await?;
receipt
}
StructuredSignature::EIP1271(signature) => {
let settle_call = upto_permit2_proxy.settle(
permit_transfer_from,
actual_amount,
payer,
witness,
signature.clone(),
);
let tx_fut = Eip155MetaTransactionProvider::send_transaction(
provider,
MetaTransaction {
to: settle_call.target(),
calldata: settle_call.calldata().clone(),
confirmations: 1,
},
);
#[cfg(feature = "telemetry")]
let receipt = tx_fut
.instrument(
tracing::info_span!("call_upto_permit2_proxy_settle.EIP1271",
from = %payer,
to = %authorization.witness.to,
max_value = %authorization.permitted.amount,
actual_value = %actual_amount,
valid_after = %authorization.witness.valid_after,
valid_before = %authorization.deadline,
nonce = %authorization.nonce,
token_contract = %authorization.permitted.token,
signature = %signature,
sig_kind="EIP1271",
otel.kind = "client",
),
)
.await?;
#[cfg(not(feature = "telemetry"))]
let receipt = tx_fut.await?;
receipt
}
};
tx_hash_from_receipt(&receipt)
}