diff --git a/packages/wasm-solana/js/explain.ts b/packages/wasm-solana/js/explain.ts new file mode 100644 index 0000000..da60da7 --- /dev/null +++ b/packages/wasm-solana/js/explain.ts @@ -0,0 +1,435 @@ +/** + * High-level transaction explanation. + * + * Builds on top of `parseTransaction` (WASM) to provide a structured + * "explain" view of a Solana transaction: type, outputs, inputs, fee, etc. + * + * The WASM parser returns raw individual instructions. This module combines + * related instruction sequences into higher-level operations and derives the + * overall transaction type. + */ + +import { parseTransactionData } from "./parser.js"; +import type { InstructionParams, ParsedTransaction } from "./parser.js"; + +// ============================================================================= +// Public types +// ============================================================================= + +export enum TransactionType { + Send = "Send", + StakingActivate = "StakingActivate", + StakingDeactivate = "StakingDeactivate", + StakingWithdraw = "StakingWithdraw", + StakingAuthorize = "StakingAuthorize", + StakingDelegate = "StakingDelegate", + WalletInitialization = "WalletInitialization", + AssociatedTokenAccountInitialization = "AssociatedTokenAccountInitialization", +} + +/** Solana base fee per signature (protocol constant). */ +const DEFAULT_LAMPORTS_PER_SIGNATURE = 5000n; + +export interface ExplainOptions { + /** Defaults to 5000 (Solana protocol constant). */ + lamportsPerSignature?: bigint | number | string; + tokenAccountRentExemptAmount?: bigint | number | string; +} + +export interface ExplainedOutput { + address: string; + amount: bigint; + tokenName?: string; +} + +export interface ExplainedInput { + address: string; + value: bigint; +} + +export interface TokenEnablement { + /** The ATA address being created */ + address: string; + /** The SPL token mint address */ + mintAddress: string; +} + +export interface StakingAuthorizeInfo { + stakingAddress: string; + oldAuthorizeAddress: string; + newAuthorizeAddress: string; + authorizeType: "Staker" | "Withdrawer"; + custodianAddress?: string; +} + +export interface ExplainedTransaction { + /** Transaction ID (base58 signature). Undefined if the transaction is unsigned. */ + id: string | undefined; + type: TransactionType; + feePayer: string; + fee: bigint; + blockhash: string; + durableNonce?: { walletNonceAddress: string; authWalletAddress: string }; + outputs: ExplainedOutput[]; + inputs: ExplainedInput[]; + outputAmount: bigint; + memo?: string; + /** + * Maps ATA address → owner address for CreateAssociatedTokenAccount instructions. + * Allows resolving newly-created token account ownership without an external lookup. + */ + ataOwnerMap: Record; + /** + * Token enablements from CreateAssociatedTokenAccount instructions. + * Contains the ATA address and mint address (consumer resolves token names). + */ + tokenEnablements: TokenEnablement[]; + /** Staking authorize details, present when the transaction changes stake authority. */ + stakingAuthorize?: StakingAuthorizeInfo; + numSignatures: number; +} + +// ============================================================================= +// Instruction combining +// ============================================================================= + +// Solana native staking requires 3 separate instructions: +// CreateAccount (fund the stake account) + StakeInitialize (set authorities) + DelegateStake (pick validator) +// Semantically this is a single "activate stake" operation. +// Marinade staking uses only CreateAccount + StakeInitialize (no Delegate) because +// Marinade's staker authority is the Marinade program, not a validator. + +interface CombinedStakeActivate { + kind: "StakingActivate"; + fromAddress: string; + stakingAddress: string; + amount: bigint; +} + +interface CombinedWalletInit { + kind: "WalletInitialization"; + fromAddress: string; + nonceAddress: string; + amount: bigint; +} + +type CombinedPattern = CombinedStakeActivate | CombinedWalletInit; + +/** + * Scan for multi-instruction patterns that should be combined: + * + * 1. CreateAccount + StakeInitialize [+ StakingDelegate] → StakingActivate + * - With Delegate following = NATIVE staking + * - Without Delegate = MARINADE staking (Marinade's program handles delegation) + * 2. CreateAccount + NonceInitialize → WalletInitialization + * - BitGo creates a nonce account during wallet initialization + */ +function detectCombinedPattern(instructions: InstructionParams[]): CombinedPattern | null { + for (let i = 0; i < instructions.length - 1; i++) { + const curr = instructions[i]; + const next = instructions[i + 1]; + + if (curr.type === "CreateAccount" && next.type === "StakeInitialize") { + return { + kind: "StakingActivate", + fromAddress: curr.fromAddress, + stakingAddress: curr.newAddress, + amount: curr.amount, + }; + } + + if (curr.type === "CreateAccount" && next.type === "NonceInitialize") { + return { + kind: "WalletInitialization", + fromAddress: curr.fromAddress, + nonceAddress: curr.newAddress, + amount: curr.amount, + }; + } + } + + return null; +} + +// ============================================================================= +// Transaction type derivation +// ============================================================================= + +const BOILERPLATE_TYPES = new Set([ + "NonceAdvance", + "Memo", + "SetComputeUnitLimit", + "SetPriorityFee", +]); + +function deriveTransactionType( + instructions: InstructionParams[], + combined: CombinedPattern | null, + memo: string | undefined, +): TransactionType { + if (combined) return TransactionType[combined.kind]; + + // Marinade deactivate: Transfer + memo containing "PrepareForRevoke" + if (memo?.includes("PrepareForRevoke")) return TransactionType.StakingDeactivate; + + // Jito pool operations map to staking types + if (instructions.some((i) => i.type === "StakePoolDepositSol")) + return TransactionType.StakingActivate; + if (instructions.some((i) => i.type === "StakePoolWithdrawStake")) + return TransactionType.StakingDeactivate; + + // ATA-only transactions (ignoring boilerplate like nonce/memo/compute budget) + const meaningful = instructions.filter((i) => !BOILERPLATE_TYPES.has(i.type)); + if (meaningful.length > 0 && meaningful.every((i) => i.type === "CreateAssociatedTokenAccount")) { + return TransactionType.AssociatedTokenAccountInitialization; + } + + // For staking instructions, the instruction type IS the transaction type + const staking = instructions.find((i) => i.type in TransactionType); + if (staking) return TransactionType[staking.type as keyof typeof TransactionType]; + + return TransactionType.Send; +} + +// ============================================================================= +// Transaction ID extraction +// ============================================================================= + +// Base58 encoding of 64 zero bytes. Unsigned transactions have all-zero +// signatures which encode to this constant. +const ALL_ZEROS_BASE58 = "1111111111111111111111111111111111111111111111111111111111111111"; + +function extractTransactionId(signatures: string[]): string | undefined { + const sig = signatures[0]; + if (!sig || sig === ALL_ZEROS_BASE58) return undefined; + return sig; +} + +// ============================================================================= +// Main export +// ============================================================================= + +/** + * Explain a Solana transaction. + * + * Takes raw transaction bytes and fee parameters, then returns a structured + * explanation including transaction type, outputs, inputs, fee, memo, and + * associated-token-account owner mappings. + * + * @param input - Raw transaction bytes (caller is responsible for decoding base64/hex) + * @param options - Fee parameters for calculating the total fee + * @returns An ExplainedTransaction with all fields populated + * + * @example + * ```typescript + * import { explainTransaction } from '@bitgo/wasm-solana'; + * + * const txBytes = Buffer.from(txBase64, 'base64'); + * const explained = explainTransaction(txBytes, { + * lamportsPerSignature: 5000n, + * tokenAccountRentExemptAmount: 2039280n, + * }); + * console.log(explained.type); // "Send", "StakingActivate", etc. + * ``` + */ +export function explainTransaction( + input: Uint8Array, + options: ExplainOptions, +): ExplainedTransaction { + const { lamportsPerSignature, tokenAccountRentExemptAmount } = options; + + const parsed: ParsedTransaction = parseTransactionData(input); + + // --- Transaction ID --- + const id = extractTransactionId(parsed.signatures); + + // --- Fee calculation --- + // Base fee = numSignatures × lamportsPerSignature + let fee = + BigInt(parsed.numSignatures) * + (lamportsPerSignature !== undefined + ? BigInt(lamportsPerSignature) + : DEFAULT_LAMPORTS_PER_SIGNATURE); + + // Each CreateAssociatedTokenAccount instruction creates a new token account, + // which requires a rent-exempt deposit. Add that to the fee. + const ataCount = parsed.instructionsData.filter( + (i) => i.type === "CreateAssociatedTokenAccount", + ).length; + if (ataCount > 0 && tokenAccountRentExemptAmount !== undefined) { + fee += BigInt(ataCount) * BigInt(tokenAccountRentExemptAmount); + } + + // --- Extract memo (needed before type derivation) --- + let memo: string | undefined; + for (const instr of parsed.instructionsData) { + if (instr.type === "Memo") { + memo = instr.memo; + } + } + + // --- Detect combined instruction patterns --- + const combined = detectCombinedPattern(parsed.instructionsData); + const txType = deriveTransactionType(parsed.instructionsData, combined, memo); + + // Marinade deactivate: Transfer + PrepareForRevoke memo. + // The Transfer is a contract interaction (not a real value transfer), + // so we skip it from outputs. + const isMarinadeDeactivate = + txType === TransactionType.StakingDeactivate && + memo !== undefined && + memo.includes("PrepareForRevoke"); + + // --- Extract outputs and inputs --- + const outputs: ExplainedOutput[] = []; + const inputs: ExplainedInput[] = []; + + if (combined?.kind === "StakingActivate") { + // Combined native/Marinade staking activate — the staking address receives + // the full amount from the funding account. + outputs.push({ + address: combined.stakingAddress, + amount: combined.amount, + }); + inputs.push({ + address: combined.fromAddress, + value: combined.amount, + }); + } else if (combined?.kind === "WalletInitialization") { + // Wallet initialization — funds the new nonce account. + outputs.push({ + address: combined.nonceAddress, + amount: combined.amount, + }); + inputs.push({ + address: combined.fromAddress, + value: combined.amount, + }); + } else { + // Process individual instructions for outputs/inputs + for (const instr of parsed.instructionsData) { + switch (instr.type) { + case "Transfer": + // Skip Transfer for Marinade deactivate — it's a program interaction, + // not a real value transfer to an external address. + if (isMarinadeDeactivate) break; + outputs.push({ + address: instr.toAddress, + amount: instr.amount, + }); + inputs.push({ + address: instr.fromAddress, + value: instr.amount, + }); + break; + + case "TokenTransfer": + outputs.push({ + address: instr.toAddress, + amount: instr.amount, + tokenName: instr.tokenAddress, + }); + inputs.push({ + address: instr.fromAddress, + value: instr.amount, + }); + break; + + case "StakingActivate": + outputs.push({ + address: instr.stakingAddress, + amount: instr.amount, + }); + inputs.push({ + address: instr.fromAddress, + value: instr.amount, + }); + break; + + case "StakingWithdraw": + // Withdraw: SOL flows FROM the staking address TO the recipient. + // `fromAddress` is the recipient (where funds go), + // `stakingAddress` is the source. + outputs.push({ + address: instr.fromAddress, + amount: instr.amount, + }); + inputs.push({ + address: instr.stakingAddress, + value: instr.amount, + }); + break; + + case "StakePoolDepositSol": + // Jito liquid staking: SOL is deposited into the stake pool. + // The funding account is debited; output goes to the pool address. + outputs.push({ + address: instr.stakePool, + amount: instr.lamports, + }); + inputs.push({ + address: instr.fundingAccount, + value: instr.lamports, + }); + break; + + // StakingDeactivate, StakingAuthorize, StakingDelegate, + // StakePoolWithdrawStake, NonceAdvance, CreateAccount, + // StakeInitialize, NonceInitialize, SetComputeUnitLimit, + // SetPriorityFee, CreateAssociatedTokenAccount, + // CloseAssociatedTokenAccount, Memo, Unknown + // — no value inputs/outputs. + } + } + } + + // --- Output amount --- + // Only count native SOL outputs (no tokenName). Token amounts are in different + // denominations and shouldn't be mixed with SOL lamports. + const outputAmount = outputs.filter((o) => !o.tokenName).reduce((sum, o) => sum + o.amount, 0n); + + // --- ATA owner mapping and token enablements --- + const ataOwnerMap: Record = {}; + const tokenEnablements: TokenEnablement[] = []; + for (const instr of parsed.instructionsData) { + if (instr.type === "CreateAssociatedTokenAccount") { + ataOwnerMap[instr.ataAddress] = instr.ownerAddress; + tokenEnablements.push({ + address: instr.ataAddress, + mintAddress: instr.mintAddress, + }); + } + } + + // --- Staking authorize --- + let stakingAuthorize: StakingAuthorizeInfo | undefined; + for (const instr of parsed.instructionsData) { + if (instr.type === "StakingAuthorize") { + stakingAuthorize = { + stakingAddress: instr.stakingAddress, + oldAuthorizeAddress: instr.oldAuthorizeAddress, + newAuthorizeAddress: instr.newAuthorizeAddress, + authorizeType: instr.authorizeType, + custodianAddress: instr.custodianAddress, + }; + break; + } + } + + return { + id, + type: txType, + feePayer: parsed.feePayer, + fee, + blockhash: parsed.nonce, + durableNonce: parsed.durableNonce, + outputs, + inputs, + outputAmount, + memo, + ataOwnerMap, + tokenEnablements, + stakingAuthorize, + numSignatures: parsed.numSignatures, + }; +} diff --git a/packages/wasm-solana/js/index.ts b/packages/wasm-solana/js/index.ts index 3aac2e5..4f27480 100644 --- a/packages/wasm-solana/js/index.ts +++ b/packages/wasm-solana/js/index.ts @@ -9,6 +9,7 @@ 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"; @@ -20,9 +21,49 @@ export { VersionedTransaction, isVersionedTransaction } from "./versioned.js"; export type { AddressLookupTableData } from "./versioned.js"; // Top-level function exports -export { parseTransaction } from "./parser.js"; +export { parseTransactionData } from "./parser.js"; export { buildFromVersionedData } from "./builder.js"; export { buildFromIntent, buildFromIntent as buildTransactionFromIntent } from "./intentBuilder.js"; +export { explainTransaction, TransactionType } from "./explain.js"; + +// Re-export Transaction import for parseTransaction +import { Transaction as _Transaction } from "./transaction.js"; + +/** + * Parse a Solana transaction from raw bytes. + * + * Returns a `Transaction` instance that can be both inspected and signed. + * Use `.parse()` on the returned Transaction to get decoded instruction data. + * + * This is the single entry point for working with transactions — like + * `BitGoPsbt.fromBytes()` in wasm-utxo. + * + * @param bytes - Raw transaction bytes + * @returns A Transaction that can be inspected (`.parse()`) and signed (`.addSignature()`) + * + * @example + * ```typescript + * import { parseTransaction } from '@bitgo/wasm-solana'; + * + * const tx = parseTransaction(txBytes); + * + * // Inspect + * const parsed = tx.parse(); + * console.log(parsed.feePayer); + * for (const instr of parsed.instructionsData) { + * if (instr.type === 'Transfer') { + * console.log(`${instr.amount} lamports to ${instr.toAddress}`); + * } + * } + * + * // Sign + * tx.addSignature(pubkey, signature); + * const signedBytes = tx.toBytes(); + * ``` + */ +export function parseTransaction(bytes: Uint8Array): _Transaction { + return _Transaction.fromBytes(bytes); +} // Intent builder type exports export type { @@ -36,6 +77,10 @@ export type { EnableTokenIntent, CloseAtaIntent, ConsolidateIntent, + AuthorizeIntent, + CustomTxIntent, + CustomTxInstruction, + CustomTxKey, SolanaIntent, StakePoolConfig, BuildFromIntentParams, @@ -68,7 +113,6 @@ export { // Type exports export type { AccountMeta, Instruction } from "./transaction.js"; export type { - TransactionInput, ParsedTransaction, DurableNonce as ParsedDurableNonce, InstructionParams, @@ -94,6 +138,16 @@ 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, diff --git a/packages/wasm-solana/js/parser.ts b/packages/wasm-solana/js/parser.ts index 01d13d6..384c795 100644 --- a/packages/wasm-solana/js/parser.ts +++ b/packages/wasm-solana/js/parser.ts @@ -5,17 +5,9 @@ * matching BitGoJS's TxData format. * * All monetary amounts (amount, fee, lamports, poolTokens) are returned as bigint. - * Accepts both raw bytes and Transaction objects for convenience. */ import { ParserNamespace } from "./wasm/wasm_solana.js"; -import type { Transaction } from "./transaction.js"; -import type { VersionedTransaction } from "./versioned.js"; - -/** - * Input type for parseTransaction - accepts bytes or Transaction objects. - */ -export type TransactionInput = Uint8Array | Transaction | VersionedTransaction; // ============================================================================= // Instruction Types - matching BitGoJS InstructionParams. @@ -277,48 +269,19 @@ export interface ParsedTransaction { } // ============================================================================= -// parseTransaction function +// parseTransactionData function // ============================================================================= /** - * Parse a Solana transaction into structured data. - * - * This is the main entry point for transaction parsing. It deserializes the - * transaction and decodes all instructions into semantic types. - * - * All monetary amounts (amount, fee, lamports, poolTokens) are returned as bigint - * directly from WASM - no post-processing needed. + * Parse raw transaction bytes into a plain data object with decoded instructions. * - * Note: This returns the raw parsed data including NonceAdvance instructions. - * Consumers (like BitGoJS) may choose to filter NonceAdvance from instructionsData - * since that info is also available in durableNonce. + * This is the low-level parsing function. Most callers should use the top-level + * `parseTransaction(bytes)` which returns a `Transaction` instance with both + * inspection (`.parse()`) and signing (`.addSignature()`) capabilities. * - * @param input - Raw transaction bytes, Transaction, or VersionedTransaction + * @param bytes - Raw transaction bytes * @returns A ParsedTransaction with all instructions decoded - * @throws Error if the transaction cannot be parsed - * - * @example - * ```typescript - * import { parseTransaction, buildTransaction, Transaction } from '@bitgo/wasm-solana'; - * - * // From bytes - * const txBytes = Buffer.from(base64EncodedTx, 'base64'); - * const parsed = parseTransaction(txBytes); - * - * // Directly from a Transaction object (no roundtrip through bytes) - * const tx = buildTransaction(intent); - * const parsed = parseTransaction(tx); - * - * console.log(parsed.feePayer); - * for (const instr of parsed.instructionsData) { - * if (instr.type === 'Transfer') { - * console.log(`Transfer ${instr.amount} from ${instr.fromAddress} to ${instr.toAddress}`); - * } - * } - * ``` */ -export function parseTransaction(input: TransactionInput): ParsedTransaction { - // If input is a Transaction or VersionedTransaction, extract bytes - const bytes = input instanceof Uint8Array ? input : input.toBytes(); +export function parseTransactionData(bytes: Uint8Array): ParsedTransaction { return ParserNamespace.parse_transaction(bytes) as ParsedTransaction; } diff --git a/packages/wasm-solana/js/transaction.ts b/packages/wasm-solana/js/transaction.ts index 0fced38..4066b5f 100644 --- a/packages/wasm-solana/js/transaction.ts +++ b/packages/wasm-solana/js/transaction.ts @@ -1,6 +1,8 @@ import { WasmTransaction } from "./wasm/wasm_solana.js"; import { Keypair } from "./keypair.js"; import { Pubkey } from "./pubkey.js"; +import { parseTransactionData } from "./parser.js"; +import type { ParsedTransaction } from "./parser.js"; /** * Account metadata for an instruction @@ -27,22 +29,29 @@ export interface Instruction { } /** - * Solana Transaction wrapper for low-level deserialization and inspection. + * Solana Transaction — the single object for inspecting and signing transactions. * - * This class provides low-level access to transaction structure. - * For high-level semantic parsing with decoded instructions, use `parseTransaction()` instead. + * Use `parseTransaction(bytes)` to create an instance. The returned Transaction + * can be both inspected (`.parse()` for decoded instructions) and signed + * (`.addSignature()`, `.signablePayload()`, `.toBytes()`). * * @example * ```typescript - * import { Transaction, parseTransaction } from '@bitgo/wasm-solana'; + * import { parseTransaction } from '@bitgo/wasm-solana'; * - * // Low-level access: - * const tx = Transaction.fromBytes(txBytes); - * console.log(tx.feePayer); + * const tx = parseTransaction(txBytes); * - * // High-level parsing (preferred): - * const parsed = parseTransaction(txBytes); - * console.log(parsed.instructionsData); // Decoded instruction types + * // Inspect decoded instructions + * const parsed = tx.parse(); + * for (const instr of parsed.instructionsData) { + * if (instr.type === 'Transfer') { + * console.log(`${instr.amount} lamports to ${instr.toAddress}`); + * } + * } + * + * // Sign and serialize + * tx.addSignature(pubkey, signature); + * const signedBytes = tx.toBytes(); * ``` */ export class Transaction { @@ -237,6 +246,26 @@ export class Transaction { this._wasm.sign_with_keypair(keypair.wasm); } + /** + * Parse the transaction into decoded instruction data. + * + * Returns structured data with all instructions decoded into semantic types + * (Transfer, StakeActivate, TokenTransfer, etc.) with amounts as bigint. + * + * @returns A ParsedTransaction with decoded instructions, feePayer, nonce, etc. + * + * @example + * ```typescript + * const tx = parseTransaction(txBytes); + * const parsed = tx.parse(); + * console.log(parsed.feePayer); + * console.log(parsed.instructionsData); // Decoded instruction types + * ``` + */ + parse(): ParsedTransaction { + return parseTransactionData(this._wasm.to_bytes()); + } + /** * Get the underlying WASM instance (internal use only) * @internal diff --git a/packages/wasm-solana/test/bitgojs-compat.ts b/packages/wasm-solana/test/bitgojs-compat.ts index a1c2f25..75fb4b7 100644 --- a/packages/wasm-solana/test/bitgojs-compat.ts +++ b/packages/wasm-solana/test/bitgojs-compat.ts @@ -5,7 +5,7 @@ * what BitGoJS's Transaction.toJson() produces. */ import * as assert from "assert"; -import { parseTransaction } from "../js/parser.js"; +import { parseTransactionData as parseTransaction } from "../js/parser.js"; // Helper to decode base64 in tests function base64ToBytes(base64: string): Uint8Array { diff --git a/packages/wasm-solana/test/intentBuilder.ts b/packages/wasm-solana/test/intentBuilder.ts index 519e3a6..178cb12 100644 --- a/packages/wasm-solana/test/intentBuilder.ts +++ b/packages/wasm-solana/test/intentBuilder.ts @@ -8,7 +8,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */ import assert from "assert"; -import { buildFromIntent, Transaction, parseTransaction } from "../dist/cjs/js/index.js"; +import { + buildFromIntent, + Transaction, + parseTransactionData as parseTransaction, +} from "../dist/cjs/js/index.js"; describe("buildFromIntent", function () { // Common test params diff --git a/packages/wasm-solana/test/parser.ts b/packages/wasm-solana/test/parser.ts index 9f7d41c..240f90f 100644 --- a/packages/wasm-solana/test/parser.ts +++ b/packages/wasm-solana/test/parser.ts @@ -1,5 +1,5 @@ import * as assert from "assert"; -import { parseTransaction } from "../js/parser.js"; +import { parseTransactionData as parseTransaction } from "../js/parser.js"; // Helper to decode base64 in tests function base64ToBytes(base64: string): Uint8Array { diff --git a/packages/webui/src/wasm-solana/transaction/index.ts b/packages/webui/src/wasm-solana/transaction/index.ts index 014b069..e65a9e4 100644 --- a/packages/webui/src/wasm-solana/transaction/index.ts +++ b/packages/webui/src/wasm-solana/transaction/index.ts @@ -516,7 +516,8 @@ class SolanaTransactionParser extends BaseComponent { try { // Parse the transaction const bytes = base64ToBytes(txData); - const parsed = parseTransaction(bytes); + const tx = parseTransaction(bytes); + const parsed = tx.parse(); // Render transaction info txInfoEl.replaceChildren(this.renderTxInfo(parsed)); @@ -533,7 +534,9 @@ class SolanaTransactionParser extends BaseComponent { "section", { class: "instructions-section" }, h("h2", {}, `Instructions (${parsed.instructionsData.length})`), - ...parsed.instructionsData.map((instr, idx) => this.renderInstruction(instr, idx)), + ...parsed.instructionsData.map((instr: InstructionParams, idx: number) => + this.renderInstruction(instr, idx), + ), ), ); } catch (e) {