use alloy_primitives::Address;
#[cfg(any(feature = "facilitator", feature = "client"))]
use alloy_sol_types::sol;
pub use r402_core::scheme::UptoScheme;
use serde::{Deserialize, Serialize};
use crate::chain::{ChecksummedAddress, TokenAmount};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum UptoAssetTransferMethod {
#[default]
Permit2,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UptoPermit2Witness {
pub to: Address,
pub facilitator: Address,
pub valid_after: TokenAmount,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UptoPermit2Authorization {
pub from: Address,
pub permitted: crate::exact::Permit2TokenPermissions,
pub spender: Address,
pub nonce: TokenAmount,
pub deadline: TokenAmount,
pub witness: UptoPermit2Witness,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UptoPermit2Payload {
pub signature: alloy_primitives::Bytes,
pub permit2_authorization: UptoPermit2Authorization,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UptoPaymentRequirementsExtra {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub version: String,
#[serde(default)]
pub asset_transfer_method: UptoAssetTransferMethod,
pub facilitator_address: ChecksummedAddress,
}
impl UptoPaymentRequirementsExtra {
#[must_use]
pub const fn new(facilitator_address: ChecksummedAddress) -> Self {
Self {
name: String::new(),
version: String::new(),
asset_transfer_method: UptoAssetTransferMethod::Permit2,
facilitator_address,
}
}
#[must_use]
pub fn with_token_eip712(mut self, name: String, version: String) -> Self {
self.name = name;
self.version = version;
self
}
}
#[cfg(any(feature = "facilitator", feature = "client"))]
sol!(
#[derive(Serialize, Deserialize)]
struct Witness {
address to;
address facilitator;
uint256 validAfter;
}
#[derive(Serialize, Deserialize)]
struct PermitWitnessTransferFrom {
TokenPermissions permitted;
address spender;
uint256 nonce;
uint256 deadline;
Witness witness;
}
#[derive(Serialize, Deserialize)]
struct TokenPermissions {
address token;
uint256 amount;
}
);
pub mod v2 {
use r402_core::wire as proto_v2;
use super::{UptoPermit2Payload, UptoScheme};
use crate::chain::{ChecksummedAddress, TokenAmount};
pub type VerifyRequest = proto_v2::TypedVerifyRequest<2, PaymentPayload, PaymentRequirements>;
pub type SettleRequest = VerifyRequest;
pub type PaymentPayload = proto_v2::PaymentPayload<PaymentRequirements, UptoPermit2Payload>;
use super::UptoPaymentRequirementsExtra;
pub type PaymentRequirements = proto_v2::PaymentRequirements<
UptoScheme,
TokenAmount,
ChecksummedAddress,
UptoPaymentRequirementsExtra,
>;
}
#[cfg(test)]
#[cfg(any(feature = "facilitator", feature = "client"))]
mod tests {
use alloy_primitives::{Address, U256, keccak256};
use alloy_sol_types::SolStruct;
use super::*;
#[test]
fn witness_typehash_equals_spec_byte_string() {
let witness = Witness {
to: Address::ZERO,
facilitator: Address::ZERO,
validAfter: U256::ZERO,
};
let expected = keccak256(b"Witness(address to,address facilitator,uint256 validAfter)");
assert_eq!(witness.eip712_type_hash(), expected);
}
#[test]
fn permit_witness_transfer_from_typehash_equals_spec_byte_string() {
let permit = PermitWitnessTransferFrom {
permitted: TokenPermissions {
token: Address::ZERO,
amount: U256::ZERO,
},
spender: Address::ZERO,
nonce: U256::ZERO,
deadline: U256::ZERO,
witness: Witness {
to: Address::ZERO,
facilitator: Address::ZERO,
validAfter: U256::ZERO,
},
};
let expected = keccak256(
b"PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,address facilitator,uint256 validAfter)",
);
assert_eq!(permit.eip712_type_hash(), expected);
}
#[test]
fn upto_extra_serialises_with_camel_case() {
let extra =
UptoPaymentRequirementsExtra::new(ChecksummedAddress::from(Address::repeat_byte(0xAA)))
.with_token_eip712("USDC".into(), "2".into());
let json = serde_json::to_value(&extra).unwrap();
let obj = json.as_object().expect("extra serialises to a JSON object");
assert_eq!(
obj.get("name").and_then(serde_json::Value::as_str),
Some("USDC")
);
assert_eq!(
obj.get("version").and_then(serde_json::Value::as_str),
Some("2")
);
assert_eq!(
obj.get("assetTransferMethod")
.and_then(serde_json::Value::as_str),
Some("permit2")
);
assert!(
obj.get("facilitatorAddress")
.and_then(serde_json::Value::as_str)
.is_some()
);
}
#[test]
fn upto_extra_round_trips() {
let extra =
UptoPaymentRequirementsExtra::new(ChecksummedAddress::from(Address::repeat_byte(0xCC)));
let json = serde_json::to_value(&extra).unwrap();
let back: UptoPaymentRequirementsExtra = serde_json::from_value(json).unwrap();
assert_eq!(back, extra);
}
}