aztec_account/entrypoint/
multi_call_entrypoint.rs

1//! Default multi-call entrypoint for unsigned transactions.
2//!
3//! Port of TS `yarn-project/entrypoints/src/default_multi_call_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::constants::protocol_contract_address;
12use aztec_core::fee::GasSettings;
13use aztec_core::hash::ChainInfo;
14use aztec_core::tx::{ExecutionPayload, FunctionCall, HashedValues, TxContext};
15use aztec_core::types::{AztecAddress, Fr};
16use aztec_core::Error;
17
18use crate::account::TxExecutionRequest;
19
20use super::encoding::EncodedAppEntrypointCalls;
21
22/// Multi-call entrypoint for unsigned transactions.
23pub struct DefaultMultiCallEntrypoint {
24    address: AztecAddress,
25}
26
27impl DefaultMultiCallEntrypoint {
28    /// Create a new multi-call entrypoint using the protocol default address.
29    pub fn new() -> Self {
30        Self {
31            address: protocol_contract_address::multi_call_entrypoint(),
32        }
33    }
34
35    /// Create a multi-call entrypoint with a custom address.
36    pub fn with_address(address: AztecAddress) -> Self {
37        Self { address }
38    }
39
40    /// Create a full transaction execution request.
41    pub fn create_tx_execution_request(
42        &self,
43        exec: ExecutionPayload,
44        gas_settings: GasSettings,
45        chain_info: &ChainInfo,
46    ) -> Result<TxExecutionRequest, Error> {
47        let call_data = self.build_entrypoint_call_data(&exec)?;
48        let entrypoint_hashed_args = HashedValues::from_args(call_data.encoded_args.clone());
49
50        let mut args_of_calls = call_data.encoded_calls.hashed_args().to_vec();
51        args_of_calls.push(entrypoint_hashed_args.clone());
52        args_of_calls.extend(exec.extra_hashed_args);
53
54        Ok(TxExecutionRequest {
55            origin: self.address,
56            function_selector: call_data.function_selector,
57            first_call_args_hash: entrypoint_hashed_args.hash,
58            tx_context: TxContext {
59                chain_id: chain_info.chain_id,
60                version: chain_info.version,
61                gas_settings,
62            },
63            args_of_calls,
64            auth_witnesses: exec.auth_witnesses,
65            capsules: exec.capsules,
66            salt: Fr::random(),
67            fee_payer: exec.fee_payer.filter(|fp| *fp != self.address),
68        })
69    }
70
71    /// Wrap an `ExecutionPayload` through the multi-call entrypoint.
72    pub fn wrap_execution_payload(
73        &self,
74        exec: ExecutionPayload,
75    ) -> Result<ExecutionPayload, Error> {
76        let call_data = self.build_entrypoint_call_data(&exec)?;
77
78        let entrypoint_call = FunctionCall {
79            to: self.address,
80            selector: call_data.function_selector,
81            args: vec![build_app_payload_value(&call_data.encoded_calls)],
82            function_type: FunctionType::Private,
83            is_static: false,
84            hide_msg_sender: false,
85        };
86
87        let mut extra_hashed_args = call_data.encoded_calls.hashed_args().to_vec();
88        extra_hashed_args.extend(exec.extra_hashed_args);
89
90        Ok(ExecutionPayload {
91            calls: vec![entrypoint_call],
92            auth_witnesses: exec.auth_witnesses,
93            capsules: exec.capsules,
94            extra_hashed_args,
95            fee_payer: exec.fee_payer,
96        })
97    }
98
99    fn build_entrypoint_call_data(
100        &self,
101        exec: &ExecutionPayload,
102    ) -> Result<EntrypointCallData, Error> {
103        let encoded_calls = EncodedAppEntrypointCalls::create(&exec.calls, None)?;
104        let abi = Self::entrypoint_abi();
105        let encoded_args = encode_arguments(&abi, &[build_app_payload_value(&encoded_calls)])?;
106        let function_selector =
107            FunctionSelector::from_name_and_parameters(&abi.name, &abi.parameters);
108
109        Ok(EntrypointCallData {
110            encoded_calls,
111            encoded_args,
112            function_selector,
113        })
114    }
115
116    fn entrypoint_abi() -> FunctionArtifact {
117        let function_selector_struct = AbiType::Struct {
118            name: "authwit::aztec::protocol_types::abis::function_selector::FunctionSelector"
119                .to_owned(),
120            fields: vec![AbiParameter {
121                name: "inner".to_owned(),
122                typ: AbiType::Integer {
123                    sign: "unsigned".to_owned(),
124                    width: 32,
125                },
126                visibility: None,
127            }],
128        };
129        let address_struct = AbiType::Struct {
130            name: "authwit::aztec::protocol_types::address::AztecAddress".to_owned(),
131            fields: vec![AbiParameter {
132                name: "inner".to_owned(),
133                typ: AbiType::Field,
134                visibility: None,
135            }],
136        };
137        let function_call_struct = AbiType::Struct {
138            name: "authwit::entrypoint::function_call::FunctionCall".to_owned(),
139            fields: vec![
140                AbiParameter {
141                    name: "args_hash".to_owned(),
142                    typ: AbiType::Field,
143                    visibility: None,
144                },
145                AbiParameter {
146                    name: "function_selector".to_owned(),
147                    typ: function_selector_struct,
148                    visibility: None,
149                },
150                AbiParameter {
151                    name: "target_address".to_owned(),
152                    typ: address_struct,
153                    visibility: None,
154                },
155                AbiParameter {
156                    name: "is_public".to_owned(),
157                    typ: AbiType::Boolean,
158                    visibility: None,
159                },
160                AbiParameter {
161                    name: "hide_msg_sender".to_owned(),
162                    typ: AbiType::Boolean,
163                    visibility: None,
164                },
165                AbiParameter {
166                    name: "is_static".to_owned(),
167                    typ: AbiType::Boolean,
168                    visibility: None,
169                },
170            ],
171        };
172        let app_payload_struct = AbiType::Struct {
173            name: "authwit::entrypoint::app::AppPayload".to_owned(),
174            fields: vec![
175                AbiParameter {
176                    name: "function_calls".to_owned(),
177                    typ: AbiType::Array {
178                        element: Box::new(function_call_struct),
179                        length: 5,
180                    },
181                    visibility: None,
182                },
183                AbiParameter {
184                    name: "tx_nonce".to_owned(),
185                    typ: AbiType::Field,
186                    visibility: None,
187                },
188            ],
189        };
190
191        FunctionArtifact {
192            name: "entrypoint".to_owned(),
193            function_type: FunctionType::Private,
194            is_initializer: false,
195            is_static: false,
196            is_only_self: Some(false),
197            parameters: vec![AbiParameter {
198                name: "app_payload".to_owned(),
199                typ: app_payload_struct,
200                visibility: Some("public".to_owned()),
201            }],
202            return_types: vec![],
203            error_types: Some(serde_json::Value::Object(Default::default())),
204            selector: None,
205            bytecode: None,
206            verification_key_hash: None,
207            verification_key: None,
208            custom_attributes: None,
209            is_unconstrained: None,
210            debug_symbols: None,
211        }
212    }
213
214    /// Get the address of this entrypoint.
215    pub fn address(&self) -> AztecAddress {
216        self.address
217    }
218}
219
220impl Default for DefaultMultiCallEntrypoint {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226struct EntrypointCallData {
227    encoded_calls: EncodedAppEntrypointCalls,
228    encoded_args: Vec<Fr>,
229    function_selector: FunctionSelector,
230}
231
232fn build_app_payload_value(encoded_calls: &EncodedAppEntrypointCalls) -> AbiValue {
233    let mut payload = BTreeMap::new();
234    payload.insert(
235        "function_calls".to_owned(),
236        AbiValue::Array(
237            encoded_calls
238                .encoded_calls()
239                .iter()
240                .map(|call| {
241                    let mut value = BTreeMap::new();
242                    value.insert("args_hash".to_owned(), AbiValue::Field(call.args_hash));
243                    value.insert(
244                        "function_selector".to_owned(),
245                        AbiValue::Field(call.function_selector),
246                    );
247                    value.insert(
248                        "target_address".to_owned(),
249                        AbiValue::Field(call.target_address),
250                    );
251                    value.insert("is_public".to_owned(), AbiValue::Boolean(call.is_public));
252                    value.insert(
253                        "hide_msg_sender".to_owned(),
254                        AbiValue::Boolean(call.hide_msg_sender),
255                    );
256                    value.insert("is_static".to_owned(), AbiValue::Boolean(call.is_static));
257                    AbiValue::Struct(value)
258                })
259                .collect(),
260        ),
261    );
262    payload.insert(
263        "tx_nonce".to_owned(),
264        AbiValue::Field(encoded_calls.tx_nonce()),
265    );
266    AbiValue::Struct(payload)
267}
268
269#[cfg(test)]
270#[allow(clippy::unwrap_used, clippy::expect_used)]
271mod tests {
272    use super::*;
273    use aztec_core::abi::FunctionSelector;
274
275    fn sample_chain_info() -> ChainInfo {
276        ChainInfo {
277            chain_id: Fr::from(31337u64),
278            version: Fr::from(1u64),
279        }
280    }
281
282    #[test]
283    fn default_address_is_multi_call_entrypoint() {
284        let ep = DefaultMultiCallEntrypoint::new();
285        assert_eq!(
286            ep.address(),
287            protocol_contract_address::multi_call_entrypoint()
288        );
289    }
290
291    #[test]
292    fn wrap_creates_single_call_to_multi_call_address() {
293        let ep = DefaultMultiCallEntrypoint::new();
294        let exec = ExecutionPayload {
295            calls: vec![FunctionCall {
296                to: AztecAddress::from(1u64),
297                selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
298                args: vec![AbiValue::Field(Fr::from(42u64))],
299                function_type: FunctionType::Private,
300                is_static: false,
301                hide_msg_sender: false,
302            }],
303            ..Default::default()
304        };
305
306        let wrapped = ep.wrap_execution_payload(exec).expect("wrap");
307
308        assert_eq!(wrapped.calls.len(), 1);
309        assert_eq!(
310            wrapped.calls[0].to,
311            protocol_contract_address::multi_call_entrypoint()
312        );
313        assert!(wrapped.auth_witnesses.is_empty());
314        assert_eq!(wrapped.extra_hashed_args.len(), 5);
315    }
316
317    #[test]
318    fn create_tx_execution_request_uses_upstream_shape() {
319        let ep = DefaultMultiCallEntrypoint::new();
320        let exec = ExecutionPayload {
321            calls: vec![FunctionCall {
322                to: AztecAddress::from(1u64),
323                selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
324                args: vec![AbiValue::Field(Fr::from(42u64))],
325                function_type: FunctionType::Private,
326                is_static: false,
327                hide_msg_sender: false,
328            }],
329            ..Default::default()
330        };
331
332        let request = ep
333            .create_tx_execution_request(exec, GasSettings::default(), &sample_chain_info())
334            .expect("request");
335
336        assert_eq!(
337            request.origin,
338            protocol_contract_address::multi_call_entrypoint()
339        );
340        assert_eq!(request.args_of_calls.len(), 6);
341        assert_ne!(request.first_call_args_hash, Fr::zero());
342    }
343
344    #[test]
345    fn custom_address() {
346        let ep = DefaultMultiCallEntrypoint::with_address(AztecAddress::from(99u64));
347        assert_eq!(ep.address(), AztecAddress::from(99u64));
348    }
349}