Skip to content

Commit 242d4da

Browse files
committed
feat(utxo-locking): Add wallet locking commands
- add wallet commands to lock, unlock and list locked utxos - update unspent command to show status of each outpoint
1 parent 205d46c commit 242d4da

5 files changed

Lines changed: 94 additions & 15 deletions

File tree

src/client.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,10 @@ impl BlockchainClient {
9898
#[cfg(feature = "cbf")]
9999
Self::KyotoClient { client } => {
100100
let txid = tx.compute_txid();
101-
let wtxid = client
102-
.requester
103-
.submit_package(tx)
104-
.await
105-
.map_err(|_| {
106-
tracing::warn!("Broadcast was unsuccessful");
107-
Error::Generic("Transaction broadcast timed out after 30 seconds".into())
108-
})?;
101+
let wtxid = client.requester.submit_package(tx).await.map_err(|_| {
102+
tracing::warn!("Broadcast was unsuccessful");
103+
Error::Generic("Transaction broadcast timed out after 30 seconds".into())
104+
})?;
109105
tracing::info!("Successfully broadcast WTXID: {wtxid}");
110106
Ok(txid)
111107
}

src/commands.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ use crate::handlers::{
2121
key::{DeriveKeyCommand, GenerateKeyCommand, RestoreKeyCommand},
2222
offline::{
2323
BalanceCommand, BumpFeeCommand, CombinePsbtCommand, CreateTxCommand, ExtractPsbtCommand,
24-
FinalizePsbtCommand, NewAddressCommand, PoliciesCommand, PublicDescriptorCommand,
25-
SignCommand, TransactionsCommand, UnspentCommand, UnusedAddressCommand,
24+
FinalizePsbtCommand, LockUtxoCommand, LockedUtxosCommand, NewAddressCommand,
25+
PoliciesCommand, PublicDescriptorCommand, SignCommand, TransactionsCommand,
26+
UnlockUtxoCommand, UnspentCommand, UnusedAddressCommand,
2627
},
2728
};
2829

@@ -359,6 +360,12 @@ pub enum OfflineWalletSubCommand {
359360
/// Verify a BIP322 signature
360361
#[cfg(feature = "bip322")]
361362
VerifyMessage(VerifyMessageCommand),
363+
/// Lock UTXO(s) so they're excluded from coin selection.
364+
LockUtxo(LockUtxoCommand),
365+
/// Unlock previously locked UTXO(s).
366+
UnlockUtxo(UnlockUtxoCommand),
367+
/// List currently locked UTXOs.
368+
LockedUtxos(LockedUtxosCommand),
362369
}
363370

364371
/// Wallet subcommands that needs a blockchain backend.

src/handlers/offline.rs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ impl OfflineWalletSubCommand {
8181
Self::VerifyMessage(verify_message_command) => verify_message_command
8282
.execute(ctx)?
8383
.write_out(std::io::stdout()),
84+
Self::LockUtxo(lock_utxo) => lock_utxo.execute(ctx)?.write_out(std::io::stdout()),
85+
Self::UnlockUtxo(unlock_utxo) => unlock_utxo.execute(ctx)?.write_out(std::io::stdout()),
86+
Self::LockedUtxos(locked_utxos) => {
87+
locked_utxos.execute(ctx)?.write_out(std::io::stdout())
88+
}
8489
}
8590
}
8691
}
@@ -119,9 +124,13 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for UnspentCommand {
119124

120125
fn execute(&self, ctx: &mut AppContext<OfflineOperations<'_>>) -> Result<Self::Output, Error> {
121126
let wallet = &mut ctx.state.wallet;
122-
let utxos = wallet
123-
.list_unspent()
124-
.map(|utxo| UnspentDetails::from_local_output(&utxo, ctx.network))
127+
let outputs: Vec<_> = wallet.list_unspent().collect();
128+
let utxos = outputs
129+
.into_iter()
130+
.map(|utxo| {
131+
let is_locked = wallet.is_outpoint_locked(utxo.outpoint);
132+
UnspentDetails::from_local_output(&utxo, ctx.network, is_locked)
133+
})
125134
.collect();
126135

127136
Ok(ListResult::new(utxos))
@@ -841,3 +850,65 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for VerifyMessageCommand {
841850
})
842851
}
843852
}
853+
854+
#[derive(Parser, Debug, Clone, PartialEq)]
855+
pub struct LockUtxoCommand {
856+
/// Outpoint(s) to lock, format TXID:VOUT.
857+
#[arg(env = "TXID:VOUT", long = "utxo", required = true, value_parser = parse_outpoint)]
858+
pub utxos: Vec<OutPoint>,
859+
}
860+
861+
impl AppCommand<AppContext<OfflineOperations<'_>>> for LockUtxoCommand {
862+
type Output = ListResult<String>;
863+
fn execute(&self, ctx: &mut AppContext<OfflineOperations<'_>>) -> Result<Self::Output, Error> {
864+
let wallet = &mut ctx.state.wallet;
865+
for out_point in &self.utxos {
866+
if wallet.get_utxo(*out_point).is_none() {
867+
eprintln!("warning: {out_point} is not a known wallet UTXO; locking anyway");
868+
}
869+
wallet.lock_outpoint(*out_point);
870+
}
871+
let locked = wallet
872+
.list_locked_outpoints()
873+
.map(|o| o.to_string())
874+
.collect();
875+
Ok(ListResult::new(locked))
876+
}
877+
}
878+
879+
#[derive(Parser, Debug, Clone, PartialEq)]
880+
pub struct UnlockUtxoCommand {
881+
/// Outpoint(s) to unlock, format TXID:VOUT.
882+
#[arg(env = "TXID:VOUT", long = "utxo", required = true, value_parser = parse_outpoint)]
883+
pub utxos: Vec<OutPoint>,
884+
}
885+
886+
impl AppCommand<AppContext<OfflineOperations<'_>>> for UnlockUtxoCommand {
887+
type Output = ListResult<String>;
888+
fn execute(&self, ctx: &mut AppContext<OfflineOperations<'_>>) -> Result<Self::Output, Error> {
889+
let wallet = &mut ctx.state.wallet;
890+
for out_point in &self.utxos {
891+
wallet.unlock_outpoint(*out_point);
892+
}
893+
let locked = wallet
894+
.list_locked_outpoints()
895+
.map(|o| o.to_string())
896+
.collect();
897+
Ok(ListResult::new(locked))
898+
}
899+
}
900+
901+
#[derive(Parser, Debug, Clone, PartialEq)]
902+
pub struct LockedUtxosCommand {}
903+
904+
impl AppCommand<AppContext<OfflineOperations<'_>>> for LockedUtxosCommand {
905+
type Output = ListResult<String>;
906+
fn execute(&self, ctx: &mut AppContext<OfflineOperations<'_>>) -> Result<Self::Output, Error> {
907+
let wallet = &mut ctx.state.wallet;
908+
let locked = wallet
909+
.list_locked_outpoints()
910+
.map(|o| o.to_string())
911+
.collect();
912+
Ok(ListResult::new(locked))
913+
}
914+
}

