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 process with the appropriate imports.
151        let process = package.get_process()?;
152
153        // Generate the deployment
154        let mut deployment =
155            package.deploy::<A>(&process, None).with_context(|| "Failed to generate the deployment")?;
156
157        // Get the consensus version.
158        let consensus_version =
159            N::CONSENSUS_VERSION(query.current_block_height().with_context(|| "Failed to query consensus height")?)?;
160
161        // If the consensus version is less than `V9`, unset the program checksum and owner in the deployment.
162        // Otherwise, set it to the appropriate values.
163        if consensus_version < ConsensusVersion::V9 {
164            deployment.set_program_checksum_raw(None);
165            deployment.set_program_owner_raw(None);
166        } else {
167            deployment.set_program_checksum_raw(Some(package.program().to_checksum()));
168            deployment.set_program_owner_raw(Some(Address::try_from(&private_key)?));
169        };
170
171        // Compute the deployment ID.
172        let deployment_id = deployment.to_deployment_id().with_context(|| "Failed to compute deployment ID")?;
173
174        // Compute the minimum deployment cost.
175        let (minimum_deployment_cost, (_, _, _, _)) = deployment_cost(&process, &deployment, consensus_version)
176            .with_context(|| "Failed to compute the minimum deployment cost")?;
177
178        // Generate the deployment transaction.
179        let transaction = {
180            // Initialize an RNG.
181            let rng = &mut rand::thread_rng();
182
183            // Initialize the storage.
184            let storage_mode = match &self.storage_path {
185                Some(path) => StorageMode::Custom(path.clone()),
186                None => StorageMode::Production,
187            };
188            let store = ConsensusStore::<N, ConsensusMemory<N>>::open(storage_mode)
189                .with_context(|| "Failed to open the consensus store")?;
190
191            // Initialize the VM.
192            let vm = VM::from(store).with_context(|| "Failed to initialize the virtual machine")?;
193
194            // Prepare the fees.
195            let fee = match &self.record {
196                Some(record) => {
197                    let fee_record =
198                        Developer::parse_record(&private_key, record).with_context(|| "Failed to parse record")?;
199                    let fee_authorization = vm.authorize_fee_private(
200                        &private_key,
201                        fee_record,
202                        minimum_deployment_cost,
203                        self.priority_fee,
204                        deployment_id,
205                        rng,
206                    )?;
207                    vm.execute_fee_authorization(fee_authorization, Some(&query), rng)
208                        .with_context(|| "Failed to execute fee authorization")?
209                }
210                None => {
211                    let fee_authorization = vm.authorize_fee_public(
212                        &private_key,
213                        minimum_deployment_cost,
214                        self.priority_fee,
215                        deployment_id,
216                        rng,
217                    )?;
218                    vm.execute_fee_authorization(fee_authorization, Some(&query), rng)
219                        .with_context(|| "Failed to execute fee authorization")?
220                }
221            };
222            // Construct the owner.
223            let owner = ProgramOwner::new(&private_key, deployment_id, rng)
224                .with_context(|| "Failed to construct program owner")?;
225
226            // Create a new transaction.
227            Transaction::from_deployment(owner, deployment, fee).with_context(|| "Failed to crate transaction")?
228        };
229        println!("✅ Created deployment transaction for '{}'", program_id.to_string().bold());
230
231        // Determine if the transaction should be broadcast, stored, or displayed to the user.
232        Developer::handle_transaction(
233            &endpoint,
234            &self.broadcast,
235            self.dry_run,
236            &self.store,
237            self.store_format,
238            self.wait,
239            self.timeout,
240            transaction,
241            program_id.to_string(),
242        )
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::commands::{CLI, Command, DeveloperCommand};
250
251    use anyhow::bail;
252
253    #[test]
254    fn clap_snarkos_deploy_missing_mode() {
255        let arg_vec = &[
256            "snarkos",
257            "developer",
258            "deploy",
259            "--private-key=PRIVATE_KEY",
260            "--endpoint=ENDPOINT",
261            "--priority-fee=77",
262            "--record=RECORD",
263            "hello.aleo",
264        ];
265
266        // Should fail because no mode is specified.
267        let err = CLI::try_parse_from(arg_vec).unwrap_err();
268        assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
269    }
270
271    #[test]
272    fn clap_snarkos_deploy() -> Result<()> {
273        let arg_vec = &[
274            "snarkos",
275            "developer",
276            "deploy",
277            "--private-key=PRIVATE_KEY",
278            "--endpoint=ENDPOINT",
279            "--priority-fee=77",
280            "--dry-run",
281            "--record=RECORD",
282            "hello.aleo",
283        ];
284        // Use try parse here, as parse calls `exit()`.
285        let cli = CLI::try_parse_from(arg_vec)?;
286
287        let Command::Developer(developer) = cli.command else {
288            bail!("Unexpected result of clap parsing!");
289        };
290        let DeveloperCommand::Deploy(deploy) = developer.command else {
291            bail!("Unexpected result of clap parsing!");
292        };
293
294        assert_eq!(developer.network, 0);
295        assert_eq!(deploy.program_id, "hello.aleo");
296        assert_eq!(deploy.private_key, Some("PRIVATE_KEY".to_string()));
297        assert_eq!(deploy.private_key_file, None);
298        assert_eq!(deploy.endpoint, "ENDPOINT");
299        assert!(deploy.dry_run);
300        assert!(deploy.broadcast.is_none());
301        assert_eq!(deploy.store, None);
302        assert_eq!(deploy.priority_fee, 77);
303        assert_eq!(deploy.record, Some("RECORD".to_string()));
304
305        Ok(())
306    }
307
308    #[test]
309    fn clap_snarkos_deploy_broadcast() -> Result<()> {
310        let arg_vec = &[
311            "snarkos",
312            "developer",
313            "deploy",
314            "--private-key=PRIVATE_KEY",
315            "--endpoint=ENDPOINT",
316            "--priority-fee=77",
317            "--broadcast=ENDPOINT2",
318            "--record=RECORD",
319            "hello.aleo",
320        ];
321        // Use try parse here, as parse calls `exit()`.
322        let cli = CLI::try_parse_from(arg_vec)?;
323
324        let Command::Developer(developer) = cli.command else {
325            bail!("Unexpected result of clap parsing!");
326        };
327        let DeveloperCommand::Deploy(deploy) = developer.command else {
328            bail!("Unexpected result of clap parsing!");
329        };
330
331        assert_eq!(developer.network, 0);
332        assert_eq!(deploy.program_id, "hello.aleo");
333        assert_eq!(deploy.private_key, Some("PRIVATE_KEY".to_string()));
334        assert_eq!(deploy.private_key_file, None);
335        assert_eq!(deploy.endpoint, "ENDPOINT");
336        assert!(!deploy.dry_run);
337        // Check that the custom endpoint for broadcasting is used.
338        assert_eq!(Some(Some(Uri::try_from("ENDPOINT2").unwrap())), deploy.broadcast);
339        assert_eq!(deploy.store, None);
340        assert_eq!(deploy.priority_fee, 77);
341        assert_eq!(deploy.record, Some("RECORD".to_string()));
342
343        Ok(())
344    }
345}