Decode base58 Anchor event data in TypeScript: strip the 8-byte CPI event discriminator, then BorshCoder.events.decode on base64
Task: turn an opaque base58 blob captured from a Solana transaction (e.g. a pump.fun-style TradeEvent inner instruction) into typed fields — mint, solAmount, tokenAmount, isBuy, user, timestamp, reserves. Manual Borsh offset arithmetic tends to produce plausible-looking garbage.
Working recipe, using the program's IDL and @coral-xyz/anchor:
import bs58 from "bs58"; import idl from "./pump-fun.json"; import { BorshCoder } from "@coral-xyz/anchor"; import { base64 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
let buffer = Buffer.from(bs58.decode(base58Data)); buffer = buffer.slice(8); // drop the CPI-event discriminator const coder = new BorshCoder(idl as any); const args = coder.events.decode(base64.encode(buffer)); // -> { name: 'TradeEvent', data: { mint: PublicKey, solAmount: BN, ... } }
Two details do all the work and neither is documented where you would look:
1. Anchor's "event CPI" wraps the event in a self-CPI whose instruction data begins with an extra 8-byte discriminator *before* the event's own 8-byte discriminator. Without the slice(8), events.decode sees the wrong discriminator and returns null rather than throwing — a silent failure that looks like a bad IDL.
2. coder.events.decode expects a **base64** string, not a Buffer and not base58, so a base58 payload has to be re-encoded. Passing raw bytes gets you null too.
Numeric fields come back as BN and pubkeys as PublicKey, so convert with .toString() before comparing to expected values. This pattern applies to any Anchor program whose IDL you have; for programs without a published IDL you are back to manual Borsh layout.
Source: adapted from a Stack Exchange answer by rckprtr (question by DannySorost), https://solana.stackexchange.com/questions/14735/decoding-solana-transaction-data — CC BY-SA 4.0