Skip to content

Commit

Permalink
feat: transaction builder (#31)
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 19, 2025
1 parent 0de86b3 commit 36b202a
Show file tree
Hide file tree
Showing 15 changed files with 471 additions and 97 deletions.
21 changes: 10 additions & 11 deletions .github/workflows/check-is-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ name: Check if Release

on:
workflow_call:
inputs:
base_ref:
description: "The base branch or commit for comparison"
required: true
type: string
outputs:
IS_RELEASE:
description: "True if Cargo.toml version has been updated"
Expand All @@ -19,20 +14,24 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
# We need:
# - the current commit (potentially a new release commit)
# - a commit just before (from a previous release)
# So we can compare them and trigger a the release workflow.
fetch-depth: 2

- name: Compare Cargo.toml versions
id: check-release
run: |
git fetch origin "${{ inputs.base_ref }}"
# 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"
prev_commit="${{ github.event.before }}"
echo "Previous commit: $prev_commit"
# 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)
previous_version=$(git show "$prev_commit:Cargo.toml" | get_version)
echo "Current version: $current_version"
echo "Previous version: $previous_version"
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,9 @@ jobs:
is-release:
needs: all-jobs-pass
if: github.event_name == 'push'
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
name: Check if release
uses: ./.github/workflows/check-is-release.yml
with:
base_ref: main

publish-release:
needs: is-release
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</p>

<p>
<a href=""><img alt="NPM Package" src="https://img.shields.io/crates/v/bdk_wallet.svg"/></a>
<a href=""><img alt="NPM Package" src="https://img.shields.io/npm/v/bitcoindevkit.svg"/></a>
<a href="https://github.com/MetaMask/bdk-wasm/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
<a href="https://coveralls.io/github/MetaMask/bdk-wasm?branch=main"><img src="https://coveralls.io/repos/github/MetaMask/bdk-wasm/badge.svg?branch=main"/></a>
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
Expand All @@ -24,7 +24,7 @@
The `bdk-wasm` library aims at providing access to the excellent [BitcoinDevKit](https://github.com/bitcoindevkit/bdk) to JS and Node environments (and eventually any device supporting WebAssembly).
It specializes in compiling BDK on the `wasm32-unknown-unknown` target and use [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) to create TypeScript bindings.

This repo handles the packaging and publishing of the `bdk` NPM package, using `wasm-pack`.
This repo handles the packaging and publishing of the `bitcoindevkit` NPM package, using `wasm-pack`.

This library offers all the desired functionality to build a Bitcoin wallet out of the box:

Expand All @@ -42,7 +42,7 @@ For a lightweight library providing stateless utility functions, see [`bitcoinjs
## Browser Usage

```sh
yarn add bdk
yarn add bitcoindevkit
```

## Development Environment
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::{FeeEstimates, FullScanRequest, SyncRequest, Transaction, Update},
types::{FeeEstimates, FullScanRequest, SyncRequest, Transaction, Txid, Update},
};
use std::time::Duration;

Expand Down Expand Up @@ -56,6 +56,11 @@ impl EsploraClient {
let fee_estimates = self.client.get_fee_estimates().await?;
Ok(fee_estimates.into())
}

pub async fn get_tx(&self, txid: Txid) -> JsResult<Option<Transaction>> {
let tx = self.client.get_tx(&txid.into()).await?;
Ok(tx.map(Into::into))
}
}

#[derive(Clone)]
Expand Down
2 changes: 2 additions & 0 deletions src/bitcoin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod descriptor;
mod tx_builder;
mod wallet;

pub use descriptor::*;
pub use tx_builder::*;
pub use wallet::*;

#[cfg(feature = "esplora")]
Expand Down
138 changes: 138 additions & 0 deletions src/bitcoin/tx_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use std::{cell::RefCell, rc::Rc};

use bdk_wallet::Wallet as BdkWallet;
use bitcoin::ScriptBuf;
use wasm_bindgen::prelude::wasm_bindgen;

use crate::{
result::JsResult,
types::{Address, FeeRate, Outpoint, Psbt, Recipient},
};

/// A transaction builder.
///
/// A `TxBuilder` is created by calling [`build_tx`] or [`build_fee_bump`] on a wallet. After
/// assigning it, you set options on it until finally calling [`finish`] to consume the builder and
/// generate the transaction.
///
/// Each option setting method on `TxBuilder` takes and returns a new builder so you can chain calls
#[wasm_bindgen]
pub struct TxBuilder {
wallet: Rc<RefCell<BdkWallet>>,
recipients: Vec<Recipient>,
unspendable: Vec<Outpoint>,
fee_rate: FeeRate,
drain_wallet: bool,
drain_to: Option<ScriptBuf>,
allow_dust: bool,
}

#[wasm_bindgen]
impl TxBuilder {
// We make this constructor only visible to the crate to hide the use of the `Rc<RefCell<BdkWallet>>` in `Wallet::build_tx`.
pub(crate) fn new(wallet: Rc<RefCell<BdkWallet>>) -> TxBuilder {
TxBuilder {
wallet,
recipients: vec![],
unspendable: vec![],
fee_rate: FeeRate::new(1),
drain_wallet: false,
allow_dust: false,
drain_to: None,
}
}

/// Replace the recipients already added with a new list
pub fn set_recipients(mut self, recipients: Vec<Recipient>) -> Self {
self.recipients = recipients;
self
}

/// Add a recipient to the internal list
pub fn add_recipient(mut self, recipient: Recipient) -> Self {
self.recipients.push(recipient);
self
}

/// Replace the internal list of unspendable utxos with a new list
pub fn unspendable(mut self, unspendable: Vec<Outpoint>) -> Self {
self.unspendable = unspendable;
self
}

/// Add a utxo to the internal list of unspendable utxos
pub fn add_unspendable(mut self, outpoint: Outpoint) -> Self {
self.unspendable.push(outpoint);
self
}

/// Set a custom fee rate.
///
/// This method sets the mining fee paid by the transaction as a rate on its size.
/// This means that the total fee paid is equal to `fee_rate` times the size
/// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
/// relay policy.
///
/// Note that this is really a minimum feerate -- it's possible to
/// overshoot it slightly since adding a change output to drain the remaining
/// excess might not be viable.
pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self {
self.fee_rate = fee_rate;
self
}

/// Spend all the available inputs. This respects filters like [`TxBuilder::unspendable`] and the change policy.
pub fn drain_wallet(mut self) -> Self {
self.drain_wallet = true;
self
}

/// Sets the address to *drain* excess coins to.
///
/// Usually, when there are excess coins they are sent to a change address generated by the
/// wallet. This option replaces the usual change address with an arbitrary `script_pubkey` of
/// your choosing. Just as with a change output, if the drain output is not needed (the excess
/// coins are too small) it will not be included in the resulting transaction. The only
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
///
/// If you choose not to set any recipients, you should provide the utxos that the
/// transaction should spend via [`add_utxos`].
pub fn drain_to(mut self, address: Address) -> Self {
self.drain_to = Some(address.script_pubkey());
self
}

/// Set whether or not the dust limit is checked.
///
/// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
pub fn allow_dust(mut self, allow_dust: bool) -> Self {
self.allow_dust = allow_dust;
self
}

/// Finish building the transaction.
///
/// Returns a new [`Psbt`] per [`BIP174`].
pub fn finish(self) -> JsResult<Psbt> {
let mut wallet = self.wallet.borrow_mut();
let mut builder = wallet.build_tx();

builder
.set_recipients(self.recipients.into_iter().map(Into::into).collect())
.unspendable(self.unspendable.into_iter().map(Into::into).collect())
.fee_rate(self.fee_rate.into())
.allow_dust(self.allow_dust);

if self.drain_wallet {
builder.drain_wallet();
}

if let Some(drain_recipient) = self.drain_to {
builder.drain_to(drain_recipient);
}

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

0 comments on commit 36b202a

Please sign in to comment.