use alloy_primitives::Address;
use r402_protocol::payment::{ExtensionEntry, Extensions};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const ERC20_APPROVAL_GAS_SPONSORING_KEY: &str = "erc20ApprovalGasSponsoring";
pub const ERC20_APPROVAL_GAS_SPONSORING_VERSION: &str = "1";
pub const ERC20_APPROVE_GAS_LIMIT: u64 = 70_000;
pub const DEFAULT_MAX_FEE_PER_GAS: u128 = 1_000_000_000;
pub const DEFAULT_MAX_PRIORITY_FEE_PER_GAS: u128 = 100_000_000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Erc20ApprovalGasSponsoringInfo {
pub from: Address,
pub asset: Address,
pub spender: Address,
pub amount: String,
pub signed_transaction: String,
pub version: String,
}
#[derive(Debug, Error)]
pub enum Erc20ApprovalParseError {
#[error("invalid erc20ApprovalGasSponsoring extension payload: {0}")]
Invalid(#[from] serde_json::Error),
}
impl Erc20ApprovalGasSponsoringInfo {
pub fn from_extensions(
extensions: &Extensions,
) -> Result<Option<Self>, Erc20ApprovalParseError> {
let Some(entry) = extensions.get(ERC20_APPROVAL_GAS_SPONSORING_KEY) else {
return Ok(None);
};
let parsed: Self = match entry {
ExtensionEntry::Structured { info, .. } => serde_json::from_value(info.clone())?,
ExtensionEntry::Raw(value) => serde_json::from_value(value.clone())?,
};
Ok(Some(parsed))
}
pub fn to_extension_entry(&self) -> Result<ExtensionEntry, serde_json::Error> {
Ok(ExtensionEntry::info(serde_json::to_value(self)?))
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::Address;
use r402_protocol::payment::Extensions;
use super::*;
fn sample() -> Erc20ApprovalGasSponsoringInfo {
Erc20ApprovalGasSponsoringInfo {
from: Address::repeat_byte(0xAA),
asset: Address::repeat_byte(0xBB),
spender: Address::repeat_byte(0xCC),
amount:
"115792089237316195423570985008687907853269984665640564039457584007913129639935"
.into(),
signed_transaction: "0xdeadbeef".into(),
version: ERC20_APPROVAL_GAS_SPONSORING_VERSION.into(),
}
}
#[test]
fn wire_uses_signed_transaction_camel_case() {
let v = serde_json::to_value(sample()).unwrap();
assert!(v.get("signedTransaction").is_some());
assert!(v.get("signed_transaction").is_none());
assert_eq!(v.get("version"), Some(&serde_json::json!("1")));
}
#[test]
fn extension_round_trips_structured() {
let info = sample();
let mut extensions = Extensions::new();
extensions.insert(
ERC20_APPROVAL_GAS_SPONSORING_KEY,
info.to_extension_entry().unwrap(),
);
let extracted = Erc20ApprovalGasSponsoringInfo::from_extensions(&extensions)
.unwrap()
.unwrap();
assert_eq!(extracted, info);
}
#[test]
fn missing_extension_ok_none() {
assert!(
Erc20ApprovalGasSponsoringInfo::from_extensions(&Extensions::new())
.unwrap()
.is_none()
);
}
}