leo_bindings/
utils.rs

1use anyhow::{Result, anyhow};
2use env_logger::{Builder, Env};
3use rand::SeedableRng;
4use rand_chacha::ChaChaRng;
5use serde::Serialize;
6use snarkvm::prelude::*;
7use std::env;
8use std::str::FromStr;
9use std::thread::sleep;
10use std::time::{Duration, Instant};
11
12/// A helper struct for an Aleo account (from snarkOS).
13#[derive(Clone, Debug)]
14pub struct Account<N: Network> {
15    private_key: PrivateKey<N>,
16    view_key: ViewKey<N>,
17    address: Address<N>,
18}
19
20impl<N: Network> Account<N> {
21    pub fn new<R: Rng + CryptoRng>(rng: &mut R) -> Result<Self> {
22        Self::try_from(PrivateKey::new(rng)?)
23    }
24
25    pub const fn private_key(&self) -> &PrivateKey<N> {
26        &self.private_key
27    }
28
29    pub const fn view_key(&self) -> &ViewKey<N> {
30        &self.view_key
31    }
32
33    pub const fn address(&self) -> Address<N> {
34        self.address
35    }
36}
37
38impl<N: Network> TryFrom<PrivateKey<N>> for Account<N> {
39    type Error = Error;
40
41    fn try_from(private_key: PrivateKey<N>) -> Result<Self, Self::Error> {
42        Self::try_from(&private_key)
43    }
44}
45
46impl<N: Network> TryFrom<&PrivateKey<N>> for Account<N> {
47    type Error = Error;
48
49    fn try_from(private_key: &PrivateKey<N>) -> Result<Self, Self::Error> {
50        let view_key = ViewKey::try_from(private_key)?;
51        let address = view_key.to_address();
52        Ok(Self {
53            private_key: *private_key,
54            view_key,
55            address,
56        })
57    }
58}
59
60impl<N: Network> FromStr for Account<N> {
61    type Err = Error;
62
63    fn from_str(private_key: &str) -> Result<Self, Self::Err> {
64        Self::try_from(PrivateKey::from_str(private_key)?)
65    }
66}
67
68pub fn get_public_balance<N: Network>(
69    address: &Address<N>,
70    endpoint: &str,
71    network_path: &str,
72) -> u64 {
73    let credits = ProgramID::<N>::from_str("credits.aleo").unwrap();
74    let account_mapping = Identifier::<N>::from_str("account").unwrap();
75
76    let response = ureq::get(&format!(
77        "{endpoint}/{network_path}/program/{credits}/mapping/{account_mapping}/{address}"
78    ))
79    .call();
80
81    let balance: Option<Value<N>> = match response {
82        Ok(mut response) => {
83            let json_text = response.body_mut().read_to_string().unwrap();
84            serde_json::from_str::<Option<Value<N>>>(&json_text).unwrap()
85        }
86        Err(err) => panic!("{}", err),
87    };
88
89    match balance {
90        Some(Value::Plaintext(Plaintext::Literal(Literal::<N>::U64(amount), _))) => *amount,
91        None => 0,
92        Some(..) => panic!("Failed to deserialize balance for {address}"),
93    }
94}
95
96pub fn get_development_key<N: Network>(index: u16) -> Result<PrivateKey<N>> {
97    let mut rng = ChaChaRng::seed_from_u64(1234567890u64);
98    for _ in 0..index {
99        let _ = PrivateKey::<N>::new(&mut rng)?;
100    }
101
102    PrivateKey::<N>::new(&mut rng)
103}
104
105pub fn get_dev_account<N: Network>(index: u16) -> Result<Account<N>> {
106    if index > 3 {
107        return Err(anyhow!(
108            "Development account index must be 0-3, got {}",
109            index
110        ));
111    }
112    let private_key = get_development_key(index)?;
113    Account::try_from(private_key).map_err(|e| anyhow!("Failed to create account: {}", e))
114}
115
116pub fn get_account_from_env<N: Network>() -> Result<Account<N>> {
117    dotenvy::dotenv().ok();
118    let private_key_str =
119        env::var("PRIVATE_KEY").map_err(|_| anyhow!("PRIVATE_KEY environment variable not set"))?;
120    let private_key = PrivateKey::<N>::from_str(&private_key_str)
121        .map_err(|e| anyhow!("Failed to parse PRIVATE_KEY: {}", e))?;
122    Account::try_from(private_key).map_err(|e| anyhow!("Failed to create account: {}", e))
123}
124
125pub fn broadcast_transaction<N: Network>(
126    transaction: Transaction<N>,
127    endpoint: &str,
128    network_path: &str,
129) -> Result<(), anyhow::Error> {
130    let url = format!("{endpoint}/{network_path}/transaction/broadcast");
131    let mut request = ureq::post(&url);
132    dotenvy::dotenv().ok();
133    if let Ok(api_key) = env::var("PROVABLE_API_KEY") {
134        request = request.header("X-Provable-API-Key", &api_key);
135    }
136    request
137        .send_json(&transaction)
138        .map(|_| ())
139        .map_err(|error| anyhow!("Failed to broadcast transaction {error}"))
140}
141
142pub fn wait_for_transaction_confirmation<N: Network>(
143    transaction_id: &N::TransactionID,
144    endpoint: &str,
145    network_path: &str,
146    timeout_secs: u64,
147) -> Result<(), anyhow::Error> {
148    let start_time = Instant::now();
149    loop {
150        if start_time.elapsed() > Duration::from_secs(timeout_secs) {
151            return Err(anyhow!("Transaction timeout after {timeout_secs} seconds"));
152        }
153        let url = &format!("{endpoint}/{network_path}/transaction/confirmed/{transaction_id}");
154        match ureq::get(url).call() {
155            Ok(mut response) => {
156                let json_text = response.body_mut().read_to_string().unwrap();
157                let json: serde_json::Value = serde_json::from_str(&json_text).unwrap();
158                let status = json.get("status").and_then(|s| s.as_str()).unwrap();
159                match status {
160                    "accepted" => return Ok(()),
161                    "rejected" => return Err(anyhow!("Transaction rejected: {json}")),
162                    _ => return Err(anyhow!("Unexpected status '{status}': {json}")),
163                }
164            }
165            Err(ureq::Error::StatusCode(500)) => {
166                sleep(Duration::from_secs(1));
167            }
168            Err(ureq::Error::StatusCode(404)) => {
169                sleep(Duration::from_secs(2));
170            }
171            Err(e) => {
172                return Err(anyhow!("Failed to fetch transaction status: {}", e));
173            }
174        }
175    }
176}
177
178pub fn wait_for_program_availability(
179    program_id: &str,
180    endpoint: &str,
181    network_path: &str,
182    timeout_secs: u64,
183) -> Result<(), anyhow::Error> {
184    let start_time = Instant::now();
185    loop {
186        if start_time.elapsed() > Duration::from_secs(timeout_secs) {
187            return Err(anyhow!("Timeout waiting for program {program_id}"));
188        }
189        match ureq::get(&format!("{endpoint}/{network_path}/program/{program_id}")).call() {
190            Ok(_) => return Ok(()),
191            Err(_) => sleep(Duration::from_secs(1)),
192        }
193    }
194}
195#[derive(Debug, Clone)]
196pub struct DelegatedProvingConfig {
197    pub api_key: String,
198    pub endpoint: String,
199    pub enabled: bool,
200}
201
202impl DelegatedProvingConfig {
203    pub fn new(api_key: &str, endpoint: Option<&str>) -> Self {
204        Self {
205            api_key: api_key.to_string(),
206            endpoint: endpoint
207                .map(|s| s.to_string())
208                .unwrap_or_else(|| "https://api.explorer.provable.com".to_string()),
209            enabled: false,
210        }
211    }
212    pub fn from_env() -> Result<Self> {
213        dotenvy::dotenv().ok();
214        let api_key =
215            env::var("PROVABLE_API_KEY").map_err(|_| anyhow!("PROVABLE_API_KEY not set"))?;
216        let endpoint = env::var("PROVABLE_ENDPOINT")
217            .unwrap_or_else(|_| "https://api.explorer.provable.com".to_string());
218
219        Ok(Self {
220            api_key,
221            endpoint,
222            enabled: false,
223        })
224    }
225
226    pub fn enabled(mut self) -> Self {
227        self.enabled = true;
228        self
229    }
230}
231
232#[derive(Debug, Serialize)]
233struct ProvingRequest {
234    authorization: serde_json::Value,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    fee_authorization: Option<serde_json::Value>,
237    broadcast: bool,
238}
239
240pub fn execute_with_delegated_proving<N: Network>(
241    config: &DelegatedProvingConfig,
242    authorization: Authorization<N>,
243) -> Result<Transaction<N>> {
244    let authorization_json = serde_json::to_value(&authorization)
245        .map_err(|e| anyhow!("Failed to serialize authorization: {}", e))?;
246
247    let proving_request = ProvingRequest {
248        authorization: authorization_json,
249        fee_authorization: None,
250        broadcast: false,
251    };
252
253    let url = format!("{}/v2/{}/prove", config.endpoint, N::SHORT_NAME);
254
255    let response = ureq::post(&url)
256        .header("X-Provable-API-Key", &config.api_key)
257        .header("X-ALEO-METHOD", "submitProvingRequest")
258        .header("Content-Type", "application/json")
259        .send_json(&proving_request);
260
261    let mut response = match response {
262        Ok(r) => r,
263        Err(ureq::Error::StatusCode(status)) => {
264            return Err(anyhow!("Request failed with status {}", status,));
265        }
266        Err(e) => {
267            return Err(anyhow!("Failed to send request: {}", e));
268        }
269    };
270
271    let response_text = response
272        .body_mut()
273        .read_to_string()
274        .map_err(|e| anyhow!("Failed to read response body: {}", e))?;
275
276    let response_json: serde_json::Value = serde_json::from_str(&response_text)
277        .map_err(|e| anyhow!("Failed to parse response as JSON: {}", e,))?;
278
279    let transaction_value = response_json
280        .get("transaction")
281        .ok_or_else(|| anyhow!("'transaction' field missing: {}", response_text))?;
282
283    let transaction_str = serde_json::to_string(transaction_value)
284        .map_err(|e| anyhow!("Failed to serialize transaction: {}", e))?;
285
286    let transaction: Transaction<N> = serde_json::from_str(&transaction_str)
287        .map_err(|e| anyhow!("Failed to deserialize transaction: {}", e))?;
288
289    log::info!("✅ Received proved transaction: {}", transaction.id());
290
291    Ok(transaction)
292}
293
294pub fn extract_outputs_from_authorization<N: Network>(
295    authorization: &Authorization<N>,
296    view_key: &ViewKey<N>,
297) -> Result<Vec<Value<N>>> {
298    let request = authorization.peek_next()?;
299    let function_id = snarkvm::console::program::compute_function_id(
300        request.network_id(),
301        request.program_id(),
302        request.function_name(),
303    )?;
304    let num_inputs = request.inputs().len();
305
306    let transitions = authorization.transitions();
307    let main_transition = transitions
308        .values()
309        .last()
310        .ok_or_else(|| anyhow!("Authorization contains no transitions"))?;
311
312    main_transition
313        .outputs()
314        .iter()
315        .enumerate()
316        .map(|(i, output)| {
317            decrypt_output(output, i, num_inputs, function_id, request.tvk(), view_key)
318        })
319        .collect()
320}
321
322fn decrypt_output<N: Network>(
323    output: &snarkvm::ledger::block::Output<N>,
324    output_index: usize,
325    num_inputs: usize,
326    function_id: Field<N>,
327    tvk: &Field<N>,
328    view_key: &ViewKey<N>,
329) -> Result<Value<N>> {
330    use snarkvm::ledger::block::Output;
331
332    match output {
333        Output::Constant(_, Some(plaintext)) | Output::Public(_, Some(plaintext)) => {
334            Ok(Value::Plaintext(plaintext.clone()))
335        }
336        Output::Private(_, Some(ciphertext)) => {
337            let index = Field::from_u16(u16::try_from(num_inputs + output_index)?);
338            let output_view_key = N::hash_psd4(&[function_id, *tvk, index])?;
339            let plaintext = ciphertext.decrypt_symmetric(output_view_key)?;
340            Ok(Value::Plaintext(plaintext))
341        }
342        Output::Record(_, _, Some(record_ciphertext), _) => {
343            let record_plaintext = record_ciphertext.decrypt(view_key)?;
344            Ok(Value::Record(record_plaintext))
345        }
346        Output::Future(_, Some(future)) => Ok(Value::Future(future.clone())),
347        Output::ExternalRecord(_) => Err(anyhow!("External record outputs are not supported")),
348        _ => Err(anyhow!("Output value is missing from transition")),
349    }
350}
351
352pub fn init_simple_logger() {
353    use std::io::Write;
354    let _ = Builder::from_env(Env::default().filter_or("RUST_LOG", "info"))
355        .format(|buf, record| writeln!(buf, "{}", record.args()))
356        .try_init();
357}
358
359pub fn init_test_logger() {
360    use std::io::Write;
361    let _ = Builder::from_env(Env::default().filter_or("RUST_LOG", "info"))
362        .is_test(true)
363        .format(|buf, record| writeln!(buf, "{}", record.args()))
364        .try_init();
365}
366
367fn fetch_latest_block_height(endpoint: &str, network_path: &str) -> Result<u32> {
368    let url = format!("{}/{}/block/height/latest", endpoint, network_path);
369    let mut response = ureq::get(&url)
370        .call()
371        .map_err(|e| anyhow!("Failed to fetch latest block height: {}", e))?;
372    let height_str = response
373        .body_mut()
374        .read_to_string()
375        .map_err(|e| anyhow!("Failed to read response: {}", e))?;
376    u32::from_str(&height_str).map_err(|e| anyhow!("Failed to parse block height: {}", e))
377}
378
379pub fn fetch_mapping_value(url: &str) -> Result<Option<String>> {
380    let max_retries = 3;
381
382    for attempt in 0..=max_retries {
383        match ureq::get(url).call() {
384            Ok(mut response) => {
385                let json_text = response.body_mut().read_to_string()?;
386                return Ok(Some(json_text));
387            }
388            Err(ureq::Error::StatusCode(404)) => {
389                return Ok(None);
390            }
391            Err(e) => {
392                if attempt >= max_retries {
393                    return Err(anyhow!("Failed to fetch mapping value: {}", e));
394                }
395                log::warn!(
396                    "Error fetching mapping (attempt {}/{}): {}. Retrying in 5s...",
397                    attempt + 1,
398                    max_retries + 1,
399                    e,
400                );
401                sleep(Duration::from_secs(5));
402            }
403        }
404    }
405    unreachable!()
406}