Skip to content

Commit

Permalink
feat: estimate fees (#28)
Browse files Browse the repository at this point in the history
Co-authored-by: Charly Chevalier <[email protected]>
  • Loading branch information
darioAnongba and ccharly authored Feb 11, 2025
1 parent c8c1811 commit 792752b
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 48 deletions.
13 changes: 8 additions & 5 deletions .github/workflows/check-is-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ jobs:
- name: Compare Cargo.toml versions
id: check-release
run: |
# Extract the version from the current Cargo.toml
current_version=$(grep '^version' Cargo.toml | sed 's/version = "//' | sed 's/"//')
git fetch origin "${{ inputs.base_ref }}"
# Extract the version from the previous commit's Cargo.toml
git fetch origin ${{ inputs.base_ref }}
previous_version=$(git show origin/${{ inputs.base_ref }}:Cargo.toml | grep '^version' | sed 's/version = "//' | sed 's/"//')
# Find the merge base between the current branch and the base branch
merge_base=$(git merge-base HEAD "origin/${{ inputs.base_ref }}")
echo "Merge base commit: $merge_base"
# Extract versions
get_version() { grep -E '^version\s*=' | sed -E 's/version\s*=\s*"([^"]+)"/\1/'; }
current_version=$(get_version < Cargo.toml)
previous_version=$(git show "$merge_base:Cargo.toml" | get_version)
echo "Current version: $current_version"
echo "Previous version: $previous_version"
Expand Down
7 changes: 6 additions & 1 deletion src/bitcoin/esplora_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use wasm_bindgen::prelude::wasm_bindgen;

use crate::{
result::JsResult,
types::{FullScanRequest, SyncRequest, Transaction, Update},
types::{FeeEstimates, FullScanRequest, SyncRequest, Transaction, Update},
};
use std::time::Duration;

Expand Down Expand Up @@ -51,6 +51,11 @@ impl EsploraClient {
self.client.broadcast(transaction).await?;
Ok(())
}

pub async fn get_fee_estimates(&self) -> JsResult<FeeEstimates> {
let fee_estimates = self.client.get_fee_estimates().await?;
Ok(fee_estimates.into())
}
}

#[derive(Clone)]
Expand Down
16 changes: 14 additions & 2 deletions src/bitcoin/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue};
use crate::{
result::JsResult,
types::{
AddressInfo, Balance, ChangeSet, CheckPoint, FeeRate, FullScanRequest, KeychainKind, Network, Psbt, Recipient,
SyncRequest, Update,
Address, AddressInfo, Balance, ChangeSet, CheckPoint, FeeRate, FullScanRequest, KeychainKind, Network, Psbt,
Recipient, SyncRequest, Update,
},
};

Expand Down Expand Up @@ -134,6 +134,18 @@ impl Wallet {
Ok(psbt.into())
}

pub fn drain_to(&mut self, fee_rate: FeeRate, to: Address) -> JsResult<Psbt> {
let mut builder = self.0.build_tx();

builder
.drain_wallet()
.drain_to(to.script_pubkey())
.fee_rate(fee_rate.into());

let psbt = builder.finish()?;
Ok(psbt.into())
}

pub fn sign(&self, psbt: &mut Psbt) -> JsResult<bool> {
let result = self.0.sign(psbt, SignOptions::default())?;
Ok(result)
Expand Down
5 changes: 5 additions & 0 deletions src/types/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ impl Address {
let address = BdkAddress::from_str(address_str)?.require_network(network.into())?;
Ok(Address(address))
}

#[wasm_bindgen(js_name = toString)]
pub fn address(&self) -> String {
self.0.to_string()
}
}

