import type { ParsedTransactionWithMeta } from "@solana/web3.js"; import { PublicKey } from "@solana/web3.js"; import bs58 from "bs58"; import { IDL_DATA } from "./idl/pit.generated.js"; /** * Transaction parser for PIT program instructions * Provides unified parsing for all PIT instruction types */ /** * Extract instruction discriminators from IDL * Converts snake_case instruction names to SCREAMING_SNAKE_CASE */ function extractDiscriminatorsFromIDL(idl: typeof IDL_DATA): Record { const discriminators: Record = {}; for (const instruction of idl.instructions) { const key = instruction.name.toUpperCase(); discriminators[key] = instruction.discriminator as readonly number[]; } return discriminators; } // Instruction discriminators (auto-extracted from IDL) export const DISCRIMINATORS = extractDiscriminatorsFromIDL(IDL_DATA); // Account indices (common for trade/redeem instructions) const TRADE_REDEEM_ACCOUNT_INDICES = { MARKET_STATE: 0, USER: 1, USER_TOKEN_ACCOUNT: 2, USER_WSOL_ACCOUNT: 3, WSOL_MINT: 4, WSOL_VAULT: 5, TOKEN_MINT: 6, // Pool token mint TOKEN_PROGRAM: 7, TOKEN_2022_PROGRAM: 8, }; // ===== Type Definitions ===== export interface TradeData { instructionType: "trade"; poolType: "high" | "low"; strikeIndex: number; isHit: boolean; amount: number; isBuy: boolean; limitLamports: number; tokenMint?: PublicKey; } export interface RedeemData { instructionType: "redeem"; poolType: "high" | "low"; strikeIndex: number; isHit: boolean; amount: number; isWin: boolean; tokenMint?: PublicKey; } export interface MarkExtremeData { instructionType: "mark_extreme"; poolType: "high" | "low"; newOutcome: number; } export interface SettleMarketData { instructionType: "settle_market"; } export interface InitializeMarketData { instructionType: "initialize_market"; weekNumber: number; endTimestamp: number; totalSubsidy: number; } export interface ClaimFeesData { instructionType: "claim_fees"; } export interface WithdrawSubsidyData { instructionType: "withdraw_subsidy"; } export interface PauseMarketData { instructionType: "pause_market"; } export interface UnpauseMarketData { instructionType: "unpause_market"; } export interface SetNicknameData { instructionType: "set_nickname"; nickname: string; } export type ParsedInstructionData = | TradeData | RedeemData | MarkExtremeData | SettleMarketData | InitializeMarketData | ClaimFeesData | WithdrawSubsidyData | PauseMarketData | UnpauseMarketData | SetNicknameData; // ===== Helper Functions ===== function matchesDiscriminator(data: Uint8Array, discriminator: readonly number[]): boolean { if (data.length < 8) return false; for (let i = 0; i < 8; i++) { if (data[i] !== discriminator[i]) return false; } return true; } function readU8(data: Uint8Array, offset: number): number { return data[offset]; } function readU32LE(data: Uint8Array, offset: number): number { return Buffer.from(data.slice(offset, offset + 4)).readUInt32LE(0); } function readI64LE(data: Uint8Array, offset: number): number { return Number(Buffer.from(data.slice(offset, offset + 8)).readBigInt64LE(0)); } function readU64LE(data: Uint8Array, offset: number): number { return Number(Buffer.from(data.slice(offset, offset + 8)).readBigUInt64LE(0)); } function readBool(data: Uint8Array, offset: number): boolean { return data[offset] === 1; } function readPoolType(data: Uint8Array, offset: number): "high" | "low" { return data[offset] === 0 ? "high" : "low"; } function readString(data: Uint8Array, offset: number): string { const length = readU32LE(data, offset); const bytes = data.slice(offset + 4, offset + 4 + length); return new TextDecoder().decode(bytes); } // ===== Extract Functions ===== /** * Extract pool token mint address from trade or redeem instruction * Returns the mint address at account index 6 (token_mint) */ export function extractPoolMint( tx: ParsedTransactionWithMeta, programId: string ): PublicKey | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("accounts" in instruction && instruction.accounts) { const accounts = instruction.accounts; if (accounts.length > TRADE_REDEEM_ACCOUNT_INDICES.TOKEN_MINT) { const mintAddress = accounts[TRADE_REDEEM_ACCOUNT_INDICES.TOKEN_MINT]; if (mintAddress) { return mintAddress; } } } } } return null; } /** * Extract market state PDA from any instruction * Market state is always account index 0 for all instructions */ export function extractMarketState( tx: ParsedTransactionWithMeta, programId: string ): PublicKey | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("accounts" in instruction && instruction.accounts && instruction.accounts.length > 0) { return instruction.accounts[0]; } } } return null; } // ===== Individual Instruction Parsers ===== export function parseTradeInstruction( tx: ParsedTransactionWithMeta, programId: string ): TradeData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.TRADE)) continue; const tokenMint = "accounts" in instruction && instruction.accounts && instruction.accounts.length > TRADE_REDEEM_ACCOUNT_INDICES.TOKEN_MINT ? instruction.accounts[TRADE_REDEEM_ACCOUNT_INDICES.TOKEN_MINT] : undefined; return { instructionType: "trade", poolType: readPoolType(data, 8), strikeIndex: readU8(data, 9), isHit: readBool(data, 10), amount: readU64LE(data, 11), isBuy: readBool(data, 19), limitLamports: readU64LE(data, 20), tokenMint, }; } catch (err) { console.error("[SDK] Failed to parse trade instruction:", err); } } } } return null; } export function parseRedeemInstruction( tx: ParsedTransactionWithMeta, programId: string ): RedeemData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.REDEEM)) continue; const tokenMint = "accounts" in instruction && instruction.accounts && instruction.accounts.length > TRADE_REDEEM_ACCOUNT_INDICES.TOKEN_MINT ? instruction.accounts[TRADE_REDEEM_ACCOUNT_INDICES.TOKEN_MINT] : undefined; return { instructionType: "redeem", poolType: readPoolType(data, 8), strikeIndex: readU8(data, 9), isHit: readBool(data, 10), amount: readU64LE(data, 11), isWin: readBool(data, 19), tokenMint, }; } catch (err) { console.error("[SDK] Failed to parse redeem instruction:", err); } } } } return null; } export function parseMarkExtremeInstruction( tx: ParsedTransactionWithMeta, programId: string ): MarkExtremeData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.MARK_EXTREME)) continue; return { instructionType: "mark_extreme", poolType: readPoolType(data, 8), newOutcome: readU8(data, 9), }; } catch (err) { console.error("[SDK] Failed to parse mark_extreme instruction:", err); } } } } return null; } export function parseSettleMarketInstruction( tx: ParsedTransactionWithMeta, programId: string ): SettleMarketData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.SETTLE_MARKET)) continue; // settle_market has no arguments (settles both pools) return { instructionType: "settle_market", }; } catch (err) { console.error("[SDK] Failed to parse settle_market instruction:", err); } } } } return null; } export function parseInitializeMarketInstruction( tx: ParsedTransactionWithMeta, programId: string ): InitializeMarketData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.INITIALIZE_MARKET)) continue; return { instructionType: "initialize_market", weekNumber: readU32LE(data, 8), endTimestamp: readI64LE(data, 12), totalSubsidy: readU64LE(data, 20), // Note: q_vectors are after this, but we don't parse them here }; } catch (err) { console.error("[SDK] Failed to parse initialize_market instruction:", err); } } } } return null; } export function parseClaimFeesInstruction( tx: ParsedTransactionWithMeta, programId: string ): ClaimFeesData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.CLAIM_FEES)) continue; return { instructionType: "claim_fees", }; } catch (err) { console.error("[SDK] Failed to parse claim_fees instruction:", err); } } } } return null; } export function parseWithdrawSubsidyInstruction( tx: ParsedTransactionWithMeta, programId: string ): WithdrawSubsidyData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.WITHDRAW_SUBSIDY)) continue; return { instructionType: "withdraw_subsidy", }; } catch (err) { console.error("[SDK] Failed to parse withdraw_subsidy instruction:", err); } } } } return null; } export function parsePauseMarketInstruction( tx: ParsedTransactionWithMeta, programId: string ): PauseMarketData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.PAUSE_MARKET)) continue; return { instructionType: "pause_market", }; } catch (err) { console.error("[SDK] Failed to parse pause_market instruction:", err); } } } } return null; } export function parseUnpauseMarketInstruction( tx: ParsedTransactionWithMeta, programId: string ): UnpauseMarketData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.UNPAUSE_MARKET)) continue; return { instructionType: "unpause_market", }; } catch (err) { console.error("[SDK] Failed to parse unpause_market instruction:", err); } } } } return null; } export function parseSetNicknameInstruction( tx: ParsedTransactionWithMeta, programId: string ): SetNicknameData | null { for (const instruction of tx.transaction.message.instructions) { if ("programId" in instruction && instruction.programId.toString() === programId) { if ("data" in instruction && instruction.data) { try { const data = bs58.decode(instruction.data); if (!matchesDiscriminator(data, DISCRIMINATORS.SET_NICKNAME)) continue; return { instructionType: "set_nickname", nickname: readString(data, 8), }; } catch (err) { console.error("[SDK] Failed to parse set_nickname instruction:", err); } } } } return null; } // ===== Unified Parser ===== /** * Parse any PIT program instruction from a transaction * Returns the parsed instruction data or null if no PIT instruction found */ export function parsePitInstruction( tx: ParsedTransactionWithMeta, programId: string ): ParsedInstructionData | null { // Try each parser in order of likelihood (trade/redeem most common) return ( parseTradeInstruction(tx, programId) || parseRedeemInstruction(tx, programId) || parseMarkExtremeInstruction(tx, programId) || parseSettleMarketInstruction(tx, programId) || parseInitializeMarketInstruction(tx, programId) || parseClaimFeesInstruction(tx, programId) || parseWithdrawSubsidyInstruction(tx, programId) || parsePauseMarketInstruction(tx, programId) || parseUnpauseMarketInstruction(tx, programId) || parseSetNicknameInstruction(tx, programId) || null ); }