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 write_data_to_hex(recipient: &str, entries_hex: &[&str]) -> Value {
204 let entries: Vec<Value> = entries_hex
205 .iter()
206 .map(|e| Value::String((*e).to_string()))
207 .collect();
208 json!({
209 "type": "writeDataTo",
210 "recipient": recipient,
211 "entry": {
212 "type": "doublehash",
213 "data": entries
214 }
215 })
216 }
217
218 pub fn create_key_page(key_hashes: &[&[u8]]) -> Value {
220 let keys: Vec<Value> = key_hashes
221 .iter()
222 .map(|h| json!({"keyHash": hex::encode(h)}))
223 .collect();
224 json!({
225 "type": "createKeyPage",
226 "keys": keys
227 })
228 }
229
230 pub fn create_key_book(url: &str, public_key_hash: &str) -> Value {
232 json!({
233 "type": "createKeyBook",
234 "url": url,
235 "publicKeyHash": public_key_hash
236 })
237 }
238
239 pub fn update_key_page_add_key(key_hash: &[u8]) -> Value {
241 json!({
242 "type": "updateKeyPage",
243 "operation": [{
244 "type": "add",
245 "entry": {
246 "keyHash": hex::encode(key_hash)
247 }
248 }]
249 })
250 }
251
252 pub fn update_key_page_remove_key(key_hash: &[u8]) -> Value {
254 json!({
255 "type": "updateKeyPage",
256 "operation": [{
257 "type": "remove",
258 "entry": {
259 "keyHash": hex::encode(key_hash)
260 }
261 }]
262 })
263 }
264
265 pub fn update_key_page_set_threshold(threshold: u64) -> Value {
267 json!({
268 "type": "updateKeyPage",
269 "operation": [{
270 "type": "setThreshold",
271 "threshold": threshold
272 }]
273 })
274 }
275
276 pub fn burn_tokens(amount: &str) -> Value {
278 json!({
279 "type": "burnTokens",
280 "amount": amount
281 })
282 }
283
284 pub fn transfer_credits(to_url: &str, amount: u64) -> Value {
286 json!({
287 "type": "transferCredits",
288 "to": [{
289 "url": to_url,
290 "amount": amount
291 }]
292 })
293 }
294
295 pub fn burn_credits(amount: u64) -> Value {
297 json!({
298 "type": "burnCredits",
299 "amount": amount
300 })
301 }
302
303 pub fn update_key(new_key_hash: &str) -> Value {
305 json!({
306 "type": "updateKey",
307 "newKeyHash": new_key_hash
308 })
309 }
310
311 pub fn lock_account(height: u64) -> Value {
313 json!({
314 "type": "lockAccount",
315 "height": height
316 })
317 }
318
319 pub fn update_account_auth(operations: &Value) -> Value {
321 json!({
322 "type": "updateAccountAuth",
323 "operations": operations
324 })
325 }
326
327 pub fn write_data_to(recipient: &str, entries: &[&str]) -> Value {
329 let entries_hex: Vec<Value> = entries
330 .iter()
331 .map(|e| Value::String(hex::encode(e.as_bytes())))
332 .collect();
333 json!({
334 "type": "writeDataTo",
335 "recipient": recipient,
336 "entry": {
337 "type": "doublehash",
338 "data": entries_hex
339 }
340 })
341 }
342
343 pub fn update_key_page(operations: &Value) -> Value {
348 json!({
349 "type": "updateKeyPage",
350 "operation": operations
351 })
352 }
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct KeyPageState {
362 pub url: String,
364 pub version: u64,
366 pub credit_balance: u64,
368 pub accept_threshold: u64,
370 pub keys: Vec<KeyEntry>,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct KeyEntry {
377 pub key_hash: String,
379 pub delegate: Option<String>,
381}
382
383#[derive(Debug, Clone, Default)]
396pub struct HeaderOptions {
397 pub memo: Option<String>,
399 pub metadata: Option<Vec<u8>>,
401 pub expire: Option<crate::generated::header::ExpireOptions>,
403 pub hold_until: Option<crate::generated::header::HoldUntilOptions>,
405 pub authorities: Option<Vec<String>>,
407}
408
409#[derive(Debug)]
415pub struct SmartSigner<'a> {
416 client: &'a AccumulateClient,
418 keypair: SigningKey,
420 signer_url: String,
422 cached_version: u64,
424}
425
426impl<'a> SmartSigner<'a> {
427 pub fn new(client: &'a AccumulateClient, keypair: SigningKey, signer_url: &str) -> Self {
429 Self {
430 client,
431 keypair,
432 signer_url: signer_url.to_string(),
433 cached_version: 1,
434 }
435 }
436
437 pub async fn refresh_version(&mut self) -> Result<u64, JsonRpcError> {
439 let params = json!({
440 "scope": &self.signer_url,
441 "query": {"queryType": "default"}
442 });
443
444 let result: Value = self.client.v3_client.call_v3("query", params).await?;
445
446 if let Some(account) = result.get("account") {
447 if let Some(version) = account.get("version").and_then(|v| v.as_u64()) {
448 self.cached_version = version;
449 return Ok(version);
450 }
451 }
452
453 Ok(self.cached_version)
454 }
455
456 pub fn version(&self) -> u64 {
458 self.cached_version
459 }
460
461 pub fn sign(&self, principal: &str, body: &Value, memo: Option<&str>) -> Result<Value, JsonRpcError> {
470 use crate::codec::signing::{
471 compute_ed25519_signature_metadata_hash,
472 compute_transaction_hash,
473 compute_write_data_body_hash,
474 compute_write_data_to_body_hash,
475 create_signing_preimage,
476 marshal_transaction_header,
477 sha256_bytes,
478 };
479
480 let timestamp = SystemTime::now()
481 .duration_since(UNIX_EPOCH)
482 .map_err(|e| JsonRpcError::General(anyhow::anyhow!("Time error: {}", e)))?
483 .as_micros() as u64;
484
485 let public_key = self.keypair.verifying_key().to_bytes();
486
487 let sig_metadata_hash = compute_ed25519_signature_metadata_hash(
490 &public_key,
491 &self.signer_url,
492 self.cached_version,
493 timestamp,
494 );
495 let initiator_hex = hex::encode(&sig_metadata_hash);
496
497 let header_bytes = marshal_transaction_header(
499 principal,
500 &sig_metadata_hash,
501 memo,
502 None,
503 );
504
505 let tx_type = body.get("type").and_then(|t| t.as_str()).unwrap_or("");
508
509 let extract_entries = |body: &Value| -> Vec<String> {
511 let mut entries_hex = Vec::new();
512 if let Some(entry) = body.get("entry") {
513 if let Some(data) = entry.get("data") {
514 if let Some(arr) = data.as_array() {
515 for item in arr {
516 if let Some(s) = item.as_str() {
517 entries_hex.push(s.to_string());
518 }
519 }
520 }
521 }
522 }
523 entries_hex
524 };
525
526 let tx_hash = if tx_type == "writeData" {
527 let header_hash = sha256_bytes(&header_bytes);
528 let entries_hex = extract_entries(body);
529 let scratch = body.get("scratch").and_then(|s| s.as_bool()).unwrap_or(false);
530 let write_to_state = body.get("writeToState").and_then(|w| w.as_bool()).unwrap_or(false);
531 let body_hash = compute_write_data_body_hash(&entries_hex, scratch, write_to_state);
532 let mut combined = Vec::with_capacity(64);
534 combined.extend_from_slice(&header_hash);
535 combined.extend_from_slice(&body_hash);
536 sha256_bytes(&combined)
537 } else if tx_type == "writeDataTo" {
538 let header_hash = sha256_bytes(&header_bytes);
539 let entries_hex = extract_entries(body);
540 let recipient = body.get("recipient").and_then(|r| r.as_str()).unwrap_or("");
541 let body_hash = compute_write_data_to_body_hash(recipient, &entries_hex);
542 let mut combined = Vec::with_capacity(64);
544 combined.extend_from_slice(&header_hash);
545 combined.extend_from_slice(&body_hash);
546 sha256_bytes(&combined)
547 } else {
548 let body_bytes = marshal_body_to_binary(body)?;
550 compute_transaction_hash(&header_bytes, &body_bytes)
551 };
552
553 let preimage = create_signing_preimage(&sig_metadata_hash, &tx_hash);
555 let signature = self.keypair.sign(&preimage);
556
557 let mut tx = json!({
559 "header": {
560 "principal": principal,
561 "initiator": &initiator_hex
562 },
563 "body": body
564 });
565
566 if let Some(m) = memo {
567 tx["header"]["memo"] = json!(m);
568 }
569
570 let envelope = json!({
573 "transaction": [tx],
574 "signatures": [{
575 "type": "ed25519",
576 "publicKey": hex::encode(&public_key),
577 "signature": hex::encode(signature.to_bytes()),
578 "signer": &self.signer_url,
579 "signerVersion": self.cached_version,
580 "timestamp": timestamp,
581 "transactionHash": hex::encode(&tx_hash)
582 }]
583 });
584
585 Ok(envelope)
586 }
587
588 pub async fn sign_submit_and_wait(
590 &mut self,
591 principal: &str,
592 body: &Value,
593 memo: Option<&str>,
594 max_attempts: u32,
595 ) -> TxResult {
596 if let Err(e) = self.refresh_version().await {
598 return TxResult::err(format!("Failed to refresh version: {}", e));
599 }
600
601 let envelope = match self.sign(principal, body, memo) {
603 Ok(env) => env,
604 Err(e) => return TxResult::err(format!("Failed to sign: {}", e)),
605 };
606
607 let submit_result: Result<Value, _> = self.client.v3_client.call_v3("submit", json!({
609 "envelope": envelope
610 })).await;
611
612 let response = match submit_result {
613 Ok(resp) => resp,
614 Err(e) => return TxResult::err(format!("Submit failed: {}", e)),
615 };
616
617 let txid = extract_txid(&response);
619 if txid.is_none() {
620 return TxResult::err("No transaction ID in response".to_string());
621 }
622 let txid = txid.unwrap();
623
624 let tx_hash = if txid.starts_with("acc://") && txid.contains('@') {
627 txid.split('@').next().unwrap_or(&txid).replace("acc://", "")
628 } else {
629 txid.clone()
630 };
631 let query_scope = format!("acc://{}@unknown", tx_hash);
632
633 for _attempt in 0..max_attempts {
634 tokio::time::sleep(Duration::from_secs(2)).await;
635
636 let query_result: Result<Value, _> = self.client.v3_client.call_v3("query", json!({
638 "scope": &query_scope,
639 "query": {"queryType": "default"}
640 })).await;
641
642 if let Ok(result) = query_result {
643 if let Some(status_value) = result.get("status") {
645 if let Some(status_str) = status_value.as_str() {
647 if status_str == "delivered" {
648 return TxResult::ok(txid, response);
649 }
650 continue;
652 }
653
654 if status_value.is_object() {
656 let delivered = status_value.get("delivered")
657 .and_then(|d| d.as_bool())
658 .unwrap_or(false);
659
660 if delivered {
661 let failed = status_value.get("failed")
663 .and_then(|f| f.as_bool())
664 .unwrap_or(false);
665
666 if failed {
667 let error_msg = status_value.get("error")
668 .and_then(|e| {
669 if let Some(msg) = e.get("message").and_then(|m| m.as_str()) {
670 Some(msg.to_string())
671 } else {
672 e.as_str().map(String::from)
673 }
674 })
675 .unwrap_or_else(|| "Unknown error".to_string());
676 return TxResult::err(error_msg);
677 }
678
679 return TxResult::ok(txid, response);
680 }
681 }
682 }
683 }
684 }
685
686 TxResult::err(format!("Timeout waiting for delivery: {}", txid))
687 }
688
689 pub async fn add_key(&mut self, public_key: &[u8]) -> TxResult {
691 let key_hash = sha256_hash(public_key);
692 let body = TxBody::update_key_page_add_key(&key_hash);
693 self.sign_submit_and_wait(&self.signer_url.clone(), &body, Some("Add key"), 30).await
694 }
695
696 pub async fn remove_key(&mut self, public_key_hash: &[u8]) -> TxResult {
698 let body = TxBody::update_key_page_remove_key(public_key_hash);
699 self.sign_submit_and_wait(&self.signer_url.clone(), &body, Some("Remove key"), 30).await
700 }
701
702 pub async fn set_threshold(&mut self, threshold: u64) -> TxResult {
704 let body = TxBody::update_key_page_set_threshold(threshold);
705 self.sign_submit_and_wait(&self.signer_url.clone(), &body, Some("Set threshold"), 30).await
706 }
707
708 #[allow(dead_code)]
710 fn public_key_hash(&self) -> [u8; 32] {
711 sha256_hash(&self.keypair.verifying_key().to_bytes())
712 }
713
714 pub fn sign_with_options(
719 &self,
720 principal: &str,
721 body: &Value,
722 options: &HeaderOptions,
723 ) -> Result<Value, JsonRpcError> {
724 use crate::codec::signing::{
725 compute_ed25519_signature_metadata_hash,
726 compute_transaction_hash,
727 compute_write_data_body_hash,
728 compute_write_data_to_body_hash,
729 create_signing_preimage,
730 marshal_transaction_header_full,
731 HeaderBinaryOptions,
732 sha256_bytes,
733 };
734
735 let timestamp = SystemTime::now()
736 .duration_since(UNIX_EPOCH)
737 .map_err(|e| JsonRpcError::General(anyhow::anyhow!("Time error: {}", e)))?
738 .as_micros() as u64;
739
740 let public_key = self.keypair.verifying_key().to_bytes();
741
742 let sig_metadata_hash = compute_ed25519_signature_metadata_hash(
744 &public_key,
745 &self.signer_url,
746 self.cached_version,
747 timestamp,
748 );
749 let initiator_hex = hex::encode(&sig_metadata_hash);
750
751 let memo_ref = options.memo.as_deref();
753 let metadata_ref = options.metadata.as_deref();
754
755 let has_extended = options.expire.is_some()
757 || options.hold_until.is_some()
758 || options.authorities.is_some();
759
760 let extended = if has_extended {
761 Some(HeaderBinaryOptions {
762 expire_at_time: options.expire.as_ref().and_then(|e| e.at_time.map(|t| t as i64)),
763 hold_until_minor_block: options.hold_until.as_ref().and_then(|h| h.minor_block),
764 authorities: options.authorities.clone(),
765 })
766 } else {
767 None
768 };
769
770 let header_bytes = marshal_transaction_header_full(
771 principal,
772 &sig_metadata_hash,
773 memo_ref,
774 metadata_ref,
775 extended.as_ref(),
776 );
777
778 let tx_type = body.get("type").and_then(|t| t.as_str()).unwrap_or("");
780
781 let extract_entries = |body: &Value| -> Vec<String> {
783 let mut entries_hex = Vec::new();
784 if let Some(entry) = body.get("entry") {
785 if let Some(data) = entry.get("data") {
786 if let Some(arr) = data.as_array() {
787 for item in arr {
788 if let Some(s) = item.as_str() {
789 entries_hex.push(s.to_string());
790 }
791 }
792 }
793 }
794 }
795 entries_hex
796 };
797
798 let tx_hash = if tx_type == "writeData" {
799 let header_hash = sha256_bytes(&header_bytes);
800 let entries_hex = extract_entries(body);
801 let scratch = body.get("scratch").and_then(|s| s.as_bool()).unwrap_or(false);
802 let write_to_state = body.get("writeToState").and_then(|w| w.as_bool()).unwrap_or(false);
803 let body_hash = compute_write_data_body_hash(&entries_hex, scratch, write_to_state);
804 let mut combined = Vec::with_capacity(64);
805 combined.extend_from_slice(&header_hash);
806 combined.extend_from_slice(&body_hash);
807 sha256_bytes(&combined)
808 } else if tx_type == "writeDataTo" {
809 let header_hash = sha256_bytes(&header_bytes);
810 let entries_hex = extract_entries(body);
811 let recipient = body.get("recipient").and_then(|r| r.as_str()).unwrap_or("");
812 let body_hash = compute_write_data_to_body_hash(recipient, &entries_hex);
813 let mut combined = Vec::with_capacity(64);
814 combined.extend_from_slice(&header_hash);
815 combined.extend_from_slice(&body_hash);
816 sha256_bytes(&combined)
817 } else {
818 let body_bytes = marshal_body_to_binary(body)?;
819 compute_transaction_hash(&header_bytes, &body_bytes)
820 };
821
822 let preimage = create_signing_preimage(&sig_metadata_hash, &tx_hash);
824 let signature = self.keypair.sign(&preimage);
825
826 let mut tx = json!({
828 "header": {
829 "principal": principal,
830 "initiator": &initiator_hex
831 },
832 "body": body
833 });
834
835 if let Some(ref m) = options.memo {
837 tx["header"]["memo"] = json!(m);
838 }
839 if let Some(ref md) = options.metadata {
840 tx["header"]["metadata"] = json!(hex::encode(md));
841 }
842 if let Some(ref expire) = options.expire {
843 if let Some(at_time) = expire.at_time {
844 let dt = chrono::DateTime::from_timestamp(at_time as i64, 0)
846 .unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap());
847 tx["header"]["expire"] = json!({ "atTime": dt.to_rfc3339() });
848 }
849 }
850 if let Some(ref hold) = options.hold_until {
851 if let Some(minor_block) = hold.minor_block {
852 tx["header"]["holdUntil"] = json!({ "minorBlock": minor_block });
853 }
854 }
855 if let Some(ref auths) = options.authorities {
856 tx["header"]["authorities"] = json!(auths);
857 }
858
859 let envelope = json!({
861 "transaction": [tx],
862 "signatures": [{
863 "type": "ed25519",
864 "publicKey": hex::encode(&public_key),
865 "signature": hex::encode(signature.to_bytes()),
866 "signer": &self.signer_url,
867 "signerVersion": self.cached_version,
868 "timestamp": timestamp,
869 "transactionHash": hex::encode(&tx_hash)
870 }]
871 });
872
873 Ok(envelope)
874 }
875
876 pub async fn sign_submit_and_wait_with_options(
880 &mut self,
881 principal: &str,
882 body: &Value,
883 options: &HeaderOptions,
884 max_attempts: u32,
885 ) -> TxResult {
886 if let Err(e) = self.refresh_version().await {
888 return TxResult::err(format!("Failed to refresh version: {}", e));
889 }
890
891 let envelope = match self.sign_with_options(principal, body, options) {
893 Ok(env) => env,
894 Err(e) => return TxResult::err(format!("Failed to sign: {}", e)),
895 };
896
897 let submit_result: Result<Value, _> = self.client.v3_client.call_v3("submit", json!({
899 "envelope": envelope
900 })).await;
901
902 let response = match submit_result {
903 Ok(resp) => resp,
904 Err(e) => return TxResult::err(format!("Submit failed: {}", e)),
905 };
906
907 let txid = extract_txid(&response);
909 if txid.is_none() {
910 return TxResult::err("No transaction ID in response".to_string());
911 }
912 let txid = txid.unwrap();
913
914 let tx_hash = if txid.starts_with("acc://") && txid.contains('@') {
916 txid.split('@').next().unwrap_or(&txid).replace("acc://", "")
917 } else {
918 txid.clone()
919 };
920 let query_scope = format!("acc://{}@unknown", tx_hash);
921
922 for _attempt in 0..max_attempts {
923 tokio::time::sleep(Duration::from_secs(2)).await;
924
925 let query_result: Result<Value, _> = self.client.v3_client.call_v3("query", json!({
926 "scope": &query_scope,
927 "query": {"queryType": "default"}
928 })).await;
929
930 if let Ok(result) = query_result {
931 if let Some(status_value) = result.get("status") {
932 if let Some(status_str) = status_value.as_str() {
933 if status_str == "delivered" {
934 return TxResult::ok(txid, response);
935 }
936 continue;
937 }
938 if status_value.is_object() {
939 let delivered = status_value.get("delivered")
940 .and_then(|d| d.as_bool())
941 .unwrap_or(false);
942 if delivered {
943 let failed = status_value.get("failed")
944 .and_then(|f| f.as_bool())
945 .unwrap_or(false);
946 if failed {
947 let error_msg = status_value.get("error")
948 .and_then(|e| {
949 if let Some(msg) = e.get("message").and_then(|m| m.as_str()) {
950 Some(msg.to_string())
951 } else {
952 e.as_str().map(String::from)
953 }
954 })
955 .unwrap_or_else(|| "Unknown error".to_string());
956 return TxResult::err(error_msg);
957 }
958 return TxResult::ok(txid, response);
959 }
960 }
961 }
962 }
963 }
964
965 TxResult::err(format!("Timeout waiting for delivery: {}", txid))
966 }
967}
968
969fn marshal_body_to_binary(body: &Value) -> Result<Vec<u8>, JsonRpcError> {
973 use crate::codec::signing::{
974 marshal_add_credits_body, marshal_send_tokens_body, marshal_create_identity_body,
975 marshal_create_token_account_body, marshal_create_data_account_body,
976 marshal_write_data_body, marshal_create_token_body, marshal_issue_tokens_body,
977 marshal_key_page_operation, marshal_update_key_page_body,
978 marshal_create_key_book_body, marshal_create_key_page_body,
979 marshal_burn_tokens_body, marshal_update_key_body,
980 marshal_burn_credits_body, marshal_transfer_credits_body,
981 marshal_write_data_to_body, marshal_lock_account_body,
982 marshal_update_account_auth_body,
983 tx_types
984 };
985 use crate::codec::writer::BinaryWriter;
986
987 let tx_type = body.get("type").and_then(|t| t.as_str()).unwrap_or("");
988
989 match tx_type {
990 "addCredits" => {
991 let recipient = body.get("recipient").and_then(|r| r.as_str()).unwrap_or("");
992 let amount_str = body.get("amount").and_then(|a| a.as_str()).unwrap_or("0");
993 let amount: u64 = amount_str.parse().unwrap_or(0);
994 let oracle = body.get("oracle").and_then(|o| o.as_u64()).unwrap_or(0);
995 Ok(marshal_add_credits_body(recipient, amount, oracle))
996 }
997 "sendTokens" => {
998 let to_array = body.get("to").and_then(|t| t.as_array());
999 let mut recipients = Vec::new();
1000 if let Some(to) = to_array {
1001 for recipient in to {
1002 let url = recipient.get("url").and_then(|u| u.as_str()).unwrap_or("");
1003 let amount_str = recipient.get("amount").and_then(|a| a.as_str()).unwrap_or("0");
1004 let amount: u64 = amount_str.parse().unwrap_or(0);
1005 recipients.push((url.to_string(), amount));
1006 }
1007 }
1008 Ok(marshal_send_tokens_body(&recipients))
1009 }
1010 "createIdentity" => {
1011 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
1012 let key_book_url = body.get("keyBookUrl").and_then(|k| k.as_str()).unwrap_or("");
1013 let key_hash_hex = body.get("keyHash")
1015 .or_else(|| body.get("publicKeyHash"))
1016 .and_then(|k| k.as_str())
1017 .unwrap_or("");
1018 let key_hash = hex::decode(key_hash_hex).unwrap_or_default();
1019 Ok(marshal_create_identity_body(url, &key_hash, key_book_url))
1020 }
1021 "createTokenAccount" => {
1022 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
1023 let token_url = body.get("tokenUrl").and_then(|t| t.as_str()).unwrap_or("");
1024 Ok(marshal_create_token_account_body(url, token_url))
1025 }
1026 "createDataAccount" => {
1027 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
1028 Ok(marshal_create_data_account_body(url))
1029 }
1030 "writeData" => {
1031 let mut entries_hex = Vec::new();
1033 if let Some(entry) = body.get("entry") {
1034 if let Some(data) = entry.get("data") {
1035 if let Some(arr) = data.as_array() {
1036 for item in arr {
1037 if let Some(s) = item.as_str() {
1038 entries_hex.push(s.to_string());
1039 }
1040 }
1041 }
1042 }
1043 }
1044 let scratch = body.get("scratch").and_then(|s| s.as_bool()).unwrap_or(false);
1045 let write_to_state = body.get("writeToState").and_then(|w| w.as_bool()).unwrap_or(false);
1046 Ok(marshal_write_data_body(&entries_hex, scratch, write_to_state))
1047 }
1048 "createToken" => {
1049 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
1050 let symbol = body.get("symbol").and_then(|s| s.as_str()).unwrap_or("");
1051 let precision = body.get("precision").and_then(|p| p.as_u64()).unwrap_or(0);
1052 let supply_limit = body.get("supplyLimit")
1053 .and_then(|s| s.as_str())
1054 .and_then(|s| s.parse::<u64>().ok());
1055 Ok(marshal_create_token_body(url, symbol, precision, supply_limit))
1056 }
1057 "issueTokens" => {
1058 let to_array = body.get("to").and_then(|t| t.as_array());
1059 let mut recipients: Vec<(&str, u64)> = Vec::new();
1060 if let Some(to) = to_array {
1061 for recipient in to {
1062 let url = recipient.get("url").and_then(|u| u.as_str()).unwrap_or("");
1063 let amount_str = recipient.get("amount").and_then(|a| a.as_str()).unwrap_or("0");
1064 let amount: u64 = amount_str.parse().unwrap_or(0);
1065 recipients.push((url, amount));
1066 }
1067 }
1068 Ok(marshal_issue_tokens_body(&recipients))
1069 }
1070 "burnTokens" => {
1071 let amount_str = body.get("amount").and_then(|a| a.as_str()).unwrap_or("0");
1072 let amount: u64 = amount_str.parse().unwrap_or(0);
1073 Ok(marshal_burn_tokens_body(amount))
1074 }
1075 "createKeyBook" => {
1076 let url = body.get("url").and_then(|u| u.as_str()).unwrap_or("");
1077 let key_hash_hex = body.get("publicKeyHash")
1078 .or_else(|| body.get("keyHash"))
1079 .and_then(|k| k.as_str())
1080 .unwrap_or("");
1081 let key_hash = hex::decode(key_hash_hex).unwrap_or_default();
1082 Ok(marshal_create_key_book_body(url, &key_hash))
1083 }
1084 "createKeyPage" => {
1085 let keys_array = body.get("keys").and_then(|k| k.as_array());
1086 let mut key_hashes: Vec<Vec<u8>> = Vec::new();
1087 if let Some(keys) = keys_array {
1088 for key in keys {
1089 let key_hash_hex = key.get("keyHash")
1090 .or_else(|| key.get("publicKeyHash"))
1091 .and_then(|k| k.as_str())
1092 .unwrap_or("");
1093 let key_hash = hex::decode(key_hash_hex).unwrap_or_default();
1094 key_hashes.push(key_hash);
1095 }
1096 }
1097 Ok(marshal_create_key_page_body(&key_hashes))
1098 }
1099 "updateKey" => {
1100 let new_key_hash_hex = body.get("newKeyHash")
1101 .or_else(|| body.get("newKey"))
1102 .and_then(|k| k.as_str())
1103 .unwrap_or("");
1104 let new_key_hash = hex::decode(new_key_hash_hex).unwrap_or_default();
1105 Ok(marshal_update_key_body(&new_key_hash))
1106 }
1107 "updateKeyPage" => {
1108 let op_array = body.get("operation").and_then(|o| o.as_array());
1110 let mut operations: Vec<Vec<u8>> = Vec::new();
1111
1112 if let Some(ops) = op_array {
1113 for op in ops {
1114 let op_type = op.get("type").and_then(|t| t.as_str()).unwrap_or("");
1115
1116 let key_hash: Option<Vec<u8>> = op.get("entry")
1119 .and_then(|e| e.get("keyHash"))
1120 .and_then(|h| h.as_str())
1121 .and_then(|hex_str| hex::decode(hex_str).ok());
1122
1123 let delegate: Option<&str> = op.get("entry")
1125 .and_then(|e| e.get("delegate"))
1126 .and_then(|d| d.as_str());
1127
1128 let old_key_hash: Option<Vec<u8>> = op.get("oldEntry")
1130 .and_then(|e| e.get("keyHash"))
1131 .and_then(|h| h.as_str())
1132 .and_then(|hex_str| hex::decode(hex_str).ok());
1133
1134 let new_key_hash: Option<Vec<u8>> = op.get("newEntry")
1135 .and_then(|e| e.get("keyHash"))
1136 .and_then(|h| h.as_str())
1137 .and_then(|hex_str| hex::decode(hex_str).ok());
1138
1139 let threshold: Option<u64> = op.get("threshold").and_then(|t| t.as_u64());
1141
1142 let op_bytes = marshal_key_page_operation(
1144 op_type,
1145 key_hash.as_deref(),
1146 delegate,
1147 old_key_hash.as_deref(),
1148 new_key_hash.as_deref(),
1149 threshold,
1150 );
1151 operations.push(op_bytes);
1152 }
1153 }
1154
1155 Ok(marshal_update_key_page_body(&operations))
1156 }
1157 "burnCredits" => {
1158 let amount = body.get("amount").and_then(|a| a.as_u64()).unwrap_or(0);
1159 Ok(marshal_burn_credits_body(amount))
1160 }
1161 "transferCredits" => {
1162 let to_array = body.get("to").and_then(|t| t.as_array());
1163 let mut recipients: Vec<(&str, u64)> = Vec::new();
1164 if let Some(to) = to_array {
1165 for recipient in to {
1166 let url = recipient.get("url").and_then(|u| u.as_str()).unwrap_or("");
1167 let amount = recipient.get("amount").and_then(|a| a.as_u64()).unwrap_or(0);
1168 recipients.push((url, amount));
1169 }
1170 }
1171 Ok(marshal_transfer_credits_body(&recipients))
1172 }
1173 "writeDataTo" => {
1174 let recipient = body.get("recipient").and_then(|r| r.as_str()).unwrap_or("");
1175 let mut entries_hex = Vec::new();
1176 if let Some(entry) = body.get("entry") {
1177 if let Some(data) = entry.get("data") {
1178 if let Some(arr) = data.as_array() {
1179 for item in arr {
1180 if let Some(s) = item.as_str() {
1181 entries_hex.push(s.to_string());
1182 }
1183 }
1184 }
1185 }
1186 }
1187 Ok(marshal_write_data_to_body(recipient, &entries_hex))
1188 }
1189 "lockAccount" => {
1190 let height = body.get("height").and_then(|h| h.as_u64()).unwrap_or(0);
1191 Ok(marshal_lock_account_body(height))
1192 }
1193 "updateAccountAuth" => {
1194 let ops_array = body.get("operations").and_then(|o| o.as_array());
1195 let mut operations: Vec<(&str, &str)> = Vec::new();
1196 if let Some(ops) = ops_array {
1197 for op in ops {
1198 let op_type = op.get("type").and_then(|t| t.as_str()).unwrap_or("");
1199 let authority = op.get("authority").and_then(|a| a.as_str()).unwrap_or("");
1200 operations.push((op_type, authority));
1201 }
1202 }
1203 Ok(marshal_update_account_auth_body(&operations))
1204 }
1205 _ => {
1208 let mut writer = BinaryWriter::new();
1210
1211 let type_num = match tx_type {
1213 "createIdentity" => tx_types::CREATE_IDENTITY,
1214 "createTokenAccount" => tx_types::CREATE_TOKEN_ACCOUNT,
1215 "createDataAccount" => tx_types::CREATE_DATA_ACCOUNT,
1216 "writeData" => tx_types::WRITE_DATA,
1217 "writeDataTo" => tx_types::WRITE_DATA_TO,
1218 "acmeFaucet" => tx_types::ACME_FAUCET,
1219 "createToken" => tx_types::CREATE_TOKEN,
1220 "issueTokens" => tx_types::ISSUE_TOKENS,
1221 "burnTokens" => tx_types::BURN_TOKENS,
1222 "createLiteTokenAccount" => tx_types::CREATE_LITE_TOKEN_ACCOUNT,
1223 "createKeyPage" => tx_types::CREATE_KEY_PAGE,
1224 "createKeyBook" => tx_types::CREATE_KEY_BOOK,
1225 "updateKeyPage" => tx_types::UPDATE_KEY_PAGE,
1226 "updateAccountAuth" => tx_types::UPDATE_ACCOUNT_AUTH,
1227 "updateKey" => tx_types::UPDATE_KEY,
1228 "lockAccount" => tx_types::LOCK_ACCOUNT,
1229 "transferCredits" => tx_types::TRANSFER_CREDITS,
1230 "burnCredits" => tx_types::BURN_CREDITS,
1231 _ => 0,
1232 };
1233
1234 let _ = writer.write_uvarint(1);
1236 let _ = writer.write_uvarint(type_num);
1237
1238 Ok(writer.into_bytes())
1242 }
1243 }
1244}
1245
1246#[derive(Debug)]
1252pub struct KeyManager<'a> {
1253 client: &'a AccumulateClient,
1255 key_page_url: String,
1257}
1258
1259impl<'a> KeyManager<'a> {
1260 pub fn new(client: &'a AccumulateClient, key_page_url: &str) -> Self {
1262 Self {
1263 client,
1264 key_page_url: key_page_url.to_string(),
1265 }
1266 }
1267
1268 pub async fn get_key_page_state(&self) -> Result<KeyPageState, JsonRpcError> {
1270 let params = json!({
1271 "scope": &self.key_page_url,
1272 "query": {"queryType": "default"}
1273 });
1274
1275 let result: Value = self.client.v3_client.call_v3("query", params).await?;
1276
1277 let account = result.get("account")
1278 .ok_or_else(|| JsonRpcError::General(anyhow::anyhow!("No account in response")))?;
1279
1280 let url = account.get("url")
1281 .and_then(|v| v.as_str())
1282 .unwrap_or(&self.key_page_url)
1283 .to_string();
1284
1285 let version = account.get("version")
1286 .and_then(|v| v.as_u64())
1287 .unwrap_or(1);
1288
1289 let credit_balance = account.get("creditBalance")
1290 .and_then(|v| v.as_u64())
1291 .unwrap_or(0);
1292
1293 let accept_threshold = account.get("acceptThreshold")
1294 .or_else(|| account.get("threshold"))
1295 .and_then(|v| v.as_u64())
1296 .unwrap_or(1);
1297
1298 let keys: Vec<KeyEntry> = if let Some(keys_arr) = account.get("keys").and_then(|k| k.as_array()) {
1299 keys_arr.iter().map(|k| {
1300 let key_hash = k.get("publicKeyHash")
1301 .or_else(|| k.get("publicKey"))
1302 .and_then(|v| v.as_str())
1303 .unwrap_or("")
1304 .to_string();
1305 let delegate = k.get("delegate").and_then(|v| v.as_str()).map(String::from);
1306 KeyEntry { key_hash, delegate }
1307 }).collect()
1308 } else {
1309 vec![]
1310 };
1311
1312 Ok(KeyPageState {
1313 url,
1314 version,
1315 credit_balance,
1316 accept_threshold,
1317 keys,
1318 })
1319 }
1320}
1321
1322pub async fn poll_for_balance(
1328 client: &AccumulateClient,
1329 account_url: &str,
1330 max_attempts: u32,
1331) -> Option<u64> {
1332 for i in 0..max_attempts {
1333 let params = json!({
1334 "scope": account_url,
1335 "query": {"queryType": "default"}
1336 });
1337
1338 match client.v3_client.call_v3::<Value>("query", params).await {
1339 Ok(result) => {
1340 if let Some(account) = result.get("account") {
1341 if let Some(balance) = account.get("balance").and_then(|b| b.as_str()) {
1343 if let Ok(bal) = balance.parse::<u64>() {
1344 if bal > 0 {
1345 return Some(bal);
1346 }
1347 }
1348 }
1349 if let Some(bal) = account.get("balance").and_then(|b| b.as_u64()) {
1351 if bal > 0 {
1352 return Some(bal);
1353 }
1354 }
1355 }
1356 println!(" Waiting for balance... (attempt {}/{})", i + 1, max_attempts);
1357 }
1358 Err(_) => {
1359 println!(" Account not found yet... (attempt {}/{})", i + 1, max_attempts);
1361 }
1362 }
1363
1364 if i < max_attempts - 1 {
1365 tokio::time::sleep(Duration::from_secs(2)).await;
1366 }
1367 }
1368 None
1369}
1370
1371pub async fn poll_for_credits(
1373 client: &AccumulateClient,
1374 key_page_url: &str,
1375 max_attempts: u32,
1376) -> Option<u64> {
1377 for i in 0..max_attempts {
1378 let params = json!({
1379 "scope": key_page_url,
1380 "query": {"queryType": "default"}
1381 });
1382
1383 if let Ok(result) = client.v3_client.call_v3::<Value>("query", params).await {
1384 if let Some(account) = result.get("account") {
1385 if let Some(credits) = account.get("creditBalance").and_then(|c| c.as_u64()) {
1386 if credits > 0 {
1387 return Some(credits);
1388 }
1389 }
1390 }
1391 }
1392
1393 if i < max_attempts - 1 {
1394 tokio::time::sleep(Duration::from_secs(2)).await;
1395 }
1396 }
1397 None
1398}
1399
1400pub async fn wait_for_tx(
1402 client: &AccumulateClient,
1403 txid: &str,
1404 max_attempts: u32,
1405) -> bool {
1406 let tx_hash = txid.split('@').next().unwrap_or(txid).replace("acc://", "");
1407
1408 for _ in 0..max_attempts {
1409 let params = json!({
1410 "scope": format!("acc://{}@unknown", tx_hash),
1411 "query": {"queryType": "default"}
1412 });
1413
1414 if let Ok(result) = client.v3_client.call_v3::<Value>("query", params).await {
1415 if let Some(status) = result.get("status") {
1416 if status.get("delivered").and_then(|d| d.as_bool()).unwrap_or(false) {
1417 return true;
1418 }
1419 }
1420 }
1421
1422 tokio::time::sleep(Duration::from_secs(2)).await;
1423 }
1424 false
1425}
1426
1427#[derive(Debug, Clone)]
1433pub struct Wallet {
1434 pub lite_identity: String,
1436 pub lite_token_account: String,
1438 keypair: SigningKey,
1440}
1441
1442impl Wallet {
1443 pub fn keypair(&self) -> &SigningKey {
1445 &self.keypair
1446 }
1447
1448 pub fn public_key(&self) -> [u8; 32] {
1450 self.keypair.verifying_key().to_bytes()
1451 }
1452
1453 pub fn public_key_hash(&self) -> [u8; 32] {
1455 sha256_hash(&self.public_key())
1456 }
1457}
1458
1459#[derive(Debug, Clone)]
1465pub struct AdiInfo {
1466 pub url: String,
1468 pub key_book_url: String,
1470 pub key_page_url: String,
1472 keypair: SigningKey,
1474}
1475
1476impl AdiInfo {
1477 pub fn keypair(&self) -> &SigningKey {
1479 &self.keypair
1480 }
1481
1482 pub fn public_key(&self) -> [u8; 32] {
1484 self.keypair.verifying_key().to_bytes()
1485 }
1486}
1487
1488#[derive(Debug, Clone)]
1494pub struct KeyPageInfo {
1495 pub credits: u64,
1497 pub version: u64,
1499 pub threshold: u64,
1501 pub key_count: usize,
1503}
1504
1505#[derive(Debug)]
1511pub struct QuickStart {
1512 client: AccumulateClient,
1514}
1515
1516impl QuickStart {
1517 pub async fn devnet() -> Result<Self, JsonRpcError> {
1519 let v2_url = Url::parse(DEVNET_V2).map_err(|e| {
1520 JsonRpcError::General(anyhow::anyhow!("Invalid URL: {}", e))
1521 })?;
1522 let v3_url = Url::parse(DEVNET_V3).map_err(|e| {
1523 JsonRpcError::General(anyhow::anyhow!("Invalid URL: {}", e))
1524 })?;
1525
1526 let client = AccumulateClient::new_with_options(v2_url, v3_url, AccOptions::default()).await?;
1527 Ok(Self { client })
1528 }
1529
1530 pub async fn kermit() -> Result<Self, JsonRpcError> {
1532 let v2_url = Url::parse(KERMIT_V2).map_err(|e| {
1533 JsonRpcError::General(anyhow::anyhow!("Invalid URL: {}", e))
1534 })?;
1535 let v3_url = Url::parse(KERMIT_V3).map_err(|e| {
1536 JsonRpcError::General(anyhow::anyhow!("Invalid URL: {}", e))
1537 })?;
1538
1539 let client = AccumulateClient::new_with_options(v2_url, v3_url, AccOptions::default()).await?;
1540 Ok(Self { client })
1541 }
1542
1543 pub async fn custom(v2_endpoint: &str, v3_endpoint: &str) -> Result<Self, JsonRpcError> {
1545 let v2_url = Url::parse(v2_endpoint).map_err(|e| {
1546 JsonRpcError::General(anyhow::anyhow!("Invalid V2 URL: {}", e))
1547 })?;
1548 let v3_url = Url::parse(v3_endpoint).map_err(|e| {
1549 JsonRpcError::General(anyhow::anyhow!("Invalid V3 URL: {}", e))
1550 })?;
1551
1552 let client = AccumulateClient::new_with_options(v2_url, v3_url, AccOptions::default()).await?;
1553 Ok(Self { client })
1554 }
1555
1556 pub fn client(&self) -> &AccumulateClient {
1558 &self.client
1559 }
1560
1561 pub fn create_wallet(&self) -> Wallet {
1563 let keypair = AccumulateClient::generate_keypair();
1564 let public_key = keypair.verifying_key().to_bytes();
1565
1566 let lite_identity = derive_lite_identity_url(&public_key);
1568 let lite_token_account = format!("{}/ACME", lite_identity);
1569
1570 Wallet {
1571 lite_identity,
1572 lite_token_account,
1573 keypair,
1574 }
1575 }
1576
1577 pub async fn fund_wallet(&self, wallet: &Wallet, times: u32) -> Result<(), JsonRpcError> {
1579 for i in 0..times {
1580 let params = json!({"account": &wallet.lite_token_account});
1581 match self.client.v3_client.call_v3::<Value>("faucet", params).await {
1582 Ok(response) => {
1583 let txid = response.get("transactionHash")
1584 .or_else(|| response.get("txid"))
1585 .and_then(|v| v.as_str())
1586 .unwrap_or("submitted");
1587 println!(" Faucet {}/{}: {}", i + 1, times, txid);
1588 }
1589 Err(e) => {
1590 println!(" Faucet {}/{} failed: {}", i + 1, times, e);
1591 }
1592 }
1593 if i < times - 1 {
1594 tokio::time::sleep(Duration::from_secs(2)).await;
1595 }
1596 }
1597
1598 println!(" Waiting for faucet to process...");
1600 tokio::time::sleep(Duration::from_secs(10)).await;
1601
1602 let balance = poll_for_balance(&self.client, &wallet.lite_token_account, 30).await;
1604 if balance.is_none() || balance == Some(0) {
1605 println!(" Warning: Account balance not confirmed yet");
1606 }
1607
1608 Ok(())
1609 }
1610
1611 pub async fn get_balance(&self, wallet: &Wallet) -> Option<u64> {
1613 poll_for_balance(&self.client, &wallet.lite_token_account, 30).await
1614 }
1615
1616 pub async fn get_oracle_price(&self) -> Result<u64, JsonRpcError> {
1618 let result: Value = self.client.v3_client.call_v3("network-status", json!({})).await?;
1619
1620 result.get("oracle")
1621 .and_then(|o| o.get("price"))
1622 .and_then(|p| p.as_u64())
1623 .ok_or_else(|| JsonRpcError::General(anyhow::anyhow!("Oracle price not found")))
1624 }
1625
1626 pub fn calculate_credits_amount(credits: u64, oracle: u64) -> u64 {
1628 (credits as u128 * 10_000_000_000u128 / oracle as u128) as u64
1630 }
1631
1632 pub async fn setup_adi(&self, wallet: &Wallet, adi_name: &str) -> Result<AdiInfo, JsonRpcError> {
1634 let adi_keypair = AccumulateClient::generate_keypair();
1635 let adi_public_key = adi_keypair.verifying_key().to_bytes();
1636 let adi_key_hash = sha256_hash(&adi_public_key);
1637
1638 let identity_url = format!("acc://{}.acme", adi_name);
1639 let book_url = format!("{}/book", identity_url);
1640 let key_page_url = format!("{}/1", book_url);
1641
1642 let oracle = self.get_oracle_price().await?;
1644 let credits_amount = Self::calculate_credits_amount(1000, oracle);
1645
1646 let mut signer = SmartSigner::new(&self.client, wallet.keypair.clone(), &wallet.lite_identity);
1647
1648 let add_credits_body = TxBody::add_credits(
1650 &wallet.lite_identity,
1651 &credits_amount.to_string(),
1652 oracle,
1653 );
1654
1655 let result = signer.sign_submit_and_wait(
1656 &wallet.lite_token_account,
1657 &add_credits_body,
1658 Some("Add credits to lite identity"),
1659 30,
1660 ).await;
1661
1662 if !result.success {
1663 return Err(JsonRpcError::General(anyhow::anyhow!(
1664 "Failed to add credits: {:?}", result.error
1665 )));
1666 }
1667
1668 let create_adi_body = TxBody::create_identity(
1670 &identity_url,
1671 &book_url,
1672 &hex::encode(adi_key_hash),
1673 );
1674
1675 let result = signer.sign_submit_and_wait(
1676 &wallet.lite_token_account,
1677 &create_adi_body,
1678 Some("Create ADI"),
1679 30,
1680 ).await;
1681
1682 if !result.success {
1683 return Err(JsonRpcError::General(anyhow::anyhow!(
1684 "Failed to create ADI: {:?}", result.error
1685 )));
1686 }
1687
1688 Ok(AdiInfo {
1689 url: identity_url,
1690 key_book_url: book_url,
1691 key_page_url,
1692 keypair: adi_keypair,
1693 })
1694 }
1695
1696 pub async fn buy_credits_for_adi(&self, wallet: &Wallet, adi: &AdiInfo, credits: u64) -> Result<TxResult, JsonRpcError> {
1698 let oracle = self.get_oracle_price().await?;
1699 let amount = Self::calculate_credits_amount(credits, oracle);
1700
1701 let mut signer = SmartSigner::new(&self.client, wallet.keypair.clone(), &wallet.lite_identity);
1702
1703 let body = TxBody::add_credits(&adi.key_page_url, &amount.to_string(), oracle);
1704
1705 Ok(signer.sign_submit_and_wait(
1706 &wallet.lite_token_account,
1707 &body,
1708 Some("Buy credits for ADI"),
1709 30,
1710 ).await)
1711 }
1712
1713 pub async fn get_key_page_info(&self, key_page_url: &str) -> Option<KeyPageInfo> {
1715 let manager = KeyManager::new(&self.client, key_page_url);
1716 match manager.get_key_page_state().await {
1717 Ok(state) => Some(KeyPageInfo {
1718 credits: state.credit_balance,
1719 version: state.version,
1720 threshold: state.accept_threshold,
1721 key_count: state.keys.len(),
1722 }),
1723 Err(_) => None,
1724 }
1725 }
1726
1727 pub async fn create_token_account(&self, adi: &AdiInfo, account_name: &str) -> Result<TxResult, JsonRpcError> {
1729 let account_url = format!("{}/{}", adi.url, account_name);
1730 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1731
1732 let body = TxBody::create_token_account(&account_url, "acc://ACME");
1733
1734 Ok(signer.sign_submit_and_wait(
1735 &adi.url,
1736 &body,
1737 Some("Create token account"),
1738 30,
1739 ).await)
1740 }
1741
1742 pub async fn create_data_account(&self, adi: &AdiInfo, account_name: &str) -> Result<TxResult, JsonRpcError> {
1744 let account_url = format!("{}/{}", adi.url, account_name);
1745 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1746
1747 let body = TxBody::create_data_account(&account_url);
1748
1749 Ok(signer.sign_submit_and_wait(
1750 &adi.url,
1751 &body,
1752 Some("Create data account"),
1753 30,
1754 ).await)
1755 }
1756
1757 pub async fn write_data(&self, adi: &AdiInfo, account_name: &str, entries: &[&str]) -> Result<TxResult, JsonRpcError> {
1759 let account_url = format!("{}/{}", adi.url, account_name);
1760 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1761
1762 let body = TxBody::write_data(entries);
1763
1764 Ok(signer.sign_submit_and_wait(
1765 &account_url,
1766 &body,
1767 Some("Write data"),
1768 30,
1769 ).await)
1770 }
1771
1772 pub async fn add_key_to_adi(&self, adi: &AdiInfo, new_keypair: &SigningKey) -> Result<TxResult, JsonRpcError> {
1774 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1775 Ok(signer.add_key(&new_keypair.verifying_key().to_bytes()).await)
1776 }
1777
1778 pub async fn set_multi_sig_threshold(&self, adi: &AdiInfo, threshold: u64) -> Result<TxResult, JsonRpcError> {
1780 let mut signer = SmartSigner::new(&self.client, adi.keypair.clone(), &adi.key_page_url);
1781 Ok(signer.set_threshold(threshold).await)
1782 }
1783
1784 pub fn close(&self) {
1786 }
1788}
1789
1790pub fn derive_lite_identity_url(public_key: &[u8; 32]) -> String {
1802 let hash = sha256_hash(public_key);
1804 let key_hash_20 = &hash[0..20];
1805
1806 let key_hash_hex = hex::encode(key_hash_20);
1808
1809 let checksum_full = sha256_hash(key_hash_hex.as_bytes());
1811 let checksum_hex = hex::encode(&checksum_full[28..32]);
1812
1813 format!("acc://{}{}", key_hash_hex, checksum_hex)
1815}
1816
1817pub fn derive_lite_token_account_url(public_key: &[u8; 32]) -> String {
1821 let lite_identity = derive_lite_identity_url(public_key);
1822 format!("{}/ACME", lite_identity)
1823}
1824
1825pub fn sha256_hash(data: &[u8]) -> [u8; 32] {
1827 let mut hasher = Sha256::new();
1828 hasher.update(data);
1829 hasher.finalize().into()
1830}
1831
1832fn extract_txid(response: &Value) -> Option<String> {
1840 if let Some(arr) = response.as_array() {
1842 if arr.len() > 1 {
1844 if let Some(status) = arr[1].get("status") {
1845 if let Some(txid) = status.get("txID").and_then(|t| t.as_str()) {
1846 return Some(txid.to_string());
1847 }
1848 }
1849 }
1850 if let Some(first) = arr.first() {
1852 if let Some(status) = first.get("status") {
1853 if let Some(txid) = status.get("txID").and_then(|t| t.as_str()) {
1854 return Some(txid.to_string());
1855 }
1856 }
1857 }
1858 }
1859
1860 response.get("txid")
1862 .or_else(|| response.get("transactionHash"))
1863 .and_then(|t| t.as_str())
1864 .map(String::from)
1865}
1866
1867#[cfg(test)]
1868mod tests {
1869 use super::*;
1870
1871 #[test]
1872 fn test_derive_lite_identity_url() {
1873 let public_key = [1u8; 32];
1874 let url = derive_lite_identity_url(&public_key);
1875 assert!(url.starts_with("acc://"));
1876 assert!(!url.ends_with(".acme"));
1878 let path = url.strip_prefix("acc://").unwrap();
1880 assert_eq!(path.len(), 48); }
1882
1883 #[test]
1884 fn test_tx_body_add_credits() {
1885 let body = TxBody::add_credits("acc://test.acme/credits", "1000000", 5000);
1886 assert_eq!(body["type"], "addCredits");
1887 assert_eq!(body["recipient"], "acc://test.acme/credits");
1888 }
1889
1890 #[test]
1891 fn test_tx_body_send_tokens() {
1892 let body = TxBody::send_tokens_single("acc://bob.acme/tokens", "100");
1893 assert_eq!(body["type"], "sendTokens");
1894 }
1895
1896 #[test]
1897 fn test_tx_body_create_identity() {
1898 let body = TxBody::create_identity(
1899 "acc://test.acme",
1900 "acc://test.acme/book",
1901 "0123456789abcdef",
1902 );
1903 assert_eq!(body["type"], "createIdentity");
1904 assert_eq!(body["url"], "acc://test.acme");
1905 }
1906
1907 #[test]
1908 fn test_wallet_creation() {
1909 let keypair = AccumulateClient::generate_keypair();
1910 let public_key = keypair.verifying_key().to_bytes();
1911 let lite_identity = derive_lite_identity_url(&public_key);
1912 let lite_token_account = derive_lite_token_account_url(&public_key);
1913
1914 assert!(lite_identity.starts_with("acc://"));
1915 assert!(lite_token_account.contains("/ACME"));
1916 }
1917}