Skip to main content

r402_evm/batch_settlement/
types.rs

1//! Wire types and constants for EVM `batch-settlement`.
2//!
3//! Addresses match `specs/schemes/batch-settlement/scheme_batch_settlement_evm.md`
4//! and the official TS/Go packages.
5
6use alloy_primitives::{Address, B256, Bytes, address};
7use r402_core::scheme::BatchSettlementScheme;
8use serde::{Deserialize, Serialize};
9
10use crate::asset::AssetTransferMethod;
11use crate::chain::{ChecksummedAddress, TokenAmount};
12
13/// CREATE2 address of `x402BatchSettlement` (all chains).
14pub const BATCH_SETTLEMENT_ADDRESS: Address =
15    address!("0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003");
16
17/// ERC-3009 deposit collector.
18pub const ERC3009_DEPOSIT_COLLECTOR_ADDRESS: Address =
19    address!("0x4020806089470a89826cB9fB1f4059150b550004");
20
21/// Permit2 deposit collector.
22pub const PERMIT2_DEPOSIT_COLLECTOR_ADDRESS: Address =
23    address!("0x4020425FAf3B746C082C2f942b4E5159887B0005");
24
25/// Minimum withdraw delay (15 minutes).
26pub const MIN_WITHDRAW_DELAY: u64 = 900;
27/// Maximum withdraw delay (30 days).
28pub const MAX_WITHDRAW_DELAY: u64 = 2_592_000;
29
30/// EIP-712 domain name.
31pub const BATCH_SETTLEMENT_DOMAIN_NAME: &str = "x402 Batch Settlement";
32/// EIP-712 domain version.
33pub const BATCH_SETTLEMENT_DOMAIN_VERSION: &str = "1";
34
35/// Immutable channel configuration (on-chain identity preimage).
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct ChannelConfig {
39    /// Client wallet (EOA or smart wallet).
40    pub payer: Address,
41    /// EOA for voucher signing, or zero for EIP-1271 via payer.
42    pub payer_authorizer: Address,
43    /// Server payment destination.
44    pub receiver: Address,
45    /// Authorizes claims/refunds.
46    pub receiver_authorizer: Address,
47    /// ERC-20 payment token.
48    pub token: Address,
49    /// Withdrawal delay in seconds (`900..=2_592_000`).
50    pub withdraw_delay: u64,
51    /// Differentiates channels with identical parameters.
52    pub salt: B256,
53}
54
55/// Cumulative voucher fields on the wire.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct VoucherFields {
59    /// `bytes32` channel id.
60    pub channel_id: B256,
61    /// Cumulative claim ceiling (decimal string via [`TokenAmount`]).
62    pub max_claimable_amount: TokenAmount,
63    /// EIP-712 voucher signature.
64    pub signature: Bytes,
65}
66
67/// Server `PaymentRequirements.extra` for batch-settlement.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct BatchSettlementExtra {
71    /// Address that authorizes claims/refunds.
72    pub receiver_authorizer: ChecksummedAddress,
73    /// Withdrawal delay in seconds.
74    pub withdraw_delay: u64,
75    /// EIP-712 token domain name.
76    pub name: String,
77    /// EIP-712 token domain version.
78    pub version: String,
79    /// Deposit transfer method; default eip3009.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub asset_transfer_method: Option<AssetTransferMethod>,
82}
83
84impl BatchSettlementExtra {
85    /// Effective transfer method.
86    #[must_use]
87    pub const fn transfer_method(&self) -> AssetTransferMethod {
88        match self.asset_transfer_method {
89            Some(m) => m,
90            None => AssetTransferMethod::Eip3009,
91        }
92    }
93}
94
95/// Local channel accounting snapshot (server/facilitator store).
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct ChannelState {
98    /// On-chain-style balance tracking (deposited − refunds − withdrawn).
99    pub balance: TokenAmount,
100    /// Cumulative claimed on-chain.
101    pub total_claimed: TokenAmount,
102    /// Server running total of actual charges (voucher base).
103    pub charged_cumulative: TokenAmount,
104    /// Cooperative refund nonce.
105    pub refund_nonce: TokenAmount,
106}
107
108impl Default for ChannelState {
109    fn default() -> Self {
110        use alloy_primitives::U256;
111        Self {
112            balance: TokenAmount::from(U256::ZERO),
113            total_claimed: TokenAmount::from(U256::ZERO),
114            charged_cumulative: TokenAmount::from(U256::ZERO),
115            refund_nonce: TokenAmount::from(U256::ZERO),
116        }
117    }
118}
119
120/// Deposit authorization: exactly one of eip3009 / permit2.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct DepositErc3009Authorization {
124    /// Unix lower bound.
125    pub valid_after: TokenAmount,
126    /// Unix upper bound.
127    pub valid_before: TokenAmount,
128    /// EIP-3009 nonce.
129    pub salt: B256,
130    /// Signature.
131    pub signature: Bytes,
132}
133
134/// Permit2 deposit authorization with channel witness.
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct DepositPermit2Authorization {
138    /// Payer.
139    pub from: Address,
140    /// Token permissions.
141    pub permitted: DepositTokenPermissions,
142    /// Permit2 deposit collector.
143    pub spender: Address,
144    /// Permit2 nonce.
145    pub nonce: TokenAmount,
146    /// Deadline.
147    pub deadline: TokenAmount,
148    /// Channel-bound witness.
149    pub witness: DepositWitness,
150    /// Signature.
151    pub signature: Bytes,
152}
153
154/// Token + amount for deposit.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156pub struct DepositTokenPermissions {
157    /// Token.
158    pub token: Address,
159    /// Amount.
160    pub amount: TokenAmount,
161}
162
163/// Deposit witness binding a Permit2 transfer to a channel.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct DepositWitness {
167    /// Channel id.
168    pub channel_id: B256,
169}
170
171/// Deposit authorization envelope (untagged: permit2 first).
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173#[serde(untagged)]
174pub enum DepositAuthorization {
175    /// Permit2 path.
176    Permit2 {
177        /// Nested permit2 auth.
178        #[serde(rename = "permit2Authorization")]
179        permit2_authorization: DepositPermit2Authorization,
180    },
181    /// EIP-3009 path.
182    Erc3009 {
183        /// Nested erc3009 auth.
184        #[serde(rename = "erc3009Authorization")]
185        erc3009_authorization: DepositErc3009Authorization,
186    },
187}
188
189/// Discriminated payment payload for request path (client → server).
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(tag = "type", rename_all = "camelCase")]
192pub enum BatchSettlementPayload {
193    /// First deposit + initial voucher.
194    Deposit {
195        /// Channel config.
196        channel_config: ChannelConfig,
197        /// Opening voucher.
198        voucher: VoucherFields,
199        /// Deposit amount + authorization (boxed: largest variant fields).
200        deposit: Box<DepositBody>,
201    },
202    /// Subsequent cumulative voucher.
203    Voucher {
204        /// Channel config.
205        channel_config: ChannelConfig,
206        /// Signed voucher.
207        voucher: VoucherFields,
208    },
209    /// Cooperative refund request.
210    Refund {
211        /// Channel config.
212        channel_config: ChannelConfig,
213        /// Zero-charge voucher (max = charged cumulative).
214        voucher: VoucherFields,
215        /// Optional refund amount.
216        #[serde(default, skip_serializing_if = "Option::is_none")]
217        amount: Option<TokenAmount>,
218    },
219}
220
221/// Deposit amount + auth.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct DepositBody {
225    /// Deposit amount.
226    pub amount: TokenAmount,
227    /// Token authorization.
228    pub authorization: DepositAuthorization,
229}
230
231impl BatchSettlementPayload {
232    /// Channel config shared by all variants.
233    #[must_use]
234    pub const fn channel_config(&self) -> &ChannelConfig {
235        match self {
236            Self::Deposit { channel_config, .. }
237            | Self::Voucher { channel_config, .. }
238            | Self::Refund { channel_config, .. } => channel_config,
239        }
240    }
241
242    /// Voucher fields shared by all variants.
243    #[must_use]
244    pub const fn voucher(&self) -> &VoucherFields {
245        match self {
246            Self::Deposit { voucher, .. }
247            | Self::Voucher { voucher, .. }
248            | Self::Refund { voucher, .. } => voucher,
249        }
250    }
251}
252
253/// Wire aliases.
254pub mod v2 {
255    use r402_core::wire;
256
257    use super::{BatchSettlementExtra, BatchSettlementPayload, BatchSettlementScheme};
258    use crate::chain::{ChecksummedAddress, TokenAmount};
259
260    /// Typed verify request.
261    pub type VerifyRequest = wire::TypedVerifyRequest<2, PaymentPayload, PaymentRequirements>;
262    /// Typed settle request.
263    pub type SettleRequest = VerifyRequest;
264    /// Payment payload.
265    pub type PaymentPayload = wire::PaymentPayload<PaymentRequirements, BatchSettlementPayload>;
266    /// Payment requirements.
267    pub type PaymentRequirements = wire::PaymentRequirements<
268        BatchSettlementScheme,
269        TokenAmount,
270        ChecksummedAddress,
271        BatchSettlementExtra,
272    >;
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn addresses() {
281        assert_eq!(
282            BATCH_SETTLEMENT_ADDRESS.to_checksum(None),
283            "0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003"
284        );
285    }
286}