src/utils/common.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,10 @@ pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool {
220220
| OfflineWalletSubCommand::BumpFee(_)
221221
| OfflineWalletSubCommand::NewAddress(_)
222222
| OfflineWalletSubCommand::UnusedAddress(_)
223-
| OfflineWalletSubCommand::CreateTx(_) => true,
223+
| OfflineWalletSubCommand::CreateTx(_)
224+
| OfflineWalletSubCommand::LockUtxo(_)
225+
| OfflineWalletSubCommand::UnlockUtxo(_)
226+
| OfflineWalletSubCommand::LockedUtxos(_) => true,
224227

225228
OfflineWalletSubCommand::Policies(_)
226229
| OfflineWalletSubCommand::PublicDescriptor(_)

src/utils/types.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ pub struct UnspentDetails {
5555
pub is_spent: bool,
5656
pub derivation_index: u32,
5757
pub chain_position: serde_json::Value,
58+
pub is_locked: bool,
5859
}
5960

6061
impl UnspentDetails {
61-
pub fn from_local_output(utxo: &LocalOutput, _network: Network) -> Self {
62+
pub fn from_local_output(utxo: &LocalOutput, _network: Network, is_locked: bool) -> Self {
6263
let outpoint_str = utxo.outpoint.to_string();
6364

6465
Self {
@@ -68,6 +69,7 @@ impl UnspentDetails {
6869
is_spent: utxo.is_spent,
6970
derivation_index: utxo.derivation_index,
7071
chain_position: serde_json::to_value(utxo.chain_position).unwrap_or(json!({})),
72+
is_locked,
7173
}
7274
}
7375
}

0 commit comments

Comments
 (0)