Skip to content

Commit 947cd01

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo)!: return transaction object from extractTransaction
Changes extractTransaction() to return a Transaction object instead of raw bytes. The transaction object provides additional functionality like getId() to get the transaction ID directly. This is a breaking change as the return type changes from Uint8Array to a Transaction object. Clients need to call toBytes() on the returned object to get the raw bytes. Issue: BTC-2978 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 9f39008 commit 947cd01

10 files changed

Lines changed: 331 additions & 9 deletions

File tree

packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { type ECPairArg, ECPair } from "../ecpair.js";
66
import type { UtxolibName } from "../utxolibCompat.js";
77
import type { CoinName } from "../coinName.js";
88
import type { InputScriptType } from "./scriptType.js";
9+
import {
10+
Transaction,
11+
DashTransaction,
12+
ZcashTransaction,
13+
type ITransaction,
14+
} from "../transaction.js";
915

1016
export type { InputScriptType };
1117

@@ -755,11 +761,19 @@ export class BitGoPsbt {
755761
/**
756762
* Extract the final transaction from a finalized PSBT
757763
*
758-
* @returns The serialized transaction bytes
764+
* @returns The extracted transaction instance
759765
* @throws Error if the PSBT is not fully finalized or extraction fails
760766
*/
761-
extractTransaction(): Uint8Array {
762-
return this._wasm.extract_transaction();
767+
extractTransaction(): ITransaction {
768+
const networkType = this._wasm.get_network_type();
769+
770+
if (networkType === "dash") {
771+
return DashTransaction.fromWasm(this._wasm.extract_dash_transaction());
772+
}
773+
if (networkType === "zcash") {
774+
return ZcashTransaction.fromWasm(this._wasm.extract_zcash_transaction());
775+
}
776+
return Transaction.fromWasm(this._wasm.extract_bitcoin_transaction());
763777
}
764778

765779
/**

packages/wasm-utxo/js/fixedScriptWallet/ZcashBitGoPsbt.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BitGoPsbt as WasmBitGoPsbt } from "../wasm/wasm_utxo.js";
22
import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js";
33
import { BitGoPsbt, type CreateEmptyOptions } from "./BitGoPsbt.js";
4+
import { ZcashTransaction } from "../transaction.js";
45

56
/** Zcash network names */
67
export type ZcashNetworkName = "zcash" | "zcashTest" | "zec" | "tzec";
@@ -160,4 +161,14 @@ export class ZcashBitGoPsbt extends BitGoPsbt {
160161
get expiryHeight(): number {
161162
return this.wasm.expiry_height();
162163
}
164+
165+
/**
166+
* Extract the final Zcash transaction from a finalized PSBT
167+
*
168+
* @returns The extracted Zcash transaction instance
169+
* @throws Error if the PSBT is not fully finalized or extraction fails
170+
*/
171+
override extractTransaction(): ZcashTransaction {
172+
return ZcashTransaction.fromWasm(this.wasm.extract_zcash_transaction());
173+
}
163174
}

packages/wasm-utxo/js/transaction.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,48 @@
11
import { WasmDashTransaction, WasmTransaction, WasmZcashTransaction } from "./wasm/wasm_utxo.js";
22

3+
/**
4+
* Common interface for all transaction types
5+
*/
6+
export interface ITransaction {
7+
toBytes(): Uint8Array;
8+
getId(): string;
9+
}
10+
311
/**
412
* Transaction wrapper (Bitcoin-like networks)
513
*
614
* Provides a camelCase, strongly-typed API over the snake_case WASM bindings.
715
*/
8-
export class Transaction {
16+
export class Transaction implements ITransaction {
917
private constructor(private _wasm: WasmTransaction) {}
1018

1119
static fromBytes(bytes: Uint8Array): Transaction {
1220
return new Transaction(WasmTransaction.from_bytes(bytes));
1321
}
1422

23+
/**
24+
* @internal Create from WASM instance directly (avoids re-parsing bytes)
25+
*/
26+
static fromWasm(wasm: WasmTransaction): Transaction {
27+
return new Transaction(wasm);
28+
}
29+
1530
toBytes(): Uint8Array {
1631
return this._wasm.to_bytes();
1732
}
1833

34+
/**
35+
* Get the transaction ID (txid)
36+
*
37+
* The txid is the double SHA256 of the transaction bytes (excluding witness
38+
* data for segwit transactions), displayed in reverse byte order as is standard.
39+
*
40+
* @returns The transaction ID as a hex string
41+
*/
42+
getId(): string {
43+
return this._wasm.get_txid();
44+
}
45+
1946
/**
2047
* Get the virtual size of the transaction
2148
*
@@ -40,17 +67,36 @@ export class Transaction {
4067
*
4168
* Provides a camelCase, strongly-typed API over the snake_case WASM bindings.
4269
*/
43-
export class ZcashTransaction {
70+
export class ZcashTransaction implements ITransaction {
4471
private constructor(private _wasm: WasmZcashTransaction) {}
4572

4673
static fromBytes(bytes: Uint8Array): ZcashTransaction {
4774
return new ZcashTransaction(WasmZcashTransaction.from_bytes(bytes));
4875
}
4976

77+
/**
78+
* @internal Create from WASM instance directly (avoids re-parsing bytes)
79+
*/
80+
static fromWasm(wasm: WasmZcashTransaction): ZcashTransaction {
81+
return new ZcashTransaction(wasm);
82+
}
83+
5084
toBytes(): Uint8Array {
5185
return this._wasm.to_bytes();
5286
}
5387

88+
/**
89+
* Get the transaction ID (txid)
90+
*
91+
* The txid is the double SHA256 of the full Zcash transaction bytes,
92+
* displayed in reverse byte order as is standard.
93+
*
94+
* @returns The transaction ID as a hex string
95+
*/
96+
getId(): string {
97+
return this._wasm.get_txid();
98+
}
99+
54100
/**
55101
* @internal
56102
*/
@@ -64,17 +110,36 @@ export class ZcashTransaction {
64110
*
65111
* Round-trip only: bytes -> parse -> bytes.
66112
*/
67-
export class DashTransaction {
113+
export class DashTransaction implements ITransaction {
68114
private constructor(private _wasm: WasmDashTransaction) {}
69115

70116
static fromBytes(bytes: Uint8Array): DashTransaction {
71117
return new DashTransaction(WasmDashTransaction.from_bytes(bytes));
72118
}
73119

120+
/**
121+
* @internal Create from WASM instance directly (avoids re-parsing bytes)
122+
*/
123+
static fromWasm(wasm: WasmDashTransaction): DashTransaction {
124+
return new DashTransaction(wasm);
125+
}
126+
74127
toBytes(): Uint8Array {
75128
return this._wasm.to_bytes();
76129
}
77130

131+
/**
132+
* Get the transaction ID (txid)
133+
*
134+
* The txid is the double SHA256 of the full Dash transaction bytes,
135+
* displayed in reverse byte order as is standard.
136+
*
137+
* @returns The transaction ID as a hex string
138+
*/
139+
getId(): string {
140+
return this._wasm.get_txid();
141+
}
142+
78143
/**
79144
* @internal
80145
*/

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,61 @@ impl BitGoPsbt {
11471147
}
11481148
}
11491149

1150+
/// Extract the Bitcoin transaction directly (for BitcoinLike networks only)
1151+
///
1152+
/// # Returns
1153+
/// * `Ok(Transaction)` - The extracted transaction
1154+
/// * `Err(String)` - If not BitcoinLike or extraction fails
1155+
pub fn extract_bitcoin_tx(self) -> Result<miniscript::bitcoin::Transaction, String> {
1156+
match self {
1157+
BitGoPsbt::BitcoinLike(psbt, _) => psbt
1158+
.extract_tx()
1159+
.map_err(|e| format!("Failed to extract transaction: {}", e)),
1160+
_ => Err("extract_bitcoin_tx only supported for BitcoinLike networks".to_string()),
1161+
}
1162+
}
1163+
1164+
/// Extract the Dash transaction parts directly
1165+
///
1166+
/// # Returns
1167+
/// * `Ok(DashTransactionParts)` - The extracted transaction parts
1168+
/// * `Err(String)` - If not Dash or extraction fails
1169+
pub fn extract_dash_tx(self) -> Result<crate::dash::transaction::DashTransactionParts, String> {
1170+
use miniscript::bitcoin::consensus::serialize;
1171+
match self {
1172+
BitGoPsbt::Dash(dash_psbt, _) => {
1173+
let tx = dash_psbt
1174+
.psbt
1175+
.extract_tx()
1176+
.map_err(|e| format!("Failed to extract transaction: {}", e))?;
1177+
let tx_bytes = serialize(&tx);
1178+
crate::dash::transaction::decode_dash_transaction_parts(&tx_bytes)
1179+
.map_err(|e| format!("Failed to decode Dash transaction: {}", e))
1180+
}
1181+
_ => Err("extract_dash_tx only supported for Dash networks".to_string()),
1182+
}
1183+
}
1184+
1185+
/// Extract the Zcash transaction parts directly
1186+
///
1187+
/// # Returns
1188+
/// * `Ok(ZcashTransactionParts)` - The extracted transaction parts
1189+
/// * `Err(String)` - If not Zcash or extraction fails
1190+
pub fn extract_zcash_tx(
1191+
self,
1192+
) -> Result<crate::zcash::transaction::ZcashTransactionParts, String> {
1193+
match self {
1194+
BitGoPsbt::Zcash(zcash_psbt, _) => {
1195+
let bytes = zcash_psbt
1196+
.extract_tx()
1197+
.map_err(|e| format!("Failed to extract transaction: {}", e))?;
1198+
crate::zcash::transaction::decode_zcash_transaction_parts(&bytes)
1199+
.map_err(|e| format!("Failed to decode Zcash transaction: {}", e))
1200+
}
1201+
_ => Err("extract_zcash_tx only supported for Zcash networks".to_string()),
1202+
}
1203+
}
1204+
11501205
/// Extract a half-signed transaction in legacy format for p2ms-based script types.
11511206
///
11521207
/// This method extracts a transaction where each input has exactly one signature,

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ pub struct WasmDashTransaction {
77
parts: crate::dash::transaction::DashTransactionParts,
88
}
99

10+
impl WasmDashTransaction {
11+
/// Create from parts (internal use)
12+
pub(crate) fn from_parts(parts: crate::dash::transaction::DashTransactionParts) -> Self {
13+
WasmDashTransaction { parts }
14+
}
15+
}
16+
1017
#[wasm_bindgen]
1118
impl WasmDashTransaction {
1219
/// Deserialize a Dash transaction from bytes (supports EVO special tx extra payload).
@@ -24,4 +31,26 @@ impl WasmDashTransaction {
2431
WasmUtxoError::new(&format!("Failed to serialize Dash transaction: {}", e))
2532
})
2633
}
34+
35+
/// Get the transaction ID (txid)
36+
///
37+
/// The txid is the double SHA256 of the full Dash transaction bytes,
38+
/// displayed in reverse byte order (big-endian) as is standard.
39+
///
40+
/// # Returns
41+
/// The transaction ID as a hex string
42+
///
43+
/// # Errors
44+
/// Returns an error if the transaction cannot be serialized
45+
pub fn get_txid(&self) -> Result<String, WasmUtxoError> {
46+
use miniscript::bitcoin::hashes::{sha256d, Hash};
47+
use miniscript::bitcoin::Txid;
48+
let tx_bytes = crate::dash::transaction::encode_dash_transaction_parts(&self.parts)
49+
.map_err(|e| {
50+
WasmUtxoError::new(&format!("Failed to serialize Dash transaction: {}", e))
51+
})?;
52+
let hash = sha256d::Hash::hash(&tx_bytes);
53+
let txid = Txid::from_raw_hash(hash);
54+
Ok(txid.to_string())
55+
}
2756
}

packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,19 @@ impl BitGoPsbt {
585585
self.psbt.network().to_string()
586586
}
587587

588+
/// Get the network type for transaction extraction
589+
///
590+
/// Returns "bitcoin", "dash", or "zcash" to indicate which transaction
591+
/// wrapper class should be used in TypeScript.
592+
pub fn get_network_type(&self) -> String {
593+
use crate::fixed_script_wallet::bitgo_psbt::BitGoPsbt as InnerBitGoPsbt;
594+
match &self.psbt {
595+
InnerBitGoPsbt::BitcoinLike(_, _) => "bitcoin".to_string(),
596+
InnerBitGoPsbt::Dash(_, _) => "dash".to_string(),
597+
InnerBitGoPsbt::Zcash(_, _) => "zcash".to_string(),
598+
}
599+
}
600+
588601
/// Get the transaction version
589602
pub fn version(&self) -> i32 {
590603
self.psbt.psbt().unsigned_tx.version.0
@@ -1490,6 +1503,53 @@ impl BitGoPsbt {
14901503
.map_err(|e| WasmUtxoError::new(&e))
14911504
}
14921505

1506+
/// Extract the final transaction as a WasmTransaction (for BitcoinLike networks)
1507+
///
1508+
/// This avoids re-parsing bytes by returning the transaction directly.
1509+
/// Only valid for Bitcoin-like networks (not Dash or Zcash).
1510+
pub fn extract_bitcoin_transaction(
1511+
&self,
1512+
) -> Result<crate::wasm::transaction::WasmTransaction, WasmUtxoError> {
1513+
let tx = self
1514+
.psbt
1515+
.clone()
1516+
.extract_bitcoin_tx()
1517+
.map_err(|e| WasmUtxoError::new(&e))?;
1518+
Ok(crate::wasm::transaction::WasmTransaction::from_tx(tx))
1519+
}
1520+
1521+
/// Extract the final transaction as a WasmDashTransaction (for Dash networks)
1522+
///
1523+
/// This avoids re-parsing bytes by returning the transaction directly.
1524+
/// Only valid for Dash networks.
1525+
pub fn extract_dash_transaction(
1526+
&self,
1527+
) -> Result<crate::wasm::dash_transaction::WasmDashTransaction, WasmUtxoError> {
1528+
let parts = self
1529+
.psbt
1530+
.clone()
1531+
.extract_dash_tx()
1532+
.map_err(|e| WasmUtxoError::new(&e))?;
1533+
Ok(crate::wasm::dash_transaction::WasmDashTransaction::from_parts(parts))
1534+
}
1535+
1536+
/// Extract the final transaction as a WasmZcashTransaction (for Zcash networks)
1537+
///
1538+
/// This avoids re-parsing bytes by returning the transaction directly.
1539+
/// Only valid for Zcash networks.
1540+
pub fn extract_zcash_transaction(
1541+
&self,
1542+
) -> Result<crate::wasm::transaction::WasmZcashTransaction, WasmUtxoError> {
1543+
let parts = self
1544+
.psbt
1545+
.clone()
1546+
.extract_zcash_tx()
1547+
.map_err(|e| WasmUtxoError::new(&e))?;
1548+
Ok(crate::wasm::transaction::WasmZcashTransaction::from_parts(
1549+
parts,
1550+
))
1551+
}
1552+
14931553
/// Extract a half-signed transaction in legacy format for p2ms-based script types.
14941554
///
14951555
/// This method extracts a transaction where each input has exactly one signature,

0 commit comments

Comments
 (0)