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,