use alloy_primitives::{Address, B256, Signature, U256};
use alloy_sol_types::{SolStruct, eip712_domain};
use super::{OfferPayload, OfferReceiptError, ReceiptPayload, SignedOffer, SignedReceipt};
alloy_sol_types::sol! {
struct Offer {
uint256 version;
string resourceUrl;
string scheme;
string network;
string asset;
string payTo;
string amount;
uint256 validUntil;
}
struct Receipt {
uint256 version;
string network;
string resourceUrl;
string payer;
uint256 issuedAt;
string transaction;
}
}
pub trait Eip712DigestSigner: Send + Sync {
fn sign_digest(&self, digest: B256) -> Result<Signature, OfferReceiptError>;
}
impl<S> Eip712DigestSigner for S
where
S: alloy_signer::SignerSync + Send + Sync,
{
fn sign_digest(&self, digest: B256) -> Result<Signature, OfferReceiptError> {
self.sign_hash_sync(&digest)
.map_err(|e| OfferReceiptError::Sign(e.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Eip712Verification<T> {
pub signer: Address,
pub payload: T,
}
#[must_use]
pub fn hash_offer_typed_data(payload: &OfferPayload) -> B256 {
let domain = eip712_domain! {
name: "x402 offer",
version: "1",
chain_id: 1u64,
};
let offer = Offer {
version: U256::from(payload.version),
resourceUrl: payload.resource_url.clone(),
scheme: payload.scheme.clone(),
network: payload.network.clone(),
asset: payload.asset.clone(),
payTo: payload.pay_to.clone(),
amount: payload.amount.clone(),
validUntil: U256::from(payload.valid_until),
};
offer.eip712_signing_hash(&domain)
}
#[must_use]
pub fn hash_receipt_typed_data(payload: &ReceiptPayload) -> B256 {
let domain = eip712_domain! {
name: "x402 receipt",
version: "1",
chain_id: 1u64,
};
let receipt = Receipt {
version: U256::from(payload.version),
network: payload.network.clone(),
resourceUrl: payload.resource_url.clone(),
payer: payload.payer.clone(),
issuedAt: U256::from(payload.issued_at),
transaction: payload.transaction.clone().unwrap_or_default(),
};
receipt.eip712_signing_hash(&domain)
}
pub(super) fn sign_offer_eip712(
payload: &OfferPayload,
signer: &dyn Eip712DigestSigner,
) -> Result<String, OfferReceiptError> {
Ok(encode_eth_signature(
signer.sign_digest(hash_offer_typed_data(payload))?,
))
}
pub(super) fn sign_receipt_eip712(
payload: &ReceiptPayload,
signer: &dyn Eip712DigestSigner,
) -> Result<String, OfferReceiptError> {
Ok(encode_eth_signature(
signer.sign_digest(hash_receipt_typed_data(payload))?,
))
}
pub fn verify_offer_signature_eip712(
offer: &SignedOffer,
) -> Result<Eip712Verification<OfferPayload>, OfferReceiptError> {
let SignedOffer::Eip712 {
payload, signature, ..
} = offer
else {
return Err(OfferReceiptError::UnknownFormat("expected eip712".into()));
};
let hash = hash_offer_typed_data(payload);
let sig = parse_eth_signature(signature)?;
let signer = sig
.recover_address_from_prehash(&hash)
.map_err(|e| OfferReceiptError::Verify(e.to_string()))?;
Ok(Eip712Verification {
signer,
payload: payload.clone(),
})
}
pub fn verify_receipt_signature_eip712(
receipt: &SignedReceipt,
) -> Result<Eip712Verification<ReceiptPayload>, OfferReceiptError> {
let SignedReceipt::Eip712 { payload, signature } = receipt else {
return Err(OfferReceiptError::UnknownFormat("expected eip712".into()));
};
let hash = hash_receipt_typed_data(payload);
let sig = parse_eth_signature(signature)?;
let signer = sig
.recover_address_from_prehash(&hash)
.map_err(|e| OfferReceiptError::Verify(e.to_string()))?;
Ok(Eip712Verification {
signer,
payload: payload.clone(),
})
}
fn parse_eth_signature(sig: &str) -> Result<Signature, OfferReceiptError> {
let hex = sig.strip_prefix("0x").unwrap_or(sig);
let bytes = hex::decode(hex).map_err(|e| OfferReceiptError::Verify(e.to_string()))?;
Signature::from_raw(&bytes).map_err(|e| OfferReceiptError::Verify(e.to_string()))
}
fn encode_eth_signature(sig: Signature) -> String {
format!("0x{}", hex::encode(sig.as_bytes()))
}