Skip to content
Draft
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
Binary file not shown.
1 change: 1 addition & 0 deletions modules/sdk-coin-sol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@bitgo/sdk-core": "^36.29.0",
"@bitgo/sdk-lib-mpc": "^10.8.1",
"@bitgo/statics": "^58.23.0",
"@bitgo/wasm-solana": "file:./bitgo-wasm-solana-0.0.1.tgz",
"@solana/spl-stake-pool": "1.1.8",
"@solana/spl-token": "0.3.1",
"@solana/web3.js": "1.92.1",
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-coin-sol/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export { TransferBuilderV2 } from './transferBuilderV2';
export { WalletInitializationBuilder } from './walletInitializationBuilder';
export { Interface, Utils };
export { MessageBuilderFactory } from './messages';
export { InstructionBuilderTypes } from './constants';
2 changes: 1 addition & 1 deletion modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1239,7 +1239,7 @@ function parseCustomInstructions(
return instructionData;
}

function findTokenName(
export function findTokenName(
mintAddress: string,
instructionMetadata?: InstructionParams[],
_useTokenAddressTokenName?: boolean
Expand Down
198 changes: 158 additions & 40 deletions modules/sdk-coin-sol/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
VersionedTransactionData,
WalletInit,
} from './iface';
import { instructionParamsFactory } from './instructionParamsFactory';
import { instructionParamsFactory, findTokenName } from './instructionParamsFactory';
import {
getInstructionType,
getTransactionType,
Expand All @@ -51,16 +51,25 @@ import {
validateRawMsgInstruction,
} from './utils';
import { SolStakingTypeEnum } from '@bitgo/public-types';
import {
parseTransaction as wasmParseTransaction,
ParsedTransaction as WasmParsedTransaction,
Transaction as WasmSolanaTransaction,
} from '@bitgo/wasm-solana';
import { combineWasmInstructions } from './wasmInstructionCombiner';

export class Transaction extends BaseTransaction {
protected _solTransaction: SolTransaction;
private _wasmTransaction: WasmSolanaTransaction | undefined; // WASM-based transaction (testnet)
private _lamportsPerSignature: number | undefined;
private _tokenAccountRentExemptAmount: string | undefined;
protected _type: TransactionType;
protected _instructionsData: InstructionParams[] = [];
private _useTokenAddressTokenName = false;
private _versionedTransaction: VersionedTransaction | undefined;
private _versionedTransactionData: VersionedTransactionData | undefined;
private _rawTransaction: string | undefined; // Stored for WASM parsing path
private _wasRebuilt = false; // Tracks if transaction went through builder.build()

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
Expand All @@ -74,6 +83,22 @@ export class Transaction extends BaseTransaction {
this._solTransaction = tx;
}

/**
* Get the WASM-based transaction (available after fromRawTransaction).
* Used for testnet path to avoid web3.js dependency.
*/
get wasmTransaction(): WasmSolanaTransaction | undefined {
return this._wasmTransaction;
}

/**
* Mark this transaction as having been rebuilt through a builder.
* Used to track whether NonceAdvance should be filtered from instructionsData.
*/
markAsRebuilt(): void {
this._wasRebuilt = true;
}

private get numberOfRequiredSignatures(): number {
return this._solTransaction.compileMessage().header.numRequiredSignatures;
}
Expand Down Expand Up @@ -283,60 +308,68 @@ export class Transaction extends BaseTransaction {
fromRawTransaction(rawTransaction: string): void {
try {
isValidRawTransaction(rawTransaction);
this._rawTransaction = rawTransaction; // Store for WASM parsing path
this._solTransaction = SolTransaction.from(Buffer.from(rawTransaction, 'base64'));

// Also create WASM transaction for testnet path
const txBytes = Buffer.from(rawTransaction, 'base64');
this._wasmTransaction = WasmSolanaTransaction.fromBytes(txBytes);

if (this._solTransaction.signature && this._solTransaction.signature !== null) {
this._id = base58.encode(this._solTransaction.signature);
}
const transactionType = getTransactionType(this._solTransaction);
switch (transactionType) {
case TransactionType.WalletInitialization:
this.setTransactionType(TransactionType.WalletInitialization);
break;
case TransactionType.Send:
this.setTransactionType(TransactionType.Send);
break;
case TransactionType.StakingActivate:
this.setTransactionType(TransactionType.StakingActivate);
break;
case TransactionType.StakingDeactivate:
this.setTransactionType(TransactionType.StakingDeactivate);
break;
case TransactionType.StakingWithdraw:
this.setTransactionType(TransactionType.StakingWithdraw);
break;
case TransactionType.AssociatedTokenAccountInitialization:
this.setTransactionType(TransactionType.AssociatedTokenAccountInitialization);
break;
case TransactionType.CloseAssociatedTokenAccount:
this.setTransactionType(TransactionType.CloseAssociatedTokenAccount);
break;
case TransactionType.StakingAuthorize:
this.setTransactionType(TransactionType.StakingAuthorize);
break;
case TransactionType.StakingAuthorizeRaw:
this.setTransactionType(TransactionType.StakingAuthorizeRaw);
break;
case TransactionType.StakingDelegate:
this.setTransactionType(TransactionType.StakingDelegate);
break;
case TransactionType.CustomTx:
this.setTransactionType(TransactionType.CustomTx);
break;

// Use WASM-based type detection for testnet, fall back to legacy for mainnet
const useWasm = this._coinConfig.name === 'tsol' && this._wasmTransaction;
let transactionType: TransactionType;

if (useWasm) {
const wasmParsed = this.parseWithWasm(rawTransaction);
const { transactionType: wasmType } = combineWasmInstructions(wasmParsed, this._coinConfig.name);
transactionType = wasmType;
} else {
transactionType = getTransactionType(this._solTransaction);
}

this.setTransactionType(transactionType);

if (transactionType !== TransactionType.StakingAuthorizeRaw) {
this.loadInputsAndOutputs();
this.loadInputsAndOutputs(rawTransaction);
}
} catch (e) {
throw e;
}
}

/**
* Parse transaction using WASM and return the parsed result.
* This can be used for transaction explanation without going through instructionParamsFactory.
*/
parseWithWasm(rawTransaction: string): WasmParsedTransaction {
const txBytes = Buffer.from(rawTransaction, 'base64');
return wasmParseTransaction(txBytes);
}

/**
* Convert all WASM instructions to BitGoJS InstructionParams format.
* Uses the centralized combineWasmInstructions utility for DRY combining logic.
*/
mapWasmInstructionsToBitGoJS(wasmParsed: WasmParsedTransaction, coinName: string): InstructionParams[] {
return combineWasmInstructions(wasmParsed, coinName).instructions;
}

/** @inheritdoc */
toJson(): TxData {
if (!this._solTransaction) {
throw new ParseTransactionError('Empty transaction');
}

// Use WASM path for testnet to validate against legacy
if (this._coinConfig.name === 'tsol' && this._rawTransaction) {
return this.toJsonWithWasm();
}

// Legacy path using web3.js
let durableNonce: DurableNonceParams | undefined;
if (this._solTransaction.nonceInfo) {
const nonceInstruction = SystemInstruction.decodeNonceAdvance(this._solTransaction.nonceInfo.nonceInstruction);
Expand Down Expand Up @@ -373,6 +406,69 @@ export class Transaction extends BaseTransaction {
return result;
}

/**
* WASM-based implementation of toJson() for testnet.
* This implementation is independent of _solTransaction (web3.js).
*/
private toJsonWithWasm(): TxData {
if (!this._rawTransaction) {
throw new InvalidTransactionError('Raw transaction is required for WASM parsing');
}
const wasmParsed = this.parseWithWasm(this._rawTransaction);
let instructionData = this.mapWasmInstructionsToBitGoJS(wasmParsed, this._coinConfig.name);

// Resolve token names for TokenTransfer/CreateATA instructions
instructionData = instructionData.map((instr) => {
if (instr.type === InstructionBuilderTypes.TokenTransfer && instr.params.tokenAddress) {
const resolvedTokenName = findTokenName(
instr.params.tokenAddress,
this._instructionsData,
this._useTokenAddressTokenName
);
return {
...instr,
params: { ...instr.params, tokenName: resolvedTokenName },
};
}
if (instr.type === InstructionBuilderTypes.CreateAssociatedTokenAccount && instr.params.mintAddress) {
const resolvedTokenName = findTokenName(
instr.params.mintAddress,
this._instructionsData,
this._useTokenAddressTokenName
);
return {
...instr,
params: { ...instr.params, tokenName: resolvedTokenName },
};
}
return instr;
});

// For rebuilt transactions, NonceAdvance is tracked separately in durableNonce,
// so filter it out from instructionsData to match legacy behavior
if (wasmParsed.durableNonce && this._wasRebuilt) {
instructionData = instructionData.filter((instr) => instr.type !== InstructionBuilderTypes.NonceAdvance);
}

// Extract valid signatures from WASM Transaction (filter out placeholder all-zero signatures)
const validSignatures = this._wasmTransaction!.signatures()
.filter((sigBytes) => sigBytes.some((b) => b !== 0))
.map((sigBytes) => base58.encode(sigBytes));

// Transaction ID is the first valid signature (if any)
const txId = validSignatures.length > 0 ? validSignatures[0] : undefined;

return {
id: txId,
feePayer: wasmParsed.feePayer,
lamportsPerSignature: this.lamportsPerSignature,
nonce: wasmParsed.nonce,
durableNonce: wasmParsed.durableNonce,
numSignatures: validSignatures.length,
instructionsData: instructionData,
};
}

/**
* Get the nonce from the Solana Transaction
* Throws if not set
Expand All @@ -389,20 +485,42 @@ export class Transaction extends BaseTransaction {

/**
* Load the input and output data on this transaction.
* @param rawTransaction - Optional raw transaction for WASM parsing (testnet only)
*/
loadInputsAndOutputs(): void {
loadInputsAndOutputs(rawTransaction?: string): void {
// Use WASM path for testnet when raw transaction and WASM transaction are available
const useWasm = rawTransaction && this._coinConfig.name === 'tsol' && this._wasmTransaction;

if (useWasm) {
// WASM path - independent of _solTransaction
const instructionParams = this.mapWasmInstructionsToBitGoJS(
this.parseWithWasm(rawTransaction),
this._coinConfig.name
);
this.processInputsAndOutputs(instructionParams);
return;
}

// Legacy path - requires _solTransaction
if (!this._solTransaction || this._solTransaction.instructions?.length === 0) {
return;
}
const outputs: Entry[] = [];
const inputs: Entry[] = [];
const instructionParams = instructionParamsFactory(
this.type,
this._solTransaction.instructions,
this._coinConfig.name,
this._instructionsData,
this._useTokenAddressTokenName
);
this.processInputsAndOutputs(instructionParams);
}

/**
* Process instruction params to populate inputs and outputs.
*/
private processInputsAndOutputs(instructionParams: InstructionParams[]): void {
const outputs: Entry[] = [];
const inputs: Entry[] = [];

for (const instruction of instructionParams) {
switch (instruction.type) {
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-coin-sol/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this.transaction.setInstructionsData(this._instructionsData);
this.transaction.loadInputsAndOutputs();
this._transaction.tokenAccountRentExemptAmount = this._tokenAccountRentExemptAmount;
// Mark transaction as rebuilt so WASM path knows to filter NonceAdvance from instructionsData
this.transaction.markAsRebuilt();
return this.transaction;
}

Expand Down
Loading
Loading