use std::fmt;
use alloy_primitives::{Address, U256};
use serde::{Deserialize, Serialize};
use crate::app_data::types::CowHook;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PermitInfo {
pub token_address: Address,
pub owner: Address,
pub spender: Address,
pub value: U256,
pub nonce: U256,
pub deadline: u64,
}
impl PermitInfo {
#[must_use]
pub const fn new(
token_address: Address,
owner: Address,
spender: Address,
value: U256,
) -> Self {
Self { token_address, owner, spender, value, nonce: U256::ZERO, deadline: 0 }
}
#[must_use]
pub const fn with_nonce(mut self, nonce: U256) -> Self {
self.nonce = nonce;
self
}
#[must_use]
pub const fn with_deadline(mut self, deadline: u64) -> Self {
self.deadline = deadline;
self
}
#[must_use]
pub const fn is_expired(&self, timestamp: u64) -> bool {
timestamp > self.deadline
}
#[must_use]
pub fn is_zero_allowance(&self) -> bool {
self.value.is_zero()
}
#[must_use]
pub fn is_unlimited_allowance(&self) -> bool {
self.value == U256::MAX
}
}
impl fmt::Display for PermitInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"permit(token={:#x}, owner={:#x}, spender={:#x})",
self.token_address, self.owner, self.spender
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Erc20PermitInfo {
pub name: String,
pub version: String,
pub chain_id: u64,
}
impl Erc20PermitInfo {
#[must_use]
pub fn new(name: impl Into<String>, version: impl Into<String>, chain_id: u64) -> Self {
Self { name: name.into(), version: version.into(), chain_id }
}
}
impl fmt::Display for Erc20PermitInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "erc20-permit({}, v{}, chain={})", self.name, self.version, self.chain_id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PermitHookData {
pub target: Address,
pub calldata: Vec<u8>,
pub gas_limit: u64,
}
impl PermitHookData {
#[must_use]
pub const fn new(target: Address, calldata: Vec<u8>, gas_limit: u64) -> Self {
Self { target, calldata, gas_limit }
}
#[must_use]
pub const fn has_calldata(&self) -> bool {
!self.calldata.is_empty()
}
#[must_use]
pub const fn calldata_len(&self) -> usize {
self.calldata.len()
}
#[must_use]
pub fn into_cow_hook(self) -> CowHook {
CowHook {
target: format!("{:#x}", self.target),
call_data: format!("0x{}", alloy_primitives::hex::encode(&self.calldata)),
gas_limit: self.gas_limit.to_string(),
dapp_id: None,
}
}
}
impl fmt::Display for PermitHookData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"permit-hook(token={:#x}, gas={}, calldata_len={})",
self.target,
self.gas_limit,
self.calldata.len()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permit_info_new_defaults() {
let info = PermitInfo::new(Address::ZERO, Address::ZERO, Address::ZERO, U256::ZERO);
assert!(info.nonce.is_zero());
assert_eq!(info.deadline, 0);
assert!(info.is_zero_allowance());
assert!(!info.is_unlimited_allowance());
}
#[test]
fn permit_info_builders() {
let info = PermitInfo::new(Address::ZERO, Address::ZERO, Address::ZERO, U256::MAX)
.with_nonce(U256::from(5u64))
.with_deadline(1_000_000);
assert_eq!(info.nonce, U256::from(5u64));
assert_eq!(info.deadline, 1_000_000);
assert!(info.is_unlimited_allowance());
assert!(!info.is_zero_allowance());
}
#[test]
fn permit_info_is_expired_boundary() {
let info = PermitInfo::new(Address::ZERO, Address::ZERO, Address::ZERO, U256::ZERO)
.with_deadline(1000);
assert!(!info.is_expired(999));
assert!(!info.is_expired(1000));
assert!(info.is_expired(1001));
}
#[test]
fn permit_info_display() {
let info = PermitInfo::new(Address::ZERO, Address::ZERO, Address::ZERO, U256::ZERO);
let s = format!("{info}");
assert!(s.starts_with("permit("));
}
#[test]
fn erc20_permit_info_new() {
let info = Erc20PermitInfo::new("USD Coin", "2", 1);
assert_eq!(info.name, "USD Coin");
assert_eq!(info.version, "2");
assert_eq!(info.chain_id, 1);
}
#[test]
fn erc20_permit_info_display() {
let info = Erc20PermitInfo::new("USDC", "1", 1);
let s = format!("{info}");
assert!(s.contains("USDC"));
assert!(s.contains("chain=1"));
}
#[test]
fn permit_hook_data_new() {
let data = PermitHookData::new(Address::ZERO, vec![1, 2, 3], 50_000);
assert!(data.has_calldata());
assert_eq!(data.calldata_len(), 3);
assert_eq!(data.gas_limit, 50_000);
}
#[test]
fn permit_hook_data_empty_calldata() {
let data = PermitHookData::new(Address::ZERO, vec![], 0);
assert!(!data.has_calldata());
assert_eq!(data.calldata_len(), 0);
}
#[test]
fn permit_hook_data_into_cow_hook() {
let data = PermitHookData::new(Address::ZERO, vec![0xab, 0xcd], 100_000);
let hook = data.into_cow_hook();
assert!(hook.call_data.starts_with("0x"));
assert!(hook.call_data.contains("abcd"));
assert_eq!(hook.gas_limit, "100000");
}
#[test]
fn permit_hook_data_display() {
let data = PermitHookData::new(Address::ZERO, vec![0; 260], 50_000);
let s = format!("{data}");
assert!(s.contains("gas=50000"));
assert!(s.contains("calldata_len=260"));
}
}