1use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use sha2::{Digest, Sha256};
19use std::collections::BTreeMap;
20
21use crate::payments::PaymentError;
22
23pub const MRC20_PROFILE: &str = "mono.mrc20.v0.1";
24pub const TRANSFER_OP: &str = "urn:mono:op:transfer";
25
26pub fn jcs(value: &Value) -> String {
33 match value {
34 Value::Null => "null".into(),
35 Value::Bool(b) => if *b { "true" } else { "false" }.into(),
36 Value::Number(n) => {
37 n.to_string()
41 }
42 Value::String(_) => {
43 serde_json::to_string(value).unwrap_or_else(|_| "\"\"".into())
45 }
46 Value::Array(arr) => {
47 let items: Vec<String> = arr.iter().map(jcs).collect();
48 format!("[{}]", items.join(","))
49 }
50 Value::Object(map) => {
51 let mut keys: Vec<&String> = map.keys().collect();
52 keys.sort();
53 let pairs: Vec<String> = keys
54 .iter()
55 .map(|k| {
56 let key_json = serde_json::to_string(*k).unwrap_or_default();
57 format!("{}:{}", key_json, jcs(&map[*k]))
58 })
59 .collect();
60 format!("{{{}}}", pairs.join(","))
61 }
62 }
63}
64
65pub fn sha256_hex(input: &str) -> String {
67 hex::encode(Sha256::digest(input.as_bytes()))
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct Mrc20State {
78 pub profile: String,
79 pub prev: String,
80 pub seq: u64,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub ticker: Option<String>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub name: Option<String>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub decimals: Option<u32>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub supply: Option<u64>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub balances: Option<BTreeMap<String, u64>>,
91 pub ops: Vec<Mrc20Op>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub anchor: Option<String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Mrc20Op {
99 pub op: String,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub from: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub to: Option<String>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub amt: Option<u64>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct Mrc20Trail {
111 pub ticker: String,
112 pub name: String,
113 pub supply: u64,
114 pub pubkey_base: String,
115 pub states: Vec<Mrc20State>,
116 pub state_strings: Vec<String>,
117 pub current_txid: String,
118 pub current_vout: u32,
119 pub current_amount: u64,
120 pub network: String,
121 pub date_created: String,
122}
123
124#[derive(Debug, Clone)]
126pub struct Mrc20DepositResult {
127 pub amount: u64,
128 pub ticker: String,
129}
130
131pub fn validate_mrc20_state(state: &Mrc20State) -> Result<(), PaymentError> {
135 if state.profile != MRC20_PROFILE {
136 return Err(PaymentError::InvalidState(format!(
137 "invalid profile: expected {MRC20_PROFILE}, got {}",
138 state.profile
139 )));
140 }
141 if state.prev.is_empty() {
142 return Err(PaymentError::InvalidState("missing prev hash".into()));
143 }
144 Ok(())
145}
146
147pub fn verify_state_link(state: &Mrc20State, prev_state: &Mrc20State) -> Result<(), PaymentError> {
152 let prev_value = serde_json::to_value(prev_state)
153 .map_err(|e| PaymentError::InvalidState(format!("serialize: {e}")))?;
154 let prev_jcs = jcs(&prev_value);
155 let expected = sha256_hex(&prev_jcs);
156
157 if state.prev != expected {
158 return Err(PaymentError::InvalidState(format!(
159 "chain break: expected prev {expected}, got {}",
160 state.prev
161 )));
162 }
163 if state.seq != prev_state.seq + 1 {
164 return Err(PaymentError::InvalidState(format!(
165 "sequence mismatch: expected {}, got {}",
166 prev_state.seq + 1,
167 state.seq
168 )));
169 }
170 Ok(())
171}
172
173pub fn extract_transfers_to<'a>(state: &'a Mrc20State, to_address: &str) -> Vec<&'a Mrc20Op> {
175 state
176 .ops
177 .iter()
178 .filter(|op| {
179 op.op == TRANSFER_OP
180 && op.to.as_deref() == Some(to_address)
181 && op.amt.map_or(false, |a| a > 0)
182 })
183 .collect()
184}
185
186pub fn total_transferred_to(state: &Mrc20State, to_address: &str) -> u64 {
188 extract_transfers_to(state, to_address)
189 .iter()
190 .filter_map(|op| op.amt)
191 .sum()
192}
193
194pub fn verify_mrc20_deposit(
197 state: &Mrc20State,
198 prev_state: &Mrc20State,
199 to_address: &str,
200) -> Result<Mrc20DepositResult, PaymentError> {
201 validate_mrc20_state(state)?;
202 validate_mrc20_state(prev_state)?;
203 verify_state_link(state, prev_state)?;
204
205 let amount = total_transferred_to(state, to_address);
206 if amount == 0 {
207 return Err(PaymentError::InvalidState(format!(
208 "no transfers to {to_address} in state ops"
209 )));
210 }
211
212 Ok(Mrc20DepositResult {
213 amount,
214 ticker: state.ticker.clone().unwrap_or_else(|| "UNKNOWN".into()),
215 })
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct Utxo {
236 pub txid: String,
238 pub vout: u32,
240 pub value: u64,
242 #[serde(default)]
244 pub confirmed: bool,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub block_height: Option<u64>,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct TxOut {
253 #[serde(default)]
255 pub value: u64,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub scriptpubkey: Option<String>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub scriptpubkey_address: Option<String>,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270pub struct TxInfo {
271 pub txid: String,
273 #[serde(default)]
275 pub vout: Vec<TxOut>,
276 #[serde(default)]
278 pub confirmed: bool,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub block_height: Option<u64>,
282}
283
284#[async_trait::async_trait(?Send)]
292pub trait MempoolLookup {
293 async fn address_utxos(&self, address: &str) -> Result<Vec<Utxo>, PaymentError>;
299
300 async fn tx(&self, txid: &str) -> Result<TxInfo, PaymentError>;
302}
303
304#[cfg(feature = "mrc20")]
307mod anchor {
308 use super::*;
309 use k256::elliptic_curve::ff::PrimeField;
310 use k256::ProjectivePoint;
311 use k256::Scalar;
312 use k256::SecretKey;
313
314 fn tagged_hash(tag: &str, msgs: &[&[u8]]) -> [u8; 32] {
315 let tag_hash = Sha256::digest(tag.as_bytes());
316 let mut hasher = Sha256::new();
317 hasher.update(&tag_hash);
318 hasher.update(&tag_hash);
319 for msg in msgs {
320 hasher.update(msg);
321 }
322 hasher.finalize().into()
323 }
324
325 fn bytes_to_scalar(bytes: &[u8; 32]) -> Option<Scalar> {
326 let wide = k256::FieldBytes::from_slice(bytes);
327 Option::from(Scalar::from_repr(*wide))
328 }
329
330 fn bt_scalar(pubkey_compressed: &[u8], state_jcs: &str) -> Option<Scalar> {
334 let x_only = if pubkey_compressed.len() == 33 {
335 &pubkey_compressed[1..]
336 } else if pubkey_compressed.len() == 32 {
337 pubkey_compressed
338 } else {
339 return None;
340 };
341 let state_hash = Sha256::digest(state_jcs.as_bytes());
342 let tweak_bytes = tagged_hash("TapTweak", &[x_only, &state_hash]);
343 bytes_to_scalar(&tweak_bytes)
344 }
345
346 pub fn bt_derive_chained_pubkey(
350 pubkey_base_hex: &str,
351 state_strings: &[String],
352 ) -> Result<Vec<u8>, PaymentError> {
353 let pubkey_bytes = hex::decode(pubkey_base_hex)
354 .map_err(|e| PaymentError::InvalidState(format!("bad pubkey hex: {e}")))?;
355
356 let point = k256::PublicKey::from_sec1_bytes(&pubkey_bytes)
357 .map_err(|e| PaymentError::InvalidState(format!("bad pubkey: {e}")))?;
358 let mut p = ProjectivePoint::from(point.as_affine().clone());
359 let mut current_compressed = pubkey_bytes;
360
361 for state_jcs in state_strings {
362 let t = bt_scalar(¤t_compressed, state_jcs)
363 .ok_or_else(|| PaymentError::InvalidState("scalar derivation failed".into()))?;
364 p = p + (ProjectivePoint::GENERATOR * t);
365 let affine = p.to_affine();
366 let encoded = k256::PublicKey::from_affine(affine)
367 .map_err(|e| PaymentError::InvalidState(format!("point at infinity: {e}")))?;
368 current_compressed = encoded.to_sec1_bytes().to_vec();
369 }
370
371 Ok(current_compressed)
372 }
373
374 pub fn bt_derive_chained_privkey(
376 privkey_hex: &str,
377 state_strings: &[String],
378 ) -> Result<Vec<u8>, PaymentError> {
379 let privkey_bytes = hex::decode(privkey_hex)
380 .map_err(|e| PaymentError::InvalidState(format!("bad privkey hex: {e}")))?;
381
382 let sk = SecretKey::from_slice(&privkey_bytes)
383 .map_err(|e| PaymentError::InvalidState(format!("bad privkey: {e}")))?;
384 let mut d = *sk.to_nonzero_scalar().as_ref();
385
386 let pubkey = sk.public_key();
387 let mut current_compressed = pubkey.to_sec1_bytes().to_vec();
388
389 for state_jcs in state_strings {
390 let t = bt_scalar(¤t_compressed, state_jcs)
391 .ok_or_else(|| PaymentError::InvalidState("scalar derivation failed".into()))?;
392 d = d + t;
393 let new_sk = SecretKey::new(d.into());
394 current_compressed = new_sk.public_key().to_sec1_bytes().to_vec();
395 }
396
397 Ok(d.to_bytes().to_vec())
398 }
399
400 const BECH32_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
403 const BECH32M_CONST: u32 = 0x2bc830a3;
404
405 fn convert_bits(data: &[u8], from: u32, to: u32, pad: bool) -> Vec<u8> {
406 let mut acc: u32 = 0;
407 let mut bits: u32 = 0;
408 let maxv = (1u32 << to) - 1;
409 let mut ret = Vec::new();
410 for &v in data {
411 acc = (acc << from) | (v as u32);
412 bits += from;
413 while bits >= to {
414 bits -= to;
415 ret.push(((acc >> bits) & maxv) as u8);
416 }
417 }
418 if pad && bits > 0 {
419 ret.push(((acc << (to - bits)) & maxv) as u8);
420 }
421 ret
422 }
423
424 fn hrp_expand(hrp: &str) -> Vec<u8> {
425 let mut r: Vec<u8> = hrp.bytes().map(|b| b >> 5).collect();
426 r.push(0);
427 r.extend(hrp.bytes().map(|b| b & 31));
428 r
429 }
430
431 fn polymod(values: &[u8]) -> u32 {
432 const GEN: [u32; 5] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
433 let mut chk: u32 = 1;
434 for &v in values {
435 let b = chk >> 25;
436 chk = ((chk & 0x1ffffff) << 5) ^ (v as u32);
437 for i in 0..5 {
438 if (b >> i) & 1 != 0 {
439 chk ^= GEN[i];
440 }
441 }
442 }
443 chk
444 }
445
446 fn bech32m_encode(hrp: &str, version: u8, program: &[u8]) -> String {
447 let conv = convert_bits(program, 8, 5, true);
448 let mut values = vec![version];
449 values.extend_from_slice(&conv);
450
451 let mut enc = hrp_expand(hrp);
452 enc.extend_from_slice(&values);
453 enc.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
454
455 let plm = polymod(&enc) ^ BECH32M_CONST;
456 let checksum: Vec<u8> = (0..6)
457 .map(|i| ((plm >> (5 * (5 - i))) & 31) as u8)
458 .collect();
459
460 let mut result = String::with_capacity(hrp.len() + 1 + values.len() + 6);
461 result.push_str(hrp);
462 result.push('1');
463 for &v in values.iter().chain(checksum.iter()) {
464 result.push(BECH32_CHARSET[v as usize] as char);
465 }
466 result
467 }
468
469 pub fn bt_address(
474 pubkey_hex: &str,
475 state_strings: &[String],
476 network: &str,
477 ) -> Result<String, PaymentError> {
478 let chained = bt_derive_chained_pubkey(pubkey_hex, state_strings)?;
479 let x_only = if chained.len() == 33 {
480 &chained[1..]
481 } else if chained.len() == 32 {
482 &chained[..]
483 } else {
484 return Err(PaymentError::InvalidState("unexpected key length".into()));
485 };
486 let hrp = if network == "mainnet" { "bc" } else { "tb" };
487 Ok(bech32m_encode(hrp, 1, x_only))
488 }
489
490 pub async fn verify_mrc20_anchor(
509 state: &Mrc20State,
510 prev_state: &Mrc20State,
511 to_address: &str,
512 pubkey_hex: &str,
513 state_strings: &[String],
514 network: &str,
515 mempool: &dyn MempoolLookup,
516 ) -> Result<Mrc20AnchorResult, PaymentError> {
517 let deposit = verify_mrc20_deposit(state, prev_state, to_address)?;
518
519 if state_strings.is_empty() {
520 return Err(PaymentError::InvalidState(
521 "stateStrings required for anchor verification".into(),
522 ));
523 }
524 if pubkey_hex.len() != 66 {
525 return Err(PaymentError::InvalidState(
526 "pubkey must be a 66-char compressed pubkey hex".into(),
527 ));
528 }
529
530 let current_value = serde_json::to_value(state)
532 .map_err(|e| PaymentError::InvalidState(format!("serialize: {e}")))?;
533 let current_jcs = jcs(¤t_value);
534 if let Some(last) = state_strings.last() {
535 if *last != current_jcs {
536 return Err(PaymentError::InvalidState(
537 "last stateString does not match JCS(state)".into(),
538 ));
539 }
540 }
541
542 let address = bt_address(pubkey_hex, state_strings, network)?;
543
544 let utxos = mempool.address_utxos(&address).await?;
547 if utxos.is_empty() {
548 return Err(PaymentError::InvalidState(format!(
549 "no UTXO at derived address {address}"
550 )));
551 }
552
553 Ok(Mrc20AnchorResult {
554 amount: deposit.amount,
555 ticker: deposit.ticker,
556 address,
557 utxos,
558 })
559 }
560
561 #[derive(Debug, Clone)]
567 pub struct Mrc20AnchorResult {
568 pub amount: u64,
569 pub ticker: String,
570 pub address: String,
571 pub utxos: Vec<Utxo>,
572 }
573}
574
575#[cfg(feature = "mrc20")]
576pub use anchor::*;
577
578#[cfg(test)]
581mod tests {
582 use super::*;
583 use serde_json::json;
584
585 #[test]
588 fn jcs_sorts_keys() {
589 let val = json!({"z": 1, "a": 2, "m": 3});
590 assert_eq!(jcs(&val), r#"{"a":2,"m":3,"z":1}"#);
591 }
592
593 #[test]
594 fn jcs_nested_objects() {
595 let val = json!({"b": {"d": 1, "c": 2}, "a": 3});
596 assert_eq!(jcs(&val), r#"{"a":3,"b":{"c":2,"d":1}}"#);
597 }
598
599 #[test]
600 fn jcs_array() {
601 let val = json!([3, 1, 2]);
602 assert_eq!(jcs(&val), "[3,1,2]");
603 }
604
605 #[test]
606 fn jcs_string_escaping() {
607 let val = json!({"key": "hello \"world\""});
608 assert_eq!(jcs(&val), r#"{"key":"hello \"world\""}"#);
609 }
610
611 #[test]
612 fn jcs_null_bool() {
613 assert_eq!(jcs(&json!(null)), "null");
614 assert_eq!(jcs(&json!(true)), "true");
615 assert_eq!(jcs(&json!(false)), "false");
616 }
617
618 #[test]
619 fn jcs_empty_object_and_array() {
620 assert_eq!(jcs(&json!({})), "{}");
621 assert_eq!(jcs(&json!([])), "[]");
622 }
623
624 #[test]
625 fn jcs_deterministic() {
626 let val = json!({"profile": "mono.mrc20.v0.1", "prev": "abc", "seq": 0, "ops": []});
627 let a = jcs(&val);
628 let b = jcs(&val);
629 assert_eq!(a, b);
630 }
631
632 #[test]
635 fn sha256_hex_known_vector() {
636 assert_eq!(
637 sha256_hex(""),
638 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
639 );
640 }
641
642 #[test]
643 fn sha256_hex_deterministic() {
644 let a = sha256_hex("hello");
645 let b = sha256_hex("hello");
646 assert_eq!(a, b);
647 }
648
649 fn genesis_state() -> Mrc20State {
652 Mrc20State {
653 profile: MRC20_PROFILE.into(),
654 prev: "0".repeat(64),
655 seq: 0,
656 ticker: Some("TEST".into()),
657 name: Some("Test Token".into()),
658 decimals: Some(0),
659 supply: Some(1000),
660 balances: Some(BTreeMap::from([("issuer".into(), 1000)])),
661 ops: vec![],
662 anchor: None,
663 }
664 }
665
666 fn transfer_state(prev_jcs_hash: &str) -> Mrc20State {
667 Mrc20State {
668 profile: MRC20_PROFILE.into(),
669 prev: prev_jcs_hash.into(),
670 seq: 1,
671 ticker: Some("TEST".into()),
672 name: Some("Test Token".into()),
673 decimals: Some(0),
674 supply: Some(1000),
675 balances: Some(BTreeMap::from([
676 ("issuer".into(), 900),
677 ("recipient".into(), 100),
678 ])),
679 ops: vec![Mrc20Op {
680 op: TRANSFER_OP.into(),
681 from: Some("issuer".into()),
682 to: Some("recipient".into()),
683 amt: Some(100),
684 }],
685 anchor: None,
686 }
687 }
688
689 #[test]
690 fn validate_valid_state() {
691 assert!(validate_mrc20_state(&genesis_state()).is_ok());
692 }
693
694 #[test]
695 fn validate_rejects_wrong_profile() {
696 let mut state = genesis_state();
697 state.profile = "wrong".into();
698 assert!(validate_mrc20_state(&state).is_err());
699 }
700
701 #[test]
702 fn validate_rejects_empty_prev() {
703 let mut state = genesis_state();
704 state.prev = String::new();
705 assert!(validate_mrc20_state(&state).is_err());
706 }
707
708 #[test]
711 fn verify_state_link_valid_chain() {
712 let genesis = genesis_state();
713 let genesis_value = serde_json::to_value(&genesis).unwrap();
714 let genesis_hash = sha256_hex(&jcs(&genesis_value));
715 let next = transfer_state(&genesis_hash);
716
717 assert!(verify_state_link(&next, &genesis).is_ok());
718 }
719
720 #[test]
721 fn verify_state_link_detects_break() {
722 let genesis = genesis_state();
723 let next = transfer_state("wrong_hash");
724
725 assert!(verify_state_link(&next, &genesis).is_err());
726 }
727
728 #[test]
729 fn verify_state_link_detects_sequence_gap() {
730 let genesis = genesis_state();
731 let genesis_value = serde_json::to_value(&genesis).unwrap();
732 let genesis_hash = sha256_hex(&jcs(&genesis_value));
733 let mut next = transfer_state(&genesis_hash);
734 next.seq = 5;
735
736 let err = verify_state_link(&next, &genesis);
737 assert!(err.is_err());
738 }
739
740 #[test]
743 fn extract_transfers_finds_matching() {
744 let genesis = genesis_state();
745 let genesis_value = serde_json::to_value(&genesis).unwrap();
746 let genesis_hash = sha256_hex(&jcs(&genesis_value));
747 let state = transfer_state(&genesis_hash);
748
749 let transfers = extract_transfers_to(&state, "recipient");
750 assert_eq!(transfers.len(), 1);
751 assert_eq!(transfers[0].amt, Some(100));
752 }
753
754 #[test]
755 fn extract_transfers_ignores_wrong_recipient() {
756 let genesis = genesis_state();
757 let genesis_value = serde_json::to_value(&genesis).unwrap();
758 let genesis_hash = sha256_hex(&jcs(&genesis_value));
759 let state = transfer_state(&genesis_hash);
760
761 let transfers = extract_transfers_to(&state, "nobody");
762 assert!(transfers.is_empty());
763 }
764
765 #[test]
766 fn extract_transfers_ignores_zero_amount() {
767 let state = Mrc20State {
768 profile: MRC20_PROFILE.into(),
769 prev: "0".repeat(64),
770 seq: 0,
771 ticker: None,
772 name: None,
773 decimals: None,
774 supply: None,
775 balances: None,
776 ops: vec![Mrc20Op {
777 op: TRANSFER_OP.into(),
778 from: Some("a".into()),
779 to: Some("b".into()),
780 amt: Some(0),
781 }],
782 anchor: None,
783 };
784 assert!(extract_transfers_to(&state, "b").is_empty());
785 }
786
787 #[test]
788 fn total_transferred_sums_multiple_ops() {
789 let state = Mrc20State {
790 profile: MRC20_PROFILE.into(),
791 prev: "0".repeat(64),
792 seq: 0,
793 ticker: None,
794 name: None,
795 decimals: None,
796 supply: None,
797 balances: None,
798 ops: vec![
799 Mrc20Op {
800 op: TRANSFER_OP.into(),
801 from: Some("a".into()),
802 to: Some("pod".into()),
803 amt: Some(50),
804 },
805 Mrc20Op {
806 op: TRANSFER_OP.into(),
807 from: Some("b".into()),
808 to: Some("pod".into()),
809 amt: Some(30),
810 },
811 Mrc20Op {
812 op: TRANSFER_OP.into(),
813 from: Some("c".into()),
814 to: Some("other".into()),
815 amt: Some(20),
816 },
817 ],
818 anchor: None,
819 };
820 assert_eq!(total_transferred_to(&state, "pod"), 80);
821 }
822
823 #[test]
826 fn verify_deposit_success() {
827 let genesis = genesis_state();
828 let genesis_value = serde_json::to_value(&genesis).unwrap();
829 let genesis_hash = sha256_hex(&jcs(&genesis_value));
830 let next = transfer_state(&genesis_hash);
831
832 let result = verify_mrc20_deposit(&next, &genesis, "recipient").unwrap();
833 assert_eq!(result.amount, 100);
834 assert_eq!(result.ticker, "TEST");
835 }
836
837 #[test]
838 fn verify_deposit_fails_no_transfers() {
839 let genesis = genesis_state();
840 let genesis_value = serde_json::to_value(&genesis).unwrap();
841 let genesis_hash = sha256_hex(&jcs(&genesis_value));
842 let next = transfer_state(&genesis_hash);
843
844 let err = verify_mrc20_deposit(&next, &genesis, "nobody");
845 assert!(err.is_err());
846 }
847
848 #[test]
849 fn verify_deposit_fails_chain_break() {
850 let genesis = genesis_state();
851 let next = transfer_state("bad_hash");
852
853 let err = verify_mrc20_deposit(&next, &genesis, "recipient");
854 assert!(err.is_err());
855 }
856
857 #[test]
860 fn trail_roundtrip() {
861 let trail = Mrc20Trail {
862 ticker: "TEST".into(),
863 name: "Test".into(),
864 supply: 1000,
865 pubkey_base: "02".to_string() + &"ab".repeat(32),
866 states: vec![genesis_state()],
867 state_strings: vec![jcs(&serde_json::to_value(&genesis_state()).unwrap())],
868 current_txid: "a".repeat(64),
869 current_vout: 0,
870 current_amount: 9700,
871 network: "testnet4".into(),
872 date_created: "2026-05-11T00:00:00Z".into(),
873 };
874 let json = serde_json::to_string(&trail).unwrap();
875 let parsed: Mrc20Trail = serde_json::from_str(&json).unwrap();
876 assert_eq!(parsed.ticker, "TEST");
877 assert_eq!(parsed.supply, 1000);
878 assert_eq!(parsed.states.len(), 1);
879 }
880
881 #[test]
884 fn jcs_state_is_deterministic_across_serializations() {
885 let state = genesis_state();
886 let v1 = serde_json::to_value(&state).unwrap();
887 let v2 = serde_json::to_value(&state).unwrap();
888 assert_eq!(jcs(&v1), jcs(&v2));
889 }
890
891 #[cfg(feature = "mrc20")]
894 mod anchor_tests {
895 use super::*;
896 use std::collections::HashMap;
897
898 const TEST_PRIVKEY: &str =
900 "0000000000000000000000000000000000000000000000000000000000000001";
901
902 fn test_pubkey_compressed() -> String {
903 let sk = k256::SecretKey::from_slice(&hex::decode(TEST_PRIVKEY).unwrap()).unwrap();
904 hex::encode(sk.public_key().to_sec1_bytes())
905 }
906
907 struct FixtureMempool {
911 utxos: HashMap<String, Vec<Utxo>>,
912 }
913 impl FixtureMempool {
914 fn empty() -> Self {
915 Self { utxos: HashMap::new() }
916 }
917 fn with_utxo_at(address: &str) -> Self {
919 let mut utxos = HashMap::new();
920 utxos.insert(
921 address.to_string(),
922 vec![Utxo {
923 txid: "ab".repeat(32),
924 vout: 0,
925 value: 9700,
926 confirmed: true,
927 block_height: Some(840_000),
928 }],
929 );
930 Self { utxos }
931 }
932 }
933 #[async_trait::async_trait(?Send)]
934 impl MempoolLookup for FixtureMempool {
935 async fn address_utxos(&self, address: &str) -> Result<Vec<Utxo>, PaymentError> {
936 Ok(self.utxos.get(address).cloned().unwrap_or_default())
937 }
938 async fn tx(&self, txid: &str) -> Result<TxInfo, PaymentError> {
939 Ok(TxInfo {
940 txid: txid.to_string(),
941 vout: vec![],
942 confirmed: true,
943 block_height: Some(840_000),
944 })
945 }
946 }
947
948 fn block_on<F: std::future::Future>(f: F) -> F::Output {
951 tokio::runtime::Builder::new_current_thread()
952 .build()
953 .unwrap()
954 .block_on(f)
955 }
956
957 fn valid_chain() -> (Mrc20State, Mrc20State, Vec<String>) {
960 let genesis = genesis_state();
961 let genesis_val = serde_json::to_value(&genesis).unwrap();
962 let genesis_jcs = jcs(&genesis_val);
963 let genesis_hash = sha256_hex(&genesis_jcs);
964 let next = transfer_state(&genesis_hash);
965 let next_jcs = jcs(&serde_json::to_value(&next).unwrap());
966 (genesis, next, vec![genesis_jcs, next_jcs])
967 }
968
969 #[test]
970 fn bt_derive_chained_pubkey_single_state() {
971 let pubkey = test_pubkey_compressed();
972 let states = vec!["genesis_state".into()];
973 let result = bt_derive_chained_pubkey(&pubkey, &states);
974 assert!(result.is_ok());
975 let chained = result.unwrap();
976 assert_eq!(chained.len(), 33);
977 assert!(chained[0] == 0x02 || chained[0] == 0x03);
978 }
979
980 #[test]
981 fn bt_derive_chained_pubkey_multiple_states() {
982 let pubkey = test_pubkey_compressed();
983 let states = vec!["state0".into(), "state1".into(), "state2".into()];
984 let result = bt_derive_chained_pubkey(&pubkey, &states);
985 assert!(result.is_ok());
986 }
987
988 #[test]
989 fn bt_derive_chained_pubkey_deterministic() {
990 let pubkey = test_pubkey_compressed();
991 let states = vec!["s1".into(), "s2".into()];
992 let a = bt_derive_chained_pubkey(&pubkey, &states).unwrap();
993 let b = bt_derive_chained_pubkey(&pubkey, &states).unwrap();
994 assert_eq!(a, b);
995 }
996
997 #[test]
998 fn bt_derive_chained_pubkey_differs_per_state() {
999 let pubkey = test_pubkey_compressed();
1000 let a = bt_derive_chained_pubkey(&pubkey, &["s1".into()]).unwrap();
1001 let b = bt_derive_chained_pubkey(&pubkey, &["s2".into()]).unwrap();
1002 assert_ne!(a, b);
1003 }
1004
1005 #[test]
1006 fn bt_derive_chained_privkey_roundtrip() {
1007 let pubkey = test_pubkey_compressed();
1008 let states = vec!["state1".into()];
1009
1010 let chained_priv = bt_derive_chained_privkey(TEST_PRIVKEY, &states).unwrap();
1011 let chained_sk = k256::SecretKey::from_slice(&chained_priv).unwrap();
1012 let chained_pub_from_priv = hex::encode(chained_sk.public_key().to_sec1_bytes());
1013
1014 let chained_pub = hex::encode(bt_derive_chained_pubkey(&pubkey, &states).unwrap());
1015 assert_eq!(chained_pub_from_priv, chained_pub);
1016 }
1017
1018 #[test]
1019 fn bt_address_testnet_format() {
1020 let pubkey = test_pubkey_compressed();
1021 let states = vec!["genesis".into()];
1022 let addr = bt_address(&pubkey, &states, "testnet4").unwrap();
1023 assert!(
1024 addr.starts_with("tb1p"),
1025 "expected tb1p prefix, got: {addr}"
1026 );
1027 }
1028
1029 #[test]
1030 fn bt_address_mainnet_format() {
1031 let pubkey = test_pubkey_compressed();
1032 let states = vec!["genesis".into()];
1033 let addr = bt_address(&pubkey, &states, "mainnet").unwrap();
1034 assert!(
1035 addr.starts_with("bc1p"),
1036 "expected bc1p prefix, got: {addr}"
1037 );
1038 }
1039
1040 #[test]
1041 fn bt_address_deterministic() {
1042 let pubkey = test_pubkey_compressed();
1043 let states = vec!["s1".into(), "s2".into()];
1044 let a = bt_address(&pubkey, &states, "testnet4").unwrap();
1045 let b = bt_address(&pubkey, &states, "testnet4").unwrap();
1046 assert_eq!(a, b);
1047 }
1048
1049 #[test]
1050 fn bt_address_differs_per_network() {
1051 let pubkey = test_pubkey_compressed();
1052 let states = vec!["s1".into()];
1053 let testnet = bt_address(&pubkey, &states, "testnet4").unwrap();
1054 let mainnet = bt_address(&pubkey, &states, "mainnet").unwrap();
1055 assert_ne!(testnet, mainnet);
1056 assert!(testnet.starts_with("tb1p"));
1057 assert!(mainnet.starts_with("bc1p"));
1058 }
1059
1060 #[test]
1061 fn verify_anchor_rejects_empty_state_strings() {
1062 let genesis = genesis_state();
1063 let genesis_val = serde_json::to_value(&genesis).unwrap();
1064 let genesis_hash = sha256_hex(&jcs(&genesis_val));
1065 let next = transfer_state(&genesis_hash);
1066 let pubkey = test_pubkey_compressed();
1067 let mp = FixtureMempool::empty();
1068
1069 let result = block_on(verify_mrc20_anchor(
1070 &next, &genesis, "recipient", &pubkey, &[], "testnet4", &mp,
1071 ));
1072 assert!(result.is_err());
1073 }
1074
1075 #[test]
1076 fn verify_anchor_rejects_bad_pubkey_length() {
1077 let genesis = genesis_state();
1078 let genesis_val = serde_json::to_value(&genesis).unwrap();
1079 let genesis_hash = sha256_hex(&jcs(&genesis_val));
1080 let next = transfer_state(&genesis_hash);
1081 let mp = FixtureMempool::empty();
1082
1083 let result = block_on(verify_mrc20_anchor(
1084 &next,
1085 &genesis,
1086 "recipient",
1087 "short",
1088 &["s1".into()],
1089 "testnet4",
1090 &mp,
1091 ));
1092 assert!(result.is_err());
1093 }
1094
1095 #[test]
1100 fn verify_anchor_true_when_utxo_present() {
1101 let (genesis, next, state_strings) = valid_chain();
1102 let pubkey = test_pubkey_compressed();
1103 let address = bt_address(&pubkey, &state_strings, "testnet4").unwrap();
1105 let mp = FixtureMempool::with_utxo_at(&address);
1106
1107 let result = block_on(verify_mrc20_anchor(
1108 &next,
1109 &genesis,
1110 "recipient",
1111 &pubkey,
1112 &state_strings,
1113 "testnet4",
1114 &mp,
1115 ))
1116 .expect("anchor with a present UTXO must verify");
1117 assert_eq!(result.amount, 100);
1118 assert_eq!(result.ticker, "TEST");
1119 assert_eq!(result.address, address);
1120 assert_eq!(result.utxos.len(), 1);
1121 assert!(result.utxos[0].confirmed);
1122 }
1123
1124 #[test]
1127 fn verify_anchor_false_when_utxo_absent() {
1128 let (genesis, next, state_strings) = valid_chain();
1129 let pubkey = test_pubkey_compressed();
1130 let mp = FixtureMempool::empty(); let err = block_on(verify_mrc20_anchor(
1133 &next,
1134 &genesis,
1135 "recipient",
1136 &pubkey,
1137 &state_strings,
1138 "testnet4",
1139 &mp,
1140 ))
1141 .unwrap_err();
1142 match err {
1143 PaymentError::InvalidState(m) => assert!(m.contains("no UTXO")),
1144 other => panic!("expected no-UTXO InvalidState, got {other:?}"),
1145 }
1146 }
1147
1148 #[test]
1152 fn verify_anchor_false_when_utxo_at_wrong_address() {
1153 let (genesis, next, state_strings) = valid_chain();
1154 let pubkey = test_pubkey_compressed();
1155 let mp = FixtureMempool::with_utxo_at("tb1pdecoy0000000000000000000000000000");
1156
1157 let result = block_on(verify_mrc20_anchor(
1158 &next,
1159 &genesis,
1160 "recipient",
1161 &pubkey,
1162 &state_strings,
1163 "testnet4",
1164 &mp,
1165 ));
1166 assert!(result.is_err(), "UTXO at a non-derived address must not verify");
1167 }
1168 }
1169}