Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.

Commit aaa9e2c

Browse files
committed
done
1 parent b712a8c commit aaa9e2c

File tree

7 files changed

+239
-57
lines changed

7 files changed

+239
-57
lines changed

src/bitcoin/tx_builder.rs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::{cell::RefCell, rc::Rc};
22

3-
use bdk_wallet::{bitcoin::ScriptBuf, error::CreateTxError, Wallet as BdkWallet};
3+
use bdk_wallet::{
4+
bitcoin::ScriptBuf as BdkScriptBuf, error::CreateTxError, TxOrdering as BdkTxOrdering, Wallet as BdkWallet,
5+
};
46
use serde::Serialize;
57
use wasm_bindgen::prelude::wasm_bindgen;
68

7-
use crate::types::{Address, Amount, BdkError, BdkErrorCode, FeeRate, OutPoint, Psbt, Recipient};
9+
use crate::types::{Amount, BdkError, BdkErrorCode, FeeRate, OutPoint, Psbt, Recipient, ScriptBuf};
810

911
/// A transaction builder.
1012
///
@@ -20,8 +22,9 @@ pub struct TxBuilder {
2022
unspendable: Vec<OutPoint>,
2123
fee_rate: FeeRate,
2224
drain_wallet: bool,
23-
drain_to: Option<ScriptBuf>,
25+
drain_to: Option<BdkScriptBuf>,
2426
allow_dust: bool,
27+
ordering: BdkTxOrdering,
2528
}
2629

2730
#[wasm_bindgen]
@@ -36,6 +39,7 @@ impl TxBuilder {
3639
drain_wallet: false,
3740
allow_dust: false,
3841
drain_to: None,
42+
ordering: BdkTxOrdering::default(),
3943
}
4044
}
4145

