bsv-wallet-cli 0.2.8

Self-hosted BSV wallet CLI and BRC-100 server, wire-compatible with MetaNet Client
Documentation
use anyhow::{anyhow, Result};
use bsv_sdk::primitives::to_hex;
use bsv_sdk::script::templates::P2PKH;
use bsv_sdk::wallet::{CreateActionArgs, CreateActionOptions, CreateActionOutput, WalletInterface};
use bsv_wallet_toolbox::ListOutputsArgs;

use crate::context::WalletContext;

const FEE_RESERVE_BASE: u64 = 50;
const FEE_RESERVE_PER_INPUT: u64 = 25;

pub async fn run(ctx: &WalletContext, address: &str) -> Result<()> {
    let (balance, n_outputs) = sum_spendable(ctx).await?;
    if n_outputs == 0 {
        return Err(anyhow!("wallet has no spendable outputs"));
    }
    let fee_reserve = FEE_RESERVE_BASE + FEE_RESERVE_PER_INPUT * n_outputs as u64;
    if balance <= fee_reserve {
        return Err(anyhow!(
            "balance {} sats is below fee reserve {} sats — nothing to drain",
            balance,
            fee_reserve
        ));
    }
    let send_amount = balance - fee_reserve;

    let lock = P2PKH::lock_from_address(address)?;
    let args = CreateActionArgs {
        description: format!(
            "Drain {} sats to {}",
            send_amount,
            &address[..8.min(address.len())]
        ),
        input_beef: None,
        inputs: Some(vec![]),
        outputs: Some(vec![CreateActionOutput {
            locking_script: lock.to_binary(),
            satoshis: send_amount,
            output_description: "P2PKH drain".to_string(),
            basket: None,
            custom_instructions: None,
            tags: Some(vec!["relinquish".to_string()]),
        }]),
        lock_time: None,
        version: None,
        labels: Some(vec!["drain".to_string()]),
        options: Some(CreateActionOptions {
            randomize_outputs: Some(false),
            accept_delayed_broadcast: Some(false),
            no_send: Some(false),
            sign_and_process: Some(true),
            known_txids: Some(vec![]),
            return_txid_only: Some(false),
            no_send_change: Some(vec![]),
            send_with: Some(vec![]),
            ..Default::default()
        }),
    };

    let result = ctx.wallet.create_action(args, "bsv-wallet-cli").await?;
    let txid_hex = to_hex(&result.txid.expect("expected txid in result"));

    if ctx.json_output {
        println!(
            "{}",
            serde_json::json!({
                "txid": txid_hex,
                "drained": send_amount,
                "fee_reserved": fee_reserve,
                "inputs": n_outputs,
            })
        );
    } else {
        println!("Drained {} sats to {}", send_amount, address);
        println!("Inputs: {}, fee reserve: {} sats", n_outputs, fee_reserve);
        println!("TxID: {}", txid_hex);
        println!("View: https://whatsonchain.com/tx/{}", txid_hex);
    }

    Ok(())
}

async fn sum_spendable(ctx: &WalletContext) -> Result<(u64, u32)> {
    let mut balance: u64 = 0;
    let mut count: u32 = 0;
    let mut offset: i32 = 0;
    loop {
        let res = ctx
            .wallet
            .list_outputs(
                ListOutputsArgs {
                    basket: "default".to_string(),
                    tags: None,
                    tag_query_mode: None,
                    include: None,
                    include_custom_instructions: None,
                    include_tags: None,
                    include_labels: None,
                    limit: Some(100),
                    offset: Some(offset),
                    seek_permission: None,
                },
                "bsv-wallet-cli",
            )
            .await?;
        for o in &res.outputs {
            balance += o.satoshis;
            count += 1;
        }
        offset += res.outputs.len() as i32;
        if res.outputs.is_empty() || offset >= res.total_outputs as i32 {
            break;
        }
    }
    Ok((balance, count))
}