use alloy_primitives::{Address, Bytes};
use r402_core::wire::{ExtensionEntry, Extensions};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::chain::TokenAmount;
pub const EIP2612_GAS_SPONSORING_KEY: &str = "eip2612GasSponsoring";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Eip2612SignedPermit {
pub from: Address,
pub asset: Address,
pub spender: Address,
pub value: TokenAmount,
pub deadline: TokenAmount,
pub signature: Bytes,
}
#[derive(Debug, Error)]
pub enum Eip2612ParseError {
#[error("invalid eip2612GasSponsoring extension payload: {0}")]
Invalid(#[from] serde_json::Error),
#[error("eip2612 signature MUST be 65 bytes long, got {0}")]
InvalidSignatureLength(usize),
}
impl Eip2612SignedPermit {
pub const SIGNATURE_LEN: usize = 65;
pub fn from_extensions(extensions: &Extensions) -> Result<Option<Self>, Eip2612ParseError> {
let Some(entry) = extensions.get(EIP2612_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())?,
};
if parsed.signature.len() != Self::SIGNATURE_LEN {
return Err(Eip2612ParseError::InvalidSignatureLength(
parsed.signature.len(),
));
}
Ok(Some(parsed))
}
#[must_use]
pub fn split_signature(&self) -> Option<(alloy_primitives::B256, alloy_primitives::B256, u8)> {
let bytes = self.signature.as_ref();
if bytes.len() != Self::SIGNATURE_LEN {
return None;
}
let mut r = [0u8; 32];
let mut s = [0u8; 32];
r.copy_from_slice(bytes.get(0..32)?);
s.copy_from_slice(bytes.get(32..64)?);
let v = *bytes.get(64)?;
Some((
alloy_primitives::B256::from(r),
alloy_primitives::B256::from(s),
v,
))
}
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, Bytes, U256};
use r402_core::wire::{ExtensionEntry, Extensions};
use serde_json::json;
use super::*;
fn sample_permit() -> Eip2612SignedPermit {
Eip2612SignedPermit {
from: Address::repeat_byte(0xAA),
asset: Address::repeat_byte(0xBB),
spender: Address::repeat_byte(0xCC),
value: TokenAmount::from(U256::from(1_000_000_u64)),
deadline: TokenAmount::from(U256::from(1_700_000_000_u64)),
signature: Bytes::from(vec![0x42u8; 65]),
}
}
#[test]
fn extension_round_trips_through_structured_envelope() {
let permit = sample_permit();
let mut extensions = Extensions::new();
extensions.insert(
EIP2612_GAS_SPONSORING_KEY,
permit.to_extension_entry().expect("serialises cleanly"),
);
let extracted = Eip2612SignedPermit::from_extensions(&extensions)
.expect("structured payload decodes")
.expect("extension present");
assert_eq!(extracted, permit);
}
#[test]
fn extension_round_trips_through_raw_envelope() {
let permit = sample_permit();
let mut extensions = Extensions::new();
extensions.insert(
EIP2612_GAS_SPONSORING_KEY,
ExtensionEntry::raw(serde_json::to_value(&permit).unwrap()),
);
let extracted = Eip2612SignedPermit::from_extensions(&extensions)
.expect("raw payload decodes")
.expect("extension present");
assert_eq!(extracted, permit);
}
#[test]
fn missing_extension_returns_ok_none() {
let extensions = Extensions::new();
let extracted = Eip2612SignedPermit::from_extensions(&extensions).expect("no error");
assert!(extracted.is_none());
}
#[test]
fn malformed_extension_returns_invalid_error() {
let mut extensions = Extensions::new();
extensions.insert(
EIP2612_GAS_SPONSORING_KEY,
ExtensionEntry::raw(json!({ "from": "not-an-address" })),
);
let err = Eip2612SignedPermit::from_extensions(&extensions).unwrap_err();
assert!(matches!(err, Eip2612ParseError::Invalid(_)));
}
#[test]
fn signature_length_validation_rejects_short_sigs() {
let mut permit = sample_permit();
permit.signature = Bytes::from(vec![0u8; 64]);
let mut extensions = Extensions::new();
extensions.insert(
EIP2612_GAS_SPONSORING_KEY,
permit.to_extension_entry().unwrap(),
);
let err = Eip2612SignedPermit::from_extensions(&extensions).unwrap_err();
assert!(matches!(err, Eip2612ParseError::InvalidSignatureLength(64)));
}
#[test]
fn split_signature_returns_components_in_order() {
let mut sig_bytes = Vec::with_capacity(65);
sig_bytes.extend_from_slice(&[0x11u8; 32]);
sig_bytes.extend_from_slice(&[0x22u8; 32]);
sig_bytes.push(27);
let permit = Eip2612SignedPermit {
signature: Bytes::from(sig_bytes),
..sample_permit()
};
let (r, s, v) = permit.split_signature().expect("65-byte signature");
assert_eq!(r.as_slice(), [0x11u8; 32]);
assert_eq!(s.as_slice(), [0x22u8; 32]);
assert_eq!(v, 27);
}
}