@@ -95,8 +99,8 @@ impl TxBuilder {
9599
///
96100
/// If you choose not to set any recipients, you should provide the utxos that the
97101
/// transaction should spend via [`add_utxos`].
98-
pub fn drain_to(mut self, address: Address) -> Self {
99-
self.drain_to = Some(address.script_pubkey());
102+
pub fn drain_to(mut self, script_pubkey: ScriptBuf) -> Self {
103+
self.drain_to = Some(script_pubkey.into());
100104
self
101105
}
102106

@@ -108,6 +112,12 @@ impl TxBuilder {
108112
self
109113
}
110114

115+
/// Choose the ordering for inputs and outputs of the transaction
116+
pub fn ordering(mut self, ordering: TxOrdering) -> Self {
117+
self.ordering = ordering.into();
118+
self
119+
}
120+
111121
/// Finish building the transaction.
112122
///
113123
/// Returns a new [`Psbt`] per [`BIP174`].
@@ -116,6 +126,7 @@ impl TxBuilder {
116126
let mut builder = wallet.build_tx();
117127

118128
builder
129+
.ordering(self.ordering.into())
119130
.set_recipients(self.recipients.into_iter().map(Into::into).collect())
120131
.unspendable(self.unspendable.into_iter().map(Into::into).collect())
121132
.fee_rate(self.fee_rate.into())
@@ -134,6 +145,36 @@ impl TxBuilder {
134145
}
135146
}
136147

148+
/// Ordering of the transaction's inputs and outputs
149+
#[derive(Clone, Default)]
150+
#[wasm_bindgen]
151+
pub enum TxOrdering {
152+
/// Randomized (default)
153+
#[default]
154+
Shuffle,
155+
/// Unchanged
156+
Untouched,
157+
}
158+
159+
impl From<BdkTxOrdering> for TxOrdering {
160+
fn from(ordering: BdkTxOrdering) -> Self {
161+
match ordering {
162+
BdkTxOrdering::Shuffle => TxOrdering::Shuffle,
163+
BdkTxOrdering::Untouched => TxOrdering::Untouched,
164+
_ => panic!("Unsupported ordering"),
165+
}
166+
}
167+
}
168+
169+
impl From<TxOrdering> for BdkTxOrdering {
170+
fn from(ordering: TxOrdering) -> Self {
171+
match ordering {
172+
TxOrdering::Shuffle => BdkTxOrdering::Shuffle,
173+
TxOrdering::Untouched => BdkTxOrdering::Untouched,
174+
}
175+
}
176+
}
177+
137178
/// Wallet's UTXO set is not enough to cover recipient's requested plus fee.
138179
#[wasm_bindgen]
139180
#[derive(Clone, Serialize)]

src/types/address.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use bdk_wallet::{
44
bitcoin::{Address as BdkAddress, AddressType as BdkAddressType, Network as BdkNetwork, ScriptBuf as BdkScriptBuf},
55
AddressInfo as BdkAddressInfo,
66
};
7-
use bitcoin::address::ParseError;
7+
use bitcoin::{
8+
address::ParseError,
9+
hashes::{sha256, Hash},
10+
};
811
use wasm_bindgen::prelude::wasm_bindgen;
912

1013
use crate::{
@@ -96,6 +99,16 @@ impl Address {
9699
pub fn to_string(&self) -> String {
97100
self.0.to_string()
98101
}
102+
103+
#[wasm_bindgen(getter)]
104+
pub fn script_pubkey(&self) -> ScriptBuf {
105+
self.0.script_pubkey().into()
106+
}
107+
108+
#[wasm_bindgen(getter)]
109+
pub fn scripthash(&self) -> String {
110+
sha256::Hash::hash(self.0.script_pubkey().as_bytes()).to_string()
111+
}
99112
}
100113

101114
impl From<BdkAddress> for Address {
@@ -133,6 +146,7 @@ impl From<ParseError> for BdkError {
133146
/// `ScriptBuf` is the most common script type that has the ownership over the contents of the
134147
/// script. It has a close relationship with its borrowed counterpart, [`Script`].
135148
#[wasm_bindgen]
149+
#[derive(Clone)]
136150
pub struct ScriptBuf(BdkScriptBuf);
137151

138152
impl Deref for ScriptBuf {
@@ -154,6 +168,18 @@ impl ScriptBuf {
154168
pub fn as_bytes(&self) -> Vec<u8> {
155169
self.0.as_bytes().to_vec()
156170
}
171+
172+
pub fn to_asm_string(&self) -> String {
173+
self.0.to_asm_string()
174+
}
175+
176+
pub fn to_hex_string(&self) -> String {
177+
self.0.to_hex_string()
178+
}
179+
180+
pub fn is_op_return(&self) -> bool {
181+
self.0.is_op_return()
182+
}
157183
}
158184

159185
impl From<BdkScriptBuf> for ScriptBuf {

src/types/psbt.rs

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use bdk_wallet::{
1010
use wasm_bindgen::prelude::wasm_bindgen;
1111

1212
use crate::result::JsResult;
13+
use crate::types::ScriptBuf;
1314

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

@@ -33,11 +34,25 @@ impl DerefMut for Psbt {
3334

3435
#[wasm_bindgen]
3536
impl Psbt {
37+
/// Extracts the [`Transaction`] from a [`Psbt`] by filling in the available signature information.
38+
///
39+
/// ## Errors
40+
///
41+
/// [`ExtractTxError`] variants will contain either the [`Psbt`] itself or the [`Transaction`]
42+
/// that was extracted. These can be extracted from the Errors in order to recover.
43+
/// See the error documentation for info on the variants. In general, it covers large fees.
44+
pub fn extract_tx_fee_rate_limit(self) -> JsResult<Transaction> {
45+
let tx = self.0.extract_tx_fee_rate_limit()?;
46+
Ok(tx.into())
47+
}
48+
49+
/// An alias for [`extract_tx_fee_rate_limit`].
3650
pub fn extract_tx(self) -> JsResult<Transaction> {
3751
let tx = self.0.extract_tx()?;
3852
Ok(tx.into())
3953
}
4054

55+
/// Extracts the [`Transaction`] from a [`Psbt`] by filling in the available signature information.
4156
pub fn extract_tx_with_fee_rate_limit(self, max_fee_rate: FeeRate) -> JsResult<Transaction> {
4257
let tx = self.0.extract_tx_with_fee_rate_limit(max_fee_rate.into())?;
4358
Ok(tx.into())
@@ -48,16 +63,41 @@ impl Psbt {
4863
Ok(fee.into())
4964
}
5065

66+
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats.
67+
/// If the PSBT is missing a TxOut for an input returns None.
5168
pub fn fee_amount(&self) -> Option<Amount> {
5269
let fee_amount = self.0.fee_amount();
5370
fee_amount.map(Into::into)
5471
}
5572

73+
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
74+
/// `Psbt` is finalized and all witness/signature data is added to the transaction.
75+
/// If the PSBT is missing a TxOut for an input returns None.
5676
pub fn fee_rate(&self) -> Option<FeeRate> {
5777
let fee_rate = self.0.fee_rate();
5878
fee_rate.map(Into::into)
5979
}
6080

81+
/// The version number of this PSBT. If omitted, the version number is 0.
82+
#[wasm_bindgen(getter)]
83+
pub fn version(&self) -> u32 {
84+
self.0.version
85+
}
86+
87+
/// Combines this [`Psbt`] with `other` PSBT as described by BIP 174. In-place.
88+
///
89+
/// In accordance with BIP 174 this function is commutative i.e., `A.combine(B) == B.combine(A)`
90+
pub fn combine(&mut self, other: Psbt) -> JsResult<()> {
91+
self.0.combine(other.into())?;
92+
Ok(())
93+
}
94+
95+
/// The unsigned transaction, scriptSigs and witnesses for each input must be empty.
96+
#[wasm_bindgen(getter)]
97+
pub fn unsigned_tx(&self) -> Transaction {
98+
self.0.unsigned_tx.clone().into()
99+
}
100+
61101
/// Serialize the PSBT to a string in base64 format
62102
#[allow(clippy::inherent_to_string)]
63103
#[wasm_bindgen(js_name = toString)]
@@ -92,30 +132,40 @@ impl From<Psbt> for BdkPsbt {
92132
#[wasm_bindgen]
93133
#[derive(Clone)]
94134
pub struct Recipient {
95-
address: Address,
96-
amount: Amount,
135+
script_pubkey: BdkScriptBuf,
136+
amount: BdkAmount,
97137
}
98138

99139
#[wasm_bindgen]
100140
impl Recipient {
101141
#[wasm_bindgen(constructor)]
102-
pub fn new(address: Address, amount: Amount) -> Self {
103-
Recipient { address, amount }
142+
pub fn new(script_pubkey: ScriptBuf, amount: Amount) -> Self {
143+
Recipient {
144+
script_pubkey: script_pubkey.into(),
145+
amount: amount.into(),
146+
}
147+
}
148+
149+
pub fn from_address(address: Address, amount: Amount) -> Self {
150+
Recipient {
151+
script_pubkey: address.script_pubkey().into(),
152+
amount: amount.into(),
153+
}
104154
}
105155

106156
#[wasm_bindgen(getter)]
107-
pub fn address(&self) -> Address {
108-
self.address.clone()
157+
pub fn script_pubkey(&self) -> ScriptBuf {
158+
self.script_pubkey.clone().into()
109159
}
110160

111161
#[wasm_bindgen(getter)]
112162
pub fn amount(&self) -> Amount {
113-
self.amount
163+
self.amount.into()
114164
}
115165
}
116166

117167
impl From<Recipient> for (BdkScriptBuf, BdkAmount) {
118168
fn from(r: Recipient) -> Self {
119-
(r.address().script_pubkey(), r.amount().into())
169+
(r.script_pubkey.clone(), r.amount)
120170
}
121171
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {
2+
Address,
3+
Amount,
4+
BdkError,
5+
BdkErrorCode,
6+
} from "../../../pkg/bitcoindevkit";
7+
import type { Network } from "../../../pkg/bitcoindevkit";
8+
9+
describe("Wallet", () => {
10+
const network: Network = "testnet";
11+
12+
it("catches fine-grained address errors", () => {
13+
try {
14+
Address.from_string(
15+
"tb1qd28npep0s8frcm3y7dxqajkcy2m40eysplyr9v",
16+
"bitcoin"
17+
);
18+
} catch (error) {
19+
expect(error).toBeInstanceOf(BdkError);
20+
21+
const { code, message, data } = error;
22+
expect(code).toBe(BdkErrorCode.NetworkValidation);
23+
expect(message.startsWith("validation error")).toBe(true);
24+
expect(data).toBeUndefined();
25+
}
26+
27+
try {
28+
Address.from_string("notAnAddress", network);
29+
} catch (error) {
30+
expect(error).toBeInstanceOf(BdkError);
31+
32+
const { code, message, data } = error;
33+
expect(code).toBe(BdkErrorCode.Base58);
34+
expect(message.startsWith("base58 error")).toBe(true);
35+
expect(data).toBeUndefined();
36+
}
37+
});
38+
39+
it("catches fine-grained amount errors", () => {
40+
try {
41+
Amount.from_btc(-100000000);
42+
} catch (error) {
43+
expect(error).toBeInstanceOf(BdkError);
44+
45+
const { code, message, data } = error;
46+
expect(code).toBe(BdkErrorCode.OutOfRange);
47+
expect(message.startsWith("amount out of range")).toBe(true);
48+
expect(data).toBeUndefined();
49+
}
50+
});
51+
});

tests/node/integration/esplora.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
UnconfirmedTx,
99
Wallet,
1010
SignOptions,
11+
Psbt,
12+
TxOrdering,
1113
} from "../../../pkg/bitcoindevkit";
1214

1315
// Tests are expected to run in order
@@ -69,7 +71,7 @@ describe("Esplora client", () => {
6971
const psbt = wallet
7072
.build_tx()
7173
.fee_rate(feeRate)
72-
.add_recipient(new Recipient(recipientAddress, sendAmount))
74+
.add_recipient(new Recipient(recipientAddress.script_pubkey, sendAmount))
7375
.finish();
7476

7577
expect(psbt.fee().to_sat()).toBeGreaterThan(100); // We cannot know the exact fees
@@ -105,4 +107,30 @@ describe("Esplora client", () => {
105107
.finish();
106108
}).toThrow();
107109
});
110+
111+
it("fills inputs of an output-only Psbt", () => {
112+
const psbtBase64 =
113+
"cHNidP8BAI4CAAAAAAM1gwEAAAAAACJRIORP1Ndiq325lSC/jMG0RlhATHYmuuULfXgEHUM3u5i4AAAAAAAAAAAxai8AAUSx+i9Igg4HWdcpyagCs8mzuRCklgA7nRMkm69rAAAAAAAAAAAAAQACAAAAACp2AAAAAAAAFgAUArpyBMj+3+/wQDj+orDWG4y4yfUAAAAAAAAAAAA=";
114+
const template = Psbt.from_string(psbtBase64);
115+
116+
let builder = wallet
117+
.build_tx()
118+
.fee_rate(new FeeRate(BigInt(1)))
119+
.ordering(TxOrdering.Untouched);
120+
121+
for (const txout of template.unsigned_tx.output) {
122+
if (wallet.is_mine(txout.script_pubkey)) {
123+
builder = builder.drain_to(txout.script_pubkey);
124+
} else {
125+
const recipient = new Recipient(txout.script_pubkey, txout.value);
126+
builder = builder.add_recipient(recipient);
127+
}
128+
}
129+
130+
const psbt = builder.finish();
131+
expect(psbt.unsigned_tx.output).toHaveLength(
132+
template.unsigned_tx.output.length
133+
);
134+
expect(psbt.unsigned_tx.tx_out(2).value.to_btc()).toBeGreaterThan(0);
135+
});
108136
});

0 commit comments

Comments
 (0)