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
16 changes: 9 additions & 7 deletions packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,14 +766,16 @@ export class BitGoPsbt {
*/
extractTransaction(): ITransaction {
const networkType = this._wasm.get_network_type();

if (networkType === "dash") {
return DashTransaction.fromWasm(this._wasm.extract_dash_transaction());
}
if (networkType === "zcash") {
return ZcashTransaction.fromWasm(this._wasm.extract_zcash_transaction());
const wasm: unknown = this._wasm.extract_transaction();

switch (networkType) {
case "dash":
return DashTransaction.fromWasm(wasm as Parameters<typeof DashTransaction.fromWasm>[0]);
case "zcash":
return ZcashTransaction.fromWasm(wasm as Parameters<typeof ZcashTransaction.fromWasm>[0]);
default:
return Transaction.fromWasm(wasm as Parameters<typeof Transaction.fromWasm>[0]);
}
return Transaction.fromWasm(this._wasm.extract_bitcoin_transaction());
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ declare module "./wasm/wasm_utxo.js" {
validateSignatureAtInput(inputIndex: number, pubkey: Uint8Array): boolean;
verifySignatureWithKey(inputIndex: number, key: WasmBIP32): boolean;

// Extraction methods
extractTransaction(): WasmTransaction;

// Metadata methods
unsignedTxId(): string;
lockTime(): number;
Expand Down
39 changes: 31 additions & 8 deletions packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1490,17 +1490,40 @@ impl BitGoPsbt {
/// Extract the final transaction from a finalized PSBT
///
/// This method should be called after all inputs have been finalized.
/// It extracts the fully signed transaction with network-appropriate serialization.
/// It extracts the fully signed transaction as a WASM transaction instance
/// appropriate for the network (WasmTransaction, WasmDashTransaction, or WasmZcashTransaction).
///
/// # Returns
/// - `Ok(Vec<u8>)` containing the serialized transaction bytes
/// - `Ok(JsValue)` containing the WASM transaction instance
/// - `Err(WasmUtxoError)` if the PSBT is not fully finalized or extraction fails
pub fn extract_transaction(&self) -> Result<Vec<u8>, WasmUtxoError> {
// Clone and use extract_tx() which handles all network-specific serialization
self.psbt
.clone()
.extract_tx()
.map_err(|e| WasmUtxoError::new(&e))
pub fn extract_transaction(&self) -> Result<JsValue, WasmUtxoError> {
use crate::fixed_script_wallet::bitgo_psbt::BitGoPsbt as InnerBitGoPsbt;
match &self.psbt {
InnerBitGoPsbt::BitcoinLike(..) => {
let tx = self
.psbt
.clone()
.extract_bitcoin_tx()
.map_err(|e| WasmUtxoError::new(&e))?;
Ok(crate::wasm::transaction::WasmTransaction::from_tx(tx).into())
}
InnerBitGoPsbt::Dash(..) => {
let parts = self
.psbt
.clone()
.extract_dash_tx()
.map_err(|e| WasmUtxoError::new(&e))?;
Ok(crate::wasm::dash_transaction::WasmDashTransaction::from_parts(parts).into())
}
InnerBitGoPsbt::Zcash(..) => {
let parts = self
.psbt
.clone()
.extract_zcash_tx()
.map_err(|e| WasmUtxoError::new(&e))?;
Ok(crate::wasm::transaction::WasmZcashTransaction::from_parts(parts).into())
}
}
}

/// Extract the final transaction as a WasmTransaction (for BitcoinLike networks)
Expand Down
19 changes: 19 additions & 0 deletions packages/wasm-utxo/src/wasm/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,25 @@ impl WrapPsbt {
})
}

/// Extract the final transaction from a finalized PSBT
///
/// This method should be called after all inputs have been finalized.
/// It extracts the fully signed transaction as a WasmTransaction instance.
///
/// # Returns
/// - `Ok(WasmTransaction)` containing the extracted transaction
/// - `Err(WasmUtxoError)` if the PSBT is not fully finalized or extraction fails
#[wasm_bindgen(js_name = extractTransaction)]
pub fn extract_transaction(
&self,
) -> Result<crate::wasm::transaction::WasmTransaction, WasmUtxoError> {
let tx =
self.0.clone().extract_tx().map_err(|e| {
WasmUtxoError::new(&format!("Failed to extract transaction: {}", e))
})?;
Ok(crate::wasm::transaction::WasmTransaction::from_tx(tx))
}

/// Get the number of inputs in the PSBT
#[wasm_bindgen(js_name = inputCount)]
pub fn input_count(&self) -> usize {
Expand Down
75 changes: 75 additions & 0 deletions packages/wasm-utxo/test/psbtFromDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getKey } from "@bitgo/utxo-lib/dist/src/testutil";
import { DescriptorNode, formatNode } from "../js/ast/index.js";
import { mockPsbtDefault } from "./psbtFromDescriptor.util.js";
import { Descriptor } from "../js/index.js";
import { WasmTransaction } from "../js/wasm/wasm_utxo.js";
import { toWrappedPsbt } from "./psbt.util.js";

function toKeyWithPath(k: BIP32Interface, path = "*"): string {
Expand Down Expand Up @@ -154,3 +155,77 @@ describeSignDescriptor(
),
{ signECPair: [[toECPair(a)]] },
);

describe("WrapPsbt extractTransaction", function () {
function signFinalizeExtract(descriptor: Descriptor, signKeys: BIP32Interface[]) {
const psbt = mockPsbtDefault({
descriptorSelf: descriptor,
descriptorOther: Descriptor.fromString(
formatNode({ wpkh: toKeyWithPath(external) }),
"derivable",
),
});
const wrappedPsbt = toWrappedPsbt(psbt);
for (const key of signKeys) {
wrappedPsbt.signWithXprv(key.toBase58());
}
wrappedPsbt.finalize();
return wrappedPsbt.extractTransaction();
}

it("should extract transaction from finalized Wsh2Of3 PSBT", function () {
const tx = signFinalizeExtract(
fromNodes(
{ wsh: { multi: [2, toKeyWithPath(a), toKeyWithPath(b), toKeyWithPath(c)] } },
"derivable",
),
[a, b],
);

assert.strictEqual(typeof tx.get_txid(), "string");
assert.strictEqual(tx.get_txid().length, 64);
assert.ok(tx.get_vsize() > 0);
assert.ok(tx.to_bytes().length > 0);
});

it("should extract transaction from finalized Tr PSBT", function () {
const tx = signFinalizeExtract(
fromNodes(
{ tr: [toKeyWithPath(a), [{ pk: toKeyWithPath(b) }, { pk: toKeyWithPath(c) }]] },
"derivable",
),
[a],
);

assert.strictEqual(typeof tx.get_txid(), "string");
assert.strictEqual(tx.get_txid().length, 64);
assert.ok(tx.get_vsize() > 0);
assert.ok(tx.to_bytes().length > 0);
});

it("should produce consistent txid across repeated calls", function () {
const tx = signFinalizeExtract(
fromNodes(
{ wsh: { multi: [2, toKeyWithPath(a), toKeyWithPath(b), toKeyWithPath(c)] } },
"derivable",
),
[a, b],
);

assert.strictEqual(tx.get_txid(), tx.get_txid());
});

it("should produce a transaction whose bytes round-trip to the same txid", function () {
const tx = signFinalizeExtract(
fromNodes(
{ wsh: { multi: [2, toKeyWithPath(a), toKeyWithPath(b), toKeyWithPath(c)] } },
"derivable",
),
[a, b],
);

const txBytes = tx.to_bytes();
const tx2 = WasmTransaction.from_bytes(txBytes);
assert.strictEqual(tx2.get_txid(), tx.get_txid());
});
});