aztec_account/entrypoint/
account_entrypoint.rs

1//! Default account entrypoint — wraps calls through the account contract.
2//!
3//! Port of TS `yarn-project/entrypoints/src/account_entrypoint.ts`.
4
5use std::collections::BTreeMap;
6
7use aztec_core::abi::{
8    encode_arguments, AbiParameter, AbiType, AbiValue, FunctionArtifact, FunctionSelector,
9    FunctionType,
10};
11use aztec_core::fee::GasSettings;
12use aztec_core::hash::ChainInfo;
13use aztec_core::tx::{ExecutionPayload, FunctionCall, HashedValues, TxContext};
14use aztec_core::types::{AztecAddress, Fr};
15use aztec_core::Error;
16
17use crate::account::{AuthorizationProvider, EntrypointOptions, TxExecutionRequest};
18use crate::wallet::MessageHashOrIntent;
19
20use super::encoding::EncodedAppEntrypointCalls;
21
22/// Fee payment method options for account entrypoints.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum AccountFeePaymentMethodOptions {
25    /// Another contract/account pays fees.
26    External = 0,
27    /// Account pays from existing Fee Juice balance.
28    PreexistingFeeJuice = 1,
29    /// Account claims Fee Juice from L1 bridge and pays in same tx.
30    FeeJuiceWithClaim = 2,
31}
32
33/// Options for `DefaultAccountEntrypoint`.
34#[derive(Clone, Debug)]
35pub struct DefaultAccountEntrypointOptions {
36    /// Whether the transaction can be cancelled.
37    pub cancellable: bool,
38    /// Optional explicit tx nonce (random if None).
39    pub tx_nonce: Option<Fr>,
40    /// Fee payment method option.
41    pub fee_payment_method_options: AccountFeePaymentMethodOptions,
42}
43
44impl Default for DefaultAccountEntrypointOptions {
45    fn default() -> Self {
46        Self {
47            cancellable: false,
48            tx_nonce: None,
49            fee_payment_method_options: AccountFeePaymentMethodOptions::External,
50        }
51    }
52}
53
54impl From<DefaultAccountEntrypointOptions> for EntrypointOptions {
55    fn from(_opts: DefaultAccountEntrypointOptions) -> Self {
56        Self {
57            fee_payer: None,
58            gas_settings: None,
59            fee_payment_method: None,
60        }
61    }
62}
63
64struct EntrypointCallData {
65    encoded_calls: EncodedAppEntrypointCalls,
66    encoded_args: Vec<Fr>,
67    function_selector: FunctionSelector,
68    payload_auth_witness: aztec_core::tx::AuthWitness,
69}
70
71/// Standard account entrypoint — wraps calls through the account contract.
72pub struct DefaultAccountEntrypoint {
73    address: AztecAddress,
74    auth: Box<dyn AuthorizationProvider>,
75}
76
77impl DefaultAccountEntrypoint {
78    /// Create a new account entrypoint for the given address and auth provider.
79    pub fn new(address: AztecAddress, auth: Box<dyn AuthorizationProvider>) -> Self {
80        Self { address, auth }
81    }
82
83    /// Create a full transaction execution request.
84    pub async fn create_tx_execution_request(
85        &self,
86        exec: ExecutionPayload,
87        gas_settings: GasSettings,
88        chain_info: &ChainInfo,
89        options: &DefaultAccountEntrypointOptions,
90    ) -> Result<TxExecutionRequest, Error> {
91        let call_data = self
92            .build_entrypoint_call_data(&exec, chain_info, options)
93            .await?;
94        let entrypoint_hashed_args = HashedValues::from_args(call_data.encoded_args.clone());
95
96        let mut args_of_calls = call_data.encoded_calls.hashed_args().to_vec();
97        args_of_calls.push(entrypoint_hashed_args.clone());
98        args_of_calls.extend(exec.extra_hashed_args);
99
100        let mut auth_witnesses = exec.auth_witnesses;
101        auth_witnesses.push(call_data.payload_auth_witness);
102
103        let fee_payer = exec.fee_payer.filter(|fp| *fp != self.address);
104
105        Ok(TxExecutionRequest {
106            origin: self.address,
107            function_selector: call_data.function_selector,
108            first_call_args_hash: entrypoint_hashed_args.hash,
109            tx_context: TxContext {
110                chain_id: chain_info.chain_id,
111                version: chain_info.version,
112                gas_settings,
113            },
114            args_of_calls,
115            auth_witnesses,
116            capsules: exec.capsules,
117            salt: Fr::random(),
118            fee_payer,
119        })
120    }
121
122    /// Create a wrapped `ExecutionPayload` by encoding calls through the account entrypoint.
123    pub async fn wrap_execution_payload(
124        &self,
125        exec: ExecutionPayload,
126        chain_info: &ChainInfo,
127        options: &DefaultAccountEntrypointOptions,
128    ) -> Result<ExecutionPayload, Error> {
129        let call_data = self
130            .build_entrypoint_call_data(&exec, chain_info, options)
131            .await?;
132
133        let entrypoint_call = FunctionCall {
134            to: self.address,
135            selector: call_data.function_selector,
136            args: vec![
137                build_app_payload_value(&call_data.encoded_calls),
138                AbiValue::Integer(options.fee_payment_method_options as i128),
139                AbiValue::Boolean(options.cancellable),
140            ],
141            function_type: FunctionType::Private,
142            is_static: false,
143            hide_msg_sender: false,
144        };
145
146        let mut wrapped_auth_witnesses = vec![call_data.payload_auth_witness];
147        wrapped_auth_witnesses.extend(exec.auth_witnesses);
148
149        let mut wrapped_hashed_args = call_data.encoded_calls.hashed_args().to_vec();
150        wrapped_hashed_args.extend(exec.extra_hashed_args);
151
152        Ok(ExecutionPayload {
153            calls: vec![entrypoint_call],
154            auth_witnesses: wrapped_auth_witnesses,
155            capsules: exec.capsules,
156            extra_hashed_args: wrapped_hashed_args,
157            fee_payer: exec.fee_payer.or(Some(self.address)),
158        })
159    }
160
161    /// Return the ABI for the standard account `entrypoint` function.
162    ///
163    /// This is useful for account contract implementations that need to
164    /// include the entrypoint in their contract artifact.
165    pub fn entrypoint_abi() -> FunctionArtifact {
166        let function_selector_struct = AbiType::Struct {
167            name: "authwit::aztec::protocol_types::abis::function_selector::FunctionSelector"
168                .to_owned(),
169            fields: vec![AbiParameter {
170                name: "inner".to_owned(),
171                typ: AbiType::Integer {
172                    sign: "unsigned".to_owned(),
173                    width: 32,
174                },
175                visibility: None,
176            }],
177        };
178        let address_struct = AbiType::Struct {
179            name: "authwit::aztec::protocol_types::address::AztecAddress".to_owned(),
180            fields: vec![AbiParameter {
181                name: "inner".to_owned(),
182                typ: AbiType::Field,
183                visibility: None,
184            }],
185        };
186        let function_call_struct = AbiType::Struct {
187            name: "authwit::entrypoint::function_call::FunctionCall".to_owned(),
188            fields: vec![
189                AbiParameter {
190                    name: "args_hash".to_owned(),
191                    typ: AbiType::Field,
192                    visibility: None,
193                },
194                AbiParameter {
195                    name: "function_selector".to_owned(),
196                    typ: function_selector_struct,
197                    visibility: None,
198                },
199                AbiParameter {
200                    name: "target_address".to_owned(),
201                    typ: address_struct,
202                    visibility: None,
203                },
204                AbiParameter {
205                    name: "is_public".to_owned(),
206                    typ: AbiType::Boolean,
207                    visibility: None,
208                },
209                AbiParameter {
210                    name: "hide_msg_sender".to_owned(),
211                    typ: AbiType::Boolean,
212                    visibility: None,
213                },
214                AbiParameter {
215                    name: "is_static".to_owned(),
216                    typ: AbiType::Boolean,
217                    visibility: None,
218                },
219            ],
220        };
221        let app_payload_struct = AbiType::Struct {
222            name: "authwit::entrypoint::app::AppPayload".to_owned(),
223            fields: vec![
224                AbiParameter {
225                    name: "function_calls".to_owned(),
226                    typ: AbiType::Array {
227                        element: Box::new(function_call_struct),
228                        length: 5,
229                    },
230                    visibility: None,
231                },
232                AbiParameter {
233                    name: "tx_nonce".to_owned(),
234                    typ: AbiType::Field,
235                    visibility: None,
236                },
237            ],
238        };
239
240        FunctionArtifact {
241            name: "entrypoint".to_owned(),
242            function_type: FunctionType::Private,
243            is_initializer: false,
244            is_static: false,
245            is_only_self: Some(false),
246            parameters: vec![
247                AbiParameter {
248                    name: "app_payload".to_owned(),
249                    typ: app_payload_struct,
250                    visibility: Some("public".to_owned()),
251                },
252                AbiParameter {
253                    name: "fee_payment_method".to_owned(),
254                    typ: AbiType::Integer {
255                        sign: "unsigned".to_owned(),
256                        width: 8,
257                    },
258                    visibility: None,
259                },
260                AbiParameter {
261                    name: "cancellable".to_owned(),
262                    typ: AbiType::Boolean,
263                    visibility: None,
264                },
265            ],
266            return_types: vec![],
267            error_types: Some(serde_json::Value::Object(Default::default())),
268            selector: None,
269            bytecode: None,
270            verification_key_hash: None,
271            verification_key: None,
272            custom_attributes: None,
273            is_unconstrained: None,
274            debug_symbols: None,
275        }
276    }
277
278    async fn build_entrypoint_call_data(
279        &self,
280        exec: &ExecutionPayload,
281        chain_info: &ChainInfo,
282        options: &DefaultAccountEntrypointOptions,
283    ) -> Result<EntrypointCallData, Error> {
284        let encoded_calls = EncodedAppEntrypointCalls::create(&exec.calls, options.tx_nonce)?;
285        let abi = Self::entrypoint_abi();
286        let encoded_args = encode_arguments(
287            &abi,
288            &[
289                build_app_payload_value(&encoded_calls),
290                AbiValue::Integer(options.fee_payment_method_options as i128),
291                AbiValue::Boolean(options.cancellable),
292            ],
293        )?;
294        let function_selector =
295            FunctionSelector::from_name_and_parameters(&abi.name, &abi.parameters);
296        let payload_auth_witness = self
297            .auth
298            .create_auth_wit(
299                MessageHashOrIntent::Hash {
300                    hash: encoded_calls.hash(),
301                },
302                chain_info,
303            )
304            .await?;
305
306        Ok(EntrypointCallData {
307            encoded_calls,
308            encoded_args,
309            function_selector,
310            payload_auth_witness,
311        })
312    }
313
314    /// Get the address of this entrypoint's account.
315    pub fn address(&self) -> AztecAddress {
316        self.address
317    }
318}
319
320fn build_app_payload_value(encoded_calls: &EncodedAppEntrypointCalls) -> AbiValue {
321    let mut payload = BTreeMap::new();
322    payload.insert(
323        "function_calls".to_owned(),
324        AbiValue::Array(
325            encoded_calls
326                .encoded_calls()
327                .iter()
328                .map(build_encoded_call_value)
329                .collect(),
330        ),
331    );
332    payload.insert(
333        "tx_nonce".to_owned(),
334        AbiValue::Field(encoded_calls.tx_nonce()),
335    );
336    AbiValue::Struct(payload)
337}
338
339fn build_encoded_call_value(call: &super::encoding::EncodedCallView) -> AbiValue {
340    let mut value = BTreeMap::new();
341    value.insert("args_hash".to_owned(), AbiValue::Field(call.args_hash));
342    value.insert(
343        "function_selector".to_owned(),
344        AbiValue::Field(call.function_selector),
345    );
346    value.insert(
347        "target_address".to_owned(),
348        AbiValue::Field(call.target_address),
349    );
350    value.insert("is_public".to_owned(), AbiValue::Boolean(call.is_public));
351    value.insert(
352        "hide_msg_sender".to_owned(),
353        AbiValue::Boolean(call.hide_msg_sender),
354    );
355    value.insert("is_static".to_owned(), AbiValue::Boolean(call.is_static));
356    AbiValue::Struct(value)
357}
358
359#[cfg(test)]
360#[allow(clippy::unwrap_used, clippy::expect_used)]
361mod tests {
362    use super::*;
363    use async_trait::async_trait;
364    use aztec_core::abi::{AbiValue, FunctionSelector, FunctionType};
365    use aztec_core::tx::AuthWitness;
366
367    struct MockAuth;
368
369    #[async_trait]
370    impl AuthorizationProvider for MockAuth {
371        async fn create_auth_wit(
372            &self,
373            intent: MessageHashOrIntent,
374            _chain_info: &ChainInfo,
375        ) -> Result<AuthWitness, Error> {
376            let hash = match intent {
377                MessageHashOrIntent::Hash { hash } => hash,
378                _ => Fr::zero(),
379            };
380            Ok(AuthWitness {
381                request_hash: hash,
382                fields: vec![hash, Fr::from(1u64)],
383            })
384        }
385    }
386
387    fn sample_chain_info() -> ChainInfo {
388        ChainInfo {
389            chain_id: Fr::from(31337u64),
390            version: Fr::from(1u64),
391        }
392    }
393
394    fn sample_exec() -> ExecutionPayload {
395        ExecutionPayload {
396            calls: vec![FunctionCall {
397                to: AztecAddress::from(1u64),
398                selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
399                args: vec![AbiValue::Field(Fr::from(99u64))],
400                function_type: FunctionType::Private,
401                is_static: false,
402                hide_msg_sender: false,
403            }],
404            ..Default::default()
405        }
406    }
407
408    #[tokio::test]
409    async fn wrap_creates_single_entrypoint_call() {
410        let entrypoint =
411            DefaultAccountEntrypoint::new(AztecAddress::from(42u64), Box::new(MockAuth));
412        let wrapped = entrypoint
413            .wrap_execution_payload(
414                sample_exec(),
415                &sample_chain_info(),
416                &DefaultAccountEntrypointOptions::default(),
417            )
418            .await
419            .expect("wrap");
420
421        assert_eq!(wrapped.calls.len(), 1);
422        assert_eq!(wrapped.calls[0].to, AztecAddress::from(42u64));
423        assert_eq!(wrapped.auth_witnesses.len(), 1);
424        assert!(!wrapped.extra_hashed_args.is_empty());
425        assert_eq!(wrapped.fee_payer, Some(AztecAddress::from(42u64)));
426    }
427
428    #[tokio::test]
429    async fn wrap_prepends_payload_auth_witness() {
430        let entrypoint =
431            DefaultAccountEntrypoint::new(AztecAddress::from(42u64), Box::new(MockAuth));
432        let exec = ExecutionPayload {
433            auth_witnesses: vec![AuthWitness {
434                request_hash: Fr::from(1u64),
435                fields: vec![Fr::from(999u64)],
436            }],
437            ..sample_exec()
438        };
439
440        let wrapped = entrypoint
441            .wrap_execution_payload(
442                exec,
443                &sample_chain_info(),
444                &DefaultAccountEntrypointOptions::default(),
445            )
446            .await
447            .expect("wrap");
448
449        assert_eq!(wrapped.auth_witnesses.len(), 2);
450        assert_ne!(wrapped.auth_witnesses[0].fields, vec![Fr::from(999u64)]);
451        assert_eq!(wrapped.auth_witnesses[1].fields, vec![Fr::from(999u64)]);
452    }
453
454    #[tokio::test]
455    async fn create_tx_execution_request_uses_upstream_shape() {
456        let entrypoint =
457            DefaultAccountEntrypoint::new(AztecAddress::from(42u64), Box::new(MockAuth));
458        let gas_settings = GasSettings::default();
459
460        let request = entrypoint
461            .create_tx_execution_request(
462                sample_exec(),
463                gas_settings.clone(),
464                &sample_chain_info(),
465                &DefaultAccountEntrypointOptions::default(),
466            )
467            .await
468            .expect("request");
469
470        assert_eq!(request.origin, AztecAddress::from(42u64));
471        assert_eq!(request.tx_context.gas_settings, gas_settings);
472        assert_eq!(request.args_of_calls.len(), 6);
473        assert_eq!(request.auth_witnesses.len(), 1);
474        assert_ne!(request.first_call_args_hash, Fr::zero());
475    }
476}