Skip to main content

aleo_rust_sdk/
client.rs

1//! # Aleo Client — the top-level entry point for the SDK.
2//!
3//! Combines [`account`](crate::account), [`program`](crate::program),
4//! [`execution`](crate::execution), and [`network`](crate::network) modules into
5//! a single, ergonomic [`AleoClient`] with a fluent API.
6//!
7//! ## Lifecycle
8//!
9//! 1. **Create** — `AleoClient::new(node_url)`
10//! 2. **Set account** — `client.set_account_from_private_key_str(pk)`
11//! 3. **Load program** — `client.load_program_from_source(source)`
12//! 4. **Query** — `client.get_block_height()`, `client.get_balance()`
13//! 5. **Execute** — `client.execute_local(...)` or `client.execute_and_broadcast(...)`
14//!
15//! ## Example
16//!
17//! ```no_run
18//! use aleo_rust_sdk::{AleoClient, AleoAccount};
19//! use snarkvm::prelude::TestRng;
20//!
21//! #[tokio::main]
22//! async fn main() -> anyhow::Result<()> {
23//!     let mut rng = TestRng::default();
24//!     let client = AleoClient::new("https://api.explorer.provable.com/v2/testnet")?;
25//!
26//!     let account = AleoAccount::new_random(&mut rng)?;
27//!     println!("Address: {}", account.address_str());
28//!
29//!     Ok(())
30//! }
31//! ```
32
33use crate::execution::ExecutionEngine;
34use crate::network::{AleoHttpClient, ProvableQuery};
35use crate::program::AleoProgram;
36use anyhow::Result;
37use snarkvm::console::program::{ProgramID, Value};
38use snarkvm::prelude::{Network, PrivateKey, TestRng, TestnetV0};
39
40/// High-level Aleo client that orchestrates the full lifecycle.
41#[derive(Clone)]
42pub struct AleoClient {
43    pub network: AleoHttpClient,
44    account: Option<crate::account::AleoAccount>,
45    program: Option<AleoProgram>,
46}
47
48impl AleoClient {
49    /// Create a new client pointing at an Aleo node.
50    pub fn new(node_url: &str) -> Result<Self> {
51        let network = AleoHttpClient::new(node_url)?;
52        Ok(Self {
53            network,
54            account: None,
55            program: None,
56        })
57    }
58
59    /// Create a client with a custom RPC URL (for testing with wiremock).
60    pub fn new_with_rpc(rest_url: &str, rpc_url: &str) -> Result<Self> {
61        let network = AleoHttpClient::new_with_rpc(rest_url, rpc_url)?;
62        Ok(Self {
63            network,
64            account: None,
65            program: None,
66        })
67    }
68
69    // ── Account management ──────────────────────────────────────────────
70
71    /// Set the account from a private key string.
72    pub fn set_account_from_private_key_str(&mut self, pk_str: &str) -> Result<()> {
73        let account = crate::account::AleoAccount::from_private_key_str(pk_str)?;
74        self.account = Some(account);
75        Ok(())
76    }
77
78    /// Get a reference to the current account, or error if not set.
79    pub fn require_account(&self) -> Result<&crate::account::AleoAccount> {
80        self.account.as_ref().ok_or_else(|| {
81            anyhow::anyhow!("No account set. Call set_account_from_private_key_str() first.")
82        })
83    }
84
85    // ── Program management ──────────────────────────────────────────────
86
87    /// Load a program from source string and store it.
88    pub fn load_program_from_source(&mut self, source: &str) -> Result<()> {
89        let program = AleoProgram::from_source(source)?;
90        self.program = Some(program);
91        Ok(())
92    }
93
94    /// Directly set a program (e.g. from `AleoProgram::credits()`).
95    pub fn set_program(&mut self, program: AleoProgram) {
96        self.program = Some(program);
97    }
98
99    /// Get a reference to the stored program, or error if not set.
100    pub fn require_program(&self) -> Result<&AleoProgram> {
101        self.program.as_ref().ok_or_else(|| {
102            anyhow::anyhow!("No program loaded. Call load_program_from_source() first.")
103        })
104    }
105
106    // ── Local execution (dry-run) ───────────────────────────────────────
107
108    /// Execute a function locally without proving or broadcasting.
109    ///
110    /// This is a dry-run: it authorizes and executes the function call
111    /// using a temporary process with the loaded program, but does NOT
112    /// generate proofs or submit anything to the network.
113    ///
114    /// If a program has been loaded via `load_program_from_source()`, it
115    /// will be registered automatically. For built-in programs (credits.aleo),
116    /// no pre-loading is needed.
117    pub fn execute_local(
118        &self,
119        program_id: &str,
120        function_name: &str,
121        inputs: &[String],
122    ) -> Result<String> {
123        use snarkvm::prelude::{FromStr, TestRng};
124
125        let account = self.require_account()?;
126        let mut rng = TestRng::default();
127
128        let pid = ProgramID::<TestnetV0>::from_str(program_id)?;
129
130        // Initialize a fresh execution engine (credits.aleo loaded by default)
131        let engine = ExecutionEngine::new()?;
132        // Optionally register the user program if one was loaded
133        if let Some(prog) = &self.program {
134            engine.add_program(prog.inner())?;
135        }
136
137        let (response, _trace) = engine.authorize_and_execute(
138            &account.private_key,
139            &pid,
140            function_name,
141            inputs.iter().map(|s| s.as_str()).collect(),
142            &mut rng,
143        )?;
144
145        Ok(format!("{response:?}"))
146    }
147
148    // ── High-level operations ──────────────────────────────────────────
149
150    /// Full pipeline: authorize → execute → prove → return Transaction JSON (no broadcast).
151    ///
152    /// Equivalent to JS SDK `run(_, _, _, proveExecution=true)` without the broadcast step.
153    /// Returns the serialized transaction as a JSON string.
154    #[allow(clippy::too_many_arguments)]
155    pub async fn prove_execution(
156        &self,
157        private_key: &PrivateKey<TestnetV0>,
158        program_id: &ProgramID<TestnetV0>,
159        function_name: &str,
160        inputs: Vec<&str>,
161        base_fee: u64,
162        priority_fee: u64,
163    ) -> Result<String> {
164        let mut rng = TestRng::default();
165
166        // Fetch program from network (needed for non-credits programs)
167        let program = self.network.fetch_program(&program_id.to_string()).await?;
168
169        // Initialize engine with V0 fee keys for testnet
170        let engine = ExecutionEngine::new_with_v0_fee_keys()?;
171        // Register the user program
172        engine.add_program(&program)?;
173
174        let (_response, trace) = engine.authorize_and_execute(
175            private_key,
176            program_id,
177            function_name,
178            inputs,
179            &mut rng,
180        )?;
181
182        // Fetch state root for proving
183        let (state_root, block_height) = self.network.fetch_state_root().await?;
184        let query = ProvableQuery::new(
185            state_root,
186            block_height,
187            "https://api.provable.com/v2/testnet",
188        );
189
190        let tx = engine.prove_and_package(
191            trace,
192            private_key,
193            program_id,
194            function_name,
195            base_fee,
196            priority_fee,
197            &query,
198            &mut rng,
199        )?;
200
201        Ok(serde_json::to_string(&tx)?)
202    }
203
204    /// Full pipeline: authorize → execute → prove → broadcast.
205    ///
206    /// Returns the transaction ID on success.
207    #[allow(clippy::too_many_arguments)]
208    pub async fn execute_and_broadcast(
209        &self,
210        private_key: &PrivateKey<TestnetV0>,
211        program_id: &ProgramID<TestnetV0>,
212        function_name: &str,
213        inputs: Vec<&str>,
214        base_fee: u64,
215        priority_fee: u64,
216    ) -> Result<String> {
217        let mut rng = TestRng::default();
218
219        // Fetch program from network
220        let program = self.network.fetch_program(&program_id.to_string()).await?;
221
222        // Initialize engine with V0 fee keys for testnet
223        let engine = ExecutionEngine::new_with_v0_fee_keys()?;
224        // Register the user program
225        engine.add_program(&program)?;
226
227        // Authorize + execute locally
228        let (_response, trace) = engine.authorize_and_execute(
229            private_key,
230            program_id,
231            function_name,
232            inputs,
233            &mut rng,
234        )?;
235
236        // Fetch state root for proving
237        let (state_root, block_height) = self.network.fetch_state_root().await?;
238        let query = ProvableQuery::new(
239            state_root,
240            block_height,
241            "https://api.provable.com/v2/testnet",
242        );
243
244        // Prove and package
245        let tx = engine.prove_and_package(
246            trace,
247            private_key,
248            program_id,
249            function_name,
250            base_fee,
251            priority_fee,
252            &query,
253            &mut rng,
254        )?;
255
256        // Serialize and broadcast
257        let tx_json = serde_json::to_string(&tx)?;
258        let raw = self.network.broadcast_transaction(tx_json).await?;
259        Ok(raw.trim_matches('"').to_string())
260    }
261
262    /// Full pipeline with pre-parsed snarkVM `Value` inputs.
263    ///
264    /// Use this when inputs include record ciphertexts — decrypt them first,
265    /// wrap as `Value::Record`, and pass here instead of raw strings.
266    /// Otherwise behaves identically to `execute_and_broadcast`.
267    #[allow(clippy::too_many_arguments)]
268    pub async fn execute_and_broadcast_with_values(
269        &self,
270        private_key: &PrivateKey<TestnetV0>,
271        program_id: &ProgramID<TestnetV0>,
272        function_name: &str,
273        values: Vec<Value<TestnetV0>>,
274        base_fee: u64,
275        priority_fee: u64,
276    ) -> Result<String> {
277        use snarkvm::prelude::TestRng;
278
279        let mut rng = TestRng::default();
280
281        // Fetch program from network
282        let program = self.network.fetch_program(&program_id.to_string()).await?;
283
284        // Initialize engine with V0 fee keys for testnet
285        let engine = ExecutionEngine::new_with_v0_fee_keys()?;
286        // Register the user program
287        engine.add_program(&program)?;
288
289        // Authorize + execute locally with pre-parsed values
290        let (_response, trace) = engine.authorize_and_execute_with_values(
291            private_key,
292            program_id,
293            function_name,
294            values,
295            &mut rng,
296        )?;
297
298        // Fetch state root for proving
299        let (state_root, block_height) = self.network.fetch_state_root().await?;
300        let query = ProvableQuery::new(
301            state_root,
302            block_height,
303            "https://api.provable.com/v2/testnet",
304        );
305
306        // Prove and package
307        let tx = engine.prove_and_package(
308            trace,
309            private_key,
310            program_id,
311            function_name,
312            base_fee,
313            priority_fee,
314            &query,
315            &mut rng,
316        )?;
317
318        // Serialize and broadcast
319        let tx_json = serde_json::to_string(&tx)?;
320        let raw = self.network.broadcast_transaction(tx_json).await?;
321        Ok(raw.trim_matches('"').to_string())
322    }
323
324    // ── Deployment (program publishing) ─────────────────────────────────
325
326    /// Full deployment pipeline: parse → prove → compute fee → fetch state root → build tx → broadcast.
327    ///
328    /// This is the equivalent of JS SDK's `ProgramManager.deploy()`.
329    ///
330    /// `program_source` is the raw `.aleo` program source code.
331    /// `priority_fee_in_microcredits` adds priority over the minimum deployment cost.
332    ///
333    /// Returns the transaction ID on success.
334    pub async fn deploy_program(
335        &self,
336        program_source: &str,
337        priority_fee_in_microcredits: u64,
338    ) -> Result<String> {
339        use snarkvm::prelude::{ConsensusVersion, Program};
340        use std::str::FromStr;
341
342        let account = self.require_account()?;
343        let mut rng = TestRng::default();
344
345        // 1. Parse the program
346        let program = Program::<TestnetV0>::from_str(program_source)
347            .map_err(|e| anyhow::anyhow!("Failed to parse program: {e}"))?;
348
349        // 2. Initialize engine with V0 fee keys
350        let engine = ExecutionEngine::new_with_v0_fee_keys()?;
351        // Register the program so its dependencies are available
352        engine.add_program(&program)?;
353
354        // 3. Generate deployment proof (pure proving, no fee yet)
355        let deployment = engine.deploy_program(&program, &mut rng)?;
356
357        // 4. Compute minimum deployment cost (TestnetV0 uses V14 consensus)
358        let min_cost = engine.deployment_cost_minimum(&deployment, ConsensusVersion::V14)?;
359        // Add a small buffer (5%) to account for node-level overhead that the static cost
360        // calculation doesn't capture. This matches how the on-chain validator counts costs.
361        let base_fee = min_cost.saturating_mul(105) / 100;
362
363        // 5. Fetch state root for proving
364        let (state_root, block_height) = self.network.fetch_state_root().await?;
365        let query = ProvableQuery::new(
366            state_root,
367            block_height,
368            "https://api.provable.com/v2/testnet",
369        );
370
371        // 6. Build full deployment transaction (prove fee + package)
372        let tx = engine.build_deployment_transaction(
373            &account.private_key,
374            &program,
375            &deployment,
376            base_fee,
377            priority_fee_in_microcredits,
378            ConsensusVersion::V14,
379            &query,
380            &mut rng,
381        )?;
382
383        // 7. Serialize and broadcast
384        let tx_json = serde_json::to_string(&tx)?;
385        let raw = self.network.broadcast_transaction(tx_json).await?;
386        Ok(raw.trim_matches('"').to_string())
387    }
388
389    // ── On-chain queries ────────────────────────────────────────────────
390
391    /// Query the public balance of the current account from `credits.aleo`.
392    ///
393    /// Returns `None` if the account has never received credits (no mapping entry).
394    pub async fn get_balance(&self) -> Result<Option<u64>> {
395        let addr = self.require_account()?.address_str();
396        // Try REST first (more reliable than JSON-RPC for mapping queries)
397        if let Some(val) =
398            self.network.fetch_mapping_value_rest("credits.aleo", "account", &addr).await?
399        {
400            return Ok(Some(val));
401        }
402        // Fallback: JSON-RPC path
403        let val = self.network.fetch_mapping_value("credits.aleo", "account", &addr).await?;
404        match val {
405            Some(s) => Ok(Some(s.trim().parse::<u64>()?)),
406            None => Ok(None),
407        }
408    }
409
410    /// Fetch unspent records for the current account's view key.
411    pub async fn fetch_unspent_records(&self) -> Result<String> {
412        let account = self.require_account()?;
413        self.network.fetch_records(&account.view_key.to_string()).await
414    }
415
416    /// Find unspent private credits records owned by the current account's view key.
417    pub async fn find_private_credits_records(&self) -> Result<Vec<(String, u64)>> {
418        let account = self.require_account()?;
419        self.network.find_private_credits_records(&account.view_key.to_string()).await
420    }
421
422    /// Fetch the current block height.
423    pub async fn get_block_height(&self) -> Result<u32> {
424        self.network.fetch_block_height().await
425    }
426
427    /// Fetch latest state root.
428    pub async fn get_state_root(&self) -> Result<<TestnetV0 as Network>::StateRoot> {
429        self.network.fetch_state_root_only().await
430    }
431
432    // ── Verification ─────────────────────────────────────────────────────
433
434    /// Fetch a transaction from the network and verify its proof.
435    ///
436    /// Equivalent to JS SDK's `verifyExecution()` — deserializes the on-chain
437    /// transaction and runs the snarkVM proof verifier locally.
438    ///
439    /// - **Execute** transactions: verifies the execution proof.
440    /// - **Deploy** transactions: verifies the deployment proof.
441    pub async fn verify_execution(&self, tx_id: &str) -> Result<String> {
442        use snarkvm::ledger::block::Transaction;
443
444        // 1. Fetch + deserialize transaction
445        let tx_json = self.network.fetch_transaction(tx_id).await?;
446        let tx: Transaction<TestnetV0> = serde_json::from_str(&tx_json)
447            .map_err(|e| anyhow::anyhow!("Failed to deserialize transaction: {e}"))?;
448
449        match tx {
450            Transaction::Execute(ref _id, ref _exec_id, ref execution, ref _fee) => {
451                // 2. Get the first transition for program/function info
452                let transition = execution.transitions().next()
453                    .ok_or_else(|| anyhow::anyhow!("Execution has no transitions"))?;
454                let program_id = *transition.program_id();
455
456                // 3. Fetch program and build engine
457                let program = self.network.fetch_program(&program_id.to_string()).await?;
458                let engine = ExecutionEngine::new()?;
459                engine.add_program(&program)?;
460
461                // 4. Verify execution proof
462                tracing::info!("Verifying execution proof...");
463                engine.verify_execution_transaction(execution)?;
464
465                Ok(format!(
466                    "✅ Execution proof VERIFIED\n   Program: {}\n   Function: {}\n   Transitions: {}",
467                    program_id,
468                    transition.function_name(),
469                    execution.len(),
470                ))
471            }
472            Transaction::Deploy(ref _id, ref _deploy_id, ref _owner, ref deployment, ref _fee) => {
473                // Verify deployment proof using an empty engine (no user program loaded)
474                tracing::info!("Verifying deployment proof...");
475                let engine = ExecutionEngine::new()?;
476                engine.verify_deployment_transaction(deployment, &mut TestRng::default())?;
477
478                Ok(format!(
479                    "✅ Deployment proof VERIFIED\n   Program: {}",
480                    deployment.program_id(),
481                ))
482            }
483            _ => Ok("⚠️  Transaction type does not contain a proof to verify".to_string()),
484        }
485    }
486}