use alloy_primitives::{Address, U256};
use alloy_sol_types::{SolStruct, eip712_domain};
use async_trait::async_trait;
use rand::{RngExt, rng};
use x402_types::proto::v2::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::Eip155ChainReference;
use crate::chain::permit2::{
PERMIT2_ADDRESS, Permit2Authorization, Permit2AuthorizationPermitted,
UPTO_PERMIT2_PROXY_ADDRESS, UptoPermit2Payload, UptoPermit2Witness,
};
use crate::v1_eip155_exact::client::SignerLike;
use crate::v2_eip155_upto::types::{ISignatureTransfer, PermitWitnessTransferFrom};
use crate::v2_eip155_upto::{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> {
signer: S,
}
#[allow(dead_code)] impl<S> V2Eip155UptoClient<S> {
pub fn new(signer: S) -> Self {
Self { signer }
}
}
impl<S> X402SchemeId for V2Eip155UptoClient<S> {
fn namespace(&self) -> &str {
V2Eip155Upto.namespace()
}
fn scheme(&self) -> &str {
V2Eip155Upto.scheme()
}
}
impl<S> X402SchemeClient for V2Eip155UptoClient<S>
where
S: SignerLike + Clone + 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(),
chain_reference,
requirements,
requirements_json: original_requirements_json.clone(),
}),
};
Some(candidate)
})
.collect::<Vec<_>>()
}
}
#[allow(dead_code)] struct PayloadSigner<S> {
signer: S,
resource_info: Option<ResourceInfo>,
chain_reference: Eip155ChainReference,
requirements: types::PaymentRequirements,
requirements_json: OriginalJson,
}
#[async_trait]
impl<S> PaymentCandidateSigner for PayloadSigner<S>
where
S: Sync + SignerLike,
{
async fn sign_payment(&self) -> Result<String, X402Error> {
let extra =
self.requirements.extra.as_ref().and_then(|v| {
serde_json::from_value::<UptoSupportedExtra<Address>>(v.clone()).ok()
});
let facilitator_address =
extra
.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 payload = v2::PaymentPayload {
x402_version: v2::X402Version2,
accepted: self.requirements_json.clone(),
resource: self.resource_info.clone(),
payload: permit2_payload,
extensions: None,
};
let json = serde_json::to_vec(&payload)?;
let b64 = Base64Bytes::encode(&json);
Ok(b64.to_string())
}
}