impl From<BdkAddress> for Address {
Expand Down
74 changes: 74 additions & 0 deletions src/types/fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use bitcoin::FeeRate as BdkFeeRate;
use std::{collections::HashMap, ops::Deref};

use wasm_bindgen::prelude::wasm_bindgen;

/// Map where the key is the confirmation target (in number of blocks) and the value is the estimated feerate (in sat/vB).
#[wasm_bindgen]
#[derive(Debug)]
pub struct FeeEstimates(HashMap<u16, f64>);

impl Deref for FeeEstimates {
type Target = HashMap<u16, f64>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[wasm_bindgen]
impl FeeEstimates {
/// Returns the feerate (in sat/vB) or undefined.
/// Available confirmation targets are 1-25, 144, 504 and 1008 blocks.
pub fn get(&self, k: u16) -> Option<f64> {
self.0.get(&k).copied()
}
}

impl From<HashMap<u16, f64>> for FeeEstimates {
fn from(inner: HashMap<u16, f64>) -> Self {
FeeEstimates(inner)
}
}

impl From<FeeEstimates> for HashMap<u16, f64> {
fn from(fee_estimates: FeeEstimates) -> Self {
fee_estimates.0
}
}

/// Represents fee rate.
///
/// This is an integer newtype representing fee rate in `sat/kwu`. It provides protection against mixing
/// up the types as well as basic formatting features.
#[wasm_bindgen]
#[derive(Debug, PartialEq, Eq)]
pub struct FeeRate(BdkFeeRate);

impl Deref for FeeRate {
type Target = BdkFeeRate;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[wasm_bindgen]
impl FeeRate {
#[wasm_bindgen(constructor)]
pub fn new(sat_vb: u64) -> Self {
FeeRate(BdkFeeRate::from_sat_per_vb_unchecked(sat_vb))
}
}

impl From<BdkFeeRate> for FeeRate {
fn from(inner: BdkFeeRate) -> Self {
FeeRate(inner)
}
}

impl From<FeeRate> for BdkFeeRate {
fn from(fee_rate: FeeRate) -> Self {
fee_rate.0
}
}
2 changes: 2 additions & 0 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod block;
mod chain;
mod changeset;
mod checkpoint;
mod fee;
mod keychain;
mod network;
mod psbt;
Expand All @@ -18,6 +19,7 @@ pub use block::*;
pub use chain::*;
pub use changeset::*;
pub use checkpoint::*;
pub use fee::*;
pub use keychain::*;
pub use network::*;
pub use psbt::*;
Expand Down
61 changes: 23 additions & 38 deletions src/types/psbt.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::ops::{Deref, DerefMut};

use bitcoin::{Amount as BdkAmount, FeeRate as BdkFeeRate, Psbt as BdkPsbt, ScriptBuf as BdkScriptBuf};
use bdk_wallet::psbt::PsbtUtils;
use bitcoin::{Amount as BdkAmount, Psbt as BdkPsbt, ScriptBuf as BdkScriptBuf};
use wasm_bindgen::prelude::wasm_bindgen;

use crate::result::JsResult;

use super::{Address, Amount, Transaction};
use super::{Address, Amount, FeeRate, Transaction};

/// A Partially Signed Transaction.
#[wasm_bindgen]
Expand All @@ -32,6 +33,26 @@ impl Psbt {
let tx = self.0.extract_tx()?;
Ok(tx.into())
}

pub fn extract_tx_with_fee_rate_limit(self, max_fee_rate: FeeRate) -> JsResult<Transaction> {
let tx = self.0.extract_tx_with_fee_rate_limit(max_fee_rate.into())?;
Ok(tx.into())
}

pub fn fee(&self) -> JsResult<Amount> {
let fee = self.0.fee()?;
Ok(fee.into())
}

pub fn fee_amount(&self) -> Option<Amount> {
let fee_amount = self.0.fee_amount();
fee_amount.map(Into::into)
}

pub fn fee_rate(&self) -> Option<FeeRate> {
let fee_rate = self.0.fee_rate();
fee_rate.map(Into::into)
}
}

impl From<BdkPsbt> for Psbt {
Expand Down Expand Up @@ -77,39 +98,3 @@ impl From<Recipient> for (BdkScriptBuf, BdkAmount) {
(r.address().script_pubkey(), r.amount().into())
}
}

/// Represents fee rate.
///
/// This is an integer newtype representing fee rate in `sat/kwu`. It provides protection against mixing
/// up the types as well as basic formatting features.
#[wasm_bindgen]
#[derive(Debug)]
pub struct FeeRate(BdkFeeRate);

impl Deref for FeeRate {
type Target = BdkFeeRate;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[wasm_bindgen]
impl FeeRate {
#[wasm_bindgen(constructor)]
pub fn new(sat_vb: u64) -> Self {
FeeRate(BdkFeeRate::from_sat_per_vb_unchecked(sat_vb))
}
}

impl From<BdkFeeRate> for FeeRate {
fn from(inner: BdkFeeRate) -> Self {
FeeRate(inner)
}
}

impl From<FeeRate> for BdkFeeRate {
fn from(fee_rate: FeeRate) -> Self {
fee_rate.0
}
}
33 changes: 31 additions & 2 deletions tests/esplora.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const NETWORK: Network = Network::Signet;
const SEND_ADMOUNT: u64 = 1000;
const FEE_RATE: u64 = 2;
const RECIPIENT_ADDRESS: &str = "tb1qd28npep0s8frcm3y7dxqajkcy2m40eysplyr9v";
const CONFIRMATION_TARGET: u16 = 2;

#[wasm_bindgen_test]
async fn test_esplora_client() {
Expand Down Expand Up @@ -54,7 +55,6 @@ async fn test_esplora_client() {
wallet.apply_update(update).expect("full_scan apply_update");

let balance = wallet.balance();
web_sys::console::log_2(&"balance: ".into(), &balance.total().to_sat().into());
assert!(balance.trusted_spendable().to_sat() > SEND_ADMOUNT);

// Important to test that we can load the wallet from a changeset with the signing descriptors and be able to sign a transaction
Expand All @@ -67,12 +67,17 @@ async fn test_esplora_client() {
.expect("load");
assert_eq!(loaded_wallet.balance(), wallet.balance());

let fees = blockchain_client.get_fee_estimates().await.expect("get_fee_estimates");
let recipient = Address::new(RECIPIENT_ADDRESS, NETWORK).expect("recipient_address");
let amount = Amount::from_sat(SEND_ADMOUNT);
let fee_rate = fees.get(CONFIRMATION_TARGET).expect("fee_estimation");
let mut psbt = loaded_wallet
.build_tx(FeeRate::new(FEE_RATE), vec![Recipient::new(recipient, amount)])
.build_tx(FeeRate::new(fee_rate as u64), vec![Recipient::new(recipient, amount)])
.expect("build_tx");

let fee = psbt.fee().expect("psbt_fee");
assert!(fee.to_sat() > 100); // We cannot know the exact fees

let finalized = loaded_wallet.sign(&mut psbt).expect("sign");
assert!(finalized);

Expand All @@ -81,3 +86,27 @@ async fn test_esplora_client() {

web_sys::console::log_1(&tx.compute_txid().into());
}

#[wasm_bindgen_test]
async fn test_drain() {
set_panic_hook();

let external_desc = "wpkh(tprv8ZgxMBicQKsPf6vydw7ixvsLKY79hmeXujBkGCNCApyft92yVYng2y28JpFZcneBYTTHycWSRpokhHE25GfHPBxnW5GpSm2dMWzEi9xxEyU/84'/1'/0'/0/*)#uel0vg9p";
let internal_desc = "wpkh(tprv8ZgxMBicQKsPf6vydw7ixvsLKY79hmeXujBkGCNCApyft92yVYng2y28JpFZcneBYTTHycWSRpokhHE25GfHPBxnW5GpSm2dMWzEi9xxEyU/84'/1'/0'/1/*)#dd6w3a4e";

let mut wallet = Wallet::create(NETWORK, external_desc.into(), internal_desc.into()).expect("wallet");
let mut blockchain_client = EsploraClient::new(ESPLORA_URL).expect("esplora_client");

let full_scan_request = wallet.start_full_scan();
let update = blockchain_client
.full_scan(full_scan_request, STOP_GAP, PARALLEL_REQUESTS)
.await
.expect("full_scan");
wallet.apply_update(update).expect("full_scan apply_update");

// No need to test actual values as we are just wrapping BDK and assume the underlying package is computing fees properly
let recipient = Address::new(RECIPIENT_ADDRESS, NETWORK).expect("recipient_address");
let psbt = wallet.drain_to(FeeRate::new(FEE_RATE), recipient).expect("drain_to");
assert!(psbt.fee_amount().is_some());
assert!(psbt.fee_rate().is_some());
}

0 comments on commit 792752b

Please sign in to comment.