Skip to content

Commit 41607f8

Browse files
authored
Merge pull request #121 from BitGo/BTC-2988
feat(wasm-solana): return Transaction types from builder functions
2 parents c2939ea + 0b8d3c8 commit 41607f8

7 files changed

Lines changed: 138 additions & 80 deletions

File tree

packages/wasm-solana/js/builder.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
*/
77

88
import { BuilderNamespace } from "./wasm/wasm_solana.js";
9+
import { Transaction } from "./transaction.js";
10+
import { VersionedTransaction } from "./versioned.js";
911

1012
// =============================================================================
1113
// Nonce Types
@@ -463,48 +465,58 @@ export interface TransactionIntent {
463465
/**
464466
* Build a Solana transaction from a high-level intent.
465467
*
466-
* This function takes a declarative TransactionIntent and produces serialized
467-
* transaction bytes that can be signed and submitted to the network.
468+
* This function takes a declarative TransactionIntent and produces a Transaction
469+
* object that can be inspected, signed, and serialized.
468470
*
469-
* The returned transaction is unsigned - signatures should be added before
470-
* broadcasting.
471+
* The returned transaction is unsigned - signatures should be added via
472+
* `addSignature()` before serializing with `toBytes()` and broadcasting.
471473
*
472474
* @param intent - The transaction intent describing what to build
473-
* @returns Serialized unsigned transaction bytes (Uint8Array)
475+
* @returns A Transaction object that can be inspected, signed, and serialized
474476
* @throws Error if the intent cannot be built (e.g., invalid addresses)
475477
*
476478
* @example
477479
* ```typescript
478480
* import { buildTransaction } from '@bitgo/wasm-solana';
479481
*
480482
* // Build a simple SOL transfer
481-
* const txBytes = buildTransaction({
483+
* const tx = buildTransaction({
482484
* feePayer: sender,
483485
* nonce: { type: 'blockhash', value: blockhash },
484486
* instructions: [
485-
* { type: 'transfer', from: sender, to: recipient, lamports: '1000000' }
487+
* { type: 'transfer', from: sender, to: recipient, lamports: 1000000n }
486488
* ]
487489
* });
488490
*
489-
* // The returned bytes can be signed and broadcast
491+
* // Inspect the transaction
492+
* console.log(tx.feePayer);
493+
* console.log(tx.recentBlockhash);
494+
*
495+
* // Get the signable payload for signing
496+
* const payload = tx.signablePayload();
497+
*
498+
* // Add signature and serialize
499+
* tx.addSignature(signerPubkey, signature);
500+
* const txBytes = tx.toBytes();
490501
* ```
491502
*
492503
* @example
493504
* ```typescript
494505
* // Build with durable nonce and priority fee
495-
* const txBytes = buildTransaction({
506+
* const tx = buildTransaction({
496507
* feePayer: sender,
497508
* nonce: { type: 'durable', address: nonceAccount, authority: sender, value: nonceValue },
498509
* instructions: [
499510
* { type: 'computeBudget', unitLimit: 200000, unitPrice: 5000 },
500-
* { type: 'transfer', from: sender, to: recipient, lamports: '1000000' },
511+
* { type: 'transfer', from: sender, to: recipient, lamports: 1000000n },
501512
* { type: 'memo', message: 'BitGo transfer' }
502513
* ]
503514
* });
504515
* ```
505516
*/
506-
export function buildTransaction(intent: TransactionIntent): Uint8Array {
507-
return BuilderNamespace.build_transaction(intent);
517+
export function buildTransaction(intent: TransactionIntent): Transaction {
518+
const wasm = BuilderNamespace.build_transaction(intent);
519+
return Transaction.fromWasm(wasm);
508520
}
509521

510522
// =============================================================================
@@ -560,17 +572,17 @@ export interface RawVersionedTransactionData {
560572
*
561573
* This function is used for the `fromVersionedTransactionData()` path where we already
562574
* have pre-compiled versioned data (indexes + ALT refs). No instruction compilation
563-
* is needed - we just serialize the raw structure to bytes.
575+
* is needed - we just serialize the raw structure.
564576
*
565577
* @param data - Raw versioned transaction data
566-
* @returns Serialized unsigned versioned transaction bytes (Uint8Array)
578+
* @returns A VersionedTransaction object that can be inspected, signed, and serialized
567579
* @throws Error if the data is invalid
568580
*
569581
* @example
570582
* ```typescript
571583
* import { buildFromVersionedData } from '@bitgo/wasm-solana';
572584
*
573-
* const txBytes = buildFromVersionedData({
585+
* const tx = buildFromVersionedData({
574586
* staticAccountKeys: ['pubkey1', 'pubkey2', ...],
575587
* addressLookupTables: [
576588
* { accountKey: 'altPubkey', writableIndexes: [0, 1], readonlyIndexes: [2] }
@@ -585,8 +597,14 @@ export interface RawVersionedTransactionData {
585597
* },
586598
* recentBlockhash: 'blockhash'
587599
* });
600+
*
601+
* // Inspect, sign, and serialize
602+
* console.log(tx.feePayer);
603+
* tx.addSignature(signerPubkey, signature);
604+
* const txBytes = tx.toBytes();
588605
* ```
589606
*/
590-
export function buildFromVersionedData(data: RawVersionedTransactionData): Uint8Array {
591-
return BuilderNamespace.build_from_versioned_data(data);
607+
export function buildFromVersionedData(data: RawVersionedTransactionData): VersionedTransaction {
608+
const wasm = BuilderNamespace.build_from_versioned_data(data);
609+
return VersionedTransaction.fromWasm(wasm);
592610
}

packages/wasm-solana/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export {
4545
// Type exports
4646
export type { AccountMeta, Instruction } from "./transaction.js";
4747
export type {
48+
TransactionInput,
4849
ParsedTransaction,
4950
DurableNonce,
5051
InstructionParams,

packages/wasm-solana/js/parser.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
*/
99

1010
import { ParserNamespace } from "./wasm/wasm_solana.js";
11+
import type { Transaction } from "./transaction.js";
12+
import type { VersionedTransaction } from "./versioned.js";
13+
14+
/**
15+
* Input type for parseTransaction - accepts bytes or Transaction objects.
16+
*/
17+
export type TransactionInput = Uint8Array | Transaction | VersionedTransaction;
1118

1219
// =============================================================================
1320
// Instruction Types - matching BitGoJS InstructionParams
@@ -273,10 +280,10 @@ export interface ParsedTransaction {
273280
// =============================================================================
274281

275282
/**
276-
* Parse a serialized Solana transaction into structured data.
283+
* Parse a Solana transaction into structured data.
277284
*
278285
* This is the main entry point for transaction parsing. It deserializes the
279-
* transaction bytes and decodes all instructions into semantic types.
286+
* transaction and decodes all instructions into semantic types.
280287
*
281288
* All monetary amounts (amount, fee, lamports, poolTokens) are returned as bigint
282289
* directly from WASM - no post-processing needed.
@@ -285,17 +292,22 @@ export interface ParsedTransaction {
285292
* Consumers (like BitGoJS) may choose to filter NonceAdvance from instructionsData
286293
* since that info is also available in durableNonce.
287294
*
288-
* @param bytes - The raw transaction bytes (wire format)
295+
* @param input - Raw transaction bytes, Transaction, or VersionedTransaction
289296
* @returns A ParsedTransaction with all instructions decoded
290297
* @throws Error if the transaction cannot be parsed
291298
*
292299
* @example
293300
* ```typescript
294-
* import { parseTransaction } from '@bitgo/wasm-solana';
301+
* import { parseTransaction, buildTransaction, Transaction } from '@bitgo/wasm-solana';
295302
*
303+
* // From bytes
296304
* const txBytes = Buffer.from(base64EncodedTx, 'base64');
297305
* const parsed = parseTransaction(txBytes);
298306
*
307+
* // Directly from a Transaction object (no roundtrip through bytes)
308+
* const tx = buildTransaction(intent);
309+
* const parsed = parseTransaction(tx);
310+
*
299311
* console.log(parsed.feePayer);
300312
* for (const instr of parsed.instructionsData) {
301313
* if (instr.type === 'Transfer') {
@@ -304,6 +316,8 @@ export interface ParsedTransaction {
304316
* }
305317
* ```
306318
*/
307-
export function parseTransaction(bytes: Uint8Array): ParsedTransaction {
319+
export function parseTransaction(input: TransactionInput): ParsedTransaction {
320+
// If input is a Transaction or VersionedTransaction, extract bytes
321+
const bytes = input instanceof Uint8Array ? input : input.toBytes();
308322
return ParserNamespace.parse_transaction(bytes) as ParsedTransaction;
309323
}

packages/wasm-solana/js/transaction.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ export class Transaction {
5757
return new Transaction(wasm);
5858
}
5959

60+
/**
61+
* Create a Transaction from a WasmTransaction instance.
62+
* @internal Used by builder functions
63+
*/
64+
static fromWasm(wasm: WasmTransaction): Transaction {
65+
return new Transaction(wasm);
66+
}
67+
6068
/**
6169
* Get the fee payer address as a base58 string
6270
* Returns null if there are no account keys (shouldn't happen for valid transactions)

packages/wasm-solana/js/versioned.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ export class VersionedTransaction {
9090
return VersionedTransaction.fromBytes(bytes);
9191
}
9292

93+
/**
94+
* Create a VersionedTransaction from a WasmVersionedTransaction instance.
95+
* @internal Used by builder functions
96+
*/
97+
static fromWasm(wasm: WasmVersionedTransaction): VersionedTransaction {
98+
return new VersionedTransaction(wasm);
99+
}
100+
93101
/**
94102
* Create a versioned transaction from raw MessageV0 data.
95103
*
@@ -120,10 +128,9 @@ export class VersionedTransaction {
120128
* ```
121129
*/
122130
static fromVersionedData(data: RawVersionedTransactionData): VersionedTransaction {
123-
// Build the transaction bytes using WASM
124-
const bytes = BuilderNamespace.build_from_versioned_data(data);
125-
// Parse the bytes to create a VersionedTransaction
126-
return VersionedTransaction.fromBytes(bytes);
131+
// Build the transaction using WASM and wrap in TypeScript class
132+
const wasm = BuilderNamespace.build_from_versioned_data(data);
133+
return VersionedTransaction.fromWasm(wasm);
127134
}
128135

129136
/**

packages/wasm-solana/src/wasm/builder.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//! WASM binding for transaction building.
22
//!
33
//! Exposes transaction building functions:
4-
//! - `buildTransaction` - Creates transactions from a high-level intent structure
5-
//! - `buildFromVersionedData` - Creates versioned transactions from raw MessageV0 data
4+
//! - `buildTransaction` - Creates a Transaction from a high-level intent structure
5+
//! - `buildFromVersionedData` - Creates a VersionedTransaction from raw MessageV0 data
66
77
use crate::builder;
8+
use crate::wasm::transaction::{WasmTransaction, WasmVersionedTransaction};
89
use wasm_bindgen::prelude::*;
910

1011
/// Namespace for transaction building operations.
@@ -46,22 +47,26 @@ impl BuilderNamespace {
4647
///
4748
/// # Returns
4849
///
49-
/// Serialized unsigned transaction bytes (Uint8Array).
50+
/// A `Transaction` object that can be inspected, signed, and serialized.
5051
/// The transaction will have empty signature placeholders that can be
51-
/// filled in later by signing.
52+
/// filled in later by signing via `addSignature()`.
5253
///
5354
/// @param intent - The transaction intent as a JSON object
54-
/// @returns Serialized transaction bytes
55+
/// @returns Transaction object
5556
#[wasm_bindgen]
56-
pub fn build_transaction(intent: JsValue) -> Result<Vec<u8>, JsValue> {
57+
pub fn build_transaction(intent: JsValue) -> Result<WasmTransaction, JsValue> {
5758
// Deserialize the intent from JavaScript
5859
let intent: builder::TransactionIntent =
5960
serde_wasm_bindgen::from_value(intent).map_err(|e| {
6061
JsValue::from_str(&format!("Failed to parse transaction intent: {}", e))
6162
})?;
6263

63-
// Build the transaction
64-
builder::build_transaction(intent).map_err(|e| JsValue::from_str(&e.to_string()))
64+
// Build the transaction bytes
65+
let bytes =
66+
builder::build_transaction(intent).map_err(|e| JsValue::from_str(&e.to_string()))?;
67+
68+
// Wrap in WasmTransaction for rich API access
69+
WasmTransaction::from_bytes(&bytes).map_err(|e| JsValue::from_str(&e.to_string()))
6570
}
6671

6772
/// Build a versioned transaction directly from raw MessageV0 data.
@@ -91,9 +96,9 @@ impl BuilderNamespace {
9196
/// ```
9297
///
9398
/// @param data - Raw versioned transaction data as a JSON object
94-
/// @returns Serialized versioned transaction bytes (unsigned)
99+
/// @returns VersionedTransaction object
95100
#[wasm_bindgen]
96-
pub fn build_from_versioned_data(data: JsValue) -> Result<Vec<u8>, JsValue> {
101+
pub fn build_from_versioned_data(data: JsValue) -> Result<WasmVersionedTransaction, JsValue> {
97102
// Deserialize the raw versioned data from JavaScript
98103
let data: builder::RawVersionedTransactionData = serde_wasm_bindgen::from_value(data)
99104
.map_err(|e| {
@@ -103,7 +108,11 @@ impl BuilderNamespace {
103108
))
104109
})?;
105110

106-
// Build the versioned transaction
107-
builder::build_from_raw_versioned_data(&data).map_err(|e| JsValue::from_str(&e.to_string()))
111+
// Build the versioned transaction bytes
112+
let bytes = builder::build_from_raw_versioned_data(&data)
113+
.map_err(|e| JsValue::from_str(&e.to_string()))?;
114+
115+
// Wrap in WasmVersionedTransaction for rich API access
116+
WasmVersionedTransaction::from_bytes(&bytes).map_err(|e| JsValue::from_str(&e.to_string()))
108117
}
109118
}

0 commit comments

Comments
 (0)