Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- version: stable
- version: 1.85.0
features:
- --no-default-features --features miniscript/no-std
- --no-default-features
- --all-features
steps:
- uses: actions/checkout@v4
Expand All @@ -44,7 +44,7 @@ jobs:
with:
toolchain: stable
- name: Check no-std
run: cargo check --no-default-features --features miniscript/no-std
run: cargo check --no-default-features

fmt-clippy:
runs-on: ubuntu-latest
Expand Down
19 changes: 17 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[workspace]
members = [".", "testenv"]

[package]
name = "bdk_tx"
version = "0.1.0"
Expand All @@ -11,17 +14,18 @@ license = "MIT OR Apache-2.0"
readme = "README.md"

[dependencies]
miniscript = { version = "12.3.5", default-features = false }
miniscript = { version = "13.0.0", default-features = false }
bdk_coin_select = "0.4.0"
rand_core = { version = "0.6.4", default-features = false }
rand = { version = "0.8", optional = true }

[dev-dependencies]
anyhow = "1"
bdk_tx = { path = "." }
bdk_tx_testenv = { path = "testenv" }
bitcoin = { version = "0.32", default-features = false, features = ["rand-std"] }
bdk_testenv = "0.13.0"
bdk_bitcoind_rpc = "0.20.0"
bdk_bitcoind_rpc = "0.22.0"
bdk_chain = { version = "0.23.0" }

[features]
Expand All @@ -37,3 +41,14 @@ crate-type = ["lib"]

[[example]]
name = "anti_fee_sniping"

[patch.crates-io]
bdk_testenv = { git = "https://github.com/evanlinjin/bdk", branch = "feature/bitcoind_rpc_emit_header" }
bdk_chain = { git = "https://github.com/evanlinjin/bdk", branch = "feature/bitcoind_rpc_emit_header" }
bdk_core = { git = "https://github.com/evanlinjin/bdk", branch = "feature/bitcoind_rpc_emit_header" }
bdk_bitcoind_rpc = { git = "https://github.com/evanlinjin/bdk", branch = "feature/bitcoind_rpc_emit_header" }
miniscript = { git = "https://github.com/evanlinjin/rust-miniscript", branch = "fix/plan-satisfy-does-not-append-witness-script-for-p2wsh" }

# bdk_chain = { git = "https://github.com/bitcoindevkit/bdk", branch = "master" }
# bdk_core = { git = "https://github.com/bitcoindevkit/bdk", branch = "master" }
# bdk_bitcoind_rpc = { git = "https://github.com/bitcoindevkit/bdk", branch = "master"}
4 changes: 0 additions & 4 deletions ci/pin-msrv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,3 @@

set -x
set -euo pipefail

cargo update -p home --precise "0.5.11"
cargo update -p time --precise "0.3.45"
cargo update -p time-core --precise "0.1.7"
34 changes: 21 additions & 13 deletions examples/anti_fee_sniping.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
#![allow(dead_code)]
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
use bdk_testenv::TestEnv;
use bdk_tx::{
filter_unspendable_now, group_by_spk, selection_algorithm_lowest_fee_bnb, FeeStrategy, Output,
PsbtParams, ScriptSource, SelectorParams,
};
use bitcoin::{absolute::LockTime, key::Secp256k1, Amount, FeeRate, Sequence};
use miniscript::Descriptor;
use bdk_tx_testenv::TestEnvExt;
use bitcoin::{absolute::LockTime, Amount, FeeRate, Sequence};

mod common;

use common::Wallet;
use common::{Wallet, EXTERNAL, INTERNAL};

