pub mod eip2612;
mod signing;
use std::sync::Arc;
use alloy_primitives::U256;
pub use eip2612::{Eip2612SigningParams, sign_eip2612_permit};
use r402_core::error::ClientError;
use r402_core::scheme::{
PaymentCandidate, PaymentCandidateSigner, SchemeClient, SchemeId, sealed::Sealed,
};
use r402_core::wire::{Base64Bytes, PaymentRequired, ResourceInfo};
pub use signing::{Permit2UptoSigningParams, sign_permit2_upto_authorization};
use crate::chain::Eip155ChainReference;
use crate::exact::PERMIT2_ADDRESS;
use crate::exact::client::{Permit2Approver, SignerLike};
use crate::upto::{Eip155Upto, types};
pub struct Eip155UptoClient<S> {
signer: S,
approver: Option<Arc<dyn Permit2Approver>>,
auto_approve: bool,
}
impl<S: std::fmt::Debug> std::fmt::Debug for Eip155UptoClient<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Eip155UptoClient")
.field("signer", &self.signer)
.field("has_approver", &self.approver.is_some())
.field("auto_approve", &self.auto_approve)
.finish()
}
}
impl<S> Eip155UptoClient<S> {
pub const fn new(signer: S) -> Self {
Self {
signer,
approver: None,
auto_approve: false,
}
}
pub fn builder(signer: S) -> Eip155UptoClientBuilder<S> {
Eip155UptoClientBuilder {
signer,
approver: None,
auto_approve: true,
}
}
}
pub struct Eip155UptoClientBuilder<S> {
signer: S,
approver: Option<Arc<dyn Permit2Approver>>,
auto_approve: bool,
}
impl<S: std::fmt::Debug> std::fmt::Debug for Eip155UptoClientBuilder<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Eip155UptoClientBuilder")
.field("signer", &self.signer)
.field("has_approver", &self.approver.is_some())
.field("auto_approve", &self.auto_approve)
.finish()
}
}
impl<S> Eip155UptoClientBuilder<S> {
#[must_use]
pub fn approver<A: Permit2Approver + 'static>(mut self, approver: A) -> Self {
self.approver = Some(Arc::new(approver));
self
}
#[must_use]
pub const fn auto_approve(mut self, auto_approve: bool) -> Self {
self.auto_approve = auto_approve;
self
}
#[cfg(feature = "client-provider")]
#[must_use]
pub fn provider<P: alloy_provider::Provider + Send + Sync + 'static>(
self,
provider: P,
) -> Self {
self.approver(crate::exact::client::permit2::BuiltinPermit2Approver { provider })
}
pub fn build(self) -> Eip155UptoClient<S> {
Eip155UptoClient {
signer: self.signer,
approver: self.approver,
auto_approve: self.auto_approve,
}
}
}
impl<S> SchemeId for Eip155UptoClient<S> {
fn namespace(&self) -> &str {
Eip155Upto.namespace()
}
fn scheme(&self) -> &str {
Eip155Upto.scheme()
}
}
impl<S> Sealed for Eip155UptoClient<S> {}
impl<S> SchemeClient for Eip155UptoClient<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 = Eip155ChainReference::try_from(&requirements.network).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(UptoPayloadSigner {
resource_info: Some(payment_required.resource.clone()),
signer: self.signer.clone(),
chain_reference,
requirements,
approver: self.approver.clone(),
auto_approve: self.auto_approve,
}),
};
Some(candidate)
})
.collect::<Vec<_>>()
}
}
struct UptoPayloadSigner<S> {
signer: S,
resource_info: Option<ResourceInfo>,
chain_reference: Eip155ChainReference,
requirements: types::v2::PaymentRequirements,
approver: Option<Arc<dyn Permit2Approver>>,
auto_approve: bool,
}
impl<S> UptoPayloadSigner<S>
where
S: Sync + SignerLike,
{
async fn ensure_permit2_allowance(&self) -> Result<(), ClientError> {
let Some(approver) = &self.approver else {
return Ok(());
};
let token = self.requirements.asset.0;
let owner = self.signer.address();
let required: U256 = self.requirements.amount.into();
let allowance = approver.check_permit2_allowance(token, owner).await?;
if allowance >= required {
return Ok(());
}
if self.auto_approve {
approver.approve_permit2(token, owner).await?;
Ok(())
} else {
Err(ClientError::PreConditionFailed(format!(
"Permit2 allowance insufficient for token {token}: \
have {allowance}, need {required}. Call approve({PERMIT2_ADDRESS}, MAX) \
on the token contract, or enable auto_approve."
)))
}
}
}
impl<S> PaymentCandidateSigner for UptoPayloadSigner<S>
where
S: Sync + SignerLike,
{
fn sign_payment(&self) -> r402_core::facilitator::BoxFuture<'_, Result<String, ClientError>> {
Box::pin(async move {
self.ensure_permit2_allowance().await?;
let extra = self.requirements.extra.as_ref().ok_or_else(|| {
ClientError::PreConditionFailed(
"upto requires paymentRequirements.extra.facilitatorAddress; \
ensure the server is wired to an upto facilitator that publishes its address"
.to_owned(),
)
})?;
let params = Permit2UptoSigningParams {
chain_id: self.chain_reference.inner(),
asset_address: self.requirements.asset.0,
pay_to: self.requirements.pay_to.into(),
facilitator_address: extra.facilitator_address.0,
max_amount: self.requirements.amount.into(),
max_timeout_seconds: self.requirements.max_timeout_seconds,
};
let upto_payload = sign_permit2_upto_authorization(&self.signer, ¶ms).await?;
let payload = types::v2::PaymentPayload::new(self.requirements.clone(), upto_payload)
.with_optional_resource(self.resource_info.clone());
let json = serde_json::to_vec(&payload)?;
let b64 = Base64Bytes::encode(&json);
Ok(b64.to_string())
})
}
}