aztec_contract/
deployment.rs

1use serde::{Deserialize, Serialize};
2
3use crate::abi::{AbiValue, ContractArtifact, FunctionType};
4use crate::contract::{merge_fee_payload, ContractFunctionInteraction};
5use crate::error::Error;
6use crate::tx::{Capsule, ExecutionPayload, FunctionCall};
7use crate::types::{AztecAddress, ContractInstance, ContractInstanceWithAddress, Fr, PublicKeys};
8use crate::wallet::{
9    ProfileOptions, SendOptions, SendResult, SimulateOptions, TxProfileResult, TxSimulationResult,
10    Wallet,
11};
12
13use aztec_core::abi::{buffer_as_fields, FunctionSelector};
14use aztec_core::constants::{
15    contract_class_registry_bytecode_capsule_slot, protocol_contract_address,
16    MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS, MAX_PROCESSABLE_L2_GAS,
17};
18use aztec_core::fee::Gas;
19use aztec_core::hash::{
20    compute_artifact_hash, compute_contract_address_from_instance, compute_contract_class_id,
21    compute_initialization_hash, compute_private_functions_root_from_artifact,
22    compute_public_bytecode_commitment,
23};
24
25// ---------------------------------------------------------------------------
26// DeployOptions
27// ---------------------------------------------------------------------------
28
29/// Options controlling contract deployment behavior.
30#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32#[allow(clippy::struct_excessive_bools)]
33pub struct DeployOptions {
34    /// Salt for deterministic address computation.
35    pub contract_address_salt: Option<Fr>,
36    /// Skip publishing the contract class on-chain.
37    #[serde(default)]
38    pub skip_class_publication: bool,
39    /// Skip publishing the contract instance on-chain.
40    #[serde(default)]
41    pub skip_instance_publication: bool,
42    /// Skip calling the initialization function.
43    #[serde(default)]
44    pub skip_initialization: bool,
45    /// Skip registering the contract with the wallet.
46    #[serde(default)]
47    pub skip_registration: bool,
48    /// Use the universal deployer for this deployment.
49    #[serde(default)]
50    pub universal_deploy: bool,
51    /// Explicit deployer address. Required when `universal_deploy` is false.
52    #[serde(default)]
53    pub from: Option<AztecAddress>,
54}
55
56// ---------------------------------------------------------------------------
57// SuggestedGasLimits / get_gas_limits
58// ---------------------------------------------------------------------------
59
60/// Suggested gas limits from simulation, with optional padding.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct SuggestedGasLimits {
63    /// Main execution phase gas limits.
64    pub gas_limits: Gas,
65    /// Teardown phase gas limits.
66    pub teardown_gas_limits: Gas,
67}
68
69/// Compute gas limits from a simulation result.
70///
71/// Applies a padding factor (default 10%) to the simulated gas usage
72/// to provide a safety margin.
73pub fn get_gas_limits(
74    simulation_result: &TxSimulationResult,
75    pad: Option<f64>,
76) -> SuggestedGasLimits {
77    let pad_factor = 1.0 + pad.unwrap_or(0.1);
78
79    let gas_used = simulation_result
80        .gas_used
81        .as_ref()
82        .cloned()
83        .unwrap_or_default();
84
85    let padded_da = (gas_used.da_gas as f64 * pad_factor).ceil() as u64;
86    let padded_l2 = (gas_used.l2_gas as f64 * pad_factor).ceil() as u64;
87
88    SuggestedGasLimits {
89        gas_limits: Gas {
90            da_gas: padded_da,
91            l2_gas: padded_l2.min(MAX_PROCESSABLE_L2_GAS),
92        },
93        teardown_gas_limits: Gas::default(),
94    }
95}
96
97// ---------------------------------------------------------------------------
98// publish_contract_class
99// ---------------------------------------------------------------------------
100
101/// Build an interaction payload that publishes a contract class on-chain.
102#[allow(clippy::unused_async)]
103pub async fn publish_contract_class<'a, W: Wallet>(
104    wallet: &'a W,
105    artifact: &ContractArtifact,
106) -> Result<ContractFunctionInteraction<'a, W>, Error> {
107    // 1. Compute class preimage components.
108    let artifact_hash = compute_artifact_hash(artifact);
109    let private_functions_root = compute_private_functions_root_from_artifact(artifact)?;
110
111    // Extract and encode packed public bytecode.
112    let packed_bytecode = extract_packed_bytecode(artifact);
113    let public_bytecode_commitment = compute_public_bytecode_commitment(&packed_bytecode);
114
115    // 2. Encode packed bytecode as field elements for capsule.
116    let bytecode_fields = if packed_bytecode.is_empty() {
117        vec![]
118    } else {
119        buffer_as_fields(&packed_bytecode, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS)?
120    };
121
122    // 3. Build function call to the Contract Class Registry.
123    let registerer_address = protocol_contract_address::contract_class_registry();
124
125    let call = FunctionCall {
126        to: registerer_address,
127        selector: FunctionSelector::from_signature("publish(Field,Field,Field)"),
128        args: vec![
129            AbiValue::Field(artifact_hash),
130            AbiValue::Field(private_functions_root),
131            AbiValue::Field(public_bytecode_commitment),
132        ],
133        function_type: FunctionType::Private,
134        is_static: false,
135        hide_msg_sender: false,
136    };
137
138    // 4. Create capsule with bytecode data.
139    let capsules = if bytecode_fields.is_empty() {
140        vec![]
141    } else {
142        vec![Capsule {
143            contract_address: registerer_address,
144            storage_slot: contract_class_registry_bytecode_capsule_slot(),
145            data: bytecode_fields,
146        }]
147    };
148
149    // 5. Return interaction with capsule attached.
150    Ok(ContractFunctionInteraction::new_with_capsules(
151        wallet, call, capsules,
152    ))
153}
154
155/// Extract packed public bytecode from an artifact.
156///
157/// The packed bytecode is the single `public_dispatch` function's bytecode.
158/// All other public functions are dispatched through it at runtime.
159/// Mirrors TS: `publicFunctions[0]?.bytecode` with `retainBytecode` filter
160/// that only keeps bytecode for `public_dispatch`.
161fn extract_packed_bytecode(artifact: &ContractArtifact) -> Vec<u8> {
162    // Only public_dispatch carries the packed bytecode.
163    // Other abi_public functions are internal and dispatched at runtime.
164    artifact
165        .functions
166        .iter()
167        .find(|f| f.function_type == FunctionType::Public && f.name == "public_dispatch")
168        .and_then(|f| f.bytecode.as_ref())
169        .map(|bc| decode_artifact_bytes(bc))
170        .unwrap_or_default()
171}
172
173// ---------------------------------------------------------------------------
174// publish_instance
175// ---------------------------------------------------------------------------
176
177/// Build an interaction payload that publishes a contract instance on-chain.
178pub fn publish_instance<'a, W: Wallet>(
179    wallet: &'a W,
180    instance: &ContractInstanceWithAddress,
181) -> Result<ContractFunctionInteraction<'a, W>, Error> {
182    let is_universal_deploy = instance.inner.deployer == AztecAddress(Fr::zero());
183
184    let deployer_address = protocol_contract_address::contract_instance_registry();
185
186    let call = FunctionCall {
187        to: deployer_address,
188        selector: FunctionSelector::from_signature(
189            "publish_for_public_execution(Field,(Field),Field,(((Field,Field,bool)),((Field,Field,bool)),((Field,Field,bool)),((Field,Field,bool))),bool)"
190        ),
191        args: vec![
192            AbiValue::Field(instance.inner.salt),
193            AbiValue::Tuple(vec![AbiValue::Field(
194                instance.inner.current_contract_class_id,
195            )]),
196            AbiValue::Field(instance.inner.initialization_hash),
197            public_keys_to_abi_value(&instance.inner.public_keys),
198            AbiValue::Boolean(is_universal_deploy),
199        ],
200        function_type: FunctionType::Private,
201        is_static: false,
202        hide_msg_sender: false,
203    };
204
205    Ok(ContractFunctionInteraction::new(wallet, call))
206}
207
208fn point_to_abi_value(point: &aztec_core::types::Point) -> AbiValue {
209    AbiValue::Tuple(vec![
210        AbiValue::Field(point.x),
211        AbiValue::Field(point.y),
212        AbiValue::Boolean(point.is_infinite),
213    ])
214}
215
216fn public_keys_to_abi_value(public_keys: &PublicKeys) -> AbiValue {
217    AbiValue::Tuple(vec![
218        point_to_abi_value(&public_keys.master_nullifier_public_key),
219        point_to_abi_value(&public_keys.master_incoming_viewing_public_key),
220        point_to_abi_value(&public_keys.master_outgoing_viewing_public_key),
221        point_to_abi_value(&public_keys.master_tagging_public_key),
222    ])
223}
224
225fn decode_artifact_bytes(encoded: &str) -> Vec<u8> {
226    if let Some(hex) = encoded.strip_prefix("0x") {
227        return hex::decode(hex).unwrap_or_else(|_| encoded.as_bytes().to_vec());
228    }
229
230    use base64::Engine;
231    base64::engine::general_purpose::STANDARD
232        .decode(encoded)
233        .unwrap_or_else(|_| encoded.as_bytes().to_vec())
234}
235
236// ---------------------------------------------------------------------------
237// Shared instance construction
238// ---------------------------------------------------------------------------
239
240/// Parameters for computing a contract instance from an artifact.
241pub struct ContractInstantiationParams<'a> {
242    /// Constructor function name (None if no initializer).
243    pub constructor_name: Option<&'a str>,
244    /// Constructor arguments.
245    pub constructor_args: Vec<AbiValue>,
246    /// Deployment salt.
247    pub salt: Fr,
248    /// Public keys for the instance.
249    pub public_keys: PublicKeys,
250    /// Deployer address (zero for universal deployment).
251    pub deployer: AztecAddress,
252}
253
254/// Compute a contract instance (with derived address) from an artifact and
255/// instantiation parameters.
256///
257/// This is the shared helper used by both generic contract deployment and
258/// account address pre-computation.
259pub fn get_contract_instance_from_instantiation_params(
260    artifact: &ContractArtifact,
261    params: ContractInstantiationParams<'_>,
262) -> Result<ContractInstanceWithAddress, Error> {
263    // Compute contract class ID from artifact.
264    let artifact_hash = compute_artifact_hash(artifact);
265    let private_functions_root = compute_private_functions_root_from_artifact(artifact)?;
266    let packed_bytecode = extract_packed_bytecode(artifact);
267    let public_bytecode_commitment = compute_public_bytecode_commitment(&packed_bytecode);
268    let class_id = compute_contract_class_id(
269        artifact_hash,
270        private_functions_root,
271        public_bytecode_commitment,
272    );
273
274    // Compute initialization hash.
275    let init_fn = params
276        .constructor_name
277        .map(|name| artifact.find_function(name))
278        .transpose()?;
279
280    let init_hash = compute_initialization_hash(init_fn, &params.constructor_args)?;
281
282    let instance = ContractInstance {
283        version: 1,
284        salt: params.salt,
285        deployer: params.deployer,
286        current_contract_class_id: class_id,
287        original_contract_class_id: class_id,
288        initialization_hash: init_hash,
289        public_keys: params.public_keys,
290    };
291
292    let address = compute_contract_address_from_instance(&instance)?;
293
294    Ok(ContractInstanceWithAddress {
295        address,
296        inner: instance,
297    })
298}
299
300// ---------------------------------------------------------------------------
301// DeployResult
302// ---------------------------------------------------------------------------
303
304/// The result of a deployment transaction.
305#[derive(Clone, Debug)]
306pub struct DeployResult {
307    /// The underlying send result (tx hash).
308    pub send_result: SendResult,
309    /// The deployed contract instance with its derived address.
310    pub instance: ContractInstanceWithAddress,
311}
312
313// ---------------------------------------------------------------------------
314// ContractDeployer
315// ---------------------------------------------------------------------------
316
317/// Builder for deploying new contract instances.
318///
319/// Created with a contract artifact and wallet reference. Use
320/// [`ContractDeployer::deploy`] to produce a [`DeployMethod`] for a
321/// specific set of constructor arguments.
322pub struct ContractDeployer<'a, W> {
323    artifact: ContractArtifact,
324    wallet: &'a W,
325    public_keys: PublicKeys,
326    constructor_name: Option<String>,
327}
328
329impl<W> std::fmt::Debug for ContractDeployer<'_, W> {
330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        f.debug_struct("ContractDeployer")
332            .field("artifact", &self.artifact.name)
333            .field("constructor_name", &self.constructor_name)
334            .finish_non_exhaustive()
335    }
336}
337
338impl<'a, W: Wallet> ContractDeployer<'a, W> {
339    /// Create a new deployer for the given artifact and wallet.
340    pub fn new(artifact: ContractArtifact, wallet: &'a W) -> Self {
341        Self {
342            artifact,
343            wallet,
344            public_keys: PublicKeys::default(),
345            constructor_name: None,
346        }
347    }
348
349    /// Set the public keys for the deployed instance.
350    #[must_use]
351    pub const fn with_public_keys(mut self, keys: PublicKeys) -> Self {
352        self.public_keys = keys;
353        self
354    }
355
356    /// Set the constructor function name (defaults to `"constructor"`).
357    #[must_use]
358    pub fn with_constructor_name(mut self, name: impl Into<String>) -> Self {
359        self.constructor_name = Some(name.into());
360        self
361    }
362
363    /// Create a [`DeployMethod`] for the given constructor arguments.
364    ///
365    /// Validates the selected initializer, when present. Contracts with no
366    /// initializer are allowed as long as no constructor arguments are passed.
367    pub fn deploy(self, args: Vec<AbiValue>) -> Result<DeployMethod<'a, W>, Error> {
368        let constructor_name = if let Some(name) = self.constructor_name {
369            let func = self.artifact.find_function(&name)?;
370            if !func.is_initializer {
371                return Err(Error::Abi(format!(
372                    "function '{name}' in artifact '{}' is not an initializer",
373                    self.artifact.name,
374                )));
375            }
376
377            let expected = func.parameters.len();
378            let got = args.len();
379            if got != expected {
380                return Err(Error::Abi(format!(
381                    "constructor '{name}' expects {expected} argument(s), got {got}",
382                )));
383            }
384
385            Some(name)
386        } else if let Some(func) = self
387            .artifact
388            .functions
389            .iter()
390            .find(|func| func.is_initializer)
391        {
392            let expected = func.parameters.len();
393            let got = args.len();
394            if got != expected {
395                return Err(Error::Abi(format!(
396                    "constructor '{}' expects {expected} argument(s), got {got}",
397                    func.name
398                )));
399            }
400
401            Some(func.name.clone())
402        } else if args.is_empty() {
403            None
404        } else {
405            return Err(Error::Abi(format!(
406                "artifact '{}' has no initializer but got {} constructor argument(s)",
407                self.artifact.name,
408                args.len()
409            )));
410        };
411
412        Ok(DeployMethod {
413            wallet: self.wallet,
414            artifact: self.artifact,
415            args,
416            public_keys: self.public_keys,
417            constructor_name,
418            default_salt: Fr::random(),
419        })
420    }
421}
422
423// ---------------------------------------------------------------------------
424// DeployMethod
425// ---------------------------------------------------------------------------
426
427/// A pending contract deployment interaction.
428///
429/// Created by [`ContractDeployer::deploy`]. Supports building the deployment
430/// payload, computing the target instance, simulating, and sending.
431pub struct DeployMethod<'a, W> {
432    wallet: &'a W,
433    artifact: ContractArtifact,
434    args: Vec<AbiValue>,
435    public_keys: PublicKeys,
436    constructor_name: Option<String>,
437    default_salt: Fr,
438}
439
440impl<W> std::fmt::Debug for DeployMethod<'_, W> {
441    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442        f.debug_struct("DeployMethod")
443            .field("artifact", &self.artifact.name)
444            .field("constructor_name", &self.constructor_name)
445            .field("args_count", &self.args.len())
446            .finish_non_exhaustive()
447    }
448}
449
450impl<W: Wallet> DeployMethod<'_, W> {
451    fn with_effective_from(opts: &DeployOptions, from: Option<AztecAddress>) -> DeployOptions {
452        if opts.universal_deploy || opts.from.is_some() {
453            opts.clone()
454        } else {
455            let mut effective = opts.clone();
456            effective.from = from;
457            effective
458        }
459    }
460
461    /// Build the deployment [`ExecutionPayload`].
462    pub async fn request(&self, opts: &DeployOptions) -> Result<ExecutionPayload, Error> {
463        let instance = self.get_instance(opts)?;
464        let mut payloads: Vec<ExecutionPayload> = Vec::new();
465
466        // 1. Register contract with wallet (unless skipped).
467        if !opts.skip_registration {
468            self.wallet
469                .register_contract(instance.clone(), Some(self.artifact.clone()), None)
470                .await?;
471        }
472
473        // 2. Publish contract class (unless skipped).
474        if !opts.skip_class_publication {
475            let class_id = instance.inner.current_contract_class_id;
476            let already_registered = self
477                .wallet
478                .get_contract_class_metadata(class_id)
479                .await
480                .map(|m| m.is_contract_class_publicly_registered)
481                .unwrap_or(false);
482
483            if !already_registered {
484                let class_interaction = publish_contract_class(self.wallet, &self.artifact).await?;
485                payloads.push(class_interaction.request()?);
486            }
487        }
488
489        // 3. Publish instance (unless skipped).
490        if !opts.skip_instance_publication {
491            let instance_interaction = publish_instance(self.wallet, &instance)?;
492            payloads.push(instance_interaction.request()?);
493        }
494
495        // 4. Call constructor (unless skipped or no constructor).
496        if !opts.skip_initialization {
497            if let Some(ref constructor_name) = self.constructor_name {
498                let func = self.artifact.find_function(constructor_name)?;
499                let selector = func.selector.unwrap_or_else(|| {
500                    FunctionSelector::from_name_and_parameters(&func.name, &func.parameters)
501                });
502
503                let encoded_fields = aztec_core::abi::encode_arguments(func, &self.args)?;
504
505                let call = FunctionCall {
506                    to: instance.address,
507                    selector,
508                    args: encoded_fields.into_iter().map(AbiValue::Field).collect(),
509                    function_type: func.function_type.clone(),
510                    is_static: false,
511                    hide_msg_sender: false,
512                };
513
514                payloads.push(ExecutionPayload {
515                    calls: vec![call],
516                    auth_witnesses: vec![],
517                    capsules: vec![],
518                    extra_hashed_args: vec![],
519                    fee_payer: None,
520                });
521            }
522        }
523
524        // 5. Merge all payloads.
525        ExecutionPayload::merge(payloads)
526    }
527
528    /// Compute the contract instance that would be deployed.
529    pub fn get_instance(&self, opts: &DeployOptions) -> Result<ContractInstanceWithAddress, Error> {
530        let salt = opts.contract_address_salt.unwrap_or(self.default_salt);
531        let deployer = if opts.universal_deploy {
532            AztecAddress::zero()
533        } else {
534            opts.from.unwrap_or(AztecAddress::zero())
535        };
536
537        get_contract_instance_from_instantiation_params(
538            &self.artifact,
539            ContractInstantiationParams {
540                constructor_name: self.constructor_name.as_deref(),
541                constructor_args: self.args.clone(),
542                salt,
543                public_keys: self.public_keys.clone(),
544                deployer,
545            },
546        )
547    }
548
549    /// Simulate the deployment without sending.
550    pub async fn simulate(
551        &self,
552        deploy_opts: &DeployOptions,
553        sim_opts: SimulateOptions,
554    ) -> Result<TxSimulationResult, Error> {
555        let effective_opts = Self::with_effective_from(deploy_opts, Some(sim_opts.from));
556        let payload = merge_fee_payload(
557            self.request(&effective_opts).await?,
558            &sim_opts.fee_execution_payload,
559        )?;
560        self.wallet.simulate_tx(payload, sim_opts).await
561    }
562
563    /// Profile the deployment transaction.
564    pub async fn profile(
565        &self,
566        deploy_opts: &DeployOptions,
567        profile_opts: ProfileOptions,
568    ) -> Result<TxProfileResult, Error> {
569        let effective_opts = Self::with_effective_from(deploy_opts, Some(profile_opts.from));
570        let payload = merge_fee_payload(
571            self.request(&effective_opts).await?,
572            &profile_opts.fee_execution_payload,
573        )?;
574        self.wallet.profile_tx(payload, profile_opts).await
575    }
576
577    /// Send the deployment transaction.
578    pub async fn send(
579        &self,
580        deploy_opts: &DeployOptions,
581        send_opts: SendOptions,
582    ) -> Result<DeployResult, Error> {
583        let effective_opts = Self::with_effective_from(deploy_opts, Some(send_opts.from));
584        let instance = self.get_instance(&effective_opts)?;
585        let payload = merge_fee_payload(
586            self.request(&effective_opts).await?,
587            &send_opts.fee_execution_payload,
588        )?;
589        let send_result = self.wallet.send_tx(payload, send_opts).await?;
590        // `wait_for_contract` polls the node for the contract instance.  When
591        // the caller opts out of instance publication, the instance will
592        // never appear on the node, so waiting would always time out.  This
593        // matches upstream TS, which only polls for node-visibility when the
594        // instance is actually published.
595        if !effective_opts.skip_instance_publication {
596            self.wallet.wait_for_contract(instance.address).await?;
597        }
598        Ok(DeployResult {
599            send_result,
600            instance,
601        })
602    }
603}
604
605// ---------------------------------------------------------------------------
606// Tests
607// ---------------------------------------------------------------------------
608
609#[cfg(test)]
610#[allow(clippy::unwrap_used, clippy::expect_used)]
611mod tests {
612    use super::*;
613    use crate::abi::AbiValue;
614    use crate::fee::Gas;
615    use crate::types::Fr;
616    use crate::wallet::{ChainInfo, MockWallet, TxSimulationResult};
617
618    const DEPLOY_ARTIFACT: &str = r#"
619    {
620      "name": "TokenContract",
621      "functions": [
622        {
623          "name": "constructor",
624          "function_type": "private",
625          "is_initializer": true,
626          "is_static": false,
627          "parameters": [
628            { "name": "admin", "type": { "kind": "field" } }
629          ],
630          "return_types": [],
631          "selector": "0xe5fb6c81"
632        },
633        {
634          "name": "transfer",
635          "function_type": "private",
636          "is_initializer": false,
637          "is_static": false,
638          "parameters": [
639            { "name": "from", "type": { "kind": "field" } },
640            { "name": "to", "type": { "kind": "field" } }
641          ],
642          "return_types": [],
643          "selector": "0xd6f42325"
644        }
645      ]
646    }
647    "#;
648
649    const NO_INITIALIZER_ARTIFACT: &str = r#"
650    {
651      "name": "NoInitContract",
652      "functions": [
653        {
654          "name": "do_stuff",
655          "function_type": "public",
656          "is_initializer": false,
657          "is_static": false,
658          "parameters": [],
659          "return_types": [],
660          "selector": "0xaabbccdd"
661        }
662      ]
663    }
664    "#;
665
666    fn sample_chain_info() -> ChainInfo {
667        ChainInfo {
668            chain_id: Fr::from(31337u64),
669            version: Fr::from(1u64),
670        }
671    }
672
673    fn load_artifact(json: &str) -> ContractArtifact {
674        ContractArtifact::from_json(json).expect("parse artifact")
675    }
676
677    // -- DeployOptions -------------------------------------------------------
678
679    #[test]
680    fn deploy_options_default() {
681        let opts = DeployOptions::default();
682        assert!(opts.contract_address_salt.is_none());
683        assert!(!opts.skip_class_publication);
684        assert!(!opts.skip_instance_publication);
685        assert!(!opts.skip_initialization);
686        assert!(!opts.skip_registration);
687        assert!(!opts.universal_deploy);
688        assert!(opts.from.is_none());
689    }
690
691    #[test]
692    fn deploy_options_roundtrip() {
693        let opts = DeployOptions {
694            contract_address_salt: Some(Fr::from(42u64)),
695            skip_class_publication: true,
696            skip_instance_publication: false,
697            skip_initialization: false,
698            skip_registration: true,
699            universal_deploy: false,
700            from: None,
701        };
702        let json = serde_json::to_string(&opts).expect("serialize");
703        let decoded: DeployOptions = serde_json::from_str(&json).expect("deserialize");
704        assert_eq!(decoded, opts);
705    }
706
707    // -- get_gas_limits -------------------------------------------------------
708
709    #[test]
710    fn get_gas_limits_default_pad() {
711        let result = TxSimulationResult {
712            return_values: serde_json::Value::Null,
713            gas_used: Some(Gas {
714                da_gas: 1000,
715                l2_gas: 2000,
716            }),
717        };
718        let limits = get_gas_limits(&result, None);
719        assert_eq!(limits.gas_limits.da_gas, 1100);
720        assert_eq!(limits.gas_limits.l2_gas, 2200);
721    }
722
723    #[test]
724    fn get_gas_limits_custom_pad() {
725        let result = TxSimulationResult {
726            return_values: serde_json::Value::Null,
727            gas_used: Some(Gas {
728                da_gas: 1000,
729                l2_gas: 2000,
730            }),
731        };
732        let limits = get_gas_limits(&result, Some(0.5));
733        assert_eq!(limits.gas_limits.da_gas, 1500);
734        assert_eq!(limits.gas_limits.l2_gas, 3000);
735    }
736
737    #[test]
738    fn get_gas_limits_zero_gas() {
739        let result = TxSimulationResult {
740            return_values: serde_json::Value::Null,
741            gas_used: Some(Gas {
742                da_gas: 0,
743                l2_gas: 0,
744            }),
745        };
746        let limits = get_gas_limits(&result, None);
747        assert_eq!(limits.gas_limits.da_gas, 0);
748        assert_eq!(limits.gas_limits.l2_gas, 0);
749    }
750
751    // -- publish_contract_class -----------------------------------------------
752
753    #[tokio::test]
754    async fn publish_contract_class_targets_registerer() {
755        let wallet = MockWallet::new(sample_chain_info());
756        let artifact = load_artifact(DEPLOY_ARTIFACT);
757        let interaction = publish_contract_class(&wallet, &artifact)
758            .await
759            .expect("publish class");
760        let payload = interaction.request().expect("build payload");
761        assert_eq!(payload.calls.len(), 1);
762        assert_eq!(
763            payload.calls[0].to,
764            protocol_contract_address::contract_class_registerer()
765        );
766    }
767
768    // -- publish_instance ----------------------------------------------------
769
770    #[test]
771    fn publish_instance_targets_deployer() {
772        let wallet = MockWallet::new(sample_chain_info());
773        let instance = ContractInstanceWithAddress {
774            address: AztecAddress(Fr::from(1u64)),
775            inner: ContractInstance {
776                version: 1,
777                salt: Fr::from(42u64),
778                deployer: AztecAddress(Fr::from(2u64)),
779                current_contract_class_id: Fr::from(100u64),
780                original_contract_class_id: Fr::from(100u64),
781                initialization_hash: Fr::from(0u64),
782                public_keys: PublicKeys::default(),
783            },
784        };
785        let interaction = publish_instance(&wallet, &instance).expect("publish instance");
786        let payload = interaction.request().expect("build payload");
787        assert_eq!(payload.calls.len(), 1);
788        assert_eq!(
789            payload.calls[0].to,
790            protocol_contract_address::contract_instance_deployer()
791        );
792    }
793
794    #[test]
795    fn publish_instance_universal_deploy_flag() {
796        let wallet = MockWallet::new(sample_chain_info());
797
798        // Non-universal (deployer is non-zero)
799        let instance_non_universal = ContractInstanceWithAddress {
800            address: AztecAddress(Fr::from(1u64)),
801            inner: ContractInstance {
802                version: 1,
803                salt: Fr::from(42u64),
804                deployer: AztecAddress(Fr::from(2u64)),
805                current_contract_class_id: Fr::from(100u64),
806                original_contract_class_id: Fr::from(100u64),
807                initialization_hash: Fr::zero(),
808                public_keys: PublicKeys::default(),
809            },
810        };
811        let interaction = publish_instance(&wallet, &instance_non_universal).expect("non-uni");
812        let payload = interaction.request().expect("payload");
813        // Last arg should be false (non-universal)
814        assert_eq!(payload.calls[0].args[4], AbiValue::Boolean(false));
815
816        // Universal (deployer is zero)
817        let instance_universal = ContractInstanceWithAddress {
818            address: AztecAddress(Fr::from(1u64)),
819            inner: ContractInstance {
820                version: 1,
821                salt: Fr::from(42u64),
822                deployer: AztecAddress(Fr::zero()),
823                current_contract_class_id: Fr::from(100u64),
824                original_contract_class_id: Fr::from(100u64),
825                initialization_hash: Fr::zero(),
826                public_keys: PublicKeys::default(),
827            },
828        };
829        let interaction2 = publish_instance(&wallet, &instance_universal).expect("uni");
830        let payload2 = interaction2.request().expect("payload2");
831        assert_eq!(payload2.calls[0].args[4], AbiValue::Boolean(true));
832    }
833
834    // -- ContractDeployer ----------------------------------------------------
835
836    #[test]
837    fn contract_deployer_creates_deploy_method() {
838        let wallet = MockWallet::new(sample_chain_info());
839        let artifact = load_artifact(DEPLOY_ARTIFACT);
840
841        let deployer = ContractDeployer::new(artifact, &wallet);
842        let deploy = deployer
843            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
844            .expect("create deploy method");
845
846        let dbg = format!("{deploy:?}");
847        assert!(dbg.contains("TokenContract"));
848        assert!(dbg.contains("constructor"));
849    }
850
851    #[test]
852    fn contract_deployer_with_builder_methods() {
853        let wallet = MockWallet::new(sample_chain_info());
854        let artifact = load_artifact(DEPLOY_ARTIFACT);
855
856        let deployer = ContractDeployer::new(artifact, &wallet)
857            .with_public_keys(PublicKeys::default())
858            .with_constructor_name("constructor");
859
860        let deploy = deployer
861            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
862            .expect("create deploy method");
863
864        let dbg = format!("{deploy:?}");
865        assert!(dbg.contains("constructor"));
866    }
867
868    #[test]
869    fn contract_deployer_rejects_missing_function() {
870        let wallet = MockWallet::new(sample_chain_info());
871        let artifact = load_artifact(DEPLOY_ARTIFACT);
872
873        let deployer =
874            ContractDeployer::new(artifact, &wallet).with_constructor_name("nonexistent");
875
876        let result = deployer.deploy(vec![]);
877        assert!(result.is_err());
878        assert!(result.unwrap_err().to_string().contains("not found"));
879    }
880
881    #[test]
882    fn contract_deployer_rejects_non_initializer() {
883        let wallet = MockWallet::new(sample_chain_info());
884        let artifact = load_artifact(DEPLOY_ARTIFACT);
885
886        let deployer = ContractDeployer::new(artifact, &wallet).with_constructor_name("transfer");
887
888        let result = deployer.deploy(vec![
889            AbiValue::Field(Fr::from(1u64)),
890            AbiValue::Field(Fr::from(2u64)),
891        ]);
892        assert!(result.is_err());
893        assert!(result
894            .unwrap_err()
895            .to_string()
896            .contains("not an initializer"));
897    }
898
899    #[test]
900    fn contract_deployer_rejects_arg_count_mismatch() {
901        let wallet = MockWallet::new(sample_chain_info());
902        let artifact = load_artifact(DEPLOY_ARTIFACT);
903
904        let deployer = ContractDeployer::new(artifact, &wallet);
905        let result = deployer.deploy(vec![]);
906        assert!(result.is_err());
907        assert!(result
908            .unwrap_err()
909            .to_string()
910            .contains("expects 1 argument(s), got 0"));
911    }
912
913    #[test]
914    fn contract_deployer_no_initializer_artifact() {
915        let wallet = MockWallet::new(sample_chain_info());
916        let artifact = load_artifact(NO_INITIALIZER_ARTIFACT);
917
918        let deploy = ContractDeployer::new(artifact, &wallet)
919            .deploy(vec![])
920            .expect("create deploy method without initializer");
921        let dbg = format!("{deploy:?}");
922        assert!(dbg.contains("NoInitContract"));
923        assert!(dbg.contains("None"));
924    }
925
926    // -- DeployMethod::get_instance ------------------------------------------
927
928    #[test]
929    fn deploy_method_get_instance_computes_real_address() {
930        let wallet = MockWallet::new(sample_chain_info());
931        let artifact = load_artifact(DEPLOY_ARTIFACT);
932        let deployer = ContractDeployer::new(artifact, &wallet);
933
934        let deploy = deployer
935            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
936            .expect("create deploy method");
937
938        let opts = DeployOptions {
939            contract_address_salt: Some(Fr::from(99u64)),
940            universal_deploy: true,
941            ..DeployOptions::default()
942        };
943        let instance = deploy.get_instance(&opts).expect("get instance");
944
945        // Address should no longer be zero.
946        assert_ne!(instance.address, AztecAddress(Fr::zero()));
947        assert_eq!(instance.inner.salt, Fr::from(99u64));
948        assert_eq!(instance.inner.version, 1);
949        assert_eq!(instance.inner.deployer, AztecAddress(Fr::zero()));
950        // Class ID should be non-zero.
951        assert_ne!(instance.inner.current_contract_class_id, Fr::zero());
952        assert_eq!(
953            instance.inner.current_contract_class_id,
954            instance.inner.original_contract_class_id
955        );
956    }
957
958    #[test]
959    fn deploy_method_get_instance_uses_provided_salt() {
960        let wallet = MockWallet::new(sample_chain_info());
961        let artifact = load_artifact(DEPLOY_ARTIFACT);
962        let deployer = ContractDeployer::new(artifact, &wallet);
963
964        let deploy = deployer
965            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
966            .expect("create deploy method");
967
968        let opts = DeployOptions {
969            contract_address_salt: Some(Fr::from(99u64)),
970            universal_deploy: true,
971            ..DeployOptions::default()
972        };
973        let instance = deploy.get_instance(&opts).expect("get instance");
974        assert_eq!(instance.inner.salt, Fr::from(99u64));
975    }
976
977    #[test]
978    fn deploy_method_get_instance_generates_random_salt() {
979        let wallet = MockWallet::new(sample_chain_info());
980        let artifact = load_artifact(DEPLOY_ARTIFACT);
981        let deployer = ContractDeployer::new(artifact, &wallet);
982
983        let deploy = deployer
984            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
985            .expect("create deploy method");
986
987        let opts = DeployOptions {
988            universal_deploy: true,
989            ..DeployOptions::default()
990        };
991        let instance = deploy.get_instance(&opts).expect("get instance");
992        assert_ne!(instance.inner.salt, Fr::zero());
993    }
994
995    #[test]
996    fn deploy_method_get_instance_is_stable_for_same_options() {
997        let wallet = MockWallet::new(sample_chain_info());
998        let artifact = load_artifact(DEPLOY_ARTIFACT);
999        let deploy = ContractDeployer::new(artifact, &wallet)
1000            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
1001            .expect("create deploy method");
1002
1003        let opts = DeployOptions {
1004            universal_deploy: true,
1005            ..DeployOptions::default()
1006        };
1007        let first = deploy.get_instance(&opts).expect("first");
1008        let second = deploy.get_instance(&opts).expect("second");
1009        assert_eq!(first.inner.salt, second.inner.salt);
1010        assert_eq!(first.address, second.address);
1011    }
1012
1013    #[test]
1014    fn deploy_method_get_instance_preserves_public_keys() {
1015        let wallet = MockWallet::new(sample_chain_info());
1016        let artifact = load_artifact(DEPLOY_ARTIFACT);
1017
1018        let keys = PublicKeys {
1019            master_nullifier_public_key: crate::types::Point {
1020                x: Fr::from(1u64),
1021                y: Fr::from(2u64),
1022                is_infinite: false,
1023            },
1024            ..PublicKeys::default()
1025        };
1026
1027        let deployer = ContractDeployer::new(artifact, &wallet).with_public_keys(keys.clone());
1028        let deploy = deployer
1029            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
1030            .expect("create deploy method");
1031
1032        let opts = DeployOptions {
1033            universal_deploy: true,
1034            ..DeployOptions::default()
1035        };
1036        let instance = deploy.get_instance(&opts).expect("get instance");
1037        assert_eq!(instance.inner.public_keys, keys);
1038    }
1039
1040    #[test]
1041    fn deploy_method_get_instance_universal_deploy() {
1042        let wallet = MockWallet::new(sample_chain_info());
1043        let artifact = load_artifact(DEPLOY_ARTIFACT);
1044        let deploy = ContractDeployer::new(artifact, &wallet)
1045            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
1046            .expect("create deploy method");
1047
1048        let opts = DeployOptions {
1049            contract_address_salt: Some(Fr::from(1u64)),
1050            universal_deploy: true,
1051            ..DeployOptions::default()
1052        };
1053        let instance = deploy.get_instance(&opts).expect("get instance");
1054        assert_eq!(instance.inner.deployer, AztecAddress(Fr::zero()));
1055    }
1056
1057    #[test]
1058    fn deploy_method_get_instance_with_explicit_from() {
1059        let wallet = MockWallet::new(sample_chain_info());
1060        let artifact = load_artifact(DEPLOY_ARTIFACT);
1061        let deploy = ContractDeployer::new(artifact, &wallet)
1062            .deploy(vec![AbiValue::Field(Fr::from(1u64))])
1063            .expect("create deploy method");
1064
1065        let deployer_addr = AztecAddress(Fr::from(42u64));
1066        let opts = DeployOptions {
1067            contract_address_salt: Some(Fr::from(1u64)),
1068            universal_deploy: false,
1069            from: Some(deployer_addr),
1070            ..DeployOptions::default()
1071        };
1072        let instance = deploy.get_instance(&opts).expect("get instance");
1073        assert_eq!(instance.inner.deployer, deployer_addr);
1074    }
1075
1076    // -- DeployMethod::request -----------------------------------------------
1077
1078    #[tokio::test]
1079    async fn deploy_method_request_full_flow() {
1080        let wallet = MockWallet::new(sample_chain_info());
1081        let artifact = load_artifact(DEPLOY_ARTIFACT);
1082        let deployer = ContractDeployer::new(artifact, &wallet);
1083
1084        let deploy = deployer
1085            .deploy(vec![AbiValue::Field(Fr::from(42u64))])
1086            .expect("create deploy method");
1087
1088        let opts = DeployOptions {
1089            contract_address_salt: Some(Fr::from(1u64)),
1090            universal_deploy: true,
1091            skip_registration: true,
1092            ..DeployOptions::default()
1093        };
1094
1095        let payload = deploy.request(&opts).await.expect("request");
1096
1097        // Should have calls for: class publication, instance publication, constructor
1098        assert!(
1099            payload.calls.len() >= 2,
1100            "expected at least 2 calls, got {}",
1101            payload.calls.len()
1102        );
1103    }
1104
1105    #[tokio::test]
1106    async fn deploy_method_request_skips_class_publication() {
1107        let wallet = MockWallet::new(sample_chain_info());
1108        let artifact = load_artifact(DEPLOY_ARTIFACT);
1109        let deployer = ContractDeployer::new(artifact, &wallet);
1110
1111        let deploy = deployer
1112            .deploy(vec![AbiValue::Field(Fr::from(42u64))])
1113            .expect("create deploy method");
1114
1115        let opts = DeployOptions {
1116            contract_address_salt: Some(Fr::from(1u64)),
1117            universal_deploy: true,
1118            skip_registration: true,
1119            skip_class_publication: true,
1120            ..DeployOptions::default()
1121        };
1122
1123        let payload = deploy.request(&opts).await.expect("request");
1124
1125        // Should have calls for: instance publication + constructor (no class publication)
1126        let has_registerer_call = payload
1127            .calls
1128            .iter()
1129            .any(|c| c.to == protocol_contract_address::contract_class_registerer());
1130        assert!(
1131            !has_registerer_call,
1132            "should not contain class publication call"
1133        );
1134    }
1135
1136    #[tokio::test]
1137    async fn deploy_method_request_skips_initialization() {
1138        let wallet = MockWallet::new(sample_chain_info());
1139        let artifact = load_artifact(DEPLOY_ARTIFACT);
1140        let deployer = ContractDeployer::new(artifact, &wallet);
1141
1142        let deploy = deployer
1143            .deploy(vec![AbiValue::Field(Fr::from(42u64))])
1144            .expect("create deploy method");
1145
1146        let opts = DeployOptions {
1147            contract_address_salt: Some(Fr::from(1u64)),
1148            universal_deploy: true,
1149            skip_registration: true,
1150            skip_class_publication: true,
1151            skip_instance_publication: true,
1152            skip_initialization: true,
1153            ..DeployOptions::default()
1154        };
1155
1156        let payload = deploy.request(&opts).await.expect("request");
1157        assert!(payload.calls.is_empty());
1158    }
1159
1160    // -- Debug impls ---------------------------------------------------------
1161
1162    #[test]
1163    fn contract_deployer_debug() {
1164        let wallet = MockWallet::new(sample_chain_info());
1165        let artifact = load_artifact(DEPLOY_ARTIFACT);
1166        let deployer = ContractDeployer::new(artifact, &wallet);
1167        let dbg = format!("{deployer:?}");
1168        assert!(dbg.contains("TokenContract"));
1169    }
1170}