Repository: ordinals/ord (rust)

Issue:
A useless postage was added to the sending runes tx?
Query Balance:

PS D:\Projects\bitcoin\ord> ordtest wallet balance
` 
{
  "cardinal": 46553843,
  "ordinal": 20000,
  "runes": {
    "xxxxxxx": "1000000",
    "MEME•OF•RUNES": "24",
    "xxxxxxx": "381000000",
    "xxxxxxx": "20000"
  },
  "runic": 1450000,
  "total": 48023843
}
`

send runes: 
PS D:\Projects\bitcoin\ord> ordtest wallet send --fee-rate 100 tb1pt0v7eye37l38e50lnywfu6atj2ec6dzteduqa4xasqnn6ndckcuqx83fsn 24:MEME•OF•RUNES
`
{
  "txid": "07537f2643f790535e12a2211f78ec82e2cfef866b694ba6d653a2caecf592f9",
  "psbt": "xxxx",
  "outgoing": "24:MEME•OF•RUNES",
  "fee": 27600
}
`


The payload in OP_RETURN:
`
0099ee9d0110e01202
`

Dechiper result:
`
{"edicts":[{"id":{"block":"2586393","tx":"16"},"amount":"2400","output":"2"}]}
`


[Tx in mempool](https://mempool.space/zh/testnet/tx/07537f2643f790535e12a2211f78ec82e2cfef866b694ba6d653a2caecf592f9)

This transaction only requires one postage, why are there two postage outputs in the transaction?


Pull request description:
Fix send runes
Fixes #3620

Patch:
diff --git a/src/api.rs b/src/api.rs
index 33f566e8..0cd03d7e 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -126,7 +126,7 @@ pub struct Output {
   pub address: Option<Address<NetworkUnchecked>>,
   pub indexed: bool,
   pub inscriptions: Vec<InscriptionId>,
-  pub runes: Vec<(SpacedRune, Pile)>,
+  pub runes: BTreeMap<SpacedRune, Pile>,
   pub sat_ranges: Option<Vec<(u64, u64)>>,
   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<SpacedRune, Pile>,
     sat_ranges: Option<Vec<(u64, u64)>>,
     spent: bool,
   ) -> Self {
diff --git a/src/index.rs b/src/index.rs
index bb2569d6..b3e26e02 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<Vec<(SpacedRune, Pile)>> {
+  ) -> Result<BTreeMap<SpacedRune, Pile>> {
     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 d67e678b..954264a7 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 c19b829e..192904a0 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 7471b13f..ddfcc4df 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<Transaction> {
     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::<HashSet<OutPoint>>();
 
-    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::<Result<BTreeMap<OutPoint, BTreeMap<Rune, Pile>>>>()?;
 
-    for output in runic_outputs {
-      if inscribed_outputs.contains(&output) {
-        continue;
-      }
+    let mut inputs = Vec::new();
+    let mut input_rune_balances: BTreeMap<Rune, u128> = 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 ef13b6e2..b68e7027 100644
--- a/src/templates/output.rs
+++ b/src/templates/output.rs
@@ -6,7 +6,7 @@ pub(crate) struct OutputHtml {
   pub(crate) inscriptions: Vec<InscriptionId>,
   pub(crate) outpoint: OutPoint,
   pub(crate) output: TxOut,
-  pub(crate) runes: Vec<(SpacedRune, Pile)>,
+  pub(crate) runes: BTreeMap<SpacedRune, Pile>,
   pub(crate) sat_ranges: Option<Vec<(u64, u64)>>,
   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 c75767e6..7633d175 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<Vec<(SpacedRune, Pile)>> {
+  ) -> Result<BTreeMap<SpacedRune, Pile>> {
     Ok(
       self
         .output_info
@@ -231,22 +231,6 @@ impl Wallet {
     )
   }
 
-  pub(crate) fn get_rune_balance_in_output(&self, output: &OutPoint, rune: Rune) -> Result<u128> {
-    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,


Test patch:
diff --git a/tests/json_api.rs b/tests/json_api.rs
index cd42c19d..2b0df462 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 23de0486..4b16e152 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 45b76caa..6779da1e 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::<ord::subcommand::balances::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::<Send>();
+
+  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::<ord::subcommand::balances::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::<Send>();
 
   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,