Repository: ordinals/ord (rust)

Issue:
"wallet resume" should wait for the commit with the most confirmations first
`ord wallet resume` waits for the etchings in alphabetical order. If you have pending etchings for RUNE.A and RUNE.B, and the commit for RUNE A is unconfirmed while the commit for RUNE.B has 6 confirmations, `wallet resume` will wait for the RUNE.A commit to have 6 confirmations before sending the reveal for RUNE.B.

It would be better if it waited for all of them independently,  because if we have 2 etchings with 0 confirmations we don't know which one will get to 6 confirmations first.

Pull request:
Refine resume
Fixes #3478 

Patch:
diff --git a/src/subcommand/wallet/resume.rs b/src/subcommand/wallet/resume.rs
index d844591b..3f235d04 100644
--- a/src/subcommand/wallet/resume.rs
+++ b/src/subcommand/wallet/resume.rs
@@ -6,13 +6,26 @@ pub struct ResumeOutput {
 }
 
 pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
-  let outputs: Result<Vec<batch::Output>> = wallet
-    .pending_etchings()?
-    .into_iter()
-    .map(|(rune, entry)| {
-      wallet.wait_for_maturation(&rune, entry.commit, entry.reveal, entry.output)
-    })
-    .collect();
+  let mut etchings = Vec::new();
+  loop {
+    if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
+      break;
+    }
 
-  outputs.map(|etchings| Some(Box::new(ResumeOutput { etchings }) as Box<dyn Output>))
+    for (rune, entry) in wallet.pending_etchings()? {
+      if wallet.is_mature(&entry.commit)? {
+        etchings.push(wallet.send_etching(rune, &entry)?);
+      }
+    }
+
+    if wallet.pending_etchings()?.is_empty() {
+      break;
+    }
+
+    if !wallet.integration_test() {
+      thread::sleep(Duration::from_secs(5));
+    }
+  }
+
+  Ok(Some(Box::new(ResumeOutput { etchings }) as Box<dyn Output>))
 }
diff --git a/src/wallet.rs b/src/wallet.rs
index 121f6089..9b066d68 100644
--- a/src/wallet.rs
+++ b/src/wallet.rs
@@ -296,44 +296,52 @@ impl Wallet {
     self.settings.integration_test()
   }
 