fn main() -> anyhow::Result<()> {
let secp = Secp256k1::new();
let (external, _) = Descriptor::parse_descriptor(&secp, bdk_testenv::utils::DESCRIPTORS[0])?;
let (internal, _) = Descriptor::parse_descriptor(&secp, bdk_testenv::utils::DESCRIPTORS[1])?;

let env = TestEnv::new()?;
let old_client = env.old_rpc_client()?;
let genesis_hash = env.genesis_hash()?;
let genesis = env
.rpc_client()
.get_block_header(&genesis_hash)?
.block_header()?;
env.mine_blocks(101, None)?;

let mut wallet = Wallet::new(genesis_hash, external, internal.clone())?;
let mut wallet = Wallet::multi_keychain(
genesis,
[
(EXTERNAL, bdk_testenv::utils::DESCRIPTORS[0]),
(INTERNAL, bdk_testenv::utils::DESCRIPTORS[1]),
],
)?;
wallet.sync(&env)?;

let addr = wallet.next_address().expect("must derive address");
let addr = wallet.next_address(EXTERNAL).expect("must derive address");

let txid1 = env.send(&addr, Amount::ONE_BTC)?;
env.mine_blocks(1, None)?;
Expand All @@ -37,13 +44,14 @@ fn main() -> anyhow::Result<()> {

println!("Balance (confirmed): {}", wallet.balance());

let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?;
let (tip_height, tip_time) = wallet.tip_info(&old_client)?;
println!("Current height: {}", tip_height);
let longterm_feerate = FeeRate::from_sat_per_vb_unchecked(1);

let recipient_addr = env
.rpc_client()
.get_new_address(None, None)?
.address()?
.assume_checked();

// When anti-fee-sniping is enabled, the transaction will either use nLockTime or nSequence.
Expand Down Expand Up @@ -71,7 +79,7 @@ fn main() -> anyhow::Result<()> {
let selection = wallet
.all_candidates()
.regroup(group_by_spk())
.filter(filter_unspendable_now(tip_height, tip_time))
.filter(filter_unspendable_now(tip_height, Some(tip_time)))
.into_selection(
selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000),
SelectorParams::new(
Expand All @@ -80,7 +88,7 @@ fn main() -> anyhow::Result<()> {
recipient_addr.script_pubkey(),
Amount::from_sat(50_000_000),
)],
ScriptSource::Descriptor(Box::new(internal.at_derivation_index(0)?)),
ScriptSource::Descriptor(Box::new(wallet.definite_descriptor(INTERNAL, 0)?)),
wallet.change_policy(),
),
)?;
Expand Down
226 changes: 1 addition & 225 deletions examples/common.rs
Original file line number Diff line number Diff line change
@@ -1,225 +1 @@
use std::sync::Arc;

use bdk_bitcoind_rpc::{Emitter, NO_EXPECTED_MEMPOOL_TXIDS};
use bdk_chain::{
bdk_core, Anchor, Balance, CanonicalizationParams, ChainPosition, ConfirmationBlockTime,
};
use bdk_coin_select::{ChangePolicy, DrainWeights};
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
use bdk_tx::{CanonicalUnspents, Input, InputCandidates, RbfParams, TxStatus, TxWithStatus};
use bitcoin::{absolute, Address, Amount, BlockHash, OutPoint, Transaction, TxOut, 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, NO_EXPECTED_MEMPOOL_TXIDS);
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.new_txs);
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(),
CanonicalizationParams::default(),
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(),
CanonicalizationParams::default(),
)
.map(|c_tx| (c_tx.tx_node.tx, status_from_position(c_tx.chain_position)))
}

/// Computes the weight of a change output plus the future weight to spend it.
pub fn drain_weights(&self) -> DrainWeights {
// Get descriptor of change keychain at a derivation index.
let desc = self
.graph
.index
.get_descriptor(INTERNAL)
.unwrap()
.at_derivation_index(0)
.unwrap();

// Compute the weight of a change output for this wallet.
let output_weight = TxOut {
script_pubkey: desc.script_pubkey(),
value: Amount::ZERO,
}
.weight()
.to_wu();

// The spend weight is the default input weight plus the plan satisfaction weight
// (this code assumes that we're only dealing with segwit transactions).
let plan = desc.plan(&self.assets()).expect("failed to create Plan");
let spend_weight =
bitcoin::TxIn::default().segwit_weight().to_wu() + plan.satisfaction_weight() as u64;

DrainWeights {
output_weight,
spend_weight,
n_outputs: 1,
}
}

/// Get the default change policy for this wallet.
pub fn change_policy(&self) -> ChangePolicy {
let spk_0 = self
.graph
.index
.spk_at_index(INTERNAL, 0)
.expect("spk should exist in wallet");
ChangePolicy {
min_value: spk_0.minimal_non_dust().to_sat(),
drain_weights: self.drain_weights(),
}
}

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(),
))
}
}
pub use bdk_tx_testenv::*;
Loading