snarkos_cli/commands/developer/
deploy.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use super::{DEFAULT_ENDPOINT, Developer};
17use crate::{
18    commands::StoreFormat,
19    helpers::args::{parse_private_key, prepare_endpoint},
20};
21
22use snarkvm::{
23    circuit::{Aleo, AleoCanaryV0, AleoTestnetV0, AleoV0},
24    console::{
25        network::{CanaryV0, MainnetV0, Network, TestnetV0},
26        program::ProgramOwner,
27    },
28    ledger::store::helpers::memory::BlockMemory,
29    prelude::{
30        ProgramID,
31        VM,
32        block::Transaction,
33        deployment_cost,
34        query::{Query, QueryTrait},
35        store::{ConsensusStore, helpers::memory::ConsensusMemory},
36    },
37};
38
39use aleo_std::StorageMode;
40use anyhow::Result;
41use clap::{Parser, builder::NonEmptyStringValueParser};
42use colored::Colorize;
43use snarkvm::prelude::{Address, ConsensusVersion};
44use std::{path::PathBuf, str::FromStr};
45use ureq::http::Uri;
46use zeroize::Zeroize;
47
48use anyhow::Context;
49
50/// Deploys an Aleo program.
51#[derive(Debug, Parser)]
52#[command(
53    group(clap::ArgGroup::new("mode").required(true).multiple(false)),
54    group(clap::ArgGroup::new("key").required(true).multiple(false))
55)]
56pub struct Deploy {
57    /// The name of the program to deploy.
58    program_id: String,
59    /// A path to a directory containing a manifest file. Defaults to the current working directory.
60    #[clap(long)]
61    path: Option<String>,
62    /// The private key used to generate the deployment.
63    #[clap(short = 'p', long, group = "key", value_parser=NonEmptyStringValueParser::default())]
64    private_key: Option<String>,
65    /// Use a developer validator key tok generate the deployment.
66    #[clap(long, group = "key")]
67    dev_key: Option<u16>,
68    /// Specify the path to a file containing the account private key of the node
69    #[clap(long, group = "key", value_parser=NonEmptyStringValueParser::default())]
70    private_key_file: Option<String>,
71    /// The endpoint to query node state from and broadcast to (if set to broadcast).
72    ///
73    /// The given value is expected to be the base URL, e.g., "https://mynode.com", and will be extended automatically
74    /// to fit the network type and query.
75    /// For example, the base URL may extend to "http://mynode.com/testnet/transaction/unconfirmed/ID" to retrieve
76    /// an unconfirmed transaction on the test network.
77    #[clap(short, long, alias="query", default_value=DEFAULT_ENDPOINT, verbatim_doc_comment)]
78    endpoint: Uri,
79    /// The priority fee in microcredits.
80    #[clap(long, default_value_t = 0)]
81    priority_fee: u64,
82    /// The record to spend the fee from.
83    #[clap(short, long)]
84    record: Option<String>,
85    /// Set the URL used to broadcast the transaction (if no value is given, the query endpoint is used).
86    ///
87    /// The given value is expected the full URL of the endpoint, not just the base URL, e.g., "http://mynode.com/testnet/transaction/broadcast".
88    #[clap(short, long, group = "mode", verbatim_doc_comment)]
89    broadcast: Option<Option<Uri>>,
90    /// Performs a dry-run of transaction generation.
91    #[clap(short, long, group = "mode")]
92    dry_run: bool,
93    /// Store generated deployment transaction to a local file.
94    #[clap(long, group = "mode")]
95    store: Option<String>,
96    /// If --store is specified, the format in which the transaction should be stored : string or
97    /// bytes, by default : bytes.
98    #[clap(long, value_enum, default_value_t = StoreFormat::Bytes, requires="store")]
99    store_format: StoreFormat,
100    /// Wait for the transaction to be accepted by the network. Requires --broadcast.
101    #[clap(long, requires = "broadcast")]
102    wait: bool,
103    /// Timeout in seconds when waiting for transaction confirmation. Default is 60 seconds.
104    #[clap(long, default_value_t = 60, requires = "wait")]
105    timeout: u64,
106    /// Specify the path to a directory containing the ledger. Overrides the default path.
107    #[clap(long = "storage_path")]
108    storage_path: Option<PathBuf>,
109}
110
111impl Drop for Deploy {
112    /// Zeroize the private key when the `Deploy` struct goes out of scope.
113    fn drop(&mut self) {
114        self.private_key.zeroize();
115    }
116}
117
118impl Deploy {
119    /// Deploys an Aleo program.
120    pub fn parse<N: Network>(self) -> Result<String> {
121        // Construct the deployment for the specified network.
122        match N::ID {
123            MainnetV0::ID => self.construct_deployment::<MainnetV0, AleoV0>(),
124            TestnetV0::ID => self.construct_deployment::<TestnetV0, AleoTestnetV0>(),
125            CanaryV0::ID => self.construct_deployment::<CanaryV0, AleoCanaryV0>(),
126            _ => unreachable!(),
127        }
128        .with_context(|| "Deployment failed")
129    }
130
131    /// Construct and process the deployment transaction.
132    fn construct_deployment<N: Network, A: Aleo<Network = N, BaseField = N::Field>>(self) -> Result<String> {
133        let endpoint = prepare_endpoint(self.endpoint.clone())?;
134
135        // Specify the query
136        let query = Query::<N, BlockMemory<N>>::from(endpoint.clone());
137
138        // Retrieve the private key.
139        let private_key = parse_private_key(self.private_key.clone(), self.private_key_file.clone(), self.dev_key)?;
140
141        // Retrieve the program ID.
142        let program_id = ProgramID::from_str(&self.program_id).with_context(|| "Failed to parse program ID")?;
143
144        // Fetch the package from the directory.
145        let package =
146            Developer::parse_package(program_id, &self.path).with_context(|| "Failed to parse program package")?;
147
148        println!("📦 Creating deployment transaction for '{}'...\n", &program_id.to_string().bold());
149
150        // Generate the deployment
151        let mut deployment = package.deploy::<A>(None).with_context(|| "Failed to generate the deployment")?;
152
153        // Get the consensus version.
154        let consensus_version =
155            N::CONSENSUS_VERSION(query.current_block_height().with_context(|| "Failed to query consensus height")?)?;
156
157        // If the consensus version is less than `V9`, unset the program checksum and owner in the deployment.
158        // Otherwise, set it to the appropriate values.
159        if consensus_version < ConsensusVersion::V9 {
160            deployment.set_program_checksum_raw(None);
161            deployment.set_program_owner_raw(None);
162        } else {
163            deployment.set_program_checksum_raw(Some(package.program().to_checksum()));
164            deployment.set_program_owner_raw(Some(Address::try_from(&private_key)?));
165        };
166
167        // Compute the deployment ID.
168        let deployment_id = deployment.to_deployment_id().with_context(|| "Failed to compute deployment ID")?;
169
170        // Generate the deployment transaction.
171        let transaction = {
172            // Initialize an RNG.
173            let rng = &mut rand::thread_rng();
174
175            // Initialize the storage.
176            let storage_mode = match &self.storage_path {
177                Some(path) => StorageMode::Custom(path.clone()),
178                None => StorageMode::Production,
179            };
180            let store = ConsensusStore::<N, ConsensusMemory<N>>::open(storage_mode)?;
181
182            // Initialize the VM.
183            let vm = VM::from(store).with_context(|| "Failed to initialize the virtual machine")?;
184
185            // Compute the minimum deployment cost.
186            let (minimum_deployment_cost, (_, _, _, _)) =
187                deployment_cost(&vm.process().read(), &deployment, consensus_version)?;
188
189            // Prepare the fees.
190            let fee = match &self.record {
191                Some(record) => {
192                    let fee_record =
193                        Developer::parse_record(&private_key, record).with_context(|| "Failed to parse record")?;
194                    let fee_authorization = vm.authorize_fee_private(
195                        &private_key,
196                        fee_record,
197                        minimum_deployment_cost,
198                        self.priority_fee,
199                        deployment_id,
200                        rng,
201                    )?;
202                    vm.execute_fee_authorization(fee_authorization, Some(&query), rng)
203                        .with_context(|| "Failed to execute fee authorization")?
204                }
205                None => {
206                    let fee_authorization = vm.authorize_fee_public(
207                        &private_key,
208                        minimum_deployment_cost,
209                        self.priority_fee,
210                        deployment_id,
211                        rng,
212                    )?;
213                    vm.execute_fee_authorization(fee_authorization, Some(&query), rng)
214                        .with_context(|| "Failed to execute fee authorization")?
215                }
216            };
217            // Construct the owner.
218            let owner = ProgramOwner::new(&private_key, deployment_id, rng)
219                .with_context(|| "Failed to construct program owner")?;
220
221            // Create a new transaction.
222            Transaction::from_deployment(owner, deployment, fee).with_context(|| "Failed to crate transaction")?
223        };
224        println!("✅ Created deployment transaction for '{}'", program_id.to_string().bold());
225
226        // Determine if the transaction should be broadcast, stored, or displayed to the user.
227        Developer::handle_transaction(
228            &endpoint,
229            &self.broadcast,
230            self.dry_run,
231            &self.store,
232            self.store_format,
233            self.wait,
234            self.timeout,
235            transaction,
236            program_id.to_string(),
237        )
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::commands::{CLI, Command, DeveloperCommand};
245
246    use anyhow::bail;
247
248    #[test]
249    fn clap_snarkos_deploy_missing_mode() {
250        let arg_vec = &[
251            "snarkos",
252            "developer",
253            "deploy",
254            "--private-key=PRIVATE_KEY",
255            "--endpoint=ENDPOINT",
256            "--priority-fee=77",
257            "--record=RECORD",
258            "hello.aleo",
259        ];
260
261        // Should fail because no mode is specified.
262        let err = CLI::try_parse_from(arg_vec).unwrap_err();
263        assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
264    }
265
266    #[test]
267    fn clap_snarkos_deploy() -> Result<()> {
268        let arg_vec = &[
269            "snarkos",
270            "developer",
271            "deploy",
272            "--private-key=PRIVATE_KEY",
273            "--endpoint=ENDPOINT",
274            "--priority-fee=77",
275            "--dry-run",
276            "--record=RECORD",
277            "hello.aleo",
278        ];
279        // Use try parse here, as parse calls `exit()`.
280        let cli = CLI::try_parse_from(arg_vec)?;
281
282        let Command::Developer(developer) = cli.command else {
283            bail!("Unexpected result of clap parsing!");
284        };
285        let DeveloperCommand::Deploy(deploy) = developer.command else {
286            bail!("Unexpected result of clap parsing!");
287        };
288
289        assert_eq!(developer.network, 0);
290        assert_eq!(deploy.program_id, "hello.aleo");
291        assert_eq!(deploy.private_key, Some("PRIVATE_KEY".to_string()));
292        assert_eq!(deploy.private_key_file, None);
293        assert_eq!(deploy.endpoint, "ENDPOINT");
294        assert!(deploy.dry_run);
295        assert!(deploy.broadcast.is_none());
296        assert_eq!(deploy.store, None);
297        assert_eq!(deploy.priority_fee, 77);
298        assert_eq!(deploy.record, Some("RECORD".to_string()));
299
300        Ok(())
301    }
302
303    #[test]
304    fn clap_snarkos_deploy_broadcast() -> Result<()> {
305        let arg_vec = &[
306            "snarkos",
307            "developer",
308            "deploy",
309            "--private-key=PRIVATE_KEY",
310            "--endpoint=ENDPOINT",
311            "--priority-fee=77",
312            "--broadcast=ENDPOINT2",
313            "--record=RECORD",
314            "hello.aleo",
315        ];
316        // Use try parse here, as parse calls `exit()`.
317        let cli = CLI::try_parse_from(arg_vec)?;
318
319        let Command::Developer(developer) = cli.command else {
320            bail!("Unexpected result of clap parsing!");
321        };
322        let DeveloperCommand::Deploy(deploy) = developer.command else {
323            bail!("Unexpected result of clap parsing!");
324        };
325
326        assert_eq!(developer.network, 0);
327        assert_eq!(deploy.program_id, "hello.aleo");
328        assert_eq!(deploy.private_key, Some("PRIVATE_KEY".to_string()));
329        assert_eq!(deploy.private_key_file, None);
330        assert_eq!(deploy.endpoint, "ENDPOINT");
331        assert!(!deploy.dry_run);
332        // Check that the custom endpoint for broadcasting is used.
333        assert_eq!(Some(Some(Uri::try_from("ENDPOINT2").unwrap())), deploy.broadcast);
334        assert_eq!(deploy.store, None);
335        assert_eq!(deploy.priority_fee, 77);
336        assert_eq!(deploy.record, Some("RECORD".to_string()));
337
338        Ok(())
339    }
340}