Skip to content
Closed
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
294 changes: 0 additions & 294 deletions package-lock.json

Large diffs are not rendered by default.

435 changes: 0 additions & 435 deletions packages/wasm-solana/js/explain.ts

This file was deleted.

13 changes: 0 additions & 13 deletions packages/wasm-solana/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export * as pubkey from "./pubkey.js";
export * as transaction from "./transaction.js";
export * as parser from "./parser.js";
export * as builder from "./builder.js";
export * as explain from "./explain.js";

// Top-level class exports for convenience
export { Keypair } from "./keypair.js";
Expand All @@ -24,8 +23,6 @@ export type { AddressLookupTableData } from "./versioned.js";
export { parseTransaction } from "./parser.js";
export { buildFromVersionedData } from "./builder.js";
export { buildFromIntent, buildFromIntent as buildTransactionFromIntent } from "./intentBuilder.js";
export { explainTransaction, TransactionType } from "./explain.js";

// Intent builder type exports
export type {
BaseIntent,
Expand Down Expand Up @@ -99,16 +96,6 @@ export type {
UnknownInstructionParams,
} from "./parser.js";

// Explain types
export type {
ExplainedTransaction,
ExplainedOutput,
ExplainedInput,
ExplainOptions,
TokenEnablement,
StakingAuthorizeInfo,
} from "./explain.js";

// Versioned transaction builder type exports
export type {
AddressLookupTable as BuilderAddressLookupTable,
Expand Down
22 changes: 15 additions & 7 deletions packages/wasm-solana/js/parser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/**
* High-level transaction parsing.
*
* Provides types and functions for parsing Solana transactions into semantic data
* matching BitGoJS's TxData format.
* Provides types and functions for parsing Solana transactions into
* semantic data matching BitGoJS's TxData format.
*
* All monetary amounts (amount, fee, lamports, poolTokens) are returned as bigint.
*/

import { ParserNamespace } from "./wasm/wasm_solana.js";
import type { Transaction } from "./transaction.js";

// =============================================================================
// Instruction Types - matching BitGoJS InstructionParams.
Expand Down Expand Up @@ -273,17 +274,24 @@ export interface ParsedTransaction {
// =============================================================================

/**
* Parse raw transaction bytes into a plain data object with decoded instructions.
* Parse a Transaction into a plain data object with decoded instructions.
*
* This is the main parsing function that returns structured data with all
* instructions decoded into semantic types (Transfer, StakingActivate, etc.)
* with amounts as bigint.
*
* For signing/serialization, use `Transaction.fromBytes()` instead.
* Accepts a `Transaction` object (from `Transaction.fromBytes()`), avoiding
* double deserialization.
*
* @param bytes - Raw transaction bytes
* @param tx - A Transaction instance (from Transaction.fromBytes())
* @returns A ParsedTransaction with all instructions decoded
*
* @example
* ```typescript
* const tx = Transaction.fromBytes(txBytes);
* const parsed = parseTransaction(tx);
* ```
*/
export function parseTransaction(bytes: Uint8Array): ParsedTransaction {
return ParserNamespace.parse_transaction(bytes) as ParsedTransaction;
export function parseTransaction(tx: Transaction): ParsedTransaction {
return ParserNamespace.parse_from_transaction(tx.wasm) as ParsedTransaction;
}
9 changes: 5 additions & 4 deletions packages/wasm-solana/js/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,23 @@ export interface Instruction {
* Solana Transaction — deserialization wrapper for signing and serialization.
*
* Use `Transaction.fromBytes(bytes)` to create an instance for signing.
* Use `parseTransaction(bytes)` from parser.ts to get decoded instruction data.
* Use `parseTransaction(tx)` from parser.ts to get decoded instruction data.
*
* @example
* ```typescript
* import { Transaction, parseTransaction } from '@bitgo/wasm-solana';
*
* const tx = Transaction.fromBytes(txBytes);
*
* // Parse for decoded instructions
* const parsed = parseTransaction(txBytes);
* const parsed = parseTransaction(tx);
* for (const instr of parsed.instructionsData) {
* if (instr.type === 'Transfer') {
* console.log(`${instr.amount} lamports to ${instr.toAddress}`);
* }
* }
*
* // Deserialize for signing
* const tx = Transaction.fromBytes(txBytes);
* // Sign and serialize
* tx.addSignature(pubkey, signature);
* const signedBytes = tx.toBytes();
* ```
Expand Down
5 changes: 2 additions & 3 deletions packages/wasm-solana/src/intent/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,7 @@ fn build_jito_stake(
// For Jito, validatorAddress is the stake pool address
let stake_pool: Pubkey = config
.stake_pool_address
.as_ref()
.map(|s| s.as_str())
.as_deref()
.unwrap_or(validator_address)
.parse()
.map_err(|_| WasmSolanaError::new("Invalid stakePoolAddress"))?;
Expand Down Expand Up @@ -1156,7 +1155,7 @@ mod tests {
.transaction
.signatures
.iter()
.filter(|s| s.as_ref() != &zero_sig)
.filter(|s| s.as_ref() != zero_sig)
.count();
assert_eq!(
non_zero_count, 1,
Expand Down
42 changes: 37 additions & 5 deletions packages/wasm-solana/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use crate::instructions::{decode_instruction, InstructionContext, ParsedInstruction};
use crate::js_obj;
use crate::transaction::Transaction;
use crate::versioned::VersionedTransactionExt;
use crate::wasm::try_into_js_value::{JsConversionError, TryIntoJsValue};
use solana_message::VersionedMessage;
Expand Down Expand Up @@ -91,20 +92,52 @@ pub fn parse_transaction(bytes: &[u8]) -> Result<ParsedTransaction, String> {
{
VersionedMessage::Legacy(msg) => (
msg.account_keys.iter().map(|k| k.to_string()).collect(),
&msg.instructions,
&msg.instructions[..],
msg.recent_blockhash.to_string(),
msg.header.num_required_signatures,
),
VersionedMessage::V0(msg) => (
msg.account_keys.iter().map(|k| k.to_string()).collect(),
&msg.instructions,
&msg.instructions[..],
msg.recent_blockhash.to_string(),
msg.header.num_required_signatures,
),
};

let account_keys: Vec<String> = account_keys;
parse_transaction_inner(
account_keys,
instructions,
recent_blockhash,
num_required_signatures,
&tx.signatures,
)
}

/// Parse a pre-deserialized legacy Transaction into structured data.
///
/// Same logic as `parse_transaction(bytes)` but skips deserialization.
/// Used when the caller already has a `Transaction` from `fromBytes()`.
pub fn parse_from_transaction(tx: &Transaction) -> Result<ParsedTransaction, String> {
let msg = &tx.message;
let account_keys: Vec<String> = msg.account_keys.iter().map(|k| k.to_string()).collect();

parse_transaction_inner(
account_keys,
&msg.instructions,
msg.recent_blockhash.to_string(),
msg.header.num_required_signatures,
&tx.signatures,
)
}

/// Shared parsing logic for both bytes-based and Transaction-based entry points.
fn parse_transaction_inner(
account_keys: Vec<String>,
instructions: &[solana_message::compiled_instruction::CompiledInstruction],
recent_blockhash: String,
num_required_signatures: u8,
signatures: &[solana_signature::Signature],
) -> Result<ParsedTransaction, String> {
// Extract fee payer (first account key)
let fee_payer = account_keys
.first()
Expand Down Expand Up @@ -156,8 +189,7 @@ pub fn parse_transaction(bytes: &[u8]) -> Result<ParsedTransaction, String> {
// Extract signatures as base58 strings.
// All-zeros signatures (unsigned placeholder slots) are returned as empty strings
// so the JS side can simply use `signatures[0] || 'UNAVAILABLE'`.
let signatures: Vec<String> = tx
.signatures
let signatures: Vec<String> = signatures
.iter()
.map(|s| {
let bytes: &[u8] = s.as_ref();
Expand Down
21 changes: 20 additions & 1 deletion packages/wasm-solana/src/wasm/parser.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! WASM binding for high-level transaction parsing.
//!
//! Exposes a single `parseTransaction` function that returns fully decoded
//! Exposes transaction parsing functions that return fully decoded
//! transaction data matching BitGoJS's TxData format.

use crate::parser;
use crate::wasm::transaction::WasmTransaction;
use crate::wasm::try_into_js_value::TryIntoJsValue;
use wasm_bindgen::prelude::*;

Expand Down Expand Up @@ -38,4 +39,22 @@ impl ParserNamespace {
.try_to_js_value()
.map_err(|e| JsValue::from_str(&format!("Conversion error: {}", e)))
}

/// Parse a pre-deserialized Transaction into structured data.
///
/// Same as `parse_transaction(bytes)` but accepts an already-deserialized
/// WasmTransaction, avoiding double deserialization when the caller already
/// has a Transaction from `fromBytes()`.
///
/// @param tx - A WasmTransaction instance
/// @returns A ParsedTransaction object
#[wasm_bindgen]
pub fn parse_from_transaction(tx: &WasmTransaction) -> Result<JsValue, JsValue> {
let parsed =
parser::parse_from_transaction(tx.inner()).map_err(|e| JsValue::from_str(&e))?;

parsed
.try_to_js_value()
.map_err(|e| JsValue::from_str(&format!("Conversion error: {}", e)))
}
}
33 changes: 17 additions & 16 deletions packages/wasm-solana/test/bitgojs-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import * as assert from "assert";
import { parseTransaction } from "../js/parser.js";
import { Transaction } from "../js/transaction.js";

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

it("should parse feePayer correctly", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));
assert.strictEqual(parsed.feePayer, EXPECTED.feePayer);
});

it("should parse nonce correctly", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));
assert.strictEqual(parsed.nonce, EXPECTED.nonce);
});

it("should parse numSignatures correctly", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));
assert.strictEqual(parsed.numSignatures, EXPECTED.numSignatures);
});

it("should detect durable nonce transaction", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));
assert.ok(parsed.durableNonce, "Should detect durable nonce");
assert.strictEqual(
parsed.durableNonce.walletNonceAddress,
Expand All @@ -83,7 +84,7 @@ describe("BitGoJS Compatibility", () => {

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

it("should parse Transfer instruction correctly", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));
// Transfer is at index 1 (after NonceAdvance)
const instr = parsed.instructionsData[1];

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

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

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

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

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

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

it("should parse all transfer recipients correctly", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));

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

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

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

it("should parse staking transaction structure", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));

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

it("should parse token transfer transaction", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));

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

it("should parse basic unsigned transfer", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));

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

it("should parse Jito DepositSol instruction", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));

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

it("should have correct fee payer for Jito transaction", () => {
const bytes = base64ToBytes(TX_BASE64);
const parsed = parseTransaction(bytes);
const parsed = parseTransaction(Transaction.fromBytes(bytes));

// Fee payer from BitGoJS tests
assert.strictEqual(parsed.feePayer, "5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe");
Expand Down
Loading