-  pub(crate) fn wait_for_maturation(
-    &self,
-    rune: &Rune,
-    commit: Transaction,
-    reveal: Transaction,
-    output: batch::Output,
-  ) -> Result<batch::Output> {
-    eprintln!("Waiting for rune commitment {} to mature…", commit.txid());
+  pub(crate) fn is_mature(&self, commit: &Transaction) -> Result<bool> {
+    let transaction = self
+      .bitcoin_client()
+      .get_transaction(&commit.txid(), Some(true))
+      .into_option()?;
+
+    if let Some(transaction) = transaction {
+      if u32::try_from(transaction.info.confirmations).unwrap() + 1
+        >= Runestone::COMMIT_CONFIRMATIONS.into()
+      {
+        let tx_out = self
+          .bitcoin_client()
+          .get_tx_out(&commit.txid(), 0, Some(true))?;
+
+        if let Some(tx_out) = tx_out {
+          if tx_out.confirmations + 1 >= Runestone::COMMIT_CONFIRMATIONS.into() {
+            return Ok(true);
+          }
+        } else {
+          bail!("rune commitment spent, can't send reveal tx");
+        }
+      }
+    }
 
-    self.save_etching(rune, &commit, &reveal, output.clone())?;
+    Ok(false)
+  }
+
+  pub(crate) fn wait_for_maturation(&self, rune: Rune) -> Result<batch::Output> {
+    let Some(entry) = self.load_etching(rune)? else {
+      bail!("no etching found");
+    };
+
+    eprintln!(
+      "Waiting for rune {} commitment {} to mature…",
+      rune,
+      entry.commit.txid()
+    );
 
     loop {
       if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
         eprintln!("Suspending batch. Run `ord wallet resume` to continue.");
-        return Ok(output);
+        return Ok(entry.output);
       }
 
-      let transaction = self
-        .bitcoin_client()
-        .get_transaction(&commit.txid(), Some(true))
-        .into_option()?;
-
-      if let Some(transaction) = transaction {
-        if u32::try_from(transaction.info.confirmations).unwrap() + 1
-          >= Runestone::COMMIT_CONFIRMATIONS.into()
-        {
-          let tx_out = self
-            .bitcoin_client()
-            .get_tx_out(&commit.txid(), 0, Some(true))?;
-
-          if let Some(tx_out) = tx_out {
-            if tx_out.confirmations + 1 >= Runestone::COMMIT_CONFIRMATIONS.into() {
-              break;
-            }
-          } else {
-            bail!("rune commitment spent, can't send reveal tx");
-          }
-        }
+      if self.is_mature(&entry.commit)? {
+        break;
       }
 
       if !self.integration_test() {
@@ -341,12 +349,16 @@ impl Wallet {
       }
     }
 
-    match self.bitcoin_client().send_raw_transaction(&reveal) {
+    self.send_etching(rune, &entry)
+  }
+
+  pub(crate) fn send_etching(&self, rune: Rune, entry: &EtchingEntry) -> Result<batch::Output> {
+    match self.bitcoin_client().send_raw_transaction(&entry.reveal) {
       Ok(txid) => txid,
       Err(err) => {
         return Err(anyhow!(
           "Failed to send reveal transaction: {err}\nCommit tx {} will be recovered once mined",
-          commit.txid()
+          entry.commit.txid()
         ))
       }
     };
@@ -355,7 +367,7 @@ impl Wallet {
 
     Ok(batch::Output {
       reveal_broadcast: true,
-      ..output
+      ..entry.output.clone()
     })
   }
 
@@ -646,7 +658,7 @@ impl Wallet {
     )
   }
 
-  pub(crate) fn clear_etching(&self, rune: &Rune) -> Result {
+  pub(crate) fn clear_etching(&self, rune: Rune) -> Result {
     let wtx = self.database.begin_write()?;
 
     wtx.open_table(RUNE_TO_ETCHING)?.remove(rune.0)?;
diff --git a/src/wallet/batch/plan.rs b/src/wallet/batch/plan.rs
index 26244fe3..704edc1c 100644
--- a/src/wallet/batch/plan.rs
+++ b/src/wallet/batch/plan.rs
@@ -137,10 +137,10 @@ impl Plan {
       let commit = consensus::encode::deserialize::<Transaction>(&signed_commit_tx)?;
       let reveal = consensus::encode::deserialize::<Transaction>(&signed_reveal_tx)?;
 
-      Ok(Some(Box::new(wallet.wait_for_maturation(
+      wallet.save_etching(
         &rune_info.rune.rune,
-        commit.clone(),
-        reveal.clone(),
+        &commit,
+        &reveal,
         self.output(
           commit.txid(),
           None,
@@ -151,7 +151,11 @@ impl Plan {
           self.inscriptions.clone(),
           rune.clone(),
         ),
-      )?)))
+      )?;
+
+      Ok(Some(Box::new(
+        wallet.wait_for_maturation(rune_info.rune.rune)?,
+      )))
     } else {
       let reveal = match wallet
         .bitcoin_client()
diff --git a/src/wallet/wallet_constructor.rs b/src/wallet/wallet_constructor.rs
index 1db7ee5c..ffa43324 100644
--- a/src/wallet/wallet_constructor.rs
+++ b/src/wallet/wallet_constructor.rs
@@ -190,14 +190,18 @@ impl WalletConstructor {
     let mut utxos = BTreeMap::new();
 
     for outpoint in outpoints {
-      let txout = bitcoin_client
-        .get_raw_transaction(&outpoint.txid, None)?
-        .output
-        .get(TryInto::<usize>::try_into(outpoint.vout).unwrap())
-        .cloned()
-        .ok_or_else(|| anyhow!("Invalid output index"))?;
-
-      utxos.insert(OutPoint::new(outpoint.txid, outpoint.vout), txout);
+      let Some(tx_out) = bitcoin_client.get_tx_out(&outpoint.txid, outpoint.vout, Some(false))?
+      else {
+        continue;
+      };
+
+      utxos.insert(
+        OutPoint::new(outpoint.txid, outpoint.vout),
+        TxOut {
+          value: tx_out.value.to_sat(),
+          script_pubkey: ScriptBuf::from_bytes(tx_out.script_pub_key.hex),
+        },
+      );
     }
 
     Ok(utxos)


Test patch:
diff --git a/tests/lib.rs b/tests/lib.rs
index ffde8862..99727d9b 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -200,7 +200,7 @@ fn batch(core: &mockcore::Handle, ord: &TestServer, batchfile: batch::File) -> E
 
   assert_regex_match!(
     buffer,
-    "Waiting for rune commitment [[:xdigit:]]{64} to mature…\n"
+    "Waiting for rune .* commitment [[:xdigit:]]{64} to mature…\n"
   );
 
   core.mine_blocks(5);
diff --git a/tests/wallet/batch_command.rs b/tests/wallet/batch_command.rs
index 2197eec2..3a91f9b4 100644
--- a/tests/wallet/batch_command.rs
+++ b/tests/wallet/batch_command.rs
@@ -2664,7 +2664,7 @@ fn batch_inscribe_errors_if_pending_etchings() {
 
     assert_regex_match!(
       buffer,
-      "Waiting for rune commitment [[:xdigit:]]{64} to mature…\n"
+      "Waiting for rune .* commitment [[:xdigit:]]{64} to mature…\n"
     );
 
     core.mine_blocks(1);
diff --git a/tests/wallet/resume.rs b/tests/wallet/resume.rs
index 54299aac..3658b789 100644
--- a/tests/wallet/resume.rs
+++ b/tests/wallet/resume.rs
@@ -56,7 +56,7 @@ fn wallet_resume() {
 
     assert_regex_match!(
       buffer,
-      "Waiting for rune commitment [[:xdigit:]]{64} to mature…\n"
+      "Waiting for rune AAAAAAAAAAAAA commitment [[:xdigit:]]{64} to mature…\n"
     );
 
     core.mine_blocks(1);
@@ -83,23 +83,11 @@ fn wallet_resume() {
 
   core.mine_blocks(6);
 
-  let mut spawn = CommandBuilder::new("--regtest --index-runes wallet resume")
+  let output = CommandBuilder::new("--regtest --index-runes wallet resume")
     .temp_dir(tempdir)
     .core(&core)
     .ord(&ord)
-    .spawn();
-
-  let mut buffer = String::new();
-  let mut reader = BufReader::new(spawn.child.stderr.as_mut().unwrap());
-
-  reader.read_line(&mut buffer).unwrap();
-
-  assert_regex_match!(
-    buffer,
-    "Waiting for rune commitment [[:xdigit:]]{64} to mature…\n"
-  );
-
-  let output = spawn.run_and_deserialize_output::<ord::subcommand::wallet::resume::ResumeOutput>();
+    .run_and_deserialize_output::<ord::subcommand::wallet::resume::ResumeOutput>();
 
   assert_eq!(
     output
@@ -167,7 +155,7 @@ fn resume_suspended() {
 
     assert_regex_match!(
       buffer,
-      "Waiting for rune commitment [[:xdigit:]]{64} to mature…\n"
+      "Waiting for rune AAAAAAAAAAAAA commitment [[:xdigit:]]{64} to mature…\n"
     );
 
     core.mine_blocks(1);
@@ -198,15 +186,7 @@ fn resume_suspended() {
     .ord(&ord)
     .spawn();
 
-  let mut buffer = String::new();
-
-  BufReader::new(spawn.child.stderr.as_mut().unwrap())
-    .read_line(&mut buffer)
-    .unwrap();
-
-  assert_regex_match!(buffer, "Waiting for rune commitment .* to mature…\n");
-
-  buffer.clear();
+  thread::sleep(Duration::from_secs(1));
 
   signal::kill(
     Pid::from_raw(spawn.child.id().try_into().unwrap()),
@@ -223,17 +203,7 @@ fn resume_suspended() {
     "Shutting down gracefully. Press <CTRL-C> again to shutdown immediately.\n"
   );
 
-  buffer.clear();
-  reader.read_line(&mut buffer).unwrap();
-
-  assert_eq!(
-    buffer,
-    "Suspending batch. Run `ord wallet resume` to continue.\n"
-  );
-
-  let output = spawn.run_and_deserialize_output::<ord::subcommand::wallet::resume::ResumeOutput>();
-
-  assert!(!output.etchings.first().unwrap().reveal_broadcast);
+  spawn.child.wait().unwrap();
 }
 
 #[test]
@@ -284,7 +254,7 @@ fn commitment_output_is_locked() {
 
   assert_regex_match!(
     buffer,
-    "Waiting for rune commitment [[:xdigit:]]{64} to mature…\n"
+    "Waiting for rune AAAAAAAAAAAAA commitment [[:xdigit:]]{64} to mature…\n"
   );
 
   let commitment = core.mempool()[0].txid();