From 821c6ed9a4a2cd3b4afbe0a2bfdca5c728f4721f Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 26 Oct 2024 15:51:04 -0700 Subject: [PATCH] Create change output when inputs containing non-outgoing runes are selected (#4028) --- crates/mockcore/src/lib.rs | 22 +++++ crates/mockcore/src/state.rs | 24 +++++- src/subcommand/wallet/send.rs | 10 ++- tests/balances.rs | 15 ++-- tests/wallet/send.rs | 156 ++++++++++++++++++++++++++++++++-- 5 files changed, 206 insertions(+), 21 deletions(-) diff --git a/crates/mockcore/src/lib.rs b/crates/mockcore/src/lib.rs index 4414513daa..63a191d694 100644 --- a/crates/mockcore/src/lib.rs +++ b/crates/mockcore/src/lib.rs @@ -294,6 +294,28 @@ impl Handle { .clone() } + #[track_caller] + pub fn tx_index(&self, txid: Txid) -> (usize, usize) { + let state = self.state(); + + for (block_hash, block) in &state.blocks { + for (t, tx) in block.txdata.iter().enumerate() { + if tx.compute_txid() == txid { + let b = state + .hashes + .iter() + .enumerate() + .find(|(_b, hash)| *hash == block_hash) + .unwrap() + .0; + return (b, t); + } + } + } + + panic!("unknown transaction"); + } + pub fn mempool(&self) -> Vec { self.state().mempool().to_vec() } diff --git a/crates/mockcore/src/state.rs b/crates/mockcore/src/state.rs index 232614b8d8..40e6ee5534 100644 --- a/crates/mockcore/src/state.rs +++ b/crates/mockcore/src/state.rs @@ -200,8 +200,28 @@ impl State { let mut total_value = 0; let mut input = Vec::new(); for (height, tx, vout, witness) in template.inputs.iter() { - let tx = &self.blocks.get(&self.hashes[*height]).unwrap().txdata[*tx]; - total_value += tx.output[*vout].value.to_sat(); + let block_hash = self + .hashes + .get(*height) + .ok_or_else(|| format!("invalid block height {height}")) + .unwrap(); + + let block = self.blocks.get(block_hash).unwrap(); + + let tx = block + .txdata + .get(*tx) + .ok_or_else(|| format!("invalid transaction index {tx}")) + .unwrap(); + + let tx_out = tx + .output + .get(*vout) + .ok_or_else(|| format!("invalid output index {vout}")) + .unwrap(); + + total_value += tx_out.value.to_sat(); + input.push(TxIn { previous_output: OutPoint::new(tx.compute_txid(), *vout as u32), script_sig: ScriptBuf::new(), diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index b3557f245f..dc26a57fc3 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -195,20 +195,22 @@ impl Send { output, balance .into_iter() - .map(|(spaced_rune, pile)| (spaced_rune.rune, pile)) + .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount)) .collect(), ) }) }) - .collect::>>>()?; + .collect::>>>()?; let mut inputs = Vec::new(); let mut input_rune_balances: BTreeMap = BTreeMap::new(); for (output, runes) in balances { if let Some(balance) = runes.get(&spaced_rune.rune) { - if balance.amount > 0 { - *input_rune_balances.entry(spaced_rune.rune).or_default() += balance.amount; + if *balance > 0 { + for (rune, balance) in runes { + *input_rune_balances.entry(rune).or_default() += balance; + } inputs.push(output); } diff --git a/tests/balances.rs b/tests/balances.rs index c290737e4c..ca64ddfdae 100644 --- a/tests/balances.rs +++ b/tests/balances.rs @@ -45,10 +45,10 @@ fn with_runes() { assert_eq!( output, Output { - runes: vec![ + runes: [ ( SpacedRune::new(Rune(RUNE), 0), - vec![( + [( OutPoint { txid: a.output.reveal, vout: 1 @@ -59,12 +59,11 @@ fn with_runes() { symbol: Some('¢') }, )] - .into_iter() - .collect() + .into() ), ( SpacedRune::new(Rune(RUNE + 1), 0), - vec![( + [( OutPoint { txid: b.output.reveal, vout: 1 @@ -75,12 +74,10 @@ fn with_runes() { symbol: Some('¢') }, )] - .into_iter() - .collect() + .into() ), ] - .into_iter() - .collect(), + .into() } ); } diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 630a08d0dd..d139c89b5e 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -816,9 +816,9 @@ fn sending_rune_with_change_works() { pretty_assert_eq!( balances, ord::subcommand::balances::Output { - runes: vec![( + runes: [( SpacedRune::new(Rune(RUNE), 0), - vec![ + [ ( OutPoint { txid: output.txid, @@ -842,11 +842,155 @@ fn sending_rune_with_change_works() { }, ) ] - .into_iter() - .collect() + .into() )] - .into_iter() - .collect(), + .into() + } + ); +} + +#[test] +fn sending_rune_creates_change_output_for_non_outgoing_runes() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); + + create_wallet(&core, &ord); + + let a = etch(&core, &ord, Rune(RUNE)); + let b = etch(&core, &ord, Rune(RUNE + 1)); + + let (a_block, a_tx) = core.tx_index(a.output.reveal); + let (b_block, b_tx) = core.tx_index(b.output.reveal); + + core.mine_blocks(1); + + let address = CommandBuilder::new("--regtest wallet receive") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::() + .addresses + .into_iter() + .next() + .unwrap(); + + let merge = core.broadcast_tx(TransactionTemplate { + inputs: &[(a_block, a_tx, 1, default()), (b_block, b_tx, 1, default())], + recipient: Some(address.require_network(Network::Regtest).unwrap()), + ..default() + }); + + core.mine_blocks(1); + + let balances = CommandBuilder::new("--regtest --index-runes balances") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + pretty_assert_eq!( + balances, + ord::subcommand::balances::Output { + runes: [ + ( + SpacedRune::new(Rune(RUNE), 0), + [( + OutPoint { + txid: merge, + vout: 0 + }, + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, + )] + .into() + ), + ( + SpacedRune::new(Rune(RUNE + 1), 0), + [( + OutPoint { + txid: merge, + vout: 0 + }, + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, + )] + .into() + ), + ] + .into() + } + ); + + let output = CommandBuilder::new(format!( + "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1000:{}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let balances = CommandBuilder::new("--regtest --index-runes balances") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + pretty_assert_eq!( + balances, + ord::subcommand::balances::Output { + runes: [ + ( + SpacedRune::new(Rune(RUNE), 0), + [( + OutPoint { + txid: output.txid, + vout: 2 + }, + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, + )] + .into() + ), + ( + SpacedRune::new(Rune(RUNE + 1), 0), + [( + OutPoint { + txid: output.txid, + vout: 1 + }, + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, + )] + .into() + ) + ] + .into() + } + ); + + pretty_assert_eq!( + CommandBuilder::new("--regtest --index-runes wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { + cardinal: 84999960160, + ordinal: 20000, + runes: Some([(SpacedRune::new(Rune(RUNE + 1), 0), "1000".parse().unwrap())].into()), + runic: Some(10000), + total: 84999990160, } ); }