Skip to content

Commit db2b2e9

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add getOutputsWithAddress method to PSBT
Adds a new method to PSBT that returns outputs with resolved address strings derived from each output script. This allows getting proper addresses formatted for specific coin networks without needing to call address.fromOutputScript separately for each output. Issue: BTC-2866 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 587eeb6 commit db2b2e9

4 files changed

Lines changed: 132 additions & 2 deletions

File tree

packages/wasm-utxo/js/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ declare module "./wasm/wasm_utxo.js" {
8888
tapBip32Derivation: PsbtBip32Derivation[];
8989
}
9090

91+
/** PSBT output data with resolved address, returned by getOutputsWithAddress() */
92+
interface PsbtOutputDataWithAddress extends PsbtOutputData {
93+
address: string;
94+
}
95+
9196
interface WrapPsbt {
9297
// Signing methods (legacy - kept for backwards compatibility)
9398
signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult;
@@ -102,6 +107,7 @@ declare module "./wasm/wasm_utxo.js" {
102107
outputCount(): number;
103108
getInputs(): PsbtInputData[];
104109
getOutputs(): PsbtOutputData[];
110+
getOutputsWithAddress(coin: import("./coinName.js").CoinName): PsbtOutputDataWithAddress[];
105111
getPartialSignatures(inputIndex: number): Array<{
106112
pubkey: Uint8Array;
107113
signature: Uint8Array;

packages/wasm-utxo/src/wasm/psbt.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::address::networks::{from_output_script_with_network_and_format, AddressFormat};
12
use crate::error::WasmUtxoError;
23
use crate::wasm::bip32::WasmBIP32;
34
use crate::wasm::descriptor::WrapDescriptorEnum;
@@ -11,7 +12,7 @@ use miniscript::bitcoin::transaction::{Transaction, Version};
1112
use miniscript::bitcoin::{
1213
bip32, psbt, Amount, OutPoint, PublicKey, ScriptBuf, Sequence, XOnlyPublicKey,
1314
};
14-
use miniscript::bitcoin::{PrivateKey, Psbt, TxIn, TxOut, Txid};
15+
use miniscript::bitcoin::{PrivateKey, Psbt, Script, TxIn, TxOut, Txid};
1516
use miniscript::descriptor::{SinglePub, SinglePubKey};
1617
use miniscript::psbt::PsbtExt;
1718
use miniscript::{DescriptorPublicKey, ToPublicKey};
@@ -176,6 +177,32 @@ impl PsbtOutputData {
176177
}
177178
}
178179

180+
/// PSBT output data with a resolved address string (requires a coin name for encoding).
181+
#[derive(Debug, Clone)]
182+
pub struct PsbtOutputDataWithAddress {
183+
pub script: Vec<u8>,
184+
pub value: u64,
185+
pub address: String,
186+
pub bip32_derivation: Vec<Bip32Derivation>,
187+
pub tap_bip32_derivation: Vec<Bip32Derivation>,
188+
}
189+
190+
impl PsbtOutputDataWithAddress {
191+
pub fn from(base: PsbtOutputData, network: crate::Network) -> Result<Self, WasmUtxoError> {
192+
let script_obj = Script::from_bytes(&base.script);
193+
let address =
194+
from_output_script_with_network_and_format(script_obj, network, AddressFormat::Default)
195+
.map_err(|e| WasmUtxoError::new(&e.to_string()))?;
196+
Ok(PsbtOutputDataWithAddress {
197+
script: base.script,
198+
value: base.value,
199+
address,
200+
bip32_derivation: base.bip32_derivation,
201+
tap_bip32_derivation: base.tap_bip32_derivation,
202+
})
203+
}
204+
}
205+
179206
#[wasm_bindgen]
180207
pub struct WrapPsbt(Psbt);
181208

@@ -579,6 +606,28 @@ impl WrapPsbt {
579606
outputs.try_to_js_value()
580607
}
581608

609+
/// Get all PSBT outputs with resolved address strings.
610+
///
611+
/// Like `getOutputs()` but each element also includes an `address` field
612+
/// derived from the output script using the given coin name (e.g. "btc", "tbtc").
613+
#[wasm_bindgen(js_name = getOutputsWithAddress)]
614+
pub fn get_outputs_with_address(&self, coin: &str) -> Result<JsValue, WasmUtxoError> {
615+
let network = crate::Network::from_coin_name(coin)
616+
.ok_or_else(|| WasmUtxoError::new(&format!("Unknown coin: {}", coin)))?;
617+
let outputs: Vec<PsbtOutputDataWithAddress> = self
618+
.0
619+
.unsigned_tx
620+
.output
621+
.iter()
622+
.zip(self.0.outputs.iter())
623+
.map(|(tx_out, psbt_out)| {
624+
let base = PsbtOutputData::from(tx_out, psbt_out);
625+
PsbtOutputDataWithAddress::from(base, network)
626+
})
627+
.collect::<Result<Vec<_>, _>>()?;
628+
outputs.try_to_js_value()
629+
}
630+
582631
/// Get partial signatures for an input
583632
/// Returns array of { pubkey: Uint8Array, signature: Uint8Array }
584633
#[wasm_bindgen(js_name = getPartialSignatures)]

packages/wasm-utxo/src/wasm/try_into_js_value.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,18 @@ impl TryIntoJsValue for crate::wasm::psbt::PsbtOutputData {
447447
}
448448
}
449449

450+
impl TryIntoJsValue for crate::wasm::psbt::PsbtOutputDataWithAddress {
451+
fn try_to_js_value(&self) -> Result<JsValue, WasmUtxoError> {
452+
js_obj!(
453+
"script" => self.script.clone(),
454+
"value" => self.value,
455+
"address" => self.address.clone(),
456+
"bip32Derivation" => self.bip32_derivation.clone(),
457+
"tapBip32Derivation" => self.tap_bip32_derivation.clone()
458+
)
459+
}
460+
}
461+
450462
/// A partial signature with its associated public key
451463
#[derive(Clone)]
452464
pub struct PartialSignature {

packages/wasm-utxo/test/testutilsDescriptor.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as assert from "node:assert";
22

3-
import { Descriptor, Psbt, testutils } from "../js/index.js";
3+
import { Descriptor, Psbt, address } from "../js/index.js";
4+
import * as dt from "../js/testutils/descriptor/index.js";
5+
import * as testutils from "../js/testutils/index.js";
46

57
describe("testutils.descriptor", () => {
68
describe("getDefaultXPubs", () => {
@@ -95,6 +97,67 @@ describe("testutils.descriptor.mockPsbt", () => {
9597
});
9698
});
9799

100+
describe("Psbt.getOutputsWithAddress", () => {
101+
it("returns outputs with address strings for btc", () => {
102+
const psbt = dt.mockPsbtDefault();
103+
const outputs = psbt.getOutputsWithAddress("btc");
104+
assert.strictEqual(outputs.length, 2);
105+
for (const output of outputs) {
106+
assert.ok(typeof output.address === "string", "address should be a string");
107+
assert.ok(output.address.length > 0, "address should not be empty");
108+
assert.ok(output.script instanceof Uint8Array, "script should be Uint8Array");
109+
assert.ok(typeof output.value === "bigint", "value should be bigint");
110+
assert.ok(Array.isArray(output.bip32Derivation), "bip32Derivation should be array");
111+
assert.ok(Array.isArray(output.tapBip32Derivation), "tapBip32Derivation should be array");
112+
}
113+
});
114+
115+
it("returns consistent addresses with address.fromOutputScriptWithCoin", () => {
116+
const psbt = dt.mockPsbtDefault();
117+
const outputsWithAddr = psbt.getOutputsWithAddress("btc");
118+
const rawOutputs = psbt.getOutputs();
119+
for (let i = 0; i < rawOutputs.length; i++) {
120+
const expected = address.fromOutputScriptWithCoin(rawOutputs[i].script, "btc");
121+
assert.strictEqual(outputsWithAddr[i].address, expected);
122+
}
123+
});
124+
125+
it("returns btc mainnet addresses starting with expected prefixes", () => {
126+
const psbt = dt.mockPsbtDefaultWithDescriptorTemplate("Wsh2Of3");
127+
const outputs = psbt.getOutputsWithAddress("btc");
128+
for (const output of outputs) {
129+
// p2wsh addresses start with bc1
130+
assert.ok(output.address.startsWith("bc1"), `expected bc1 prefix, got ${output.address}`);
131+
}
132+
});
133+
134+
it("returns tbtc testnet addresses starting with expected prefixes", () => {
135+
const psbt = dt.mockPsbtDefaultWithDescriptorTemplate("Wsh2Of3");
136+
const outputs = psbt.getOutputsWithAddress("tbtc");
137+
for (const output of outputs) {
138+
// p2wsh testnet addresses start with tb1
139+
assert.ok(output.address.startsWith("tb1"), `expected tb1 prefix, got ${output.address}`);
140+
}
141+
});
142+
143+
it("works for all wsh/tr templates", () => {
144+
for (const t of [
145+
"Wsh2Of3",
146+
"Wsh2Of2",
147+
"Wsh2Of3CltvDrop",
148+
"Tr2Of3-NoKeyPath",
149+
"Tr1Of3-NoKeyPath-Tree",
150+
] as dt.DescriptorTemplate[]) {
151+
const psbt = dt.mockPsbtDefaultWithDescriptorTemplate(t, dt.getPsbtParams(t));
152+
const outputs = psbt.getOutputsWithAddress("btc");
153+
assert.strictEqual(outputs.length, 2, `${t}: expected 2 outputs`);
154+
for (const output of outputs) {
155+
assert.ok(output.address.length > 0, `${t}: address should not be empty`);
156+
}
157+
}
158+
});
159+
});
160+
98161
describe("testutils.fixtures", () => {
99162
describe("jsonNormalize", () => {
100163
it("round-trips a simple object", () => {

0 commit comments

Comments
 (0)