aztec_contract/
contract.rs

1use crate::abi::{AbiValue, ContractArtifact};
2use crate::deployment::{ContractDeployer, DeployMethod};
3use crate::error::Error;
4use crate::tx::{AuthWitness, Capsule, ExecutionPayload, FunctionCall, HashedValues};
5use crate::types::{AztecAddress, PublicKeys};
6use crate::wallet::{
7    ProfileOptions, SendOptions, SendResult, SimulateOptions, TxProfileResult, TxSimulationResult,
8    Wallet,
9};
10
11// ---------------------------------------------------------------------------
12// Contract
13// ---------------------------------------------------------------------------
14
15/// A handle to a deployed contract at a specific address.
16///
17/// Provides dynamic method lookup and call construction driven by the
18/// contract artifact (ABI). Use [`Contract::at`] to create a handle.
19pub struct Contract<W> {
20    /// The deployed contract's address.
21    pub address: AztecAddress,
22    /// The contract's ABI artifact.
23    pub artifact: ContractArtifact,
24    wallet: W,
25}
26
27impl<W: Wallet> Contract<W> {
28    /// Create a contract handle at the given address.
29    pub const fn at(address: AztecAddress, artifact: ContractArtifact, wallet: W) -> Self {
30        Self {
31            address,
32            artifact,
33            wallet,
34        }
35    }
36
37    /// Create a deployment interaction for the given artifact and constructor args.
38    ///
39    /// Uses default (empty) public keys. For custom public keys, use
40    /// [`deploy_with_public_keys`](Self::deploy_with_public_keys).
41    pub fn deploy<'a>(
42        wallet: &'a W,
43        artifact: ContractArtifact,
44        args: Vec<AbiValue>,
45        constructor_name: Option<&str>,
46    ) -> Result<DeployMethod<'a, W>, Error> {
47        let mut deployer = ContractDeployer::new(artifact, wallet);
48        if let Some(name) = constructor_name {
49            deployer = deployer.with_constructor_name(name);
50        }
51        deployer.deploy(args)
52    }
53
54    /// Create a deployment interaction with custom public keys.
55    pub fn deploy_with_public_keys<'a>(
56        public_keys: PublicKeys,
57        wallet: &'a W,
58        artifact: ContractArtifact,
59        args: Vec<AbiValue>,
60        constructor_name: Option<&str>,
61    ) -> Result<DeployMethod<'a, W>, Error> {
62        let mut deployer = ContractDeployer::new(artifact, wallet).with_public_keys(public_keys);
63        if let Some(name) = constructor_name {
64            deployer = deployer.with_constructor_name(name);
65        }
66        deployer.deploy(args)
67    }
68
69    /// Return a new Contract handle using a different wallet.
70    pub fn with_wallet<W2: Wallet>(self, wallet: W2) -> Contract<W2> {
71        Contract {
72            address: self.address,
73            artifact: self.artifact,
74            wallet,
75        }
76    }
77
78    /// Look up a function by name and build a call interaction.
79    ///
80    /// The function's type (`Private`, `Public`, `Utility`) and `is_static`
81    /// flag are taken from the artifact metadata. The selector must be present
82    /// in the artifact; if missing, an error is returned.
83    pub fn method(
84        &self,
85        name: &str,
86        args: Vec<AbiValue>,
87    ) -> Result<ContractFunctionInteraction<'_, W>, Error> {
88        let func = self.artifact.find_function(name)?;
89        let expected = func.parameters.len();
90        let got = args.len();
91        if got != expected {
92            return Err(Error::Abi(format!(
93                "function '{name}' expects {expected} argument(s), got {got}"
94            )));
95        }
96        let selector = func.selector.ok_or_else(|| {
97            Error::Abi(format!(
98                "function '{}' in artifact '{}' has no selector",
99                name, self.artifact.name
100            ))
101        })?;
102        let encoded_args = aztec_core::abi::encode_arguments(func, &args)?;
103        let call = FunctionCall {
104            to: self.address,
105            selector,
106            args: encoded_args.into_iter().map(AbiValue::Field).collect(),
107            function_type: func.function_type.clone(),
108            is_static: func.is_static,
109            hide_msg_sender: false,
110        };
111        Ok(ContractFunctionInteraction {
112            wallet: &self.wallet,
113            call,
114            capsules: vec![],
115            auth_witnesses: vec![],
116            extra_hashed_args: vec![],
117        })
118    }
119}
120
121// ---------------------------------------------------------------------------
122// Fee payload merging
123// ---------------------------------------------------------------------------
124
125/// Merge an optional fee execution payload into the main payload.
126pub(crate) fn merge_fee_payload(
127    mut payload: ExecutionPayload,
128    fee: &Option<ExecutionPayload>,
129) -> Result<ExecutionPayload, Error> {
130    if let Some(fee_payload) = fee {
131        payload.calls.extend(fee_payload.calls.clone());
132        payload
133            .auth_witnesses
134            .extend(fee_payload.auth_witnesses.clone());
135        payload.capsules.extend(fee_payload.capsules.clone());
136        payload
137            .extra_hashed_args
138            .extend(fee_payload.extra_hashed_args.clone());
139        if let Some(payer) = fee_payload.fee_payer {
140            payload.fee_payer = Some(payer);
141        }
142    }
143    Ok(payload)
144}
145
146// ---------------------------------------------------------------------------
147// ContractFunctionInteraction
148// ---------------------------------------------------------------------------
149
150/// A pending interaction with a single contract function.
151///
152/// Created by [`Contract::method`]. Use [`request`](Self::request) to get the
153/// raw execution payload, [`simulate`](Self::simulate) to dry-run, or
154/// [`send`](Self::send) to submit to the network.
155pub struct ContractFunctionInteraction<'a, W> {
156    wallet: &'a W,
157    call: FunctionCall,
158    capsules: Vec<Capsule>,
159    auth_witnesses: Vec<AuthWitness>,
160    extra_hashed_args: Vec<HashedValues>,
161}
162
163impl<W> std::fmt::Debug for ContractFunctionInteraction<'_, W> {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.debug_struct("ContractFunctionInteraction")
166            .field("call", &self.call)
167            .finish_non_exhaustive()
168    }
169}
170
171impl<'a, W: Wallet> ContractFunctionInteraction<'a, W> {
172    /// Create a new interaction for a single function call.
173    pub fn new(wallet: &'a W, call: FunctionCall) -> Self {
174        Self {
175            wallet,
176            call,
177            capsules: vec![],
178            auth_witnesses: vec![],
179            extra_hashed_args: vec![],
180        }
181    }
182
183    /// Create a new interaction with capsules attached.
184    pub fn new_with_capsules(wallet: &'a W, call: FunctionCall, capsules: Vec<Capsule>) -> Self {
185        Self {
186            wallet,
187            call,
188            capsules,
189            auth_witnesses: vec![],
190            extra_hashed_args: vec![],
191        }
192    }
193
194    /// Return a new interaction with additional auth witnesses and capsules.
195    pub fn with(mut self, auth_witnesses: Vec<AuthWitness>, capsules: Vec<Capsule>) -> Self {
196        self.auth_witnesses.extend(auth_witnesses);
197        self.capsules.extend(capsules);
198        self
199    }
200
201    /// Returns the underlying [`FunctionCall`] for use in authwit hash computation.
202    pub fn get_function_call(&self) -> &FunctionCall {
203        &self.call
204    }
205
206    /// Build an [`ExecutionPayload`] containing this single call.
207    pub fn request(&self) -> Result<ExecutionPayload, Error> {
208        Ok(ExecutionPayload {
209            calls: vec![self.call.clone()],
210            capsules: self.capsules.clone(),
211            auth_witnesses: self.auth_witnesses.clone(),
212            extra_hashed_args: self.extra_hashed_args.clone(),
213            ..ExecutionPayload::default()
214        })
215    }
216
217    /// Simulate the call without sending it.
218    pub async fn simulate(&self, opts: SimulateOptions) -> Result<TxSimulationResult, Error> {
219        let payload = merge_fee_payload(self.request()?, &opts.fee_execution_payload)?;
220        self.wallet.simulate_tx(payload, opts).await
221    }
222
223    /// Profile the gate count / execution steps for this call.
224    pub async fn profile(&self, opts: ProfileOptions) -> Result<TxProfileResult, Error> {
225        let payload = merge_fee_payload(self.request()?, &opts.fee_execution_payload)?;
226        self.wallet.profile_tx(payload, opts).await
227    }
228
229    /// Send the call as a transaction.
230    pub async fn send(&self, opts: SendOptions) -> Result<SendResult, Error> {
231        let payload = merge_fee_payload(self.request()?, &opts.fee_execution_payload)?;
232        self.wallet.send_tx(payload, opts).await
233    }
234}
235
236// ---------------------------------------------------------------------------
237// BatchCall
238// ---------------------------------------------------------------------------
239
240/// A batch of interactions aggregated into a single transaction.
241///
242/// Merges multiple [`ExecutionPayload`]s into one, preserving all calls,
243/// auth witnesses, capsules, and extra hashed args from each payload.
244/// The fee payer is taken from the last payload that specifies one.
245pub struct BatchCall<'a, W> {
246    wallet: &'a W,
247    payloads: Vec<ExecutionPayload>,
248}
249
250impl<W> std::fmt::Debug for BatchCall<'_, W> {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        f.debug_struct("BatchCall")
253            .field("payload_count", &self.payloads.len())
254            .finish_non_exhaustive()
255    }
256}
257
258impl<'a, W: Wallet> BatchCall<'a, W> {
259    /// Create a new batch from a list of execution payloads.
260    pub const fn new(wallet: &'a W, payloads: Vec<ExecutionPayload>) -> Self {
261        Self { wallet, payloads }
262    }
263
264    /// Merge all payloads into a single [`ExecutionPayload`].
265    pub fn request(&self) -> Result<ExecutionPayload, Error> {
266        let mut merged = ExecutionPayload::default();
267
268        for payload in &self.payloads {
269            merged.calls.extend(payload.calls.clone());
270            merged.auth_witnesses.extend(payload.auth_witnesses.clone());
271            merged.capsules.extend(payload.capsules.clone());
272            merged
273                .extra_hashed_args
274                .extend(payload.extra_hashed_args.clone());
275
276            if payload.fee_payer.is_some() {
277                merged.fee_payer = payload.fee_payer;
278            }
279        }
280
281        Ok(merged)
282    }
283
284    /// Simulate the batch without sending.
285    pub async fn simulate(&self, opts: SimulateOptions) -> Result<TxSimulationResult, Error> {
286        let payload = merge_fee_payload(self.request()?, &opts.fee_execution_payload)?;
287        self.wallet.simulate_tx(payload, opts).await
288    }
289
290    /// Profile the batch as a single transaction.
291    pub async fn profile(&self, opts: ProfileOptions) -> Result<TxProfileResult, Error> {
292        let payload = merge_fee_payload(self.request()?, &opts.fee_execution_payload)?;
293        self.wallet.profile_tx(payload, opts).await
294    }
295
296    /// Send the batch as a single transaction.
297    pub async fn send(&self, opts: SendOptions) -> Result<SendResult, Error> {
298        let payload = merge_fee_payload(self.request()?, &opts.fee_execution_payload)?;
299        self.wallet.send_tx(payload, opts).await
300    }
301}
302
303// ---------------------------------------------------------------------------
304// Tests
305// ---------------------------------------------------------------------------
306
307#[cfg(test)]
308#[allow(clippy::unwrap_used, clippy::expect_used)]
309mod tests {
310    use super::*;
311    use crate::abi::{AbiValue, FunctionType};
312    use crate::fee::Gas;
313    use crate::tx::TxHash;
314    use crate::types::Fr;
315    use crate::wallet::{ChainInfo, MockWallet, SendResult, TxSimulationResult};
316
317    const TOKEN_ARTIFACT: &str = r#"
318    {
319      "name": "TokenContract",
320      "functions": [
321        {
322          "name": "constructor",
323          "function_type": "private",
324          "is_initializer": true,
325          "is_static": false,
326          "parameters": [
327            { "name": "admin", "type": { "kind": "field" } }
328          ],
329          "return_types": [],
330          "selector": "0xe5fb6c81"
331        },
332        {
333          "name": "transfer",
334          "function_type": "private",
335          "is_initializer": false,
336          "is_static": false,
337          "parameters": [
338            { "name": "from", "type": { "kind": "field" } },
339            { "name": "to", "type": { "kind": "field" } },
340            { "name": "amount", "type": { "kind": "integer", "sign": "unsigned", "width": 64 } }
341          ],
342          "return_types": [],
343          "selector": "0xd6f42325"
344        },
345        {
346          "name": "balance_of",
347          "function_type": "utility",
348          "is_initializer": false,
349          "is_static": true,
350          "parameters": [
351            { "name": "owner", "type": { "kind": "field" } }
352          ],
353          "return_types": [
354            { "kind": "integer", "sign": "unsigned", "width": 64 }
355          ],
356          "selector": "0x12345678"
357        },
358        {
359          "name": "total_supply",
360          "function_type": "public",
361          "is_initializer": false,
362          "is_static": true,
363          "parameters": [],
364          "return_types": [
365            { "kind": "integer", "sign": "unsigned", "width": 64 }
366          ],
367          "selector": "0xabcdef01"
368        }
369      ]
370    }
371    "#;
372
373    const NO_SELECTOR_ARTIFACT: &str = r#"
374    {
375      "name": "NoSelector",
376      "functions": [
377        {
378          "name": "foo",
379          "function_type": "public",
380          "is_initializer": false,
381          "is_static": false,
382          "parameters": [],
383          "return_types": []
384        }
385      ]
386    }
387    "#;
388
389    fn sample_chain_info() -> ChainInfo {
390        ChainInfo {
391            chain_id: Fr::from(31337u64),
392            version: Fr::from(1u64),
393        }
394    }
395
396    fn sample_address() -> AztecAddress {
397        AztecAddress(Fr::from(42u64))
398    }
399
400    fn load_token_artifact() -> ContractArtifact {
401        ContractArtifact::from_json(TOKEN_ARTIFACT).expect("parse token artifact")
402    }
403
404    // -- Contract::at --
405
406    #[test]
407    fn contract_at_creates_handle() {
408        let wallet = MockWallet::new(sample_chain_info());
409        let artifact = load_token_artifact();
410        let addr = sample_address();
411
412        let contract = Contract::at(addr, artifact, wallet);
413        assert_eq!(contract.address, addr);
414        assert_eq!(contract.artifact.name, "TokenContract");
415    }
416
417    // -- Contract::method --
418
419    #[test]
420    fn method_finds_function_and_builds_call() {
421        let wallet = MockWallet::new(sample_chain_info());
422        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
423
424        let interaction = contract
425            .method(
426                "transfer",
427                vec![
428                    AbiValue::Field(Fr::from(1u64)),
429                    AbiValue::Field(Fr::from(2u64)),
430                    AbiValue::Integer(100),
431                ],
432            )
433            .expect("find transfer");
434
435        assert_eq!(interaction.call.to, sample_address());
436        assert_eq!(interaction.call.function_type, FunctionType::Private);
437        assert!(!interaction.call.is_static);
438        assert_eq!(interaction.call.args.len(), 3);
439        assert_eq!(interaction.call.selector.to_string(), "0xd6f42325");
440    }
441
442    #[test]
443    fn method_preserves_private_type() {
444        let wallet = MockWallet::new(sample_chain_info());
445        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
446
447        let interaction = contract
448            .method("constructor", vec![AbiValue::Field(Fr::from(1u64))])
449            .expect("find constructor");
450        assert_eq!(interaction.call.function_type, FunctionType::Private);
451        assert!(!interaction.call.is_static);
452    }
453
454    #[test]
455    fn method_preserves_utility_static() {
456        let wallet = MockWallet::new(sample_chain_info());
457        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
458
459        let interaction = contract
460            .method("balance_of", vec![AbiValue::Field(Fr::from(1u64))])
461            .expect("find balance_of");
462        assert_eq!(interaction.call.function_type, FunctionType::Utility);
463        assert!(interaction.call.is_static);
464    }
465
466    #[test]
467    fn method_preserves_public_static() {
468        let wallet = MockWallet::new(sample_chain_info());
469        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
470
471        let interaction = contract
472            .method("total_supply", vec![])
473            .expect("find total_supply");
474        assert_eq!(interaction.call.function_type, FunctionType::Public);
475        assert!(interaction.call.is_static);
476    }
477
478    #[test]
479    fn method_not_found_returns_error() {
480        let wallet = MockWallet::new(sample_chain_info());
481        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
482
483        let result = contract.method("nonexistent", vec![]);
484        assert!(result.is_err());
485        let err = result.unwrap_err();
486        assert!(
487            err.to_string().contains("nonexistent"),
488            "error should mention function name: {err}"
489        );
490    }
491
492    #[test]
493    fn method_without_selector_returns_error() {
494        let wallet = MockWallet::new(sample_chain_info());
495        let artifact =
496            ContractArtifact::from_json(NO_SELECTOR_ARTIFACT).expect("parse no-selector artifact");
497        let contract = Contract::at(sample_address(), artifact, wallet);
498
499        let result = contract.method("foo", vec![]);
500        assert!(result.is_err());
501        let err = result.unwrap_err();
502        assert!(
503            err.to_string().contains("no selector"),
504            "error should mention missing selector: {err}"
505        );
506    }
507
508    #[test]
509    fn method_argument_count_mismatch_returns_error() {
510        let wallet = MockWallet::new(sample_chain_info());
511        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
512
513        let result = contract.method(
514            "transfer",
515            vec![
516                AbiValue::Field(Fr::from(1u64)),
517                AbiValue::Field(Fr::from(2u64)),
518            ],
519        );
520
521        assert!(result.is_err());
522        let err = result.unwrap_err();
523        assert!(
524            err.to_string().contains("expects 3 argument(s), got 2"),
525            "error should mention argument mismatch: {err}"
526        );
527    }
528
529    // -- ContractFunctionInteraction::request --
530
531    #[test]
532    fn request_wraps_single_call() {
533        let wallet = MockWallet::new(sample_chain_info());
534        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
535
536        let interaction = contract
537            .method(
538                "transfer",
539                vec![
540                    AbiValue::Field(Fr::from(1u64)),
541                    AbiValue::Field(Fr::from(2u64)),
542                    AbiValue::Integer(100),
543                ],
544            )
545            .expect("find transfer");
546        let payload = interaction.request().expect("build payload");
547
548        assert_eq!(payload.calls.len(), 1);
549        assert_eq!(payload.calls[0].to, sample_address());
550        assert_eq!(payload.calls[0].selector.to_string(), "0xd6f42325");
551        assert!(payload.auth_witnesses.is_empty());
552        assert!(payload.capsules.is_empty());
553        assert!(payload.extra_hashed_args.is_empty());
554        assert!(payload.fee_payer.is_none());
555    }
556
557    // -- ContractFunctionInteraction::simulate --
558
559    #[tokio::test]
560    async fn simulate_delegates_to_wallet() {
561        let wallet =
562            MockWallet::new(sample_chain_info()).with_simulate_result(TxSimulationResult {
563                return_values: serde_json::json!({"balance": 1000}),
564                gas_used: Some(Gas {
565                    da_gas: 10,
566                    l2_gas: 20,
567                }),
568            });
569        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
570
571        let result = contract
572            .method("balance_of", vec![AbiValue::Field(Fr::from(1u64))])
573            .expect("find balance_of")
574            .simulate(SimulateOptions::default())
575            .await
576            .expect("simulate");
577
578        assert_eq!(result.return_values, serde_json::json!({"balance": 1000}));
579        assert_eq!(result.gas_used.as_ref().map(|g| g.l2_gas), Some(20));
580    }
581
582    // -- ContractFunctionInteraction::send --
583
584    #[tokio::test]
585    async fn send_delegates_to_wallet() {
586        let tx_hash =
587            TxHash::from_hex("0x00000000000000000000000000000000000000000000000000000000deadbeef")
588                .expect("valid hex");
589        let wallet = MockWallet::new(sample_chain_info()).with_send_result(SendResult { tx_hash });
590        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
591
592        let result = contract
593            .method(
594                "transfer",
595                vec![
596                    AbiValue::Field(Fr::from(1u64)),
597                    AbiValue::Field(Fr::from(2u64)),
598                    AbiValue::Integer(100),
599                ],
600            )
601            .expect("find transfer")
602            .send(SendOptions::default())
603            .await
604            .expect("send");
605
606        assert_eq!(result.tx_hash, tx_hash);
607    }
608
609    // -----------------------------------------------------------------------
610    // BatchCall tests
611    // -----------------------------------------------------------------------
612
613    use crate::abi::FunctionSelector;
614    use crate::tx::{AuthWitness, Capsule, HashedValues};
615
616    fn make_call(addr: u64, selector: &str) -> FunctionCall {
617        FunctionCall {
618            to: AztecAddress(Fr::from(addr)),
619            selector: FunctionSelector::from_hex(selector).expect("valid selector"),
620            args: vec![AbiValue::Field(Fr::from(addr))],
621            function_type: FunctionType::Private,
622            is_static: false,
623            hide_msg_sender: false,
624        }
625    }
626
627    fn make_payload(addr: u64, selector: &str) -> ExecutionPayload {
628        ExecutionPayload {
629            calls: vec![make_call(addr, selector)],
630            ..ExecutionPayload::default()
631        }
632    }
633
634    // -- BatchCall::request --
635
636    #[test]
637    fn batch_call_empty() {
638        let wallet = MockWallet::new(sample_chain_info());
639        let batch = BatchCall::new(&wallet, vec![]);
640        let payload = batch.request().expect("empty batch");
641        assert!(payload.calls.is_empty());
642        assert!(payload.auth_witnesses.is_empty());
643        assert!(payload.capsules.is_empty());
644        assert!(payload.extra_hashed_args.is_empty());
645        assert!(payload.fee_payer.is_none());
646    }
647
648    #[test]
649    fn batch_call_single_payload() {
650        let wallet = MockWallet::new(sample_chain_info());
651        let p = make_payload(1, "0xaabbccdd");
652        let batch = BatchCall::new(&wallet, vec![p]);
653        let payload = batch.request().expect("single payload");
654        assert_eq!(payload.calls.len(), 1);
655        assert_eq!(payload.calls[0].to, AztecAddress(Fr::from(1u64)));
656    }
657
658    #[test]
659    fn batch_call_merges_multiple_payloads() {
660        let wallet = MockWallet::new(sample_chain_info());
661        let p1 = ExecutionPayload {
662            calls: vec![make_call(1, "0xaabbccdd")],
663            auth_witnesses: vec![AuthWitness {
664                fields: vec![Fr::from(10u64)],
665                ..Default::default()
666            }],
667            capsules: vec![Capsule {
668                contract_address: AztecAddress(Fr::from(10u64)),
669                storage_slot: Fr::from(1u64),
670                data: vec![Fr::from(1u64)],
671            }],
672            extra_hashed_args: vec![HashedValues::from_args(vec![Fr::from(20u64)])],
673            fee_payer: None,
674        };
675        let p2 = ExecutionPayload {
676            calls: vec![make_call(2, "0x11223344")],
677            auth_witnesses: vec![AuthWitness {
678                fields: vec![Fr::from(30u64)],
679                ..Default::default()
680            }],
681            capsules: vec![],
682            extra_hashed_args: vec![],
683            fee_payer: Some(AztecAddress(Fr::from(99u64))),
684        };
685
686        let batch = BatchCall::new(&wallet, vec![p1, p2]);
687        let payload = batch.request().expect("merge payloads");
688
689        assert_eq!(payload.calls.len(), 2);
690        assert_eq!(payload.calls[0].to, AztecAddress(Fr::from(1u64)));
691        assert_eq!(payload.calls[1].to, AztecAddress(Fr::from(2u64)));
692        assert_eq!(payload.auth_witnesses.len(), 2);
693        assert_eq!(payload.capsules.len(), 1);
694        assert_eq!(payload.extra_hashed_args.len(), 1);
695        assert_eq!(payload.fee_payer, Some(AztecAddress(Fr::from(99u64))));
696    }
697
698    #[test]
699    fn batch_call_fee_payer_uses_last_non_none() {
700        let wallet = MockWallet::new(sample_chain_info());
701        let p1 = ExecutionPayload {
702            fee_payer: Some(AztecAddress(Fr::from(1u64))),
703            ..ExecutionPayload::default()
704        };
705        let p2 = ExecutionPayload {
706            fee_payer: None,
707            ..ExecutionPayload::default()
708        };
709        let p3 = ExecutionPayload {
710            fee_payer: Some(AztecAddress(Fr::from(3u64))),
711            ..ExecutionPayload::default()
712        };
713
714        let batch = BatchCall::new(&wallet, vec![p1, p2, p3]);
715        let payload = batch.request().expect("merge payloads");
716        assert_eq!(payload.fee_payer, Some(AztecAddress(Fr::from(3u64))));
717    }
718
719    // -- BatchCall::simulate --
720
721    #[tokio::test]
722    async fn batch_call_simulate_delegates_to_wallet() {
723        let wallet =
724            MockWallet::new(sample_chain_info()).with_simulate_result(TxSimulationResult {
725                return_values: serde_json::json!({"batch": true}),
726                gas_used: Some(Gas {
727                    da_gas: 10,
728                    l2_gas: 20,
729                }),
730            });
731
732        let batch = BatchCall::new(
733            &wallet,
734            vec![make_payload(1, "0xaabbccdd"), make_payload(2, "0x11223344")],
735        );
736
737        let result = batch
738            .simulate(SimulateOptions::default())
739            .await
740            .expect("simulate batch");
741
742        assert_eq!(result.return_values, serde_json::json!({"batch": true}));
743        assert_eq!(result.gas_used.as_ref().map(|g| g.l2_gas), Some(20));
744    }
745
746    // -- BatchCall::send --
747
748    #[tokio::test]
749    async fn batch_call_send_delegates_to_wallet() {
750        let tx_hash =
751            TxHash::from_hex("0x00000000000000000000000000000000000000000000000000000000deadbeef")
752                .expect("valid hex");
753        let wallet = MockWallet::new(sample_chain_info()).with_send_result(SendResult { tx_hash });
754
755        let batch = BatchCall::new(
756            &wallet,
757            vec![make_payload(1, "0xaabbccdd"), make_payload(2, "0x11223344")],
758        );
759
760        let result = batch
761            .send(SendOptions::default())
762            .await
763            .expect("send batch");
764
765        assert_eq!(result.tx_hash, tx_hash);
766    }
767
768    // -- BatchCall debug --
769
770    #[test]
771    fn batch_call_debug() {
772        let wallet = MockWallet::new(sample_chain_info());
773        let batch = BatchCall::new(&wallet, vec![make_payload(1, "0xaabbccdd")]);
774        let dbg = format!("{batch:?}");
775        assert!(dbg.contains("payload_count: 1"));
776    }
777
778    // -- ContractFunctionInteraction::with --
779
780    #[test]
781    fn with_adds_capsules_and_auth_witnesses() {
782        let wallet = MockWallet::new(sample_chain_info());
783        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
784
785        let aw = AuthWitness {
786            fields: vec![Fr::from(99u64)],
787            ..Default::default()
788        };
789        let cap = Capsule {
790            contract_address: AztecAddress(Fr::from(10u64)),
791            storage_slot: Fr::from(1u64),
792            data: vec![Fr::from(42u64)],
793        };
794
795        let interaction = contract
796            .method("total_supply", vec![])
797            .expect("find total_supply")
798            .with(vec![aw.clone()], vec![cap.clone()]);
799
800        let payload = interaction.request().expect("build payload");
801        assert_eq!(payload.auth_witnesses.len(), 1);
802        assert_eq!(payload.auth_witnesses[0].fields, aw.fields);
803        assert_eq!(payload.capsules.len(), 1);
804        assert_eq!(payload.capsules[0].storage_slot, cap.storage_slot);
805    }
806
807    #[test]
808    fn get_function_call_returns_call() {
809        let wallet = MockWallet::new(sample_chain_info());
810        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
811
812        let interaction = contract
813            .method("total_supply", vec![])
814            .expect("find total_supply");
815
816        let call = interaction.get_function_call();
817        assert_eq!(call.to, sample_address());
818        assert_eq!(call.selector.to_string(), "0xabcdef01");
819    }
820
821    #[test]
822    fn request_includes_auth_witnesses() {
823        let wallet = MockWallet::new(sample_chain_info());
824        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
825
826        let aw = AuthWitness {
827            fields: vec![Fr::from(1u64), Fr::from(2u64)],
828            ..Default::default()
829        };
830
831        let interaction = contract
832            .method("total_supply", vec![])
833            .expect("find total_supply")
834            .with(vec![aw], vec![]);
835
836        let payload = interaction.request().expect("build payload");
837        assert_eq!(payload.auth_witnesses.len(), 1);
838        assert_eq!(payload.auth_witnesses[0].fields.len(), 2);
839    }
840
841    // -- profile tests --
842
843    #[tokio::test]
844    async fn profile_delegates_to_wallet() {
845        let wallet = MockWallet::new(sample_chain_info());
846        let contract = Contract::at(sample_address(), load_token_artifact(), wallet);
847
848        let result = contract
849            .method("total_supply", vec![])
850            .expect("find total_supply")
851            .profile(ProfileOptions::default())
852            .await
853            .expect("profile");
854
855        assert_eq!(result.return_values, serde_json::Value::Null);
856    }
857
858    #[tokio::test]
859    async fn batch_profile_delegates_to_wallet() {
860        let wallet = MockWallet::new(sample_chain_info());
861
862        let batch = BatchCall::new(
863            &wallet,
864            vec![make_payload(1, "0xaabbccdd"), make_payload(2, "0x11223344")],
865        );
866
867        let result = batch
868            .profile(ProfileOptions::default())
869            .await
870            .expect("profile batch");
871
872        assert_eq!(result.return_values, serde_json::Value::Null);
873    }
874
875    // -- Fee payload merging --
876
877    #[test]
878    fn send_options_with_fee_payload_merged() {
879        let fee_payload = ExecutionPayload {
880            calls: vec![make_call(99, "0x11111111")],
881            fee_payer: Some(AztecAddress(Fr::from(99u64))),
882            ..ExecutionPayload::default()
883        };
884        let main_payload = ExecutionPayload {
885            calls: vec![make_call(1, "0xaabbccdd")],
886            ..ExecutionPayload::default()
887        };
888        let merged = merge_fee_payload(main_payload, &Some(fee_payload)).expect("merge");
889        assert_eq!(merged.calls.len(), 2);
890        assert_eq!(merged.fee_payer, Some(AztecAddress(Fr::from(99u64))));
891    }
892
893    #[test]
894    fn simulate_options_with_gas_estimation_flags() {
895        let opts = SimulateOptions {
896            estimate_gas: true,
897            estimated_gas_padding: Some(0.1),
898            ..SimulateOptions::default()
899        };
900        assert!(opts.estimate_gas);
901        assert_eq!(opts.estimated_gas_padding, Some(0.1));
902    }
903
904    // -- Contract::deploy --
905
906    #[test]
907    fn contract_deploy_creates_deploy_method() {
908        let wallet = MockWallet::new(sample_chain_info());
909        let artifact = load_token_artifact();
910        let result = Contract::deploy(
911            &wallet,
912            artifact,
913            vec![AbiValue::Field(Fr::from(1u64))],
914            None,
915        );
916        assert!(result.is_ok(), "deploy should succeed");
917    }
918
919    #[test]
920    fn contract_deploy_with_public_keys() {
921        use crate::types::PublicKeys;
922
923        let wallet = MockWallet::new(sample_chain_info());
924        let artifact = load_token_artifact();
925        let keys = PublicKeys::default();
926        let result = Contract::deploy_with_public_keys(
927            keys,
928            &wallet,
929            artifact,
930            vec![AbiValue::Field(Fr::from(1u64))],
931            None,
932        );
933        assert!(result.is_ok(), "deploy_with_public_keys should succeed");
934    }
935
936    #[test]
937    fn contract_with_wallet_changes_wallet() {
938        let wallet1 = MockWallet::new(sample_chain_info());
939        let wallet2 = MockWallet::new(ChainInfo {
940            chain_id: Fr::from(999u64),
941            version: Fr::from(2u64),
942        });
943        let addr = sample_address();
944        let contract = Contract::at(addr, load_token_artifact(), wallet1);
945        let contract2 = contract.with_wallet(wallet2);
946        assert_eq!(contract2.address, addr);
947        assert_eq!(contract2.artifact.name, "TokenContract");
948    }
949}