1#![allow(clippy::unwrap_used, clippy::expect_used)]
11
12use crate::client::AccumulateClient;
13use crate::json_rpc_client::JsonRpcError;
14use crate::AccOptions;
15use ed25519_dalek::{SigningKey, Signer};
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18use sha2::{Digest, Sha256};
19use std::time::{Duration, SystemTime, UNIX_EPOCH};
20use url::Url;
21
22pub const KERMIT_V2: &str = "https://kermit.accumulatenetwork.io/v2";
28pub const KERMIT_V3: &str = "https://kermit.accumulatenetwork.io/v3";
30
31pub const DEVNET_V2: &str = "http://127.0.0.1:26660/v2";
33pub const DEVNET_V3: &str = "http://127.0.0.1:26660/v3";
35
36#[derive(Debug, Clone)]
42pub struct TxResult {
43 pub success: bool,
45 pub txid: Option<String>,
47 pub error: Option<String>,
49 pub response: Option<Value>,
51}
52
53impl TxResult {
54 pub fn ok(txid: String, response: Value) -> Self {
56 Self {
57 success: true,
58 txid: Some(txid),
59 error: None,
60 response: Some(response),
61 }
62 }
63
64 pub fn err(error: String) -> Self {
66 Self {
67 success: false,
68 txid: None,
69 error: Some(error),
70 response: None,
71 }
72 }
73}
74
75#[derive(Debug)]
81pub struct TxBody;
82
83impl TxBody {
84 pub fn add_credits(recipient: &str, amount: &str, oracle: u64) -> Value {
86 json!({
87 "type": "addCredits",
88 "recipient": recipient,
89 "amount": amount,
90 "oracle": oracle
91 })
92 }
93
94 pub fn create_identity(url: &str, key_book_url: &str, public_key_hash: &str) -> Value {
96 json!({
97 "type": "createIdentity",
98 "url": url,
99 "keyBookUrl": key_book_url,
100 "keyHash": public_key_hash
101 })
102 }
103
104 pub fn create_token_account(url: &str, token_url: &str) -> Value {
106 json!({
107 "type": "createTokenAccount",
108 "url": url,
109 "tokenUrl": token_url
110 })
111 }
112
113 pub fn create_data_account(url: &str) -> Value {
115 json!({
116 "type": "createDataAccount",
117 "url": url
118 })
119 }
120
121 pub fn create_token(url: &str, symbol: &str, precision: u64, supply_limit: Option<&str>) -> Value {
123 let mut body = json!({
124 "type": "createToken",
125 "url": url,
126 "symbol": symbol,
127 "precision": precision
128 });
129 if let Some(limit) = supply_limit {
130 body["supplyLimit"] = json!(limit);
131 }
132 body
133 }
134
135 pub fn send_tokens_single(to_url: &str, amount: &str) -> Value {
137 json!({
138 "type": "sendTokens",
139 "to": [{
140 "url": to_url,
141 "amount": amount
142 }]
143 })
144 }
145
146 pub fn send_tokens_multi(recipients: &[(&str, &str)]) -> Value {
148 let to: Vec<Value> = recipients
149 .iter()
150 .map(|(url, amount)| json!({"url": url, "amount": amount}))
151 .collect();
152 json!({
153 "type": "sendTokens",
154 "to": to
155 })
156 }
157
158 pub fn issue_tokens_single(to_url: &str, amount: &str) -> Value {
160 json!({
161 "type": "issueTokens",
162 "to": [{
163 "url": to_url,
164 "amount": amount
165 }]
166 })
167 }
168
169 pub fn write_data(entries: &[&str]) -> Value {
172 let entries_hex: Vec<Value> = entries
174 .iter()
175 .map(|e| Value::String(hex::encode(e.as_bytes())))
176 .collect();
177 json!({
178 "type": "writeData",
179 "entry": {
180 "type": "doublehash", "data": entries_hex }
183 })
184 }
185
186 pub fn write_data_hex(entries_hex: &[&str]) -> Value {
188 let entries: Vec<Value> = entries_hex
190 .iter()
191 .map(|e| Value::String((*e).to_string()))
192 .collect();
193 json!({
194 "type": "writeData",
195 "entry": {
196 "type": "doublehash", "data": entries }
199 })
200 }
201
202 pub fn create_key_page(key_hashes: &[&[u8]]) -> Value {
204 let keys: Vec<Value> = key_hashes
205 .iter()
206 .map(|h| json!({"publicKeyHash": hex::encode(h)}))
207 .collect();
208 json!({
209 "type": "createKeyPage",
210 "keys": keys
211 })
212 }
213
214 pub fn create_key_book(url: &str, public_key_hash: &str) -> Value {
216 json!({
217 "type": "createKeyBook",
218 "url": url,
219 "publicKeyHash": public_key_hash
220 })
221 }
222
223 pub fn update_key_page_add_key(key_hash: &[u8]) -> Value {
225 json!({
226 "type": "updateKeyPage",
227 "operation": [{
228 "type": "add",
229 "entry": {
230 "keyHash": hex::encode(key_hash)
231 }
232 }]
233 })
234 }
235
236 pub fn update_key_page_remove_key(key_hash: &[u8]) -> Value {
238 json!({
239 "type": "updateKeyPage",
240 "operation": [{
241 "type": "remove",
242 "entry": {
243 "keyHash": hex::encode(key_hash)
244 }
245 }]
246 })
247 }
248
249 pub fn update_key_page_set_threshold(threshold: u64) -> Value {
251 json!({
252 "type": "updateKeyPage",
253 "operation": [{
254 "type": "setThreshold",
255 "threshold": threshold
256 }]
257 })
258 }
259
260 pub fn burn_tokens(amount: &str) -> Value {
262 json!({
263 "type": "burnTokens",
264 "amount": amount
265 })
266 }
267
268 pub fn transfer_credits(to_url: &str, amount: u64) -> Value {
270 json!({
271 "type": "transferCredits",
272 "to": [{
273 "url": to_url,
274 "amount": amount
275 }]
276 })
277 }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct KeyPageState {
287 pub url: String,
289 pub version: u64,
291 pub credit_balance: u64,
293 pub accept_threshold: u64,
295 pub keys: Vec<KeyEntry>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct KeyEntry {
302 pub key_hash: String,
304 pub delegate: Option<String>,
306}
307
308#[derive(Debug)]
314pub struct SmartSigner<'a> {
315 client: &'a AccumulateClient,
317 keypair: SigningKey,
319 signer_url: String,
321 cached_version: u64,
323}
324
325impl<'a> SmartSigner<'a> {
326 pub fn new(client: &'a AccumulateClient, keypair: SigningKey, signer_url: &str) -> Self {
328 Self {
329 client,
330 keypair,
331 signer_url: signer_url.to_string(),
332 cached_version: 1,
333 }
334 }
335
336 pub async fn refresh_version(&mut self) -> Result<u64, JsonRpcError> {
338 let params = json!({
339 "scope": &self.signer_url,
340 "query": {"queryType": "default"}
341 });
342
343 let result: Value = self.client.v3_client.call_v3("query", params).await?;
344
345 if let Some(account) = result.get("account") {
346 if let Some(version) = account.get("version").and_then(|v| v.as_u64()) {
347 self.cached_version = version;
348 return Ok(version);
349 }
350 }
351
352 Ok(self.cached_version)
353 }
354
355 pub fn version(&self) -> u64 {
357 self.cached_version
358 }
359
360 pub fn sign(&self, principal: &str, body: &Value, memo: Option<&str>) -> Result<Value, JsonRpcError> {
369 use crate::codec::signing::{
370 compute_ed25519_signature_metadata_hash,
371 compute_transaction_hash,
372 compute_write_data_body_hash,
373 create_signing_preimage,
374 marshal_transaction_header,
375 sha256_bytes,
376 };
377
378 let timestamp = SystemTime::now()
379 .duration_since(UNIX_EPOCH)
380 .map_err(|e| JsonRpcError::General(anyhow::anyhow!("Time error: {}", e)))?
381 .as_micros() as u64;
382
383 let public_key = self.keypair.verifying_key().to_bytes();
384
385 let sig_metadata_hash = compute_ed25519_signature_metadata_hash(
388 &public_key,
389 &self.signer_url,
390 self.cached_version,
391 timestamp,
392 );
393 let initiator_hex = hex::encode(&sig_metadata_hash);
394
395 let header_bytes = marshal_transaction_header(
397 principal,
398 &sig_metadata_hash,
399 memo,
400 None,
401 );
402
403 let tx_type = body.get("type").and_then(|t| t.as_str()).unwrap_or("");
406 let tx_hash = if tx_type == "writeData" || tx_type == "writeDataTo" {
407 let header_hash = sha256_bytes(&header_bytes);
409
410 let mut entries_hex = Vec::new();
412 if let Some(entry) = body.get("entry") {
413 if let Some(data) = entry.get("data") {
414 if let Some(arr) = data.as_array() {
415 for item in arr {
416 if let Some(s) = item.as_str() {
417 entries_hex.push(s.to_string());
418 }
419 }
420 }
421 }
422 }
423 let scratch = body.get("scratch").and_then(|s| s.as_bool()).unwrap_or(false);
424 let write_to_state = body.get("writeToState").and_then(|w| w.as_bool()).unwrap_or(false);
425
426 let body_hash = compute_write_data_body_hash(&entries_hex, scratch, write_to_state);
427
428 let mut combined = Vec::with_capacity(64);
430 combined.extend_from_slice(&header_hash);
431 combined.extend_from_slice(&body_hash);
432 sha256_bytes(&combined)
433 } else {
434 let body_bytes = marshal_body_to_binary(body)?;
436 compute_transaction_hash(&header_bytes, &body_bytes)
437 };
438
439 let preimage = create_signing_preimage(&sig_metadata_hash, &tx_hash);
441 let signature = self.keypair.sign(&preimage);
442
443 let mut tx = json!({
445 "header": {
446 "principal": principal,
447 "initiator": &initiator_hex
448 },
449 "body": body
450 });
451
452 if let Some(m) = memo {
453 tx["header"]["memo"] = json!(m);
454 }
455
456 let envelope = json!({
459 "transaction": [tx],
460 "signatures": [{
461 "type": "ed25519",
462 "publicKey": hex::encode(&public_key),
463 "signature": hex::encode(signature.to_bytes()),
464 "signer": &self.signer_url,
465 "signerVersion": self.cached_version,
466 "timestamp": timestamp,
467 "transactionHash": hex::encode(&tx_hash)
468 }]
469 });
470
471 Ok(envelope)
472 }
473
474 pub async fn sign_submit_and_wait(
476 &mut self,
477 principal: &str,
478 body: &Value,
479 memo: Option<&str>,
480 max_attempts: u32,
481 ) -> TxResult {
482 if let Err(e) = self.refresh_version().await {
484 return TxResult::err(format!("Failed to refresh version: {}", e));
485 }
486
487 let envelope = match self.sign(principal, body, memo) {
489 Ok(env) => env,
490 Err(e) => return TxResult::err(format!("Failed to sign: {}", e)),
491 };
492
493 let submit_result: Result<Value, _> = self.client.v3_client.call_v3("submit", json!({
495 "envelope": envelope
496 })).await;
497
498 let response = match submit_result {
499 Ok(resp) => resp,
500 Err(e) => return TxResult::err(format!("Submit failed: {}", e)),
501 };
502
503 let txid = extract_txid(&response);
505 if txid.is_none() {
506 return TxResult::err("No transaction ID in response".to_string());
507 }
508 let txid = txid.unwrap();
509
510 let tx_hash = if txid.starts_with("acc://") && txid.contains('@') {
513 txid.split('@').next().unwrap_or(&txid).replace("acc://", "")
514 } else {
515 txid.clone()
516 };
517 let query_scope = format!("acc://{}@unknown", tx_hash);
518
519 for _attempt in 0..max_attempts {
520 tokio::time::sleep(Duration::from_secs(2)).await;
521
522 let query_result: Result<Value, _> = self.client.v3_client.call_v3("query", json!({
524 "scope": &query_scope,
525 "query": {"queryType": "default"}
526 })).await;
527
528 if let Ok(result) = query_result {
529 if let Some(status_value) = result.get("status") {
531 if let Some(status_str) = status_value.as_str() {
533 if status_str == "delivered" {
534 return TxResult::ok(txid, response);
535 }
536 continue;
538 }
539
540 if status_value.is_object() {
542 let delivered = status_value.get("delivered")
543 .and_then(|d| d.as_bool())
544 .unwrap_or(false);
545
546 if delivered {
547 let failed = status_value.get("failed")
549 .and_then(|f| f.as_bool())
550 .unwrap_or(false);
551
552 if failed {
553 let error_msg = status_value.get("error")
554 .and_then(|e| {
555 if let Some(msg) = e.get("message").and_then(|m| m.as_str()) {
556 Some(msg.to_string())
557 } else {
558 e.as_str().map(String::from)
559 }
560 })
561 .unwrap_or_else(|| "Unknown error".to_string());
562 return TxResult::err(error_msg);
563 }
564
565 return TxResult::ok(txid, response);
566 }
567 }
568 }
569 }
570 }
571
572 TxResult::err(format!("Timeout waiting for delivery: {}", txid))
573 }
574
575 pub async fn add_key(&mut self, public_key: &[u8]) -> TxResult {
577 let key_hash = sha256_hash(public_key);
578 let body = TxBody::update_key_page_add_key(&key_hash);
579 self.sign_submit_and_wait(&self.signer_url.clone(), &body, Some("Add key"), 30).await
580 }
581
582 pub async fn remove_key(&mut self, public_key_hash: &[u8]) -> TxResult {
584 let body = TxBody::update_key_page_remove_key(public_key_hash);
585 self.sign_submit_and_wait(&self.signer_url.clone(), &body, Some("Remove key"), 30).await
586 }
587
588 pub async fn set_threshold(&mut self, threshold: u64) -> TxResult {
590 let body = TxBody::update_key_page_set_threshold(threshold);
591 self.sign_submit_and_wait(&self.signer_url.clone(), &body, Some("Set threshold"), 30).await
592 }
593
594 #[allow(dead_code)]
596 fn public_key_hash(&self) -> [u8; 32] {
597 sha256_hash(&self.keypair.verifying_key().to_bytes())
598 }
599}
600
601fn marshal_body_to_binary(body: &Value) -> Result<Vec<u8>, JsonRpcError> {
605 use crate::codec::signing::{
606 marshal_add_credits_body, marshal_send_tokens_body, marshal_create_identity_body,
607 marshal_create_token_account_body, marshal_create_data_account_body,
608 marshal_write_data_body, marshal_create_token_body, marshal_issue_tokens_body,
609 marshal_key_page_operation, marshal_update_key_page_body,
610 tx_types
611 };
612 use crate::codec::writer::BinaryWriter;
613
614 let tx_type = body.get("type").and_then(|t| t.as_str()).unwrap_or("");
615
616 match tx_type {
617 "addCredits" => {
618 let recipient = body.get("recipient").and_then(|r| r.as_str()).unwrap_or("");
619 let amount_str = body.get("amount").and_then(|a| a.as_str()).unwrap_or("0");
620 let amount: u64 = amount_str.parse().unwrap_or(0);
621 let oracle = body.get("oracle").and_then(|o| o.as_u64()).unwrap_or(0);
622 Ok(marshal_add_credits_body(recipient, amount, oracle))
623 }
624 "sendTokens" => {
625 let to_array = body.get("to").and_then(|t| t.as_array());
626 let mut recipients = Vec::new();
627 if let Some(to) = to_array {
628 for recipient in to {
629 let url = recipient.get("url").and_then(|u| u.as_str()).unwrap_or("");
630 let amount_str = recipient.get("amount").and_then(|a| a.as_str()).unwrap_or("0");
631 let amount: u64 = amount_str.parse().unwrap_or(0);
632 recipients.push((url.to_string(), amount));
633 }
634 }
635 Ok(marshal_send_tokens_body(&recipients))
636 }
637 "createIdentity" => {
638 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
639 let key_book_url = body.get("keyBookUrl").and_then(|k| k.as_str()).unwrap_or("");
640 let key_hash_hex = body.get("keyHash")
642 .or_else(|| body.get("publicKeyHash"))
643 .and_then(|k| k.as_str())
644 .unwrap_or("");
645 let key_hash = hex::decode(key_hash_hex).unwrap_or_default();
646 Ok(marshal_create_identity_body(url, &key_hash, key_book_url))
647 }
648 "createTokenAccount" => {
649 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
650 let token_url = body.get("tokenUrl").and_then(|t| t.as_str()).unwrap_or("");
651 Ok(marshal_create_token_account_body(url, token_url))
652 }
653 "createDataAccount" => {
654 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
655 Ok(marshal_create_data_account_body(url))
656 }
657 "writeData" => {
658 let mut entries_hex = Vec::new();
660 if let Some(entry) = body.get("entry") {
661 if let Some(data) = entry.get("data") {
662 if let Some(arr) = data.as_array() {
663 for item in arr {
664 if let Some(s) = item.as_str() {
665 entries_hex.push(s.to_string());
666 }
667 }
668 }
669 }
670 }
671 let scratch = body.get("scratch").and_then(|s| s.as_bool()).unwrap_or(false);
672 let write_to_state = body.get("writeToState").and_then(|w| w.as_bool()).unwrap_or(false);
673 Ok(marshal_write_data_body(&entries_hex, scratch, write_to_state))
674 }
675 "createToken" => {
676 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
677 let symbol = body.get("symbol").and_then(|s| s.as_str()).unwrap_or("");
678 let precision = body.get("precision").and_then(|p| p.as_u64()).unwrap_or(0);
679 let supply_limit = body.get("supplyLimit")
680 .and_then(|s| s.as_str())
681 .and_then(|s| s.parse::<u64>().ok());
682 Ok(marshal_create_token_body(url, symbol, precision, supply_limit))
683 }
684 "issueTokens" => {
685 let to_array = body.get("to").and_then(|t| t.as_array());
686 let mut recipients: Vec<(&str, u64)> = Vec::new();
687 if let Some(to) = to_array {
688 for recipient in to {
689 let url = recipient.get("url").and_then(|u| u.as_str()).unwrap_or("");
690 let amount_str = recipient.get("amount").and_then(|a| a.as_str()).unwrap_or("0");
691 let amount: u64 = amount_str.parse().unwrap_or(0);
692 recipients.push((url, amount));
693 }
694 }
695 Ok(marshal_issue_tokens_body(&recipients))
696 }
697 "updateKeyPage" => {
698 let op_array = body.get("operation").and_then(|o| o.as_array());
700 let mut operations: Vec<Vec<u8>> = Vec::new();
701
702 if let Some(ops) = op_array {
703 for op in ops {
704 let op_type = op.get("type").and_then(|t| t.as_str()).unwrap_or("");
705
706 let key_hash: Option<Vec<u8>> = op.get("entry")
709 .and_then(|e| e.get("keyHash"))
710 .and_then(|h| h.as_str())
711 .and_then(|hex_str| hex::decode(hex_str).ok());
712
713 let delegate: Option<&str> = op.get("entry")
715 .and_then(|e| e.get("delegate"))
716 .and_then(|d| d.as_str());
717
718 let old_key_hash: Option<Vec<u8>> = op.get("oldEntry")
720 .and_then(|e| e.get("keyHash"))
721 .and_then(|h| h.as_str())
722 .and_then(|hex_str| hex::decode(hex_str).ok());
723
724 let new_key_hash: Option<Vec<u8>> = op.get("newEntry")
725 .and_then(|e| e.get("keyHash"))
726 .and_then(|h| h.as_str())
727 .and_then(|hex_str| hex::decode(hex_str).ok());
728
729 let threshold: Option<u64> = op.get("threshold").and_then(|t| t.as_u64());
731
732 let op_bytes = marshal_key_page_operation(
734 op_type,
735 key_hash.as_deref(),
736 delegate,
737 old_key_hash.as_deref(),
738 new_key_hash.as_deref(),
739 threshold,
740 );
741 operations.push(op_bytes);
742 }
743 }
744
745 Ok(marshal_update_key_page_body(&operations))
746 }
747 _ => {
750 let mut writer = BinaryWriter::new();
752
753 let type_num = match tx_type {
755 "createIdentity" => tx_types::CREATE_IDENTITY,
756 "createTokenAccount" => tx_types::CREATE_TOKEN_ACCOUNT,
757 "createDataAccount" => tx_types::CREATE_DATA_ACCOUNT,
758 "writeData" => tx_types::WRITE_DATA,
759 "writeDataTo" => tx_types::WRITE_DATA_TO,
760 "acmeFaucet" => tx_types::ACME_FAUCET,
761 "createToken" => tx_types::CREATE_TOKEN,
762 "issueTokens" => tx_types::ISSUE_TOKENS,
763 "burnTokens" => tx_types::BURN_TOKENS,
764 "createLiteTokenAccount" => tx_types::CREATE_LITE_TOKEN_ACCOUNT,
765 "createKeyPage" => tx_types::CREATE_KEY_PAGE,
766 "createKeyBook" => tx_types::CREATE_KEY_BOOK,
767 "updateKeyPage" => tx_types::UPDATE_KEY_PAGE,
768 "updateAccountAuth" => tx_types::UPDATE_ACCOUNT_AUTH,
769 "updateKey" => tx_types::UPDATE_KEY,
770 "lockAccount" => tx_types::LOCK_ACCOUNT,
771 "transferCredits" => tx_types::TRANSFER_CREDITS,
772 "burnCredits" => tx_types::BURN_CREDITS,
773 _ => 0,
774 };
775
776 let _ = writer.write_uvarint(1);
778 let _ = writer.write_uvarint(type_num);
779
780 Ok(writer.into_bytes())
784 }
785 }
786}
787
788#[derive(Debug)]
794pub struct KeyManager<'a> {
795 client: &'a AccumulateClient,
797 key_page_url: String,
799}
800
801impl<'a> KeyManager<'a> {
802 pub fn new(client: &'a AccumulateClient, key_page_url: &str) -> Self {
804 Self {
805 client,
806 key_page_url: key_page_url.to_string(),
807 }
808 }
809
810 pub async fn get_key_page_state(&self) -> Result<KeyPageState, JsonRpcError> {
812 let params = json!({
813 "scope": &self.key_page_url,
814 "query": {"queryType": "default"}
815 });
816
817 let result: Value = self.client.v3_client.call_v3("query", params).await?;
818
819 let account = result.get("account")
820 .ok_or_else(|| JsonRpcError::General(anyhow::anyhow!("No account in response")))?;
821
822 let url = account.get("url")
823 .and_then(|v| v.as_str())
824 .unwrap_or(&self.key_page_url)
825 .to_string();
826
827 let version = account.get("version")
828 .and_then(|v| v.as_u64())
829 .unwrap_or(1);
830
831 let credit_balance = account.get("creditBalance")
832 .and_then(|v| v.as_u64())
833 .unwrap_or(0);
834
835 let accept_threshold = account.get("acceptThreshold")
836 .or_else(|| account.get("threshold"))
837 .and_then(|v| v.as_u64())
838 .unwrap_or(1);
839
840 let keys: Vec<KeyEntry> = if let Some(keys_arr) = account.get("keys").and_then(|k| k.as_array()) {
841 keys_arr.iter().map(|k| {
842 let key_hash = k.get("publicKeyHash")
843 .or_else(|| k.get("publicKey"))
844 .and_then(|v| v.as_str())
845 .unwrap_or("")
846 .to_string();
847 let delegate = k.get("delegate").and_then(|v| v.as_str()).map(String::from);
848 KeyEntry { key_hash, delegate }
849 }).collect()
850 } else {
851 vec![]
852 };
853
854 Ok(KeyPageState {
855 url,
856 version,
857 credit_balance,
858 accept_threshold,
859 keys,
860 })
861 }
862}
863
864pub async fn poll_for_balance(
870 client: &AccumulateClient,
871 account_url: &str,
872 max_attempts: u32,
873) -> Option<u64> {
874 for i in 0..max_attempts {
875 let params = json!({
876 "scope": account_url,
877 "query": {"queryType": "default"}
878 });
879
880 match client.v3_client.call_v3::<Value>("query", params).await {
881 Ok(result) => {
882 if let Some(account) = result.get("account") {
883 if let Some(balance) = account.get("balance").and_then(|b| b.as_str()) {
885 if let Ok(bal) = balance.parse::<u64>() {
886 if bal > 0 {
887 return Some(bal);
888 }
889 }
890 }
891 if let Some(bal) = account.get("balance").and_then(|b| b.as_u64()) {
893 if bal > 0 {
894 return Some(bal);
895 }
896 }
897 }
898 println!(" Waiting for balance... (attempt {}/{})", i + 1, max_attempts);
899 }
900 Err(_) => {
901 println!(" Account not found yet... (attempt {}/{})", i + 1, max_attempts);
903 }
904 }
905
906 if i < max_attempts - 1 {
907 tokio::time::sleep(Duration::from_secs(2)).await;
908 }
909 }
910 None
911}
912
913pub async fn poll_for_credits(
915 client: &AccumulateClient,
916 key_page_url: &str,
917 max_attempts: u32,
918) -> Option<u64> {
919 for i in 0..max_attempts {
920 let params = json!({
921 "scope": key_page_url,
922 "query": {"queryType": "default"}
923 });
924
925 if let Ok(result) = client.v3_client.call_v3::<Value>("query", params).await {
926 if let Some(account) = result.get("account") {
927 if let Some(credits) = account.get("creditBalance").and_then(|c| c.as_u64()) {
928 if credits > 0 {
929 return Some(credits);
930 }
931 }
932 }
933 }
934
935 if i < max_attempts - 1 {
936 tokio::time::sleep(Duration::from_secs(2)).await;
937 }
938 }
939 None
940}
941
942pub async fn wait_for_tx(
944 client: &AccumulateClient,
945 txid: &str,
946 max_attempts: u32,
947) -> bool {
948 let tx_hash = txid.split('@').next().unwrap_or(txid).replace("acc://", "");
949
950 for _ in 0..max_attempts {
951 let params = json!({
952 "scope": format!("acc://{}@unknown", tx_hash),
953 "query": {"queryType": "default"}
954 });
955
956 if let Ok(result) = client.v3_client.call_v3::<Value>("query", params).await {
957 if let Some(status) = result.get("status") {
958 if status.get("delivered").and_then(|d| d.as_bool()).unwrap_or(false) {
959 return true;
960 }
961 }
962 }
963
964 tokio::time::sleep(Duration::from_secs(2)).await;
965 }
966 false
967}
968
969#[derive(Debug, Clone)]
975pub struct Wallet {
976 pub lite_identity: String,
978 pub lite_token_account: String,
980 keypair: SigningKey,
982}
983
984impl Wallet {
985 pub fn keypair(&self) -> &SigningKey {
987 &self.keypair
988 }
989
990 pub fn public_key(&self) -> [u8; 32] {
992 self.keypair.verifying_key().to_bytes()
993 }
994
995 pub fn public_key_hash(&self) -> [u8; 32] {
997 sha256_hash(&self.public_key())
998 }
999}
1000
1001#[derive(Debug, Clone)]
1007pub struct AdiInfo {
1008 pub url: String,
1010 pub key_book_url: String,
1012 pub key_page_url: String,
1014 keypair: SigningKey,
1016}
1017
1018impl AdiInfo {
1019 pub fn keypair(&self) -> &SigningKey {
1021 &self.keypair
1022 }
1023
1024 pub fn public_key(&self) -> [u8; 32] {
1026 self.keypair.verifying_key().to_bytes()
1027 }
1028}
1029
1030#[derive(Debug, Clone)]
1036pub struct KeyPageInfo {
1037 pub credits: u64,
1039 pub version: u64,
1041 pub threshold: u64,
1043 pub key_count: usize,
1045}
1046
1047#[derive(Debug)]
1053pub struct QuickStart {
1054 client: AccumulateClient,
1056}
1057
1058impl QuickStart {
1059 pub async fn devnet() -> Result<Self, JsonRpcError> {
1061 let v2_url = Url::parse(DEVNET_V2).map_err(|e| {
1062 JsonRpcError::General(anyhow::anyhow!("Invalid URL: {}", e))
1063 })?;
1064 let v3_url = Url::parse(DEVNET_V3).map_err(|e| {
1065 JsonRpcError::General(anyhow::anyhow!("Invalid URL: {}", e))
1066 })?;
1067
1068 let client = AccumulateClient::new_with_options(v2_url, v3_url, AccOptions::default()).await?;
1069 Ok(Self { client })
1070 }
1071
1072 pub async fn kermit() -> Result<Self, JsonRpcError> {
1074 let v2_url = Url::parse(KERMIT_V2).map_err(|e| {
1075 JsonRpcError::General(anyhow::anyhow!("Invalid URL: {}", e))
1076 })?;
1077 let v3_url = Url::parse(KERMIT_V3).map_err(|e| {
1078 JsonRpcError::General(anyhow::anyhow!("Invalid URL: {}", e))
1079 })?;
1080
1081 let client = AccumulateClient::new_with_options(v2_url, v3_url, AccOptions::default()).await?;
1082 Ok(Self { client })
1083 }
1084
1085 pub async fn custom(v2_endpoint: &str, v3_endpoint: &str) -> Result<Self, JsonRpcError> {
1087 let v2_url = Url::parse(v2_endpoint).map_err(|e| {
1088 JsonRpcError::General(anyhow::anyhow!("Invalid V2 URL: {}", e))
1089 })?;
1090 let v3_url = Url::parse(v3_endpoint).map_err(|e| {
1091 JsonRpcError::General(anyhow::anyhow!("Invalid V3 URL: {}", e))
1092 })?;
1093
1094 let client = AccumulateClient::new_with_options(v2_url, v3_url, AccOptions::default()).await?;
1095 Ok(Self { client })
1096 }
1097
1098 pub fn client(&self) -> &AccumulateClient {
1100 &self.client
1101 }
1102
1103 pub fn create_wallet(&self) -> Wallet {
1105 let keypair = AccumulateClient::generate_keypair();
1106 let public_key = keypair.verifying_key().to_bytes();
1107
1108 let lite_identity = derive_lite_identity_url(&public_key);
1110 let lite_token_account = format!("{}/ACME", lite_identity);
1111
1112 Wallet {
1113 lite_identity,
1114 lite_token_account,
1115 keypair,
1116 }
1117 }
1118
1119 pub async fn fund_wallet(&self, wallet: &Wallet, times: u32) -> Result<(), JsonRpcError> {
1121 for i in 0..times {
1122 let params = json!({"account": &wallet.lite_token_account});
1123 match self.client.v3_client.call_v3::<Value>("faucet", params).await {
1124 Ok(response) => {
1125 let txid = response.get("transactionHash")
1126 .or_else(|| response.get("txid"))
1127 .and_then(|v| v.as_str())
1128 .unwrap_or("submitted");
1129 println!(" Faucet {}/{}: {}", i + 1, times, txid);
1130 }
1131 Err(e) => {
1132 println!(" Faucet {}/{} failed: {}", i + 1, times, e);
1133 }
1134 }
1135 if i < times - 1 {
1136 tokio::time::sleep(Duration::from_secs(2)).await;
1137 }
1138 }
1139
1140 println!(" Waiting for faucet to process...");
1142 tokio::time::sleep(Duration::from_secs(10)).await;
1143
1144 let balance = poll_for_balance(&self.client, &wallet.lite_token_account, 30).await;
1146 if balance.is_none() || balance == Some(0) {
1147 println!(" Warning: Account balance not confirmed yet");
1148 }
1149
1150 Ok(())
1151 }
1152
1153 pub async fn get_balance(&self, wallet: &Wallet) -> Option<u64> {
1155 poll_for_balance(&self.client, &wallet.lite_token_account, 30).await
1156 }
1157
1158 pub async fn get_oracle_price(&self) -> Result<u64, JsonRpcError> {
1160 let result: Value = self.client.v3_client.call_v3("network-status", json!({})).await?;
1161
1162 result.get("oracle")
1163 .and_then(|o| o.get("price"))
1164 .and_then(|p| p.as_u64())
1165 .ok_or_else(|| JsonRpcError::General(anyhow::anyhow!("Oracle price not found")))
1166 }
1167
1168 pub fn calculate_credits_amount(credits: u64, oracle: u64) -> u64 {
1170 (credits as u128 * 10_000_000_000u128 / oracle as u128) as u64
1172 }
1173
1174 pub async fn setup_adi(&self, wallet: &Wallet, adi_name: &str) -> Result<AdiInfo, JsonRpcError> {
1176 let adi_keypair = AccumulateClient::generate_keypair();
1177 let adi_public_key = adi_keypair.verifying_key().to_bytes();
1178 let adi_key_hash = sha256_hash(&adi_public_key);
1179
1180 let identity_url = format!("acc://{}.acme", adi_name);
1181 let book_url = format!("{}/book", identity_url);
1182 let key_page_url = format!("{}/1", book_url);
1183
1184 let oracle = self.get_oracle_price().await?;
1186 let credits_amount = Self::calculate_credits_amount(1000, oracle);
1187
1188 let mut signer = SmartSigner::new(&self.client, wallet.keypair.clone(), &wallet.lite_identity);
1189
1190 let add_credits_body = TxBody::add_credits(
1192 &wallet.lite_identity,
1193 &credits_amount.to_string(),
1194 oracle,
1195 );
1196
1197 let result = signer.sign_submit_and_wait(
1198 &wallet.lite_token_account,
1199 &add_credits_body,
1200 Some("Add credits to lite identity"),
1201 30,
1202 ).await;
1203
1204 if !result.success {
1205 return Err(JsonRpcError::General(anyhow::anyhow!(
1206 "Failed to add credits: {:?}", result.error
1207 )));
1208 }
1209
1210 let create_adi_body = TxBody::create_identity(
1212 &identity_url,
1213 &book_url,
1214 &hex::encode(adi_key_hash),
1215 );
1216
1217 let result = signer.sign_submit_and_wait(
1218 &wallet.lite_token_account,
1219 &create_adi_body,
1220 Some("Create ADI"),
1221 30,
1222 ).await;
1223
1224 if !result.success {
1225 return Err(JsonRpcError::General(anyhow::anyhow!(
1226 "Failed to create ADI: {:?}", result.error
1227 )));
1228 }
1229
1230 Ok(AdiInfo {
1231 url: identity_url,
1232 key_book_url: book_url,
1233 key_page_url,
1234 keypair: adi_keypair,
1235 })
1236 }
1237
1238 pub async fn buy_credits_for_adi(&self, wallet: &Wallet, adi: &AdiInfo, credits: u64) -> Result<TxResult, JsonRpcError> {
1240 let oracle = self.get_oracle_price().await?;
1241 let amount = Self::calculate_credits_amount(credits, oracle);
1242
1243 let mut signer = SmartSigner::new(&self.client, wallet.keypair.clone(), &wallet.lite_identity);
1244
1245 let body = TxBody::add_credits(&adi.key_page_url, &amount.to_string(), oracle);
1246
1247 Ok(signer.sign_submit_and_wait(
1248 &wallet.lite_token_account,
1249 &body,
1250 Some("Buy credits for ADI"),
1251 30,
1252 ).await)
1253 }
1254
1255 pub async fn get_key_page_info(&self, key_page_url: &str) -> Option<KeyPageInfo> {
1257 let manager = KeyManager::new(&self.client, key_page_url);
1258 match manager.get_key_page_state().await {
1259 Ok(state) => Some(KeyPageInfo {
1260 credits: state.credit_balance,
1261 version: state.version,
1262 threshold: state.accept_threshold,
1263 key_count: state.keys.len(),
1264 }),
1265 Err(_) => None,
1266 }
1267 }
1268
1269 pub async fn create_token_account(&self, adi: &AdiInfo, account_name: &str) -> Result<TxResult, JsonRpcError> {
1271 let account_url = format!("{}/{}", adi.url, account_name);
1272 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1273
1274 let body = TxBody::create_token_account(&account_url, "acc://ACME");
1275
1276 Ok(signer.sign_submit_and_wait(
1277 &adi.url,
1278 &body,
1279 Some("Create token account"),
1280 30,
1281 ).await)
1282 }
1283
1284 pub async fn create_data_account(&self, adi: &AdiInfo, account_name: &str) -> Result<TxResult, JsonRpcError> {
1286 let account_url = format!("{}/{}", adi.url, account_name);
1287 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1288
1289 let body = TxBody::create_data_account(&account_url);
1290
1291 Ok(signer.sign_submit_and_wait(
1292 &adi.url,
1293 &body,
1294 Some("Create data account"),
1295 30,
1296 ).await)
1297 }
1298
1299 pub async fn write_data(&self, adi: &AdiInfo, account_name: &str, entries: &[&str]) -> Result<TxResult, JsonRpcError> {
1301 let account_url = format!("{}/{}", adi.url, account_name);
1302 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1303
1304 let body = TxBody::write_data(entries);
1305
1306 Ok(signer.sign_submit_and_wait(
1307 &account_url,
1308 &body,
1309 Some("Write data"),
1310 30,
1311 ).await)
1312 }
1313
1314 pub async fn add_key_to_adi(&self, adi: &AdiInfo, new_keypair: &SigningKey) -> Result<TxResult, JsonRpcError> {
1316 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1317 Ok(signer.add_key(&new_keypair.verifying_key().to_bytes()).await)
1318 }
1319
1320 pub async fn set_multi_sig_threshold(&self, adi: &AdiInfo, threshold: u64) -> Result<TxResult, JsonRpcError> {
1322 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1323 Ok(signer.set_threshold(threshold).await)
1324 }
1325
1326 pub fn close(&self) {
1328 }
1330}
1331
1332pub fn derive_lite_identity_url(public_key: &[u8; 32]) -> String {
1344 let hash = sha256_hash(public_key);
1346 let key_hash_20 = &hash[0..20];
1347
1348 let key_hash_hex = hex::encode(key_hash_20);
1350
1351 let checksum_full = sha256_hash(key_hash_hex.as_bytes());
1353 let checksum_hex = hex::encode(&checksum_full[28..32]);
1354
1355 format!("acc://{}{}", key_hash_hex, checksum_hex)
1357}
1358
1359pub fn derive_lite_token_account_url(public_key: &[u8; 32]) -> String {
1363 let lite_identity = derive_lite_identity_url(public_key);
1364 format!("{}/ACME", lite_identity)
1365}
1366
1367pub fn sha256_hash(data: &[u8]) -> [u8; 32] {
1369 let mut hasher = Sha256::new();
1370 hasher.update(data);
1371 hasher.finalize().into()
1372}
1373
1374fn extract_txid(response: &Value) -> Option<String> {
1382 if let Some(arr) = response.as_array() {
1384 if arr.len() > 1 {
1386 if let Some(status) = arr[1].get("status") {
1387 if let Some(txid) = status.get("txID").and_then(|t| t.as_str()) {
1388 return Some(txid.to_string());
1389 }
1390 }
1391 }
1392 if let Some(first) = arr.first() {
1394 if let Some(status) = first.get("status") {
1395 if let Some(txid) = status.get("txID").and_then(|t| t.as_str()) {
1396 return Some(txid.to_string());
1397 }
1398 }
1399 }
1400 }
1401
1402 response.get("txid")
1404 .or_else(|| response.get("transactionHash"))
1405 .and_then(|t| t.as_str())
1406 .map(String::from)
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411 use super::*;
1412
1413 #[test]
1414 fn test_derive_lite_identity_url() {
1415 let public_key = [1u8; 32];
1416 let url = derive_lite_identity_url(&public_key);
1417 assert!(url.starts_with("acc://"));
1418 assert!(!url.ends_with(".acme"));
1420 let path = url.strip_prefix("acc://").unwrap();
1422 assert_eq!(path.len(), 48); }
1424
1425 #[test]
1426 fn test_tx_body_add_credits() {
1427 let body = TxBody::add_credits("acc://test.acme/credits", "1000000", 5000);
1428 assert_eq!(body["type"], "addCredits");
1429 assert_eq!(body["recipient"], "acc://test.acme/credits");
1430 }
1431
1432 #[test]
1433 fn test_tx_body_send_tokens() {
1434 let body = TxBody::send_tokens_single("acc://bob.acme/tokens", "100");
1435 assert_eq!(body["type"], "sendTokens");
1436 }
1437
1438 #[test]
1439 fn test_tx_body_create_identity() {
1440 let body = TxBody::create_identity(
1441 "acc://test.acme",
1442 "acc://test.acme/book",
1443 "0123456789abcdef",
1444 );
1445 assert_eq!(body["type"], "createIdentity");
1446 assert_eq!(body["url"], "acc://test.acme");
1447 }
1448
1449 #[test]
1450 fn test_wallet_creation() {
1451 let keypair = AccumulateClient::generate_keypair();
1452 let public_key = keypair.verifying_key().to_bytes();
1453 let lite_identity = derive_lite_identity_url(&public_key);
1454 let lite_token_account = derive_lite_token_account_url(&public_key);
1455
1456 assert!(lite_identity.starts_with("acc://"));
1457 assert!(lite_token_account.contains("/ACME"));
1458 }
1459}