Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the --fee-rate when sending an amount #1922

Merged
merged 12 commits into from
Aug 17, 2023
4 changes: 4 additions & 0 deletions src/fee_rate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ impl FeeRate {
#[allow(clippy::cast_sign_loss)]
Amount::from_sat((self.0 * vsize as f64).round() as u64)
}

pub(crate) fn n(&self) -> f64 {
self.0
}
}

#[cfg(test)]
Expand Down
68 changes: 48 additions & 20 deletions src/subcommand/wallet/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ pub struct Output {

impl Send {
pub(crate) fn run(self, options: Options) -> Result {
let address = self.address.require_network(options.chain().network())?;
let address = self
.address
.clone()
.require_network(options.chain().network())?;

let index = Index::open(&options)?;
index.update()?;
Expand All @@ -44,25 +47,9 @@ impl Send {
.get_inscription_satpoint_by_id(id)?
.ok_or_else(|| anyhow!("Inscription {id} not found"))?,
Outgoing::Amount(amount) => {
let all_inscription_outputs = inscriptions
.keys()
.map(|satpoint| satpoint.outpoint)
.collect::<HashSet<OutPoint>>();

let wallet_inscription_outputs = unspent_outputs
.keys()
.filter(|utxo| all_inscription_outputs.contains(utxo))
.cloned()
.collect::<Vec<OutPoint>>();

if !client.lock_unspent(&wallet_inscription_outputs)? {
bail!("failed to lock ordinal UTXOs");
}

let txid = client.send_to_address(&address, amount, None, None, None, None, None, None)?;

Self::lock_inscriptions(&client, inscriptions, unspent_outputs)?;
let txid = Self::send_amount(&client, amount, address, self.fee_rate.n())?;
print_json(Output { transaction: txid })?;

return Ok(());
}
};
Expand All @@ -82,7 +69,7 @@ impl Send {
satpoint,
inscriptions,
unspent_outputs,
address,
address.clone(),
change,
self.fee_rate,
postage,
Expand All @@ -99,4 +86,45 @@ impl Send {

Ok(())
}

fn lock_inscriptions(
client: &Client,
inscriptions: BTreeMap<SatPoint, InscriptionId>,
unspent_outputs: BTreeMap<bitcoin::OutPoint, bitcoin::Amount>,
) -> Result {
let all_inscription_outputs = inscriptions
.keys()
.map(|satpoint| satpoint.outpoint)
.collect::<HashSet<OutPoint>>();

let wallet_inscription_outputs = unspent_outputs
.keys()
.filter(|utxo| all_inscription_outputs.contains(utxo))
.cloned()
.collect::<Vec<OutPoint>>();

if !client.lock_unspent(&wallet_inscription_outputs)? {
bail!("failed to lock ordinal UTXOs");
}

Ok(())
}

fn send_amount(client: &Client, amount: Amount, address: Address, fee_rate: f64) -> Result<Txid> {
Ok(client.call(
"sendtoaddress",
&[
address.to_string().into(), // 1. address
amount.to_btc().into(), // 2. amount
serde_json::Value::Null, // 3. comment
serde_json::Value::Null, // 4. comment_to
serde_json::Value::Null, // 5. subtractfeefromamount
serde_json::Value::Null, // 6. replaceable
serde_json::Value::Null, // 7. conf_target
serde_json::Value::Null, // 8. estimate_mode
serde_json::Value::Null, // 9. avoid_reuse
fee_rate.into(), // 10. fee_rate
],
)?)
}
}
3 changes: 3 additions & 0 deletions test-bitcoincore-rpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ pub trait Api {
replaceable: Option<bool>,
confirmation_target: Option<u32>,
estimate_mode: Option<EstimateMode>,
avoid_reuse: Option<bool>,
fee_rate: Option<f64>,
verbose: Option<bool>,
) -> Result<Txid, jsonrpc_core::Error>;

#[rpc(name = "gettransaction")]
Expand Down
40 changes: 40 additions & 0 deletions test-bitcoincore-rpc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,17 +279,57 @@ impl Api for Server {
replaceable: Option<bool>,
confirmation_target: Option<u32>,
estimate_mode: Option<EstimateMode>,
avoid_reuse: Option<bool>,
fee_rate: Option<f64>,
verbose: Option<bool>,
) -> Result<Txid, jsonrpc_core::Error> {
assert_eq!(comment, None);
assert_eq!(comment_to, None);
assert_eq!(subtract_fee, None);
assert_eq!(replaceable, None);
assert_eq!(confirmation_target, None);
assert_eq!(estimate_mode, None);
assert_eq!(avoid_reuse, None);
assert_eq!(verbose, None);

let mut state = self.state.lock().unwrap();
let locked = state.locked.iter().cloned().collect();

let value = Amount::from_btc(amount).expect("error converting amount to sat");

let utxo = state.utxos.first_entry().expect("failed to get a utxo");
let outpoint = utxo.key();
let utxo_amount = utxo.get();

let mut transaction = Transaction {
version: 1,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: *outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
witness: Witness::new(),
}],
output: vec![
TxOut {
value: value.to_sat(),
script_pubkey: address.payload.script_pubkey(),
},
TxOut {
value: (*utxo_amount - value).to_sat(),
script_pubkey: address.payload.script_pubkey(),
},
],
};

#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let fee = (fee_rate.unwrap_or(1.0) * transaction.vsize() as f64).round() as u64;

transaction.output[1].value -= fee;

state.mempool.push(transaction);

state.sent.push(Sent {
address: address.assume_checked(),
amount,
Expand Down
29 changes: 23 additions & 6 deletions tests/wallet/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,33 @@ fn inscriptions_cannot_be_sent_by_satpoint() {
}

#[test]
fn send_btc() {
fn send_btc_with_fee_rate() {
let rpc_server = test_bitcoincore_rpc::spawn();
create_wallet(&rpc_server);

rpc_server.mine_blocks(1);

let output =
CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc")
.rpc_server(&rpc_server)
.run_and_check_output::<Output>();
let output = CommandBuilder::new(
"wallet send --fee-rate 13.3 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc",
)
.rpc_server(&rpc_server)
.run_and_check_output::<Output>();

let tx = &rpc_server.mempool()[0];
let mut fee = 0;
for input in &tx.input {
fee += rpc_server
.get_utxo_amount(&input.previous_output)
.unwrap()
.to_sat();
}
for output in &tx.output {
fee -= output.value;
}

let fee_rate = fee as f64 / tx.vsize() as f64;

assert!(f64::abs(fee_rate - 13.3) < 0.1);

assert_eq!(
output.transaction,
Expand All @@ -239,7 +256,7 @@ fn send_btc() {
.assume_checked(),
locked: Vec::new(),
}]
)
);
}

#[test]
Expand Down
Loading