Skip to content

Commit 7dad774

Browse files
committed
refactor: follow parse convention for wasm-solana
Remove `explainTransaction` from wasm-solana per the convention that explain logic belongs in BitGoJS, not in wasm packages (PR #176). - Delete `explain.ts` — type derivation, output/input extraction, fee calculation all move to BitGoJS sdk-coin-sol - Change `parseTransaction` to accept a `Transaction` object instead of raw bytes, avoiding double deserialization when the caller already has a Transaction from `fromBytes()` - Add `parse_from_transaction` Rust entry point that accepts a pre-deserialized Transaction, with shared logic in `parse_transaction_inner` - Remove all explain-related exports from index.ts (ExplainedTransaction, TransactionType, StakingAuthorizeInfo, etc.) - Fix pre-existing clippy warnings in intent/build.rs The wasm package now only provides: - `Transaction.fromBytes(bytes)` for deserialization/signing - `parseTransaction(tx)` for decoded instruction data - Builder functions for transaction construction Ticket: BTC-3091
1 parent 71af785 commit 7dad774

12 files changed

Lines changed: 119 additions & 798 deletions

File tree

package-lock.json

Lines changed: 0 additions & 294 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/wasm-solana/js/explain.ts

Lines changed: 0 additions & 435 deletions
This file was deleted.

packages/wasm-solana/js/index.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export * as pubkey from "./pubkey.js";
99
export * as transaction from "./transaction.js";
1010
export * as parser from "./parser.js";
1111
export * as builder from "./builder.js";
12-
export * as explain from "./explain.js";
1312

1413
// Top-level class exports for convenience
1514
export { Keypair } from "./keypair.js";
@@ -24,8 +23,6 @@ export type { AddressLookupTableData } from "./versioned.js";
2423
export { parseTransaction } from "./parser.js";
2524
export { buildFromVersionedData } from "./builder.js";
2625
export { buildFromIntent, buildFromIntent as buildTransactionFromIntent } from "./intentBuilder.js";
27-
export { explainTransaction, TransactionType } from "./explain.js";
28-
2926
// Intent builder type exports
3027
export type {
3128
BaseIntent,
@@ -99,16 +96,6 @@ export type {
9996
UnknownInstructionParams,
10097
} from "./parser.js";
10198

102-
// Explain types
103-
export type {
104-
ExplainedTransaction,
105-
ExplainedOutput,
106-
ExplainedInput,
107-
ExplainOptions,
108-
TokenEnablement,
109-
StakingAuthorizeInfo,
110-
} from "./explain.js";
111-
11299
// Versioned transaction builder type exports
113100
export type {
114101
AddressLookupTable as BuilderAddressLookupTable,

packages/wasm-solana/js/parser.ts

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

1010
import { ParserNamespace } from "./wasm/wasm_solana.js";
11+
import type { Transaction } from "./transaction.js";
1112

1213
// =============================================================================
1314
// Instruction Types - matching BitGoJS InstructionParams.
@@ -273,17 +274,24 @@ export interface ParsedTransaction {
273274
// =============================================================================
274275

275276
/**
276-
* Parse raw transaction bytes into a plain data object with decoded instructions.
277+
* Parse a Transaction into a plain data object with decoded instructions.
277278
*
278279
* This is the main parsing function that returns structured data with all
279280
* instructions decoded into semantic types (Transfer, StakingActivate, etc.)
280281
* with amounts as bigint.
281282
*
282-
* For signing/serialization, use `Transaction.fromBytes()` instead.
283+
* Accepts a `Transaction` object (from `Transaction.fromBytes()`), avoiding
284+
* double deserialization.
283285
*
284-
* @param bytes - Raw transaction bytes
286+
* @param tx - A Transaction instance (from Transaction.fromBytes())
285287
* @returns A ParsedTransaction with all instructions decoded
288+
*
289+
* @example
290+
* ```typescript
291+
* const tx = Transaction.fromBytes(txBytes);
292+
* const parsed = parseTransaction(tx);
293+
* ```
286294
*/
287-
export function parseTransaction(bytes: Uint8Array): ParsedTransaction {
288-
return ParserNamespace.parse_transaction(bytes) as ParsedTransaction;
295+
export function parseTransaction(tx: Transaction): ParsedTransaction {
296+
return ParserNamespace.parse_from_transaction(tx.wasm) as ParsedTransaction;
289297
}

packages/wasm-solana/js/transaction.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,23 @@ export interface Instruction {
3030
* Solana Transaction — deserialization wrapper for signing and serialization.
3131
*
3232
* Use `Transaction.fromBytes(bytes)` to create an instance for signing.
33-
* Use `parseTransaction(bytes)` from parser.ts to get decoded instruction data.
33+
* Use `parseTransaction(tx)` from parser.ts to get decoded instruction data.
3434
*
3535
* @example
3636
* ```typescript
3737
* import { Transaction, parseTransaction } from '@bitgo/wasm-solana';
3838
*
39+
* const tx = Transaction.fromBytes(txBytes);
40+
*
3941
* // Parse for decoded instructions
40-
* const parsed = parseTransaction(txBytes);
42+
* const parsed = parseTransaction(tx);
4143
* for (const instr of parsed.instructionsData) {
4244
* if (instr.type === 'Transfer') {
4345
* console.log(`${instr.amount} lamports to ${instr.toAddress}`);
4446
* }
4547
* }
4648
*
47-
* // Deserialize for signing
48-
* const tx = Transaction.fromBytes(txBytes);
49+
* // Sign and serialize
4950
* tx.addSignature(pubkey, signature);
5051
* const signedBytes = tx.toBytes();
5152
* ```

packages/wasm-solana/src/intent/build.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,7 @@ fn build_jito_stake(
313313
// For Jito, validatorAddress is the stake pool address
314314
let stake_pool: Pubkey = config
315315
.stake_pool_address
316-
.as_ref()
317-
.map(|s| s.as_str())
316+
.as_deref()
318317
.unwrap_or(validator_address)
319318
.parse()
320319
.map_err(|_| WasmSolanaError::new("Invalid stakePoolAddress"))?;
@@ -1156,7 +1155,7 @@ mod tests {
11561155
.transaction
11571156
.signatures
11581157
.iter()
1159-
.filter(|s| s.as_ref() != &zero_sig)
1158+
.filter(|s| s.as_ref() != zero_sig)
11601159
.count();
11611160
assert_eq!(
11621161
non_zero_count, 1,

packages/wasm-solana/src/parser.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
1010
use crate::instructions::{decode_instruction, InstructionContext, ParsedInstruction};
1111
use crate::js_obj;
12+
use crate::transaction::Transaction;
1213
use crate::versioned::VersionedTransactionExt;
1314
use crate::wasm::try_into_js_value::{JsConversionError, TryIntoJsValue};
1415
use solana_message::VersionedMessage;
@@ -91,20 +92,52 @@ pub fn parse_transaction(bytes: &[u8]) -> Result<ParsedTransaction, String> {
9192
{
9293
VersionedMessage::Legacy(msg) => (
9394
msg.account_keys.iter().map(|k| k.to_string()).collect(),
94-
&msg.instructions,
95+
&msg.instructions[..],
9596
msg.recent_blockhash.to_string(),
9697
msg.header.num_required_signatures,
9798
),
9899
VersionedMessage::V0(msg) => (
99100
msg.account_keys.iter().map(|k| k.to_string()).collect(),
100-
&msg.instructions,
101+
&msg.instructions[..],
101102
msg.recent_blockhash.to_string(),
102103
msg.header.num_required_signatures,
103104
),
104105
};
105106

106-
let account_keys: Vec<String> = account_keys;
107+
parse_transaction_inner(
108+
account_keys,
109+
instructions,
110+
recent_blockhash,
111+
num_required_signatures,
112+
&tx.signatures,
113+
)
114+
}
115+
116+
/// Parse a pre-deserialized legacy Transaction into structured data.
117+
///
118+
/// Same logic as `parse_transaction(bytes)` but skips deserialization.
119+
/// Used when the caller already has a `Transaction` from `fromBytes()`.
120+
pub fn parse_from_transaction(tx: &Transaction) -> Result<ParsedTransaction, String> {
121+
let msg = &tx.message;
122+
let account_keys: Vec<String> = msg.account_keys.iter().map(|k| k.to_string()).collect();
123+
124+
parse_transaction_inner(
125+
account_keys,
126+
&msg.instructions,
127+
msg.recent_blockhash.to_string(),
128+
msg.header.num_required_signatures,
129+
&tx.signatures,
130+
)
131+
}
107132

133+
/// Shared parsing logic for both bytes-based and Transaction-based entry points.
134+
fn parse_transaction_inner(
135+
account_keys: Vec<String>,
136+
instructions: &[solana_message::compiled_instruction::CompiledInstruction],
137+
recent_blockhash: String,
138+
num_required_signatures: u8,
139+
signatures: &[solana_signature::Signature],
140+
) -> Result<ParsedTransaction, String> {
108141
// Extract fee payer (first account key)
109142
let fee_payer = account_keys
110143
.first()
@@ -156,8 +189,7 @@ pub fn parse_transaction(bytes: &[u8]) -> Result<ParsedTransaction, String> {
156189
// Extract signatures as base58 strings.
157190
// All-zeros signatures (unsigned placeholder slots) are returned as empty strings
158191
// so the JS side can simply use `signatures[0] || 'UNAVAILABLE'`.
159-
let signatures: Vec<String> = tx
160-
.signatures
192+
let signatures: Vec<String> = signatures
161193
.iter()
162194
.map(|s| {
163195
let bytes: &[u8] = s.as_ref();

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! WASM binding for high-level transaction parsing.
22
//!
3-
//! Exposes a single `parseTransaction` function that returns fully decoded
3+
//! Exposes transaction parsing functions that return fully decoded
44
//! transaction data matching BitGoJS's TxData format.
55
66
use crate::parser;
7+
use crate::wasm::transaction::WasmTransaction;
78
use crate::wasm::try_into_js_value::TryIntoJsValue;
89
use wasm_bindgen::prelude::*;
910

@@ -38,4 +39,22 @@ impl ParserNamespace {
3839
.try_to_js_value()
3940
.map_err(|e| JsValue::from_str(&format!("Conversion error: {}", e)))
4041
}
42+
43+
/// Parse a pre-deserialized Transaction into structured data.
44+
///
45+
/// Same as `parse_transaction(bytes)` but accepts an already-deserialized
46+
/// WasmTransaction, avoiding double deserialization when the caller already
47+
/// has a Transaction from `fromBytes()`.
48+
///
49+
/// @param tx - A WasmTransaction instance
50+
/// @returns A ParsedTransaction object
51+
#[wasm_bindgen]
52+
pub fn parse_from_transaction(tx: &WasmTransaction) -> Result<JsValue, JsValue> {
53+
let parsed =
54+
parser::parse_from_transaction(tx.inner()).map_err(|e| JsValue::from_str(&e))?;
55+
56+
parsed
57+
.try_to_js_value()
58+
.map_err(|e| JsValue::from_str(&format!("Conversion error: {}", e)))
59+
}
4160
}

packages/wasm-solana/test/bitgojs-compat.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
import * as assert from "assert";
88
import { parseTransaction } from "../js/parser.js";
9+
import { Transaction } from "../js/transaction.js";
910

1011
// Helper to decode base64 in tests
1112
function base64ToBytes(base64: string): Uint8Array {
@@ -51,25 +52,25 @@ describe("BitGoJS Compatibility", () => {
5152

5253
it("should parse feePayer correctly", () => {
5354
const bytes = base64ToBytes(TX_BASE64);
54-
const parsed = parseTransaction(bytes);
55+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
5556
assert.strictEqual(parsed.feePayer, EXPECTED.feePayer);
5657
});
5758

5859
it("should parse nonce correctly", () => {
5960
const bytes = base64ToBytes(TX_BASE64);
60-
const parsed = parseTransaction(bytes);
61+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
6162
assert.strictEqual(parsed.nonce, EXPECTED.nonce);
6263
});
6364

6465
it("should parse numSignatures correctly", () => {
6566
const bytes = base64ToBytes(TX_BASE64);
66-
const parsed = parseTransaction(bytes);
67+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
6768
assert.strictEqual(parsed.numSignatures, EXPECTED.numSignatures);
6869
});
6970

7071
it("should detect durable nonce transaction", () => {
7172
const bytes = base64ToBytes(TX_BASE64);
72-
const parsed = parseTransaction(bytes);
73+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
7374
assert.ok(parsed.durableNonce, "Should detect durable nonce");
7475
assert.strictEqual(
7576
parsed.durableNonce.walletNonceAddress,
@@ -83,7 +84,7 @@ describe("BitGoJS Compatibility", () => {
8384

8485
it("should have NonceAdvance in both instructionsData and durableNonce", () => {
8586
const bytes = base64ToBytes(TX_BASE64);
86-
const parsed = parseTransaction(bytes);
87+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
8788
// WASM returns all instructions including NonceAdvance
8889
const nonceAdvance = parsed.instructionsData.find((i) => i.type === "NonceAdvance");
8990
assert.ok(nonceAdvance, "NonceAdvance should be in instructionsData");
@@ -97,7 +98,7 @@ describe("BitGoJS Compatibility", () => {
9798

9899
it("should parse Transfer instruction correctly", () => {
99100
const bytes = base64ToBytes(TX_BASE64);
100-
const parsed = parseTransaction(bytes);
101+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
101102
// Transfer is at index 1 (after NonceAdvance)
102103
const instr = parsed.instructionsData[1];
103104

@@ -114,7 +115,7 @@ describe("BitGoJS Compatibility", () => {
114115

115116
it("should parse Memo instruction correctly", () => {
116117
const bytes = base64ToBytes(TX_BASE64);
117-
const parsed = parseTransaction(bytes);
118+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
118119
// Memo is at index 2 (after NonceAdvance and Transfer)
119120
const instr = parsed.instructionsData[2];
120121

@@ -129,7 +130,7 @@ describe("BitGoJS Compatibility", () => {
129130

130131
it("should have correct number of instructions", () => {
131132
const bytes = base64ToBytes(TX_BASE64);
132-
const parsed = parseTransaction(bytes);
133+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
133134
// 3 instructions: NonceAdvance + Transfer + Memo
134135
assert.strictEqual(parsed.instructionsData.length, 3);
135136
});
@@ -154,7 +155,7 @@ describe("BitGoJS Compatibility", () => {
154155

155156
it("should parse multi-transfer with correct structure", () => {
156157
const bytes = base64ToBytes(TX_BASE64);
157-
const parsed = parseTransaction(bytes);
158+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
158159

159160
assert.strictEqual(parsed.feePayer, EXPECTED_FEE_PAYER);
160161
assert.strictEqual(parsed.nonce, EXPECTED_NONCE);
@@ -166,7 +167,7 @@ describe("BitGoJS Compatibility", () => {
166167

167168
it("should parse all transfer recipients correctly", () => {
168169
const bytes = base64ToBytes(TX_BASE64);
169-
const parsed = parseTransaction(bytes);
170+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
170171

171172
// Transfers are at indices 1-6 (index 0 is NonceAdvance)
172173
const transfers = parsed.instructionsData.slice(1, 7);
@@ -185,7 +186,7 @@ describe("BitGoJS Compatibility", () => {
185186

186187
it("should have memo as last instruction", () => {
187188
const bytes = base64ToBytes(TX_BASE64);
188-
const parsed = parseTransaction(bytes);
189+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
189190
const lastInstr = parsed.instructionsData[parsed.instructionsData.length - 1];
190191

191192
assert.strictEqual(lastInstr.type, "Memo");
@@ -202,7 +203,7 @@ describe("BitGoJS Compatibility", () => {
202203

203204
it("should parse staking transaction structure", () => {
204205
const bytes = base64ToBytes(TX_BASE64);
205-
const parsed = parseTransaction(bytes);
206+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
206207

207208
assert.strictEqual(parsed.feePayer, "5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe");
208209
assert.ok(parsed.instructionsData.length >= 1, "Should have instructions");
@@ -225,7 +226,7 @@ describe("BitGoJS Compatibility", () => {
225226

226227
it("should parse token transfer transaction", () => {
227228
const bytes = base64ToBytes(TX_BASE64);
228-
const parsed = parseTransaction(bytes);
229+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
229230

230231
// Should have 4 instructions: NonceAdvance, SetPriorityFee, TokenTransfer, Memo
231232
assert.strictEqual(parsed.instructionsData.length, 4);
@@ -252,7 +253,7 @@ describe("BitGoJS Compatibility", () => {
252253

253254
it("should parse basic unsigned transfer", () => {
254255
const bytes = base64ToBytes(TX_BASE64);
255-
const parsed = parseTransaction(bytes);
256+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
256257

257258
// This is a durable nonce transaction: NonceAdvance + Transfer
258259
assert.strictEqual(parsed.instructionsData.length, 2);
@@ -275,7 +276,7 @@ describe("BitGoJS Compatibility", () => {
275276

276277
it("should parse Jito DepositSol instruction", () => {
277278
const bytes = base64ToBytes(TX_BASE64);
278-
const parsed = parseTransaction(bytes);
279+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
279280

280281
// Find the StakePoolDepositSol instruction
281282
const depositSolInstr = parsed.instructionsData.find((i) => i.type === "StakePoolDepositSol");
@@ -294,7 +295,7 @@ describe("BitGoJS Compatibility", () => {
294295

295296
it("should have correct fee payer for Jito transaction", () => {
296297
const bytes = base64ToBytes(TX_BASE64);
297-
const parsed = parseTransaction(bytes);
298+
const parsed = parseTransaction(Transaction.fromBytes(bytes));
298299

299300
// Fee payer from BitGoJS tests
300301
assert.strictEqual(parsed.feePayer, "5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe");

0 commit comments

Comments
 (0)