Skip to content

Commit f8de846

Browse files
committed
Wallet bid replacement and bidout selection clean up
1 parent c305d1e commit f8de846

File tree

5 files changed

+142
-74
lines changed

5 files changed

+142
-74
lines changed

node/src/bin/space-cli.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ impl SpaceCli {
262262
async fn send_request(
263263
&self,
264264
req: Option<RpcWalletRequest>,
265-
auction_outputs: Option<u8>,
265+
bidouts: Option<u8>,
266266
fee_rate: Option<u64>,
267267
) -> Result<(), ClientError> {
268268
let fee_rate = fee_rate.map(|fee| FeeRate::from_sat_per_vb(fee).unwrap());
@@ -271,7 +271,7 @@ impl SpaceCli {
271271
.wallet_send_request(
272272
&self.wallet,
273273
RpcWalletTxBuilder {
274-
auction_outputs,
274+
bidouts,
275275
requests: match req {
276276
None => vec![],
277277
Some(req) => vec![req],
@@ -533,7 +533,7 @@ async fn handle_commands(
533533
println!("{}", serde_json::to_string_pretty(&spaces)?);
534534
}
535535
Commands::ListBidOuts => {
536-
let spaces = cli.client.wallet_list_auction_outputs(&cli.wallet).await?;
536+
let spaces = cli.client.wallet_list_bidouts(&cli.wallet).await?;
537537
println!("{}", serde_json::to_string_pretty(&spaces)?);
538538
}
539539
Commands::ListSpaces => {

node/src/rpc.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ pub trait Rpc {
186186
wallet: &str,
187187
) -> Result<Vec<WalletOutput>, ErrorObjectOwned>;
188188

189-
#[method(name = "walletlistauctionoutputs")]
190-
async fn wallet_list_auction_outputs(
189+
#[method(name = "walletlistbidouts")]
190+
async fn wallet_list_bidouts(
191191
&self,
192192
wallet: &str,
193193
) -> Result<Vec<DoubleUtxo>, ErrorObjectOwned>;
@@ -199,7 +199,7 @@ pub trait Rpc {
199199
#[derive(Clone, Serialize, Deserialize)]
200200
pub struct RpcWalletTxBuilder {
201201
#[serde(skip_serializing_if = "Option::is_none")]
202-
pub auction_outputs: Option<u8>,
202+
pub bidouts: Option<u8>,
203203
pub requests: Vec<RpcWalletRequest>,
204204
pub fee_rate: Option<FeeRate>,
205205
pub dust: Option<Amount>,
@@ -764,13 +764,13 @@ impl RpcServer for RpcServerImpl {
764764
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
765765
}
766766

767-
async fn wallet_list_auction_outputs(
767+
async fn wallet_list_bidouts(
768768
&self,
769769
wallet: &str,
770770
) -> Result<Vec<DoubleUtxo>, ErrorObjectOwned> {
771771
self.wallet(&wallet)
772772
.await?
773-
.send_list_auction_outputs()
773+
.send_list_bidouts()
774774
.await
775775
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
776776
}

node/src/wallets.rs

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
use std::{collections::BTreeMap, str::FromStr, time::Duration};
2-
32
use anyhow::anyhow;
43
use clap::ValueEnum;
54
use futures::{stream::FuturesUnordered, StreamExt};
65
use log::{info, warn};
7-
use protocol::{
8-
bitcoin::Txid,
9-
constants::ChainAnchor,
10-
hasher::{KeyHasher, SpaceKey},
11-
prepare::DataSource,
12-
script::SpaceScript,
13-
slabel::SLabel,
14-
Space,
15-
};
6+
use protocol::{bitcoin::Txid, constants::ChainAnchor, hasher::{KeyHasher, SpaceKey}, prepare::DataSource, script::SpaceScript, slabel::SLabel, FullSpaceOut, Space};
167
use serde::{Deserialize, Serialize};
178
use serde_json::json;
189
use tokio::{
@@ -34,6 +25,7 @@ use wallet::{
3425
DoubleUtxo, SpacesWallet, WalletInfo,
3526
};
3627
use wallet::bdk_wallet::wallet::tx_builder::TxOrdering;
28+
use wallet::builder::SelectionOutput;
3729
use crate::{
3830
config::ExtendedNetwork,
3931
node::BlockSource,
@@ -46,17 +38,18 @@ use crate::{
4638

4739
#[derive(Debug, Clone, Serialize, Deserialize)]
4840
pub struct TxResponse {
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
pub error: Option<BTreeMap<String, String>>,
4943
pub txid: Txid,
5044
pub tags: Vec<TransactionTag>,
5145
#[serde(skip_serializing_if = "Option::is_none")]
52-
pub error: Option<BTreeMap<String, String>>,
46+
pub raw: Option<String>,
5347
}
5448

49+
5550
#[derive(Debug, Clone, Serialize, Deserialize)]
5651
pub struct WalletResponse {
57-
pub sent: Vec<TxResponse>,
58-
#[serde(skip_serializing_if = "Option::is_none")]
59-
pub raw: Option<Vec<String>>,
52+
pub result: Vec<TxResponse>,
6053
}
6154

6255
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -87,7 +80,7 @@ pub enum WalletCommand {
8780
ListSpaces {
8881
resp: crate::rpc::Responder<anyhow::Result<Vec<WalletOutput>>>,
8982
},
90-
ListAuctionOutputs {
83+
ListBidouts {
9184
resp: crate::rpc::Responder<anyhow::Result<Vec<DoubleUtxo>>>,
9285
},
9386
ListUnspent {
@@ -182,7 +175,11 @@ impl RpcWallet {
182175
txid: Txid,
183176
fee_rate: FeeRate,
184177
) -> anyhow::Result<Vec<TxResponse>> {
185-
let coin_selection = Self::get_spaces_coin_selection(wallet, state)?;
178+
let coin_selection = Self::get_spaces_coin_selection(
179+
wallet,
180+
state,
181+
false /* generally bdk won't use unconfirmed for replacements anyways */
182+
)?;
186183
let mut builder = wallet.spaces
187184
.build_fee_bump(txid)?
188185
.coin_selection(coin_selection);
@@ -204,6 +201,7 @@ impl RpcWallet {
204201
txid: new_txid,
205202
tags: vec![TransactionTag::FeeBump],
206203
error: None,
204+
raw: None,
207205
}])
208206
}
209207

@@ -214,7 +212,8 @@ impl RpcWallet {
214212
output: OutPoint,
215213
fee_rate: FeeRate,
216214
) -> anyhow::Result<TxResponse> {
217-
let coin_selection = Self::get_spaces_coin_selection(wallet, state)?;
215+
let coin_selection =
216+
Self::get_spaces_coin_selection(wallet, state, true)?;
218217
let addre = wallet.spaces.next_unused_address(KeychainKind::External);
219218
let mut builder = wallet
220219
.spaces
@@ -239,6 +238,7 @@ impl RpcWallet {
239238
txid,
240239
tags: vec![TransactionTag::ForceSpendTestOnly],
241240
error: None,
241+
raw: None,
242242
})
243243
}
244244

@@ -299,8 +299,10 @@ impl RpcWallet {
299299
}
300300
}
301301
}
302-
WalletCommand::ListAuctionOutputs { resp } => {
303-
let result = wallet.list_auction_outputs();
302+
WalletCommand::ListBidouts { resp } => {
303+
let sel =
304+
Self::get_spaces_coin_selection(wallet, state, false)?;
305+
let result = wallet.list_bidouts(&sel);
304306
_ = resp.send(result);
305307
}
306308
WalletCommand::GetBalance { resp } => {
@@ -422,6 +424,7 @@ impl RpcWallet {
422424
fn get_spaces_coin_selection(
423425
wallet: &mut SpacesWallet,
424426
state: &mut LiveSnapshot,
427+
confirmed_only: bool,
425428
) -> anyhow::Result<SpacesAwareCoinSelection> {
426429
// Filters out all "space outs" from the selection.
427430
// Note: This exclusion only applies to confirmed space outs; unconfirmed ones are not excluded.
@@ -430,10 +433,14 @@ impl RpcWallet {
430433
let excluded = Self::list_unspent(wallet, state)?
431434
.into_iter()
432435
.filter(|out| out.is_spaceout)
433-
.map(|out| out.output.outpoint)
436+
.map(|out| SelectionOutput {
437+
outpoint: out.output.outpoint,
438+
is_space: out.space.is_some(),
439+
is_spaceout: true,
440+
})
434441
.collect::<Vec<_>>();
435442

436-
Ok(SpacesAwareCoinSelection::new(excluded))
443+
Ok(SpacesAwareCoinSelection::new(excluded, confirmed_only))
437444
}
438445

439446
fn list_unspent(
@@ -496,6 +503,17 @@ impl RpcWallet {
496503
)?))
497504
}
498505

506+
fn replaces_unconfirmed_bid(wallet: &SpacesWallet, bid_spaceout: &FullSpaceOut) -> bool {
507+
let outpoint = bid_spaceout.outpoint();
508+
wallet.spaces.transactions().filter(
509+
|tx|
510+
!tx.chain_position.is_confirmed()
511+
).any(
512+
|tx|
513+
tx.tx_node.input.iter().any(|input| input.previous_output == outpoint)
514+
)
515+
}
516+
499517
fn batch_tx(
500518
network: ExtendedNetwork,
501519
source: &BitcoinBlockSource,
@@ -526,11 +544,13 @@ impl RpcWallet {
526544
let mut builder = wallet::builder::Builder::new();
527545
builder = builder.fee_rate(fee_rate);
528546

529-
if tx.auction_outputs.is_some() {
530-
builder = builder.auction_outputs(tx.auction_outputs.unwrap());
547+
if tx.bidouts.is_some() {
548+
builder = builder.bidouts(tx.bidouts.unwrap());
531549
}
532550
builder = builder.force(tx.force);
533551

552+
let mut bid_replacement = false;
553+
534554
for req in tx.requests {
535555
match req {
536556
RpcWalletRequest::SendCoins(params) => {
@@ -603,7 +623,13 @@ impl RpcWallet {
603623
if spaceout.is_none() {
604624
return Err(anyhow!("bid '{}': space does not exist", params.name));
605625
}
606-
builder = builder.add_bid(spaceout.unwrap(), Amount::from_sat(params.amount));
626+
627+
let spaceout = spaceout.unwrap();
628+
if Self::replaces_unconfirmed_bid(wallet, &spaceout) {
629+
bid_replacement = true;
630+
}
631+
632+
builder = builder.add_bid(spaceout, Amount::from_sat(params.amount));
607633
}
608634
RpcWalletRequest::Register(params) => {
609635
let name = SLabel::from_str(&params.name)?;
@@ -686,13 +712,11 @@ impl RpcWallet {
686712
}
687713

688714
let median_time = source.get_median_time()?;
689-
let coin_selection = Self::get_spaces_coin_selection(wallet, store)?;
715+
let coin_selection = Self::get_spaces_coin_selection(wallet, store, bid_replacement)?;
690716

691717
let mut tx_iter = builder.build_iter(tx.dust, median_time, wallet, coin_selection)?;
692718

693719
let mut result_set = Vec::new();
694-
let mut raw_set = Vec::new();
695-
let mut has_errors = false;
696720
while let Some(tx_result) = tx_iter.next() {
697721
let tagged = tx_result?;
698722

@@ -701,27 +725,27 @@ impl RpcWallet {
701725
txid: tagged.tx.compute_txid(),
702726
tags: tagged.tags,
703727
error: None,
728+
raw: None,
704729
});
705730

706731
let raw = bitcoin::consensus::encode::serialize_hex(&tagged.tx);
707-
raw_set.push(raw);
708732
let result = source.rpc.broadcast_tx(&source.client, &tagged.tx);
709733
match result {
710734
Ok(confirmation) => {
711735
tx_iter.wallet.insert_tx(tagged.tx, confirmation)?;
712736
tx_iter.wallet.commit()?;
713737
}
714738
Err(e) => {
715-
has_errors = true;
739+
result_set.last_mut().unwrap().raw = Some(raw);
740+
716741
let mut error_data = BTreeMap::new();
717742
if let BitcoinRpcError::Rpc(rpc) = e {
718743
if is_bid {
719744
if rpc.message.contains("replacement-adds-unconfirmed") {
720745
error_data.insert(
721746
"hint".to_string(),
722-
"If you have don't have confirmed auction outputs, you cannot \
723-
replace bids in the mempool."
724-
.to_string(),
747+
"Competing bid in mempool but wallet has no confirmed bid \
748+
outputs (required to replace mempool bids)".to_string(),
725749
);
726750
}
727751

@@ -750,8 +774,7 @@ impl RpcWallet {
750774
}
751775

752776
Ok(WalletResponse {
753-
sent: result_set,
754-
raw: if has_errors { Some(raw_set) } else { None },
777+
result: result_set,
755778
})
756779
}
757780

@@ -876,10 +899,10 @@ impl RpcWallet {
876899
resp_rx.await?
877900
}
878901

879-
pub async fn send_list_auction_outputs(&self) -> anyhow::Result<Vec<DoubleUtxo>> {
902+
pub async fn send_list_bidouts(&self) -> anyhow::Result<Vec<DoubleUtxo>> {
880903
let (resp, resp_rx) = oneshot::channel();
881904
self.sender
882-
.send(WalletCommand::ListAuctionOutputs { resp })
905+
.send(WalletCommand::ListBidouts { resp })
883906
.await?;
884907
resp_rx.await?
885908
}

0 commit comments

Comments
 (0)