@bitgo/wasm-solana 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,307 +0,0 @@
1
- /**
2
- * High-level transaction explanation.
3
- *
4
- * Builds on top of `parseTransaction` (WASM) to provide a structured
5
- * "explain" view of a Solana transaction: type, outputs, inputs, fee, etc.
6
- *
7
- * The WASM parser returns raw individual instructions. This module combines
8
- * related instruction sequences into higher-level operations and derives the
9
- * overall transaction type.
10
- */
11
- import { parseTransactionData } from "./parser.js";
12
- // =============================================================================
13
- // Public types
14
- // =============================================================================
15
- export var TransactionType;
16
- (function (TransactionType) {
17
- TransactionType["Send"] = "Send";
18
- TransactionType["StakingActivate"] = "StakingActivate";
19
- TransactionType["StakingDeactivate"] = "StakingDeactivate";
20
- TransactionType["StakingWithdraw"] = "StakingWithdraw";
21
- TransactionType["StakingAuthorize"] = "StakingAuthorize";
22
- TransactionType["StakingDelegate"] = "StakingDelegate";
23
- TransactionType["WalletInitialization"] = "WalletInitialization";
24
- TransactionType["AssociatedTokenAccountInitialization"] = "AssociatedTokenAccountInitialization";
25
- })(TransactionType || (TransactionType = {}));
26
- /** Solana base fee per signature (protocol constant). */
27
- const DEFAULT_LAMPORTS_PER_SIGNATURE = 5000n;
28
- /**
29
- * Scan for multi-instruction patterns that should be combined:
30
- *
31
- * 1. CreateAccount + StakeInitialize [+ StakingDelegate] → StakingActivate
32
- * - With Delegate following = NATIVE staking
33
- * - Without Delegate = MARINADE staking (Marinade's program handles delegation)
34
- * 2. CreateAccount + NonceInitialize → WalletInitialization
35
- * - BitGo creates a nonce account during wallet initialization
36
- */
37
- function detectCombinedPattern(instructions) {
38
- for (let i = 0; i < instructions.length - 1; i++) {
39
- const curr = instructions[i];
40
- const next = instructions[i + 1];
41
- if (curr.type === "CreateAccount" && next.type === "StakeInitialize") {
42
- return {
43
- kind: "StakingActivate",
44
- fromAddress: curr.fromAddress,
45
- stakingAddress: curr.newAddress,
46
- amount: curr.amount,
47
- };
48
- }
49
- if (curr.type === "CreateAccount" && next.type === "NonceInitialize") {
50
- return {
51
- kind: "WalletInitialization",
52
- fromAddress: curr.fromAddress,
53
- nonceAddress: curr.newAddress,
54
- amount: curr.amount,
55
- };
56
- }
57
- }
58
- return null;
59
- }
60
- // =============================================================================
61
- // Transaction type derivation
62
- // =============================================================================
63
- const BOILERPLATE_TYPES = new Set([
64
- "NonceAdvance",
65
- "Memo",
66
- "SetComputeUnitLimit",
67
- "SetPriorityFee",
68
- ]);
69
- function deriveTransactionType(instructions, combined, memo) {
70
- if (combined)
71
- return TransactionType[combined.kind];
72
- // Marinade deactivate: Transfer + memo containing "PrepareForRevoke"
73
- if (memo?.includes("PrepareForRevoke"))
74
- return TransactionType.StakingDeactivate;
75
- // Jito pool operations map to staking types
76
- if (instructions.some((i) => i.type === "StakePoolDepositSol"))
77
- return TransactionType.StakingActivate;
78
- if (instructions.some((i) => i.type === "StakePoolWithdrawStake"))
79
- return TransactionType.StakingDeactivate;
80
- // ATA-only transactions (ignoring boilerplate like nonce/memo/compute budget)
81
- const meaningful = instructions.filter((i) => !BOILERPLATE_TYPES.has(i.type));
82
- if (meaningful.length > 0 && meaningful.every((i) => i.type === "CreateAssociatedTokenAccount")) {
83
- return TransactionType.AssociatedTokenAccountInitialization;
84
- }
85
- // For staking instructions, the instruction type IS the transaction type
86
- const staking = instructions.find((i) => i.type in TransactionType);
87
- if (staking)
88
- return TransactionType[staking.type];
89
- return TransactionType.Send;
90
- }
91
- // =============================================================================
92
- // Transaction ID extraction
93
- // =============================================================================
94
- // Base58 encoding of 64 zero bytes. Unsigned transactions have all-zero
95
- // signatures which encode to this constant.
96
- const ALL_ZEROS_BASE58 = "1111111111111111111111111111111111111111111111111111111111111111";
97
- function extractTransactionId(signatures) {
98
- const sig = signatures[0];
99
- if (!sig || sig === ALL_ZEROS_BASE58)
100
- return undefined;
101
- return sig;
102
- }
103
- // =============================================================================
104
- // Main export
105
- // =============================================================================
106
- /**
107
- * Explain a Solana transaction.
108
- *
109
- * Takes raw transaction bytes and fee parameters, then returns a structured
110
- * explanation including transaction type, outputs, inputs, fee, memo, and
111
- * associated-token-account owner mappings.
112
- *
113
- * @param input - Raw transaction bytes (caller is responsible for decoding base64/hex)
114
- * @param options - Fee parameters for calculating the total fee
115
- * @returns An ExplainedTransaction with all fields populated
116
- *
117
- * @example
118
- * ```typescript
119
- * import { explainTransaction } from '@bitgo/wasm-solana';
120
- *
121
- * const txBytes = Buffer.from(txBase64, 'base64');
122
- * const explained = explainTransaction(txBytes, {
123
- * lamportsPerSignature: 5000n,
124
- * tokenAccountRentExemptAmount: 2039280n,
125
- * });
126
- * console.log(explained.type); // "Send", "StakingActivate", etc.
127
- * ```
128
- */
129
- export function explainTransaction(input, options) {
130
- const { lamportsPerSignature, tokenAccountRentExemptAmount } = options;
131
- const parsed = parseTransactionData(input);
132
- // --- Transaction ID ---
133
- const id = extractTransactionId(parsed.signatures);
134
- // --- Fee calculation ---
135
- // Base fee = numSignatures × lamportsPerSignature
136
- let fee = BigInt(parsed.numSignatures) *
137
- (lamportsPerSignature !== undefined
138
- ? BigInt(lamportsPerSignature)
139
- : DEFAULT_LAMPORTS_PER_SIGNATURE);
140
- // Each CreateAssociatedTokenAccount instruction creates a new token account,
141
- // which requires a rent-exempt deposit. Add that to the fee.
142
- const ataCount = parsed.instructionsData.filter((i) => i.type === "CreateAssociatedTokenAccount").length;
143
- if (ataCount > 0 && tokenAccountRentExemptAmount !== undefined) {
144
- fee += BigInt(ataCount) * BigInt(tokenAccountRentExemptAmount);
145
- }
146
- // --- Extract memo (needed before type derivation) ---
147
- let memo;
148
- for (const instr of parsed.instructionsData) {
149
- if (instr.type === "Memo") {
150
- memo = instr.memo;
151
- }
152
- }
153
- // --- Detect combined instruction patterns ---
154
- const combined = detectCombinedPattern(parsed.instructionsData);
155
- const txType = deriveTransactionType(parsed.instructionsData, combined, memo);
156
- // Marinade deactivate: Transfer + PrepareForRevoke memo.
157
- // The Transfer is a contract interaction (not a real value transfer),
158
- // so we skip it from outputs.
159
- const isMarinadeDeactivate = txType === TransactionType.StakingDeactivate &&
160
- memo !== undefined &&
161
- memo.includes("PrepareForRevoke");
162
- // --- Extract outputs and inputs ---
163
- const outputs = [];
164
- const inputs = [];
165
- if (combined?.kind === "StakingActivate") {
166
- // Combined native/Marinade staking activate — the staking address receives
167
- // the full amount from the funding account.
168
- outputs.push({
169
- address: combined.stakingAddress,
170
- amount: combined.amount,
171
- });
172
- inputs.push({
173
- address: combined.fromAddress,
174
- value: combined.amount,
175
- });
176
- }
177
- else if (combined?.kind === "WalletInitialization") {
178
- // Wallet initialization — funds the new nonce account.
179
- outputs.push({
180
- address: combined.nonceAddress,
181
- amount: combined.amount,
182
- });
183
- inputs.push({
184
- address: combined.fromAddress,
185
- value: combined.amount,
186
- });
187
- }
188
- else {
189
- // Process individual instructions for outputs/inputs
190
- for (const instr of parsed.instructionsData) {
191
- switch (instr.type) {
192
- case "Transfer":
193
- // Skip Transfer for Marinade deactivate — it's a program interaction,
194
- // not a real value transfer to an external address.
195
- if (isMarinadeDeactivate)
196
- break;
197
- outputs.push({
198
- address: instr.toAddress,
199
- amount: instr.amount,
200
- });
201
- inputs.push({
202
- address: instr.fromAddress,
203
- value: instr.amount,
204
- });
205
- break;
206
- case "TokenTransfer":
207
- outputs.push({
208
- address: instr.toAddress,
209
- amount: instr.amount,
210
- tokenName: instr.tokenAddress,
211
- });
212
- inputs.push({
213
- address: instr.fromAddress,
214
- value: instr.amount,
215
- });
216
- break;
217
- case "StakingActivate":
218
- outputs.push({
219
- address: instr.stakingAddress,
220
- amount: instr.amount,
221
- });
222
- inputs.push({
223
- address: instr.fromAddress,
224
- value: instr.amount,
225
- });
226
- break;
227
- case "StakingWithdraw":
228
- // Withdraw: SOL flows FROM the staking address TO the recipient.
229
- // `fromAddress` is the recipient (where funds go),
230
- // `stakingAddress` is the source.
231
- outputs.push({
232
- address: instr.fromAddress,
233
- amount: instr.amount,
234
- });
235
- inputs.push({
236
- address: instr.stakingAddress,
237
- value: instr.amount,
238
- });
239
- break;
240
- case "StakePoolDepositSol":
241
- // Jito liquid staking: SOL is deposited into the stake pool.
242
- // The funding account is debited; output goes to the pool address.
243
- outputs.push({
244
- address: instr.stakePool,
245
- amount: instr.lamports,
246
- });
247
- inputs.push({
248
- address: instr.fundingAccount,
249
- value: instr.lamports,
250
- });
251
- break;
252
- // StakingDeactivate, StakingAuthorize, StakingDelegate,
253
- // StakePoolWithdrawStake, NonceAdvance, CreateAccount,
254
- // StakeInitialize, NonceInitialize, SetComputeUnitLimit,
255
- // SetPriorityFee, CreateAssociatedTokenAccount,
256
- // CloseAssociatedTokenAccount, Memo, Unknown
257
- // — no value inputs/outputs.
258
- }
259
- }
260
- }
261
- // --- Output amount ---
262
- // Only count native SOL outputs (no tokenName). Token amounts are in different
263
- // denominations and shouldn't be mixed with SOL lamports.
264
- const outputAmount = outputs.filter((o) => !o.tokenName).reduce((sum, o) => sum + o.amount, 0n);
265
- // --- ATA owner mapping and token enablements ---
266
- const ataOwnerMap = {};
267
- const tokenEnablements = [];
268
- for (const instr of parsed.instructionsData) {
269
- if (instr.type === "CreateAssociatedTokenAccount") {
270
- ataOwnerMap[instr.ataAddress] = instr.ownerAddress;
271
- tokenEnablements.push({
272
- address: instr.ataAddress,
273
- mintAddress: instr.mintAddress,
274
- });
275
- }
276
- }
277
- // --- Staking authorize ---
278
- let stakingAuthorize;
279
- for (const instr of parsed.instructionsData) {
280
- if (instr.type === "StakingAuthorize") {
281
- stakingAuthorize = {
282
- stakingAddress: instr.stakingAddress,
283
- oldAuthorizeAddress: instr.oldAuthorizeAddress,
284
- newAuthorizeAddress: instr.newAuthorizeAddress,
285
- authorizeType: instr.authorizeType,
286
- custodianAddress: instr.custodianAddress,
287
- };
288
- break;
289
- }
290
- }
291
- return {
292
- id,
293
- type: txType,
294
- feePayer: parsed.feePayer,
295
- fee,
296
- blockhash: parsed.nonce,
297
- durableNonce: parsed.durableNonce,
298
- outputs,
299
- inputs,
300
- outputAmount,
301
- memo,
302
- ataOwnerMap,
303
- tokenEnablements,
304
- stakingAuthorize,
305
- numSignatures: parsed.numSignatures,
306
- };
307
- }