Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 69 additions & 23 deletions app/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ fn update_wallet(node: &Node, wallet: &Wallet) -> Result<(), Error> {
.into_iter()
.map(|(outpoint, spent_output)| (outpoint, spent_output.inpoint))
.collect();
wallet.put_utxos(&utxos)?;
wallet.spend_utxos(&spent)?;
wallet.sync_confirmed(&utxos, &spent)?;
let confirmed: HashSet<OutPoint> =
wallet.get_utxos()?.into_keys().collect();
let (unconfirmed, mempool_spent) =
node.get_mempool_view(&addresses, &confirmed)?;
wallet.set_mempool_view(&unconfirmed, &mempool_spent)?;

tracing::debug!("finished wallet update");
Ok(())
Expand All @@ -88,14 +92,34 @@ fn update(
node: &Node,
utxos: &mut HashMap<OutPoint, Output>,
wallet: &Wallet,
spend_zero_conf_change: bool,
) -> Result<(), Error> {
tracing::trace!("Updating wallet");
let () = update_wallet(node, wallet)?;
*utxos = wallet.get_utxos()?;
*utxos = spendable_utxos(wallet, spend_zero_conf_change)?;
tracing::trace!("Updated wallet");
Ok(())
}

/// The coins the wallet may put in a new transaction. It matches what
/// `Wallet::select_coins` takes under the same option.
fn spendable_utxos(
wallet: &Wallet,
spend_zero_conf_change: bool,
) -> Result<HashMap<OutPoint, Output>, Error> {
let mut utxos = wallet.get_utxos()?;
let spent = wallet.get_mempool_spent_utxos()?;
if spend_zero_conf_change {
utxos.extend(wallet.get_unconfirmed_utxos()?);
}
// A withdrawal output belongs to a bundle, and the state refuses a
// transaction that spends one, so `select_coins` skips it too.
utxos.retain(|outpoint, output| {
!spent.contains(outpoint) && !output.content.is_withdrawal()
});
Ok(utxos)
}

struct ProtoSupport {
block_producer: bool,
miner: bool,
Expand All @@ -122,6 +146,7 @@ pub struct Config {
pub network: thunder::types::Network,
pub network_magic_override: Option<thunder::net::peer_message::MagicBytes>,
pub server_names: HashSet<String>,
pub spend_zero_conf_change: bool,
pub wallet_dir: PathBuf,
}

Expand All @@ -131,6 +156,7 @@ pub struct App {
pub wallet: Wallet,
pub miner: Option<Arc<TokioRwLock<Miner>>>,
pub utxos: Arc<RwLock<HashMap<OutPoint, Output>>>,
pub spend_zero_conf_change: bool,
task: Arc<JoinHandle<()>>,
pub transaction: Arc<RwLock<Transaction>>,
pub runtime: Arc<tokio::runtime::Runtime>,
Expand All @@ -142,10 +168,20 @@ impl App {
node: Arc<Node>,
utxos: Arc<RwLock<HashMap<OutPoint, Output>>>,
wallet: Wallet,
spend_zero_conf_change: bool,
) -> Result<(), Error> {
let mut state_changes = node.watch_state();
while let Some(()) = state_changes.next().await {
let () = update(&node, &mut utxos.write(), &wallet)?;
// The mempool signal matters as much as the tip: a payment that a peer
// sends appears without a block, and a coin that a transaction spends
// leaves the spendable set the moment the mempool takes it.
let mut changes =
futures::stream::select(node.watch_state(), node.watch_mempool());
while let Some(()) = changes.next().await {
let () = update(
&node,
&mut utxos.write(),
&wallet,
spend_zero_conf_change,
)?;
}
Ok(())
}
Expand All @@ -154,11 +190,15 @@ impl App {
node: Arc<Node>,
utxos: Arc<RwLock<HashMap<OutPoint, Output>>>,
wallet: Wallet,
spend_zero_conf_change: bool,
) -> JoinHandle<()> {
spawn(Self::task(node, utxos, wallet).unwrap_or_else(|err| {
let err = anyhow::Error::from(err);
tracing::error!("{err:#}")
}))
spawn(
Self::task(node, utxos, wallet, spend_zero_conf_change)
.unwrap_or_else(|err| {
let err = anyhow::Error::from(err);
tracing::error!("{err:#}")
}),
)
}

async fn check_status_serving(
Expand Down Expand Up @@ -336,26 +376,27 @@ impl App {
&mut rng,
&runtime,
)?;
let utxos = {
let mut utxos = wallet.get_utxos()?;
let transactions = node.get_all_transactions()?;
for transaction in &transactions {
for (outpoint, _) in &transaction.transaction.inputs {
utxos.remove(outpoint);
}
}
Arc::new(RwLock::new(utxos))
};
let node = Arc::new(node);
let spend_zero_conf_change = config.spend_zero_conf_change;
let () = update_wallet(&node, &wallet)?;
let utxos = Arc::new(RwLock::new(spendable_utxos(
&wallet,
spend_zero_conf_change,
)?));
let miner = miner.map(|miner| Arc::new(TokioRwLock::new(miner)));
let task =
Self::spawn_task(node.clone(), utxos.clone(), wallet.clone());
let task = Self::spawn_task(
node.clone(),
utxos.clone(),
wallet.clone(),
spend_zero_conf_change,
);
drop(rt_guard);
Ok(Self {
node,
wallet,
miner,
utxos,
spend_zero_conf_change,
task: Arc::new(task),
transaction: Arc::new(RwLock::new(Transaction {
inputs: vec![].into(),
Expand All @@ -369,7 +410,12 @@ impl App {

/// Update utxos & wallet
fn update(&self) -> Result<(), Error> {
update(self.node.as_ref(), &mut self.utxos.write(), &self.wallet)
update(
self.node.as_ref(),
&mut self.utxos.write(),
&self.wallet,
self.spend_zero_conf_change,
)
}

/// Regenerate proofs and submit transaction
Expand Down
10 changes: 10 additions & 0 deletions app/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub struct Config {
pub private_rpc_addr: SocketAddr,
pub rpc_addr: SocketAddr,
pub server_names: HashSet<String>,
pub spend_zero_conf_change: bool,
pub wallet_dir: PathBuf,
}

Expand All @@ -50,6 +51,7 @@ impl Config {
private_rpc_addr,
rpc_addr,
server_names,
spend_zero_conf_change,
wallet_dir,
} = self;
let add_peers = std::fmt::from_fn(|f| {
Expand Down Expand Up @@ -79,6 +81,7 @@ impl Config {
%private_rpc_addr,
%rpc_addr,
?server_names,
%spend_zero_conf_change,
wallet_dir = %wallet_dir.display(),
msg,
)
Expand Down Expand Up @@ -232,6 +235,12 @@ pub(super) struct Cli {
/// This option can be specified multiple times.
#[arg(long = "server-name")]
server_names: Vec<String>,
/// Spend the wallet's own unconfirmed change. The wallet takes an output
/// only when it funded every input of the transaction that made it, so an
/// unconfirmed payment from someone else waits for a block. This is the
/// rule that Bitcoin Core calls `-spendzeroconfchange`.
#[arg(default_value_t = true, long, action = clap::ArgAction::Set)]
spend_zero_conf_change: bool,
/// Data directory for storing wallet data
#[arg(long)]
wallet_dir: Option<PathBuf>,
Expand Down Expand Up @@ -276,6 +285,7 @@ impl Cli {
private_rpc_addr: self.private_rpc_addr,
rpc_addr: self.rpc_addr,
server_names: HashSet::from_iter(self.server_names),
spend_zero_conf_change: self.spend_zero_conf_change,
wallet_dir,
})
}
Expand Down
10 changes: 7 additions & 3 deletions app/gui/coins/transfer_receive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ fn create_transfer(
fee: bitcoin::Amount,
) -> anyhow::Result<()> {
let accumulator = app.node.get_tip_accumulator()?;
let tx = app
.wallet
.create_transaction(&accumulator, dest, amount, fee)?;
let tx = app.wallet.create_transaction(
&accumulator,
app.spend_zero_conf_change,
dest,
amount,
fee,
)?;
app.sign_and_send(tx)?;
Ok(())
}
Expand Down
7 changes: 7 additions & 0 deletions app/gui/coins/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ impl TxBuilder {
ui.monospace("outpoint");
ui.monospace("value");
ui.end_row();
// A mempool transaction may spend a chosen coin between two
// frames, which drops it from the spendable set. Drop it here, so
// the grid, the value and the transaction all agree.
self.base_tx
.inputs
.0
.retain(|(outpoint, _)| utxos_read.contains_key(outpoint));
let mut remove = None;
for (vout, (outpoint, _)) in self.base_tx.inputs.iter().enumerate()
{
Expand Down
34 changes: 28 additions & 6 deletions app/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::task::Poll;

use eframe::egui::{self, RichText};
use strum::{EnumIter, IntoEnumIterator};
use thunder::{util::Watchable, wallet::Wallet};
use thunder::{types::wallet::Balance, util::Watchable, wallet::Wallet};
use util::{BITCOIN_LOGO_FA, BITCOIN_ORANGE, show_btc_amount};

use crate::{app::App, line_buffer::LineBuffer, util::PromiseStream};
Expand Down Expand Up @@ -86,7 +86,7 @@ struct BottomPanel {
initialized: Option<BottomPanelInitialized>,
/// None if uninitialized
/// Some(None) if failed to initialize
balance: Option<Option<bitcoin::Amount>>,
balance: Option<Option<Balance>>,
}

impl BottomPanel {
Expand All @@ -107,8 +107,12 @@ impl BottomPanel {
let rt_guard = initialized.app.runtime.enter();
match initialized.wallet_updated.poll_next() {
Some(Poll::Ready(())) => {
self.balance = match initialized.app.wallet.get_balance() {
Ok(balance) => Some(Some(balance.total)),
self.balance = match initialized
.app
.wallet
.get_balance(initialized.app.spend_zero_conf_change)
{
Ok(balance) => Some(Some(balance)),
Err(err) => {
let err = anyhow::Error::from(err);
tracing::error!("Failed to update balance: {err:#}");
Expand All @@ -122,16 +126,34 @@ impl BottomPanel {
}

fn show_balance(&self, ui: &mut egui::Ui) {
match self.balance {
match &self.balance {
Some(Some(balance)) => {
ui.monospace(
RichText::new(BITCOIN_LOGO_FA.to_string())
.color(BITCOIN_ORANGE),
);
ui.monospace_selectable_singleline(
false,
format!("Balance: {}", show_btc_amount(balance)),
format!("Balance: {}", show_btc_amount(balance.total)),
);
if balance.unconfirmed != bitcoin::Amount::ZERO {
ui.monospace_selectable_singleline(
false,
format!(
"Unconfirmed: {}",
show_btc_amount(balance.unconfirmed)
),
);
}
if balance.available != balance.total {
ui.monospace_selectable_singleline(
false,
format!(
"Available: {}",
show_btc_amount(balance.available)
),
);
}
}
Some(None) => {
ui.monospace_selectable_singleline(
Expand Down
1 change: 1 addition & 0 deletions app/gui/parent_chain/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ fn create_withdrawal(
let accumulator = app.node.get_tip_accumulator()?;
let tx = app.wallet.create_withdrawal(
&accumulator,
app.spend_zero_conf_change,
mainchain_address,
amount,
mainchain_fee,
Expand Down
1 change: 1 addition & 0 deletions app/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ fn main() -> anyhow::Result<()> {
network: config.network,
network_magic_override: config.network_magic_override,
server_names: config.server_names,
spend_zero_conf_change: config.spend_zero_conf_change,
wallet_dir: config.wallet_dir,
};
let app = app::App::new(app_config).inspect(|app| {
Expand Down
22 changes: 21 additions & 1 deletion app/rpc_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,10 @@ impl<const ENABLE_PRIVATE_API: bool> rpc_api::node::RpcServer
#[async_trait]
impl rpc_api::wallet::RpcServer for RpcServerImpl<true> {
async fn balance(&self) -> RpcResult<Balance> {
self.app.wallet.get_balance().map_err(custom_err)
self.app
.wallet
.get_balance(self.app.spend_zero_conf_change)
.map_err(custom_err)
}

async fn create_deposit(
Expand Down Expand Up @@ -411,6 +414,7 @@ impl rpc_api::wallet::RpcServer for RpcServerImpl<true> {
.wallet
.create_transaction(
&accumulator,
self.app.spend_zero_conf_change,
dest,
Amount::from_sat(value_sats),
Amount::from_sat(fee_sats),
Expand All @@ -435,6 +439,7 @@ impl rpc_api::wallet::RpcServer for RpcServerImpl<true> {
.wallet
.create_withdrawal(
&accumulator,
self.app.spend_zero_conf_change,
mainchain_address,
Amount::from_sat(amount_sats),
Amount::from_sat(mainchain_fee_sats),
Expand Down Expand Up @@ -506,6 +511,21 @@ impl rpc_api::wallet::RpcServer for RpcServerImpl<true> {
Ok(utxos)
}

async fn get_unconfirmed_wallet_utxos(
&self,
) -> RpcResult<Vec<PointedOutput>> {
let utxos = self
.app
.wallet
.get_unconfirmed_utxos()
.map_err(custom_err)?;
let utxos = utxos
.into_iter()
.map(|(outpoint, output)| PointedOutput { outpoint, output })
.collect();
Ok(utxos)
}

async fn mine(&self, fee: Option<u64>) -> RpcResult<()> {
let fee = fee.map(bitcoin::Amount::from_sat);
self.app
Expand Down
6 changes: 6 additions & 0 deletions cli/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ pub enum Command {
GetWalletAddresses,
/// Get wallet UTXOs
GetWalletUtxos,
/// Get the unconfirmed wallet UTXOs that the wallet may spend
GetUnconfirmedWalletUtxos,
/// Get withdrawal bundle by M6id
GetWithdrawalBundle { m6id: M6id },
/// Invalidate a block, potentially re-orging to a valid ancestor of the
Expand Down Expand Up @@ -311,6 +313,10 @@ where
let utxos = rpc_client.get_wallet_utxos().await?;
serde_json::to_string_pretty(&utxos)?
}
Command::GetUnconfirmedWalletUtxos => {
let utxos = rpc_client.get_unconfirmed_wallet_utxos().await?;
serde_json::to_string_pretty(&utxos)?
}
Command::GetWithdrawalBundle { m6id } => {
let withdrawal_bundle =
rpc_client.get_withdrawal_bundle(m6id).await?;
Expand Down
Loading
Loading