Skip to main content

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