#![cfg(feature = "mrc20")]
use std::collections::HashMap;
use solid_pod_rs::bitcoin_tx::{
anchor_state, mint_token, transfer_token_with_key, verify_keypath_signature, MempoolBroadcast,
TxoVoucher,
};
use solid_pod_rs::mrc20::{
bt_address, jcs, sha256_hex, verify_mrc20_anchor, verify_state_link, MempoolLookup, TxInfo,
TxOut, Utxo,
};
use solid_pod_rs::payments::PaymentError;
#[derive(Default, Clone)]
struct FixtureMempool {
utxos: HashMap<String, Vec<Utxo>>,
txs: HashMap<String, Vec<TxOut>>,
broadcasts: std::sync::Arc<std::sync::Mutex<Vec<(String, String)>>>,
}
impl FixtureMempool {
fn new() -> Self {
Self::default()
}
fn add_output(&mut self, txid: &str, vout: u32, script_pubkey_hex: &str) {
let outs = self.txs.entry(txid.to_string()).or_default();
while outs.len() <= vout as usize {
outs.push(TxOut {
value: 0,
scriptpubkey: None,
scriptpubkey_address: None,
});
}
outs[vout as usize] = TxOut {
value: 0,
scriptpubkey: Some(script_pubkey_hex.to_string()),
scriptpubkey_address: None,
};
}
fn add_utxo_at(&mut self, address: &str, value: u64) {
self.utxos.entry(address.to_string()).or_default().push(Utxo {
txid: "00".repeat(32),
vout: 0,
value,
confirmed: true,
block_height: Some(840_000),
});
}
}
#[async_trait::async_trait(?Send)]
impl MempoolLookup for FixtureMempool {
async fn address_utxos(&self, address: &str) -> Result<Vec<Utxo>, PaymentError> {
Ok(self.utxos.get(address).cloned().unwrap_or_default())
}
async fn tx(&self, txid: &str) -> Result<TxInfo, PaymentError> {
Ok(TxInfo {
txid: txid.to_string(),
vout: self.txs.get(txid).cloned().unwrap_or_default(),
confirmed: true,
block_height: Some(840_000),
})
}
}
#[async_trait::async_trait(?Send)]
impl MempoolBroadcast for FixtureMempool {
async fn broadcast_tx(&self, raw_hex: &str) -> Result<String, PaymentError> {
let txid = sha256_hex(raw_hex);
self.broadcasts
.lock()
.unwrap()
.push((txid.clone(), raw_hex.to_string()));
Ok(txid)
}
}
const ISSUER_PRIVKEY: &str = "0000000000000000000000000000000000000000000000000000000000000007";
fn issuer_pubkey_hex() -> String {
let sk = k256::SecretKey::from_slice(&hex::decode(ISSUER_PRIVKEY).unwrap()).unwrap();
hex::encode(sk.public_key().to_sec1_bytes())
}
fn issuer_voucher(mempool: &mut FixtureMempool, txid: &str, amount: u64) -> TxoVoucher {
let sk = k256::SecretKey::from_slice(&hex::decode(ISSUER_PRIVKEY).unwrap()).unwrap();
let compressed = sk.public_key().to_sec1_bytes();
let xonly_hex = hex::encode(&compressed[1..]);
let spk_hex = format!("5120{xonly_hex}");
mempool.add_output(txid, 0, &spk_hex);
TxoVoucher {
txid: txid.to_string(),
vout: 0,
amount,
privkey: ISSUER_PRIVKEY.to_string(),
}
}
#[tokio::test]
async fn mint_then_transfer_twice_is_a_valid_hash_chained_trail() {
let network = "testnet4";
let mut mempool = FixtureMempool::new();
let voucher_txid = "11".repeat(32);
let voucher = issuer_voucher(&mut mempool, &voucher_txid, 100_000);
let mint = mint_token("PROV", Some("Provenance Token"), 1_000, &voucher, network, 300, &mempool)
.await
.expect("mint must build");
assert_eq!(mint.state.seq, 0);
assert_eq!(mint.state.prev, "0".repeat(64));
let genesis_addr = bt_address(&issuer_pubkey_hex(), &[mint.state_jcs.clone()], network).unwrap();
assert_eq!(mint.address, genesis_addr);
assert!(
verify_keypath_signature(&mint.tx.signing_xonly, &mint.tx.sighashes[0], &mint.tx.signatures[0])
.unwrap(),
"mint signature must verify"
);
let mint_txid = mempool.broadcast_tx(&mint.tx.raw_hex).await.unwrap();
let mut trail = mint.trail.clone();
trail.current_txid = mint_txid.clone();
let genesis_xonly = {
let chained = solid_pod_rs::mrc20::bt_derive_chained_pubkey(
&issuer_pubkey_hex(),
&[mint.state_jcs.clone()],
)
.unwrap();
hex::encode(&chained[1..])
};
mempool.add_output(&mint_txid, 0, &format!("5120{genesis_xonly}"));
let recipient_a = "aa".repeat(32); let t1 = transfer_token_with_key(&trail, ISSUER_PRIVKEY, None, &recipient_a, 100, 300, &mempool)
.await
.expect("transfer 1 must build");
assert_eq!(t1.state.seq, 1);
assert_eq!(t1.state.prev, sha256_hex(&mint.state_jcs));
verify_state_link(&t1.state, &mint.state).expect("state link genesis→1 must verify");
assert!(
verify_keypath_signature(&t1.tx.signing_xonly, &t1.tx.sighashes[0], &t1.tx.signatures[0])
.unwrap(),
"transfer 1 signature must verify"
);
let t1_txid = mempool.broadcast_tx(&t1.tx.raw_hex).await.unwrap();
let mut trail = t1.trail.clone();
trail.current_txid = t1_txid.clone();
let t1_xonly = {
let chained =
solid_pod_rs::mrc20::bt_derive_chained_pubkey(&issuer_pubkey_hex(), &trail.state_strings)
.unwrap();
hex::encode(&chained[1..])
};
mempool.add_output(&t1_txid, 0, &format!("5120{t1_xonly}"));
let recipient_b = "bb".repeat(32);
let t2 = transfer_token_with_key(&trail, ISSUER_PRIVKEY, None, &recipient_b, 50, 300, &mempool)
.await
.expect("transfer 2 must build");
assert_eq!(t2.state.seq, 2);
assert_eq!(t2.state.prev, sha256_hex(&t1.state_jcs));
verify_state_link(&t2.state, &t1.state).expect("state link 1→2 must verify");
assert!(
verify_keypath_signature(&t2.tx.signing_xonly, &t2.tx.sighashes[0], &t2.tx.signatures[0])
.unwrap(),
"transfer 2 signature must verify"
);
let final_balances = t2.state.balances.clone().unwrap();
assert_eq!(final_balances.get(&issuer_pubkey_hex()).copied(), Some(850));
assert_eq!(final_balances.get(&recipient_a).copied(), Some(100));
assert_eq!(final_balances.get(&recipient_b).copied(), Some(50));
let total: u64 = final_balances.values().sum();
assert_eq!(total, 1_000, "conservation of supply across the chain");
assert_eq!(t2.trail.state_strings.len(), 3);
assert_eq!(t2.trail.state_strings[0], mint.state_jcs);
assert_eq!(t2.trail.state_strings[1], t1.state_jcs);
assert_eq!(t2.trail.state_strings[2], t2.state_jcs);
assert_eq!(
t2.trail.state_strings[2],
jcs(&serde_json::to_value(&t2.state).unwrap())
);
}
#[tokio::test]
async fn verify_mrc20_anchor_accepts_a_produced_transfer_state() {
let network = "testnet4";
let mut mempool = FixtureMempool::new();
let voucher_txid = "22".repeat(32);
let voucher = issuer_voucher(&mut mempool, &voucher_txid, 100_000);
let mint = mint_token("ANCH", None, 1_000, &voucher, network, 300, &mempool)
.await
.unwrap();
let mint_txid = mempool.broadcast_tx(&mint.tx.raw_hex).await.unwrap();
let mut trail = mint.trail.clone();
trail.current_txid = mint_txid.clone();
let genesis_xonly = {
let chained = solid_pod_rs::mrc20::bt_derive_chained_pubkey(
&issuer_pubkey_hex(),
&[mint.state_jcs.clone()],
)
.unwrap();
hex::encode(&chained[1..])
};
mempool.add_output(&mint_txid, 0, &format!("5120{genesis_xonly}"));
let recipient = "cc".repeat(32);
let t = transfer_token_with_key(&trail, ISSUER_PRIVKEY, None, &recipient, 200, 300, &mempool)
.await
.unwrap();
let derived_addr = bt_address(&issuer_pubkey_hex(), &t.trail.state_strings, network).unwrap();
assert_eq!(t.address, derived_addr);
mempool.add_utxo_at(&derived_addr, t.output_amount);
let result = verify_mrc20_anchor(
&t.state,
&mint.state,
&recipient,
&issuer_pubkey_hex(),
&t.trail.state_strings,
network,
&mempool,
)
.await
.expect("Phase-3 verify must ACCEPT the write-side-produced state");
assert_eq!(result.amount, 200);
assert_eq!(result.address, derived_addr);
assert_eq!(result.ticker, "ANCH");
}
#[tokio::test]
async fn anchor_state_notarises_a_state_hash() {
let network = "testnet4";
let mut mempool = FixtureMempool::new();
let voucher_txid = "33".repeat(32);
let voucher = issuer_voucher(&mut mempool, &voucher_txid, 100_000);
let mint = mint_token("GITP", None, 1_000, &voucher, network, 300, &mempool)
.await
.unwrap();
let mint_txid = mempool.broadcast_tx(&mint.tx.raw_hex).await.unwrap();
let mut trail = mint.trail.clone();
trail.current_txid = mint_txid.clone();
let genesis_xonly = {
let chained = solid_pod_rs::mrc20::bt_derive_chained_pubkey(
&issuer_pubkey_hex(),
&[mint.state_jcs.clone()],
)
.unwrap();
hex::encode(&chained[1..])
};
mempool.add_output(&mint_txid, 0, &format!("5120{genesis_xonly}"));
let commit_sha = "a1b2c3d4e5f60718293a4b5c6d7e8f9001122334";
let anchored =
anchor_state(&trail, ISSUER_PRIVKEY, commit_sha, 300, &mempool).await.unwrap();
assert_eq!(anchored.state.seq, 1);
assert_eq!(anchored.state.prev, sha256_hex(&mint.state_jcs));
assert_eq!(anchored.state.anchor.as_deref(), Some(commit_sha));
assert_eq!(anchored.state.ops.len(), 1);
assert_eq!(anchored.state.ops[0].op, "urn:mono:op:anchor");
assert_eq!(
anchored.state.balances.clone().unwrap().get(&issuer_pubkey_hex()).copied(),
Some(1_000)
);
verify_state_link(&anchored.state, &mint.state).expect("anchor state links to genesis");
assert!(
verify_keypath_signature(
&anchored.tx.signing_xonly,
&anchored.tx.sighashes[0],
&anchored.tx.signatures[0]
)
.unwrap(),
"anchor tx signature must verify"
);
}