aztec_account/
meta_payment.rs

1//! Meta payment method that routes fee payment through an account entrypoint.
2//!
3//! Port of TS `yarn-project/aztec.js/src/wallet/account_entrypoint_meta_payment_method.ts`.
4
5use async_trait::async_trait;
6use std::sync::Arc;
7
8use aztec_core::constants::protocol_contract_address;
9use aztec_core::tx::ExecutionPayload;
10use aztec_core::types::AztecAddress;
11use aztec_core::Error;
12use aztec_fee::FeePaymentMethod;
13
14use crate::account::Account;
15use crate::entrypoint::{AccountFeePaymentMethodOptions, DefaultAccountEntrypointOptions};
16
17/// Wraps a `FeePaymentMethod` so that fee payment is routed through the
18/// account contract's entrypoint. This enables an account to pay for
19/// its own deployment transaction.
20///
21/// The inner payment method's execution payload gets wrapped as a call
22/// to the account's `entrypoint()` function with the appropriate
23/// fee payment method option.
24pub struct AccountEntrypointMetaPaymentMethod {
25    account: Arc<dyn Account>,
26    inner: Option<Arc<dyn FeePaymentMethod>>,
27    fee_entrypoint_options: Option<DefaultAccountEntrypointOptions>,
28}
29
30impl AccountEntrypointMetaPaymentMethod {
31    /// Create a new meta payment method.
32    ///
33    /// - `account`: The account whose entrypoint will handle fee payment.
34    /// - `inner`: Optional inner fee payment method. If `None`, assumes the
35    ///   account pays from its existing Fee Juice balance.
36    /// - `fee_entrypoint_options`: Optional explicit entrypoint options.
37    ///   If `None`, auto-detects based on fee payer and inner calls.
38    pub fn new(
39        account: Arc<dyn Account>,
40        inner: Option<Arc<dyn FeePaymentMethod>>,
41        fee_entrypoint_options: Option<DefaultAccountEntrypointOptions>,
42    ) -> Self {
43        Self {
44            account,
45            inner,
46            fee_entrypoint_options,
47        }
48    }
49}
50
51#[async_trait]
52impl FeePaymentMethod for AccountEntrypointMetaPaymentMethod {
53    async fn get_asset(&self) -> Result<AztecAddress, Error> {
54        match &self.inner {
55            Some(method) => method.get_asset().await,
56            None => Ok(protocol_contract_address::fee_juice()),
57        }
58    }
59
60    async fn get_fee_payer(&self) -> Result<AztecAddress, Error> {
61        match &self.inner {
62            Some(method) => method.get_fee_payer().await,
63            None => Ok(self.account.address()),
64        }
65    }
66
67    async fn get_fee_execution_payload(&self) -> Result<ExecutionPayload, Error> {
68        // 1. Get inner payload (or empty).
69        let inner_payload = match &self.inner {
70            Some(method) => method.get_fee_execution_payload().await?,
71            None => ExecutionPayload::default(),
72        };
73
74        // 2. Determine fee entrypoint options.
75        let options = match self.fee_entrypoint_options.clone() {
76            Some(opts) => opts,
77            None => {
78                let fee_payer = self.get_fee_payer().await?;
79                let is_payer = fee_payer == self.account.address();
80                let fee_payment_method_options = if is_payer && !inner_payload.calls.is_empty() {
81                    AccountFeePaymentMethodOptions::FeeJuiceWithClaim
82                } else if is_payer {
83                    AccountFeePaymentMethodOptions::PreexistingFeeJuice
84                } else {
85                    AccountFeePaymentMethodOptions::External
86                };
87
88                DefaultAccountEntrypointOptions {
89                    cancellable: false,
90                    tx_nonce: None,
91                    fee_payment_method_options,
92                }
93            }
94        };
95
96        // 3. Wrap the inner payload through the account's entrypoint.
97        self.account
98            .wrap_execution_payload(inner_payload, options.into())
99            .await
100    }
101}
102
103#[cfg(test)]
104#[allow(clippy::unwrap_used, clippy::expect_used)]
105mod tests {
106    use super::*;
107    use async_trait::async_trait;
108    use aztec_core::tx::AuthWitness;
109    use aztec_core::types::{CompleteAddress, Fr, PublicKeys};
110
111    use crate::account::{AuthorizationProvider, EntrypointOptions};
112    use crate::wallet::{ChainInfo, MessageHashOrIntent};
113
114    struct MockPayerAccount {
115        addr: CompleteAddress,
116    }
117
118    #[async_trait]
119    impl AuthorizationProvider for MockPayerAccount {
120        async fn create_auth_wit(
121            &self,
122            _intent: MessageHashOrIntent,
123            _chain_info: &ChainInfo,
124        ) -> Result<AuthWitness, Error> {
125            Ok(AuthWitness::default())
126        }
127    }
128
129    #[async_trait]
130    impl Account for MockPayerAccount {
131        fn complete_address(&self) -> &CompleteAddress {
132            &self.addr
133        }
134
135        fn address(&self) -> AztecAddress {
136            self.addr.address
137        }
138
139        async fn create_tx_execution_request(
140            &self,
141            _exec: ExecutionPayload,
142            _gas_settings: aztec_core::fee::GasSettings,
143            _chain_info: &ChainInfo,
144            _options: EntrypointOptions,
145        ) -> Result<crate::account::TxExecutionRequest, Error> {
146            unimplemented!()
147        }
148
149        async fn wrap_execution_payload(
150            &self,
151            exec: ExecutionPayload,
152            _options: EntrypointOptions,
153        ) -> Result<ExecutionPayload, Error> {
154            // Simple mock: just return the payload with fee_payer set.
155            Ok(ExecutionPayload {
156                fee_payer: Some(self.addr.address),
157                ..exec
158            })
159        }
160    }
161
162    fn mock_account(addr: u64) -> Arc<dyn Account> {
163        Arc::new(MockPayerAccount {
164            addr: CompleteAddress {
165                address: AztecAddress::from(addr),
166                public_keys: PublicKeys::default(),
167                partial_address: Fr::zero(),
168            },
169        })
170    }
171
172    struct MockInnerFeeMethod {
173        payer: AztecAddress,
174    }
175
176    #[async_trait]
177    impl FeePaymentMethod for MockInnerFeeMethod {
178        async fn get_asset(&self) -> Result<AztecAddress, Error> {
179            Ok(protocol_contract_address::fee_juice())
180        }
181
182        async fn get_fee_payer(&self) -> Result<AztecAddress, Error> {
183            Ok(self.payer)
184        }
185
186        async fn get_fee_execution_payload(&self) -> Result<ExecutionPayload, Error> {
187            Ok(ExecutionPayload::default())
188        }
189    }
190
191    #[tokio::test]
192    async fn no_inner_returns_wrapped_empty_payload() {
193        let account = mock_account(42);
194        let meta = AccountEntrypointMetaPaymentMethod::new(account, None, None);
195
196        let payload = meta.get_fee_execution_payload().await.expect("payload");
197        // Should have the account's address as fee payer (from mock wrap)
198        assert_eq!(payload.fee_payer, Some(AztecAddress::from(42u64)));
199    }
200
201    #[tokio::test]
202    async fn get_asset_delegates_to_inner() {
203        let account = mock_account(42);
204        let inner: Arc<dyn FeePaymentMethod> = Arc::new(MockInnerFeeMethod {
205            payer: AztecAddress::from(42u64),
206        });
207        let meta = AccountEntrypointMetaPaymentMethod::new(account, Some(inner), None);
208
209        let asset = meta.get_asset().await.expect("asset");
210        assert_eq!(asset, protocol_contract_address::fee_juice());
211    }
212
213    #[tokio::test]
214    async fn get_fee_payer_with_no_inner() {
215        let account = mock_account(42);
216        let meta = AccountEntrypointMetaPaymentMethod::new(account, None, None);
217
218        let payer = meta.get_fee_payer().await.expect("payer");
219        assert_eq!(payer, AztecAddress::from(42u64));
220    }
221
222    #[tokio::test]
223    async fn get_fee_payer_delegates_to_inner() {
224        let account = mock_account(42);
225        let inner: Arc<dyn FeePaymentMethod> = Arc::new(MockInnerFeeMethod {
226            payer: AztecAddress::from(99u64),
227        });
228        let meta = AccountEntrypointMetaPaymentMethod::new(account, Some(inner), None);
229
230        let payer = meta.get_fee_payer().await.expect("payer");
231        assert_eq!(payer, AztecAddress::from(99u64));
232    }
233
234    #[tokio::test]
235    async fn explicit_options_override_auto_detection() {
236        let account = mock_account(42);
237        let explicit_opts = DefaultAccountEntrypointOptions {
238            cancellable: true,
239            tx_nonce: Some(Fr::from(7u64)),
240            fee_payment_method_options: AccountFeePaymentMethodOptions::External,
241        };
242        let meta = AccountEntrypointMetaPaymentMethod::new(account, None, Some(explicit_opts));
243
244        // Should not panic (auto-detection not triggered)
245        let _payload = meta.get_fee_execution_payload().await.expect("payload");
246    }
247}