use alloy_primitives::{Address, U256};
use crate::{
erc20::{build_eip2612_nonces_calldata, build_eip2612_version_calldata},
error::CowError,
onchain::{OnchainReader, decode_string, decode_u256},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OnchainTokenInfo {
pub balance: U256,
pub allowance: U256,
pub nonce: U256,
pub decimals: u8,
pub version: String,
}
impl OnchainTokenInfo {
#[must_use]
pub fn has_balance(&self) -> bool {
!self.balance.is_zero()
}
#[must_use]
pub fn allowance_covers(&self, amount: U256) -> bool {
self.allowance >= amount
}
}
impl OnchainReader {
pub async fn eip2612_nonce(&self, token: Address, owner: Address) -> Result<U256, CowError> {
let cd = build_eip2612_nonces_calldata(owner);
let ret = self.eth_call(token, &cd).await?;
decode_u256(&ret)
}
pub async fn eip2612_version(&self, token: Address) -> Result<String, CowError> {
let cd = build_eip2612_version_calldata();
let ret = self.eth_call(token, &cd).await?;
decode_string(&ret)
}
pub async fn read_token_permit_info(
&self,
token: Address,
owner: Address,
spender: Address,
) -> Result<OnchainTokenInfo, CowError> {
let (balance, allowance, nonce, decimals, version) = futures::try_join!(
self.erc20_balance(token, owner),
self.erc20_allowance(token, owner, spender),
self.eip2612_nonce(token, owner),
self.erc20_decimals(token),
self.eip2612_version(token),
)?;
Ok(OnchainTokenInfo { balance, allowance, nonce, decimals, version })
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::address;
use super::*;
fn token() -> Address {
address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
}
fn owner() -> Address {
address!("1111111111111111111111111111111111111111")
}
#[test]
fn nonces_calldata_len() {
let cd = build_eip2612_nonces_calldata(owner());
assert_eq!(cd.len(), 36);
}
#[test]
fn version_calldata_len() {
let cd = build_eip2612_version_calldata();
assert_eq!(cd.len(), 4);
}
#[test]
fn nonces_selector_correct() {
let cd = build_eip2612_nonces_calldata(owner());
let sel = &alloy_primitives::keccak256(b"nonces(address)")[..4];
assert_eq!(&cd[..4], sel);
}
#[test]
fn version_selector_correct() {
let cd = build_eip2612_version_calldata();
let sel = &alloy_primitives::keccak256(b"version()")[..4];
assert_eq!(&*cd, sel);
}
#[test]
fn nonces_encodes_owner_address() {
let cd = build_eip2612_nonces_calldata(owner());
let owner_bytes = owner();
assert_eq!(&cd[16..36], owner_bytes.as_slice());
}
#[test]
fn onchain_token_info_has_balance() {
let info = OnchainTokenInfo {
balance: U256::from(100u64),
allowance: U256::ZERO,
nonce: U256::ZERO,
decimals: 6,
version: "1".into(),
};
assert!(info.has_balance());
}
#[test]
fn onchain_token_info_no_balance() {
let info = OnchainTokenInfo {
balance: U256::ZERO,
allowance: U256::ZERO,
nonce: U256::ZERO,
decimals: 6,
version: "1".into(),
};
assert!(!info.has_balance());
}
#[test]
fn onchain_token_info_allowance_covers() {
let info = OnchainTokenInfo {
balance: U256::ZERO,
allowance: U256::from(1000u64),
nonce: U256::ZERO,
decimals: 6,
version: "1".into(),
};
assert!(info.allowance_covers(U256::from(999u64)));
assert!(info.allowance_covers(U256::from(1000u64)));
assert!(!info.allowance_covers(U256::from(1001u64)));
}
#[test]
fn token_address_non_zero() {
assert_ne!(token(), Address::ZERO);
}
#[test]
fn onchain_token_info_debug_and_clone() {
let info = OnchainTokenInfo {
balance: U256::from(100u64),
allowance: U256::from(50u64),
nonce: U256::from(3u64),
decimals: 18,
version: "2".into(),
};
let cloned = info.clone();
assert_eq!(info, cloned);
let debug = format!("{info:?}");
assert!(debug.contains("balance"));
}
#[test]
fn onchain_token_info_allowance_exact_zero() {
let info = OnchainTokenInfo {
balance: U256::ZERO,
allowance: U256::ZERO,
nonce: U256::ZERO,
decimals: 6,
version: "1".into(),
};
assert!(info.allowance_covers(U256::ZERO));
assert!(!info.allowance_covers(U256::from(1u64)));
}
#[test]
fn nonces_calldata_address_padded() {
let cd = build_eip2612_nonces_calldata(Address::ZERO);
assert!(cd[4..16].iter().all(|&b| b == 0));
}
}