Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ jobs:
strategy:
matrix:
rust:
- toolchain: stable
- toolchain: 1.63.0
- version: stable
- version: 1.63.0
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ matrix.rust.toolchain }}
toolchain: ${{ matrix.rust.version }}
- name: Pin dependencies for MSRV
if: matrix.rust.version == '1.63.0'
run: ./ci/pin-msrv.sh
- name: Test
run: cargo test --no-fail-fast --all-features

Expand Down
12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,23 @@ readme = "README.md"

[dependencies]
miniscript = { version = "12", default-features = false }
bdk_coin_select = "0.4.0"

[dev-dependencies]
anyhow = "1"
bdk_chain = { version = "0.21" }
bdk_tx = { path = "." }
bitcoin = { version = "0.32", features = ["rand-std"] }
bdk_testenv = "0.11.1"
bdk_bitcoind_rpc = "0.18.0"
bdk_chain = { version = "0.21" }

[features]
default = ["std"]
std = ["miniscript/std"]

[[example]]
name = "synopsis"

[[example]]
name = "common"
crate-type = ["lib"]
24 changes: 4 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `bdk-tx`
# `bdk_tx`

This is a transaction building library based on `rust-miniscript` that lets you build, update, and finalize PSBTs with minimal dependencies.

Expand All @@ -7,26 +7,9 @@ Because the project builds upon [miniscript] we support [descriptors] natively.

Refer to [BIP174], [BIP370], and [BIP371] to learn more about partially signed bitcoin transactions (PSBT).

## Example
**Note:**
The library is unstable and API changes should be expected. Check the [examples] directory for detailed usage examples.

To get started see the `DataProvider` trait and the methods for adding inputs and outputs.

```rust
use bdk_tx::Builder;
use bdk_tx::DataProvider;

impl DataProvider for MyType { ... }

let mut builder = Builder::new();
builder.add_input(plan_utxo);
builder.add_output(script_pubkey, amount);
let (mut psbt, finalizer) = builder.build_tx(data_provider)?;

// Your PSBT signing flow...

let result = finalizer.finalize(&mut psbt)?;
assert!(result.is_finalized());
```

## Contributing
Found a bug, have an issue or a feature request? Feel free to open an issue on GitHub. This library is open source licensed under MIT.
Expand All @@ -36,3 +19,4 @@ Found a bug, have an issue or a feature request? Feel free to open an issue on G
[BIP174]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
[BIP370]: https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki
[BIP371]: https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki
[examples]: ./examples
23 changes: 23 additions & 0 deletions ci/pin-msrv.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

set -x
set -euo pipefail

# Script to pin dependencies for MSRV

# cargo clean

# rm -f Cargo.lock

# rustup default 1.63.0

cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
cargo update -p time --precise "0.3.20"
cargo update -p home --precise "0.5.5"
cargo update -p flate2 --precise "1.0.35"
cargo update -p once_cell --precise "1.20.3"
cargo update -p bzip2-sys --precise "0.1.12"
cargo update -p ring --precise "0.17.12"
cargo update -p once_cell --precise "1.20.3"
cargo update -p base64ct --precise "1.6.0"
cargo update -p minreq --precise "2.13.2"
170 changes: 170 additions & 0 deletions examples/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::sync::Arc;

use bdk_bitcoind_rpc::Emitter;
use bdk_chain::{bdk_core, Anchor, Balance, ChainPosition, ConfirmationBlockTime};
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
use bdk_tx::{CanonicalUnspents, Input, InputCandidates, RbfParams, TxStatus, TxWithStatus};
use bitcoin::{absolute, Address, BlockHash, OutPoint, Transaction, Txid};
use miniscript::{
plan::{Assets, Plan},
Descriptor, DescriptorPublicKey, ForEachKey,
};

const EXTERNAL: &str = "external";
const INTERNAL: &str = "internal";

pub struct Wallet {
pub chain: bdk_chain::local_chain::LocalChain,
pub graph: bdk_chain::IndexedTxGraph<
bdk_core::ConfirmationBlockTime,
bdk_chain::keychain_txout::KeychainTxOutIndex<&'static str>,
>,
}

