diff --git a/src/api.rs b/src/api.rs index 33f566e868..0cd03d7e1a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -126,7 +126,7 @@ pub struct Output { pub address: Option>, pub indexed: bool, pub inscriptions: Vec, - pub runes: Vec<(SpacedRune, Pile)>, + pub runes: BTreeMap, pub sat_ranges: Option>, pub script_pubkey: String, pub spent: bool, @@ -141,7 +141,7 @@ impl Output { outpoint: OutPoint, tx_out: TxOut, indexed: bool, - runes: Vec<(SpacedRune, Pile)>, + runes: BTreeMap, sat_ranges: Option>, spent: bool, ) -> Self { diff --git a/src/index.rs b/src/index.rs index bb2569d6e7..b3e26e02d0 100644 --- a/src/index.rs +++ b/src/index.rs @@ -973,7 +973,7 @@ impl Index { pub(crate) fn get_rune_balances_for_outpoint( &self, outpoint: OutPoint, - ) -> Result> { + ) -> Result> { let rtx = self.database.begin_read()?; let outpoint_to_balances = rtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; @@ -981,12 +981,12 @@ impl Index { let id_to_rune_entries = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; let Some(balances) = outpoint_to_balances.get(&outpoint.store())? else { - return Ok(Vec::new()); + return Ok(BTreeMap::new()); }; let balances_buffer = balances.value(); - let mut balances = Vec::new(); + let mut balances = BTreeMap::new(); let mut i = 0; while i < balances_buffer.len() { let ((id, amount), length) = Index::decode_rune_balance(&balances_buffer[i..]).unwrap(); @@ -994,14 +994,14 @@ impl Index { let entry = RuneEntry::load(id_to_rune_entries.get(id.store())?.unwrap().value()); - balances.push(( + balances.insert( entry.spaced_rune, Pile { amount, divisibility: entry.divisibility, symbol: entry.symbol, }, - )); + ); } Ok(balances) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index d67e678b33..954264a79b 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -3225,7 +3225,9 @@ mod tests { divisibility: 1, symbol: None, } - )], + )] + .into_iter() + .collect(), spent: false, } ); diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index c19b829e92..192904a069 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -26,7 +26,7 @@ pub(crate) fn run(wallet: Wallet) -> SubcommandResult { let mut runic = 0; for (output, txout) in unspent_outputs { - let rune_balances = wallet.get_runes_balances_for_output(output)?; + let rune_balances = wallet.get_runes_balances_in_output(output)?; let is_ordinal = inscription_outputs.contains(output); let is_runic = !rune_balances.is_empty(); diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 7471b13f31..ddfcc4df50 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -39,6 +39,7 @@ impl Send { address, rune, decimal, + self.postage.unwrap_or(TARGET_POSTAGE), self.fee_rate, )?, Outgoing::InscriptionId(id) => Self::create_unsigned_send_satpoint_transaction( @@ -209,6 +210,7 @@ impl Send { destination: Address, spaced_rune: SpacedRune, decimal: Decimal, + postage: Amount, fee_rate: FeeRate, ) -> Result { ensure!( @@ -216,10 +218,6 @@ impl Send { "sending runes with `ord send` requires index created with `--index-runes` flag", ); - let inscriptions = wallet.inscriptions(); - let runic_outputs = wallet.get_runic_outputs()?; - let bitcoin_client = wallet.bitcoin_client(); - wallet.lock_non_cardinal_outputs()?; let (id, entry, _parent) = wallet @@ -228,37 +226,64 @@ impl Send { let amount = decimal.to_integer(entry.divisibility)?; - let inscribed_outputs = inscriptions + let inscribed_outputs = wallet + .inscriptions() .keys() .map(|satpoint| satpoint.outpoint) .collect::>(); - let mut input_runes = 0; - let mut input = Vec::new(); + let balances = wallet + .get_runic_outputs()? + .into_iter() + .filter(|output| !inscribed_outputs.contains(output)) + .map(|output| { + wallet.get_runes_balances_in_output(&output).map(|balance| { + ( + output, + balance + .into_iter() + .map(|(spaced_rune, pile)| (spaced_rune.rune, pile)) + .collect(), + ) + }) + }) + .collect::>>>()?; - for output in runic_outputs { - if inscribed_outputs.contains(&output) { - continue; - } + let mut inputs = Vec::new(); + let mut input_rune_balances: BTreeMap = BTreeMap::new(); - let balance = wallet.get_rune_balance_in_output(&output, entry.spaced_rune.rune)?; + 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 { - input_runes += balance; - input.push(output); + inputs.push(output); + } } - if input_runes >= amount { + if input_rune_balances + .get(&spaced_rune.rune) + .cloned() + .unwrap_or_default() + >= amount + { break; } } + let input_rune_balance = input_rune_balances + .get(&spaced_rune.rune) + .cloned() + .unwrap_or_default(); + + let needs_runes_change_output = input_rune_balance > amount || input_rune_balances.len() > 1; + ensure! { - input_runes >= amount, + input_rune_balance >= amount, "insufficient `{}` balance, only {} in wallet", spaced_rune, Pile { - amount: input_runes, + amount: input_rune_balance, divisibility: entry.divisibility, symbol: entry.symbol }, @@ -276,7 +301,7 @@ impl Send { let unfunded_transaction = Transaction { version: 2, lock_time: LockTime::ZERO, - input: input + input: inputs .into_iter() .map(|previous_output| TxIn { previous_output, @@ -285,31 +310,40 @@ impl Send { witness: Witness::new(), }) .collect(), - output: vec![ - TxOut { - script_pubkey: runestone.encipher(), - value: 0, - }, - TxOut { - script_pubkey: wallet.get_change_address()?.script_pubkey(), - value: TARGET_POSTAGE.to_sat(), - }, - TxOut { + output: if needs_runes_change_output { + vec![ + TxOut { + script_pubkey: runestone.encipher(), + value: 0, + }, + TxOut { + script_pubkey: wallet.get_change_address()?.script_pubkey(), + value: postage.to_sat(), + }, + TxOut { + script_pubkey: destination.script_pubkey(), + value: postage.to_sat(), + }, + ] + } else { + vec![TxOut { script_pubkey: destination.script_pubkey(), - value: TARGET_POSTAGE.to_sat(), - }, - ], + value: postage.to_sat(), + }] + }, }; let unsigned_transaction = - fund_raw_transaction(bitcoin_client, fee_rate, &unfunded_transaction)?; + fund_raw_transaction(wallet.bitcoin_client(), fee_rate, &unfunded_transaction)?; let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?; - assert_eq!( - Runestone::decipher(&unsigned_transaction), - Some(Artifact::Runestone(runestone)), - ); + if needs_runes_change_output { + assert_eq!( + Runestone::decipher(&unsigned_transaction), + Some(Artifact::Runestone(runestone)), + ); + } Ok(unsigned_transaction) } diff --git a/src/templates/output.rs b/src/templates/output.rs index ef13b6e2b8..b68e702733 100644 --- a/src/templates/output.rs +++ b/src/templates/output.rs @@ -6,7 +6,7 @@ pub(crate) struct OutputHtml { pub(crate) inscriptions: Vec, pub(crate) outpoint: OutPoint, pub(crate) output: TxOut, - pub(crate) runes: Vec<(SpacedRune, Pile)>, + pub(crate) runes: BTreeMap, pub(crate) sat_ranges: Option>, pub(crate) spent: bool, } @@ -32,7 +32,7 @@ mod tests { inscriptions: Vec::new(), outpoint: outpoint(1), output: TxOut { value: 3, script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: Some(vec![(0, 1), (1, 3)]), spent: false, }, @@ -66,7 +66,7 @@ mod tests { value: 1, script_pubkey: script::Builder::new().push_int(0).into_script(), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: None, spent: true, }, @@ -91,7 +91,7 @@ mod tests { inscriptions: Vec::new(), outpoint: outpoint(1), output: TxOut { value: 3, script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: Some(vec![(0, 1), (1, 3)]), spent: true, }, @@ -122,7 +122,7 @@ mod tests { inscriptions: Vec::new(), outpoint: outpoint(1), output: TxOut { value: 3, script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: None, spent: false, } @@ -152,7 +152,7 @@ mod tests { value: 3, script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: None, spent: false, }, @@ -191,7 +191,9 @@ mod tests { divisibility: 1, symbol: None, } - )], + )] + .into_iter() + .collect(), sat_ranges: None, spent: false, }, diff --git a/src/wallet.rs b/src/wallet.rs index c75767e6d8..7633d17587 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -217,10 +217,10 @@ impl Wallet { Ok(runic_outputs) } - pub(crate) fn get_runes_balances_for_output( + pub(crate) fn get_runes_balances_in_output( &self, output: &OutPoint, - ) -> Result> { + ) -> Result> { Ok( self .output_info @@ -231,22 +231,6 @@ impl Wallet { ) } - pub(crate) fn get_rune_balance_in_output(&self, output: &OutPoint, rune: Rune) -> Result { - Ok( - self - .get_runes_balances_for_output(output)? - .iter() - .map(|(spaced_rune, pile)| { - if spaced_rune.rune == rune { - pile.amount - } else { - 0 - } - }) - .sum(), - ) - } - pub(crate) fn get_rune( &self, rune: Rune, diff --git a/tests/json_api.rs b/tests/json_api.rs index cd42c19d6a..2b0df4628d 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -343,7 +343,7 @@ fn get_output() { InscriptionId { txid, index: 2 }, ], indexed: true, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: Some(vec![ (5000000000, 10000000000,), (10000000000, 15000000000,), diff --git a/tests/wallet/selection.rs b/tests/wallet/selection.rs index 23de048661..4b16e15204 100644 --- a/tests/wallet/selection.rs +++ b/tests/wallet/selection.rs @@ -198,6 +198,7 @@ fn sending_rune_does_not_send_inscription() { --chain regtest --index-runes wallet send + --postage 11111sat --fee-rate 0 bcrt1pyrmadgg78e38ewfv0an8c6eppk2fttv5vnuvz04yza60qau5va0saknu8k 1000:{rune} diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 45b76caab2..6779da1ec9 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -362,7 +362,7 @@ inscriptions: }, ], indexed: true, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: Some(vec![(5_000_000_000, 5_000_030_000)]), script_pubkey: destination.payload.script_pubkey().to_asm_string(), spent: false, @@ -769,7 +769,7 @@ fn sending_rune_works() { .ord(&ord) .run_and_deserialize_output::(); - assert_eq!( + pretty_assert_eq!( balances, ord::subcommand::balances::Output { runes: vec![( @@ -777,7 +777,7 @@ fn sending_rune_works() { vec![( OutPoint { txid: output.txid, - vout: 2 + vout: 0 }, Pile { amount: 1000, @@ -795,7 +795,75 @@ fn sending_rune_works() { } #[test] -fn sending_spaced_rune_works() { +fn sending_rune_with_change_works() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); + + create_wallet(&core, &ord); + + etch(&core, &ord, Rune(RUNE)); + + let output = CommandBuilder::new(format!( + "--chain regtest --index-runes wallet send --postage 1234sat --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 777:{}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let tx = core.tx_by_id(output.txid); + + assert_eq!(tx.output[1].value, 1234); + assert_eq!(tx.output[2].value, 1234); + + 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: vec![( + SpacedRune::new(Rune(RUNE), 0), + vec![ + ( + OutPoint { + txid: output.txid, + vout: 1 + }, + Pile { + amount: 223, + divisibility: 0, + symbol: Some('¢') + }, + ), + ( + OutPoint { + txid: output.txid, + vout: 2 + }, + Pile { + amount: 777, + divisibility: 0, + symbol: Some('¢') + }, + ) + ] + .into_iter() + .collect() + )] + .into_iter() + .collect(), + } + ); +} + +#[test] +fn sending_spaced_rune_works_with_no_change() { let core = mockcore::builder().network(Network::Regtest).build(); let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); @@ -808,11 +876,15 @@ fn sending_spaced_rune_works() { "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1000:A•AAAAAAAAAAAA", ) .core(&core) - .ord(&ord) + .ord(&ord) .run_and_deserialize_output::(); core.mine_blocks(1); + let tx = core.tx_by_id(output.txid); + + assert_eq!(tx.output.len(), 1); + let balances = CommandBuilder::new("--regtest --index-runes balances") .core(&core) .ord(&ord) @@ -826,7 +898,7 @@ fn sending_spaced_rune_works() { vec![( OutPoint { txid: output.txid, - vout: 2 + vout: 0 }, Pile { amount: 1000,