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 added bitgo-wasm-solana-0.0.1.tgz
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 @@ -42,6 +42,7 @@
"dependencies": {
"@bitgo/public-types": "5.63.0",
"@bitgo/sdk-core": "^36.28.0",
"@bitgo/wasm-solana": "file:../../bitgo-wasm-solana-0.0.1.tgz",
"@bitgo/sdk-lib-mpc": "^10.8.1",
"@bitgo/statics": "^58.22.0",
"@solana/spl-stake-pool": "1.1.8",
Expand Down
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
171 changes: 171 additions & 0 deletions modules/sdk-coin-sol/src/sol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ import {
} from '@bitgo/sdk-core';
import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc';
import { BaseNetwork, CoinFamily, coins, SolCoin, BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
import { parseTransaction as wasmParseTransaction } from '@bitgo/wasm-solana';
import { KeyPair as SolKeyPair, Transaction, TransactionBuilder, TransactionBuilderFactory } from './lib';
import { TransactionExplanation as SolLibTransactionExplanation } from './lib/iface';
import {
getAssociatedTokenAccountAddress,
getSolTokenFromAddress,
Expand All @@ -66,6 +68,7 @@ import {
isValidPublicKey,
validateRawTransaction,
} from './lib/utils';
import { findTokenName } from './lib/instructionParamsFactory';

export const DEFAULT_SCAN_FACTOR = 20; // default number of receive addresses to scan for funds

Expand Down Expand Up @@ -695,6 +698,7 @@ export class Sol extends BaseCoin {
}

async parseTransaction(params: SolParseTransactionOptions): Promise<SolParsedTransaction> {
// explainTransaction now uses WASM for testnet automatically
const transactionExplanation = await this.explainTransaction({
txBase64: params.txBase64,
feeInfo: params.feeInfo,
Expand Down Expand Up @@ -740,9 +744,16 @@ export class Sol extends BaseCoin {

/**
* Explain a Solana transaction from txBase64
* Uses WASM-based parsing for testnet, with fallback to legacy builder approach.
* @param params
*/
async explainTransaction(params: ExplainTransactionOptions): Promise<SolTransactionExplanation> {
// Use WASM-based parsing for testnet (simpler, faster, no @solana/web3.js rebuild)
if (this.getChain() === 'tsol') {
return this.explainTransactionWithWasm(params) as SolTransactionExplanation;
}

// Legacy approach for mainnet (until WASM is fully validated)
const factory = this.getBuilder();
let rebuiltTransaction;

Expand All @@ -766,6 +777,166 @@ export class Sol extends BaseCoin {
return explainedTransaction as SolTransactionExplanation;
}

/**
* Explain a Solana transaction using WASM parsing (bypasses @solana/web3.js rebuild).
* This provides a simpler, more direct parsing path.
* @param params
*/
explainTransactionWithWasm(params: ExplainTransactionOptions): SolLibTransactionExplanation {
const txBytes = Buffer.from(params.txBase64, 'base64');
const parsed = wasmParseTransaction(txBytes);

const outputs: TransactionRecipient[] = [];
const tokenEnablements: ITokenEnablement[] = [];
let outputAmount = new BigNumber(0);
let memo: string | undefined;
let transactionType = 'Send'; // Default type

// Process instructions to derive type, outputs, memo, and tokenEnablements
for (const instr of parsed.instructionsData) {
switch (instr.type) {
case 'Transfer':
outputs.push({
address: instr.toAddress,
amount: instr.amount,
});
outputAmount = outputAmount.plus(instr.amount);
transactionType = 'Send';
break;

case 'TokenTransfer':
outputs.push({
address: instr.toAddress,
amount: instr.amount,
tokenName: findTokenName(instr.tokenAddress ?? '', undefined, true),
});
// Token transfers don't add to outputAmount (matches original behavior)
transactionType = 'Send';
break;

case 'CreateNonceAccount':
outputs.push({
address: instr.nonceAddress,
amount: instr.amount,
});
outputAmount = outputAmount.plus(instr.amount);
transactionType = 'WalletInitialization';
break;

case 'StakingActivate':
outputs.push({
address: instr.stakingAddress,
amount: instr.amount,
});
outputAmount = outputAmount.plus(instr.amount);
transactionType = 'StakingActivate';
break;

case 'StakingDeactivate':
transactionType = 'StakingDeactivate';
break;

case 'StakingWithdraw':
outputs.push({
address: instr.fromAddress,
amount: instr.amount,
});
outputAmount = outputAmount.plus(instr.amount);
transactionType = 'StakingWithdraw';
break;

case 'StakingDelegate':
transactionType = 'StakingDelegate';
break;

case 'StakingAuthorize':
transactionType = 'StakingAuthorize';
break;

case 'CreateAssociatedTokenAccount':
tokenEnablements.push({
address: instr.ataAddress,
tokenName: findTokenName(instr.mintAddress, undefined, true),
tokenAddress: instr.mintAddress,
});
// If no other type-determining instruction, this is ATA init
if (outputs.length === 0) {
transactionType = 'AssociatedTokenAccountInitialization';
}
break;

case 'CloseAssociatedTokenAccount':
transactionType = 'CloseAssociatedTokenAccount';
break;

case 'Memo':
memo = instr.memo;
break;

case 'NonceAdvance':
// NonceAdvance is handled via durableNonce field, not instructionsData (BitGoJS convention)
break;
}
}

// Calculate fee: lamportsPerSignature * numSignatures + (rent * numATAs)
const lamportsPerSignature = parseInt(params.feeInfo?.fee || '0', 10);
const rentPerAta = parseInt(params.tokenAccountRentExemptAmount || '0', 10);
const signatureFee = lamportsPerSignature * parsed.numSignatures;
const rentFee = rentPerAta * tokenEnablements.length;
const totalFee = (signatureFee + rentFee).toString();

// Get transaction id from first signature (base58 encoded) or UNAVAILABLE
let txId = 'UNAVAILABLE';
if (parsed.signatures.length > 0) {
const firstSig = parsed.signatures[0];
// Signatures from WASM are base64 encoded, check if it's not all zeros
const sigBytes = Buffer.from(firstSig, 'base64');
const isEmptySignature = sigBytes.every((b) => b === 0);
if (!isEmptySignature) {
txId = base58.encode(sigBytes);
}
}

// Build durableNonce from WASM parsed data
const durableNonce = parsed.durableNonce
? {
walletNonceAddress: parsed.durableNonce.walletNonceAddress,
authWalletAddress: parsed.durableNonce.authWalletAddress,
}
: undefined;

return {
displayOrder: [
'id',
'type',
'blockhash',
'durableNonce',
'outputAmount',
'changeAmount',
'outputs',
'changeOutputs',
'tokenEnablements',
'fee',
'memo',
],
id: txId,
type: transactionType,
changeOutputs: [],
changeAmount: '0',
outputAmount: outputAmount.toFixed(0),
outputs,
fee: {
fee: totalFee,
feeRate: lamportsPerSignature,
},
memo,
blockhash: parsed.nonce,
durableNonce,
tokenEnablements,
};
}

/** @inheritDoc */
async getSignablePayload(serializedTx: string): Promise<Buffer> {
const factory = this.getBuilder();
Expand Down
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,10 @@
monocle-ts "^2.3.13"
newtype-ts "^0.3.5"

"@bitgo/wasm-solana@file:bitgo-wasm-solana-0.0.1.tgz":
version "0.0.1"
resolved "file:bitgo-wasm-solana-0.0.1.tgz#754a5e15c68f17e40c6eabf832baaace3f2d9d48"

"@bitgo/wasm-utxo@^1.27.0":
version "1.27.0"
resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.27.0.tgz#c8ebe108ce8b55d3df70cd3968211a6ef3001bef"
Expand Down
Loading