use std::future::Future;
use std::sync::Arc;
use alloy_primitives::{Address as EvmAddress, FixedBytes, Signature, U256};
use alloy_signer_local::PrivateKeySigner;
use alloy_sol_types::{SolStruct, eip712_domain};
use r402_core::error::ClientError;
use r402_core::scheme::SchemeId;
use r402_core::scheme::{PaymentCandidate, PaymentCandidateSigner, SchemeClient};
use r402_core::wire::{Base64Bytes, PaymentRequired, ResourceInfo, UnixTimestamp};
use rand::RngExt;
use rand::rng;
use crate::chain::TronChainReference;
use crate::exact::types;
use crate::exact::{
AssetTransferMethod, Eip3009Authorization, Eip3009Payload, ExactPayload,
PaymentRequirementsExtra, Permit2Authorization, Permit2Payload, Permit2TokenPermissions,
Permit2Witness, TransferWithAuthorization, TronExact, TronPermitWitnessTransferFrom,
TronTokenPermissions, TronWitness,
};
pub trait SignerLike: Send + Sync {
fn address(&self) -> EvmAddress;
fn sign_hash(
&self,
hash: &FixedBytes<32>,
) -> impl Future<Output = Result<Signature, alloy_signer::Error>> + Send;
}
impl SignerLike for PrivateKeySigner {
fn address(&self) -> EvmAddress {
alloy_signer::Signer::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) -> EvmAddress {
(**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_reference: TronChainReference,
pub asset_address: EvmAddress,
pub pay_to: EvmAddress,
pub amount: U256,
pub max_timeout_seconds: u64,
pub extra: PaymentRequirementsExtra,
}
pub async fn sign_eip3009_authorization<S: SignerLike + Sync>(
signer: &S,
params: &Eip3009SigningParams,
) -> Result<Eip3009Payload, ClientError> {
if params.extra.name.is_empty() || params.extra.version.is_empty() {
return Err(ClientError::Signing(
"EIP-3009 TIP-712 domain requires non-empty `name` and `version`".into(),
));
}
let domain = eip712_domain! {
name: params.extra.name.clone(),
version: params.extra.version.clone(),
chain_id: u64::from(params.chain_reference.inner()),
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 = FixedBytes(rng().random::<[u8; 32]>());
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 signing_hash = transfer_with_authorization.eip712_signing_hash(&domain);
let signature = signer
.sign_hash(&signing_hash)
.await
.map_err(|e| ClientError::Signing(format!("{e:?}")))?;
Ok(Eip3009Payload {
signature: signature.as_bytes().into(),
authorization,
})
}
#[derive(Debug, Clone, Copy)]
pub struct Permit2SigningParams {
pub chain_reference: TronChainReference,
pub asset_address: EvmAddress,
pub pay_to: EvmAddress,
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 permit2 = params.chain_reference.sun_permit2().ok_or_else(|| {
ClientError::Signing(format!(
"no known SUN.io Permit2 deployment for chain {}",
params.chain_reference
))
})?;
let proxy = params
.chain_reference
.x402_exact_permit2_proxy()
.ok_or_else(|| {
ClientError::Signing(format!(
"no known x402ExactPermit2Proxy deployment for chain {}",
params.chain_reference
))
})?;
let domain = eip712_domain! {
name: "Permit2",
chain_id: u64::from(params.chain_reference.inner()),
verifying_contract: permit2.as_evm(),
};
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 = U256::from_be_bytes(rng().random::<[u8; 32]>());
let permit_witness = TronPermitWitnessTransferFrom {
permitted: TronTokenPermissions {
token: params.asset_address,
amount: params.amount,
},
spender: proxy.as_evm(),
nonce,
deadline: U256::from(deadline_secs),
witness: TronWitness {
to: params.pay_to,
validAfter: U256::from(valid_after_secs),
},
};
let signing_hash = permit_witness.eip712_signing_hash(&domain);
let signature = signer
.sign_hash(&signing_hash)
.await
.map_err(|e| ClientError::Signing(format!("{e:?}")))?;
let authorization = Permit2Authorization {
from: signer.address(),
permitted: Permit2TokenPermissions {
token: params.asset_address,
amount: params.amount.into(),
},
spender: proxy.as_evm(),
nonce: nonce.into(),
deadline: U256::from(deadline_secs).into(),
witness: Permit2Witness {
to: params.pay_to,
valid_after: U256::from(valid_after_secs).into(),
},
};
Ok(Permit2Payload {
signature: signature.as_bytes().into(),
permit2_authorization: authorization,
})
}
#[derive(Clone)]
pub struct TronExactClient<S> {
signer: S,
}
impl<S: std::fmt::Debug> std::fmt::Debug for TronExactClient<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TronExactClient")
.field("signer", &self.signer)
.finish()
}
}
impl<S> TronExactClient<S> {
pub const fn new(signer: S) -> Self {
Self { signer }
}
}
impl<S> SchemeId for TronExactClient<S> {
fn namespace(&self) -> &str {
TronExact.namespace()
}
fn scheme(&self) -> &str {
TronExact.scheme()
}
}
impl<S> r402_core::scheme::sealed::Sealed for TronExactClient<S> {}
impl<S> SchemeClient for TronExactClient<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 =
TronChainReference::try_from(requirements.network.clone()).ok()?;
let candidate = PaymentCandidate {
chain_id: requirements.network.clone(),
asset: requirements.asset.to_string().into(),
amount: requirements.amount.0.to_string().into(),
scheme: self.scheme().into(),
pay_to: requirements.pay_to.to_string().into(),
signer: Box::new(V2PayloadSigner {
resource_info: Some(payment_required.resource.clone()),
signer: self.signer.clone(),
chain_reference,
requirements,
}),
};
Some(candidate)
})
.collect::<Vec<_>>()
}
}
struct V2PayloadSigner<S> {
signer: S,
resource_info: Option<ResourceInfo>,
chain_reference: TronChainReference,
requirements: types::v2::PaymentRequirements,
}
impl<S> PaymentCandidateSigner for V2PayloadSigner<S>
where
S: Sync + SignerLike,
{
fn sign_payment(&self) -> r402_core::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 {
let params = Permit2SigningParams {
chain_reference: self.chain_reference,
asset_address: self.requirements.asset.as_evm(),
pay_to: self.requirements.pay_to.as_evm(),
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 {
const ERR_MSG: &str = "EIP-3009 payment requires `paymentRequirements.extra` \
with the token's TIP-712 domain `name` and `version`";
let extra = self
.requirements
.extra
.clone()
.ok_or_else(|| ClientError::Signing(ERR_MSG.into()))?;
let params = Eip3009SigningParams {
chain_reference: self.chain_reference,
asset_address: self.requirements.asset.as_evm(),
pay_to: self.requirements.pay_to.as_evm(),
amount: self.requirements.amount.into(),
max_timeout_seconds: self.requirements.max_timeout_seconds,
extra,
};
let eip3009_payload = sign_eip3009_authorization(&self.signer, ¶ms).await?;
ExactPayload::Eip3009(eip3009_payload)
};
let payload = types::v2::PaymentPayload::new(self.requirements.clone(), exact_payload)
.with_optional_resource(self.resource_info.clone());
let json = serde_json::to_vec(&payload)?;
let b64 = Base64Bytes::encode(&json);
Ok(b64.to_string())
})
}
}