impl Wallet {
pub fn new(
genesis_hash: BlockHash,
external: Descriptor<DescriptorPublicKey>,
internal: Descriptor<DescriptorPublicKey>,
) -> anyhow::Result<Self> {
let mut indexer = bdk_chain::keychain_txout::KeychainTxOutIndex::default();
indexer.insert_descriptor(EXTERNAL, external)?;
indexer.insert_descriptor(INTERNAL, internal)?;
let graph = bdk_chain::IndexedTxGraph::new(indexer);
let (chain, _) = bdk_chain::local_chain::LocalChain::from_genesis_hash(genesis_hash);
Ok(Self { chain, graph })
}

pub fn sync(&mut self, env: &TestEnv) -> anyhow::Result<()> {
let client = env.rpc_client();
let last_cp = self.chain.tip();
let mut emitter = Emitter::new(client, last_cp, 0);
while let Some(event) = emitter.next_block()? {
let _ = self
.graph
.apply_block_relevant(&event.block, event.block_height());
let _ = self.chain.apply_update(event.checkpoint);
}
let mempool = emitter.mempool()?;
let _ = self.graph.batch_insert_relevant_unconfirmed(mempool);
Ok(())
}

pub fn next_address(&mut self) -> Option<Address> {
let ((_, spk), _) = self.graph.index.next_unused_spk(EXTERNAL)?;
Address::from_script(&spk, bitcoin::consensus::Params::REGTEST).ok()
}

pub fn balance(&self) -> Balance {
let outpoints = self.graph.index.outpoints().clone();
self.graph.graph().balance(
&self.chain,
self.chain.tip().block_id(),
outpoints,
|_, _| true,
)
}

/// TODO: Add to chain sources.
pub fn tip_info(
&self,
client: &impl RpcApi,
) -> anyhow::Result<(absolute::Height, absolute::Time)> {
let tip = self.chain.tip().block_id();
let tip_info = client.get_block_header_info(&tip.hash)?;
let tip_height = absolute::Height::from_consensus(tip.height)?;
let tip_time =
absolute::Time::from_consensus(tip_info.median_time.unwrap_or(tip_info.time) as _)?;
Ok((tip_height, tip_time))
}

// TODO: Maybe create an `AssetsBuilder` or `AssetsExt` that makes it easier to add
// assets from descriptors, etc.
pub fn assets(&self) -> Assets {
let index = &self.graph.index;
let tip = self.chain.tip().block_id();
Assets::new()
.after(absolute::LockTime::from_height(tip.height).expect("must be valid height"))
.add({
let mut pks = vec![];
for (_, desc) in index.keychains() {
desc.for_each_key(|k| {
pks.extend(k.clone().into_single_keys());
true
});
}
pks
})
}

pub fn plan_of_output(&self, outpoint: OutPoint, assets: &Assets) -> Option<Plan> {
let index = &self.graph.index;
let ((k, i), _txout) = index.txout(outpoint)?;
let desc = index.get_descriptor(k)?.at_derivation_index(i).ok()?;
let plan = desc.plan(assets).ok()?;
Some(plan)
}

pub fn canonical_txs(&self) -> impl Iterator<Item = TxWithStatus<Arc<Transaction>>> + '_ {
pub fn status_from_position(pos: ChainPosition<ConfirmationBlockTime>) -> Option<TxStatus> {
match pos {
bdk_chain::ChainPosition::Confirmed { anchor, .. } => Some(TxStatus {
height: absolute::Height::from_consensus(
anchor.confirmation_height_upper_bound(),
)
.expect("must convert to height"),
time: absolute::Time::from_consensus(anchor.confirmation_time as _)
.expect("must convert from time"),
}),
bdk_chain::ChainPosition::Unconfirmed { .. } => None,
}
}
self.graph
.graph()
.list_canonical_txs(&self.chain, self.chain.tip().block_id())
.map(|c_tx| (c_tx.tx_node.tx, status_from_position(c_tx.chain_position)))
}

pub fn all_candidates(&self) -> bdk_tx::InputCandidates {
let index = &self.graph.index;
let assets = self.assets();
let canon_utxos = CanonicalUnspents::new(self.canonical_txs());
let can_select = canon_utxos.try_get_unspents(
index
.outpoints()
.iter()
.filter_map(|(_, op)| Some((*op, self.plan_of_output(*op, &assets)?))),
);
InputCandidates::new([], can_select)
}

pub fn rbf_candidates(
&self,
replace: impl IntoIterator<Item = Txid>,
tip_height: absolute::Height,
) -> anyhow::Result<(bdk_tx::InputCandidates, RbfParams)> {
let index = &self.graph.index;
let assets = self.assets();
let mut canon_utxos = CanonicalUnspents::new(self.canonical_txs());

// Exclude txs that reside-in `rbf_set`.
let rbf_set = canon_utxos.extract_replacements(replace)?;
let must_select = rbf_set
.must_select_largest_input_of_each_original_tx(&canon_utxos)?
.into_iter()
.map(|op| canon_utxos.try_get_unspent(op, self.plan_of_output(op, &assets)?))
.collect::<Option<Vec<Input>>>()
.ok_or(anyhow::anyhow!(
"failed to find input of tx we are intending to replace"
))?;

let can_select = index.outpoints().iter().filter_map(|(_, op)| {
canon_utxos.try_get_unspent(*op, self.plan_of_output(*op, &assets)?)
});
Ok((
InputCandidates::new(must_select, can_select)
.filter(rbf_set.candidate_filter(tip_height)),
rbf_set.selector_rbf_params(),
))
}
}
Loading