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#[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}