| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- import { BorshInstructionCoder } from '@coral-xyz/anchor'
- import { IDL } from '@meteora-ag/dlmm'
- import {
- MessageCompiledInstruction,
- PublicKey,
- TransactionResponse,
- VersionedTransactionResponse,
- } from '@solana/web3.js'
- import bs58 from 'bs58'
- import BN from 'bn.js'
- import {
- ADD_LIQUIDITY_NAMES,
- CLOSE_POSITION_NAMES,
- DLMM_PROGRAM_ID,
- INIT_POSITION_NAMES,
- REBALANCE_NAMES,
- REMOVE_LIQUIDITY_NAMES,
- } from '../constants'
- import type { DlmmActionType, ParsedDlmmOperation } from '../meteora/types'
- // Instruction name mapping: Anchor IDL uses snake_case, we use camelCase internally
- const INSTRUCTION_NAME_MAP: Record<string, string> = {
- add_liquidity: 'addLiquidity',
- add_liquidity_by_strategy: 'addLiquidityByStrategy',
- add_liquidity_by_strategy2: 'addLiquidityByStrategy2',
- add_liquidity_by_strategy_one_side: 'addLiquidityByStrategyOneSide',
- remove_liquidity: 'removeLiquidity',
- remove_liquidity_by_range: 'removeLiquidityByRange',
- remove_liquidity_by_range2: 'removeLiquidityByRange2',
- close_position: 'closePosition',
- close_position2: 'closePosition2',
- close_position_if_empty: 'closePositionIfEmpty',
- initialize_position: 'initializePosition',
- initialize_position2: 'initializePosition2',
- rebalance_liquidity: 'rebalanceLiquidity',
- }
- // Account index mapping for each instruction (snake_case IDL names)
- // Verified against IDL account layouts
- const ACCOUNT_MAPS: Record<
- string,
- { position: number; lbPair: number; sender: number }
- > = {
- // Add liquidity
- add_liquidity: { position: 0, lbPair: 1, sender: 11 },
- add_liquidity_by_strategy: { position: 0, lbPair: 1, sender: 11 },
- add_liquidity_by_strategy2: { position: 0, lbPair: 1, sender: 9 },
- add_liquidity_by_strategy_one_side: { position: 0, lbPair: 1, sender: 8 },
- // Remove liquidity
- remove_liquidity: { position: 0, lbPair: 1, sender: 11 },
- remove_liquidity_by_range: { position: 0, lbPair: 1, sender: 11 },
- remove_liquidity_by_range2: { position: 0, lbPair: 1, sender: 9 },
- // Close position
- close_position: { position: 0, lbPair: 1, sender: 4 },
- close_position2: { position: 0, lbPair: -1, sender: 1 },
- // close_position_if_empty: accounts = [position, sender, rent_receiver, event_authority, program]
- close_position_if_empty: { position: 0, lbPair: -1, sender: 1 },
- // Initialize position
- initialize_position: { position: 1, lbPair: 2, sender: 0 },
- initialize_position2: { position: 1, lbPair: 2, sender: 0 },
- // Rebalance liquidity
- // IDL: [position, lb_pair, bin_array_bitmap_extension, user_token_x, user_token_y,
- // reserve_x, reserve_y, token_x_mint, token_y_mint, owner, ...]
- rebalance_liquidity: { position: 0, lbPair: 1, sender: 9 },
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const coder = new BorshInstructionCoder(IDL as any)
- function classifyAction(name: string): DlmmActionType | null {
- const camelName = INSTRUCTION_NAME_MAP[name] ?? name
- if (ADD_LIQUIDITY_NAMES.has(camelName)) return 'ADD_LIQUIDITY'
- if (REMOVE_LIQUIDITY_NAMES.has(camelName)) return 'REMOVE_LIQUIDITY'
- if (CLOSE_POSITION_NAMES.has(camelName)) return 'CLOSE_POSITION'
- if (INIT_POSITION_NAMES.has(camelName)) return 'INIT_POSITION'
- if (REBALANCE_NAMES.has(camelName)) return 'REBALANCE_LIQUIDITY'
- return null
- }
- function resolveAllAccountKeys(
- tx: VersionedTransactionResponse | TransactionResponse
- ): PublicKey[] {
- const msg = tx.transaction.message
- // Versioned (v0) transactions
- if ('getAccountKeys' in msg) {
- // Must pass loaded addresses from lookup tables to avoid
- // "address table lookups were not resolved" error on v0 txs
- const toKey = (k: string | PublicKey) =>
- typeof k === 'string' ? new PublicKey(k) : k
- const accountKeysFromLookups = tx.meta?.loadedAddresses
- ? {
- writable: tx.meta.loadedAddresses.writable.map(toKey),
- readonly: tx.meta.loadedAddresses.readonly.map(toKey),
- }
- : undefined
- const keys = (
- msg as {
- getAccountKeys: (opts: object) => {
- length: number
- get: (i: number) => PublicKey | undefined
- }
- }
- ).getAccountKeys({ accountKeysFromLookups })
- const allKeys: PublicKey[] = []
- for (let i = 0; i < keys.length; i++) {
- const key = keys.get(i)
- if (key) allKeys.push(key)
- }
- return allKeys
- }
- // Legacy transactions
- if ('accountKeys' in msg) {
- return (msg as { accountKeys: PublicKey[] }).accountKeys
- }
- return []
- }
- interface ParsedInstruction {
- programIdIndex: number
- accounts: number[]
- dataBuffer: Buffer
- }
- function extractInstructions(
- tx: VersionedTransactionResponse | TransactionResponse
- ): ParsedInstruction[] {
- const msg = tx.transaction.message
- if ('compiledInstructions' in msg) {
- // Versioned transaction: data is Uint8Array
- return (
- msg as { compiledInstructions: MessageCompiledInstruction[] }
- ).compiledInstructions.map((ix) => ({
- programIdIndex: ix.programIdIndex,
- accounts: [...ix.accountKeyIndexes],
- dataBuffer: Buffer.from(ix.data),
- }))
- }
- if ('instructions' in msg) {
- // Legacy transaction: data is base58 encoded string
- return (
- msg as {
- instructions: {
- programIdIndex: number
- accounts: number[]
- data: string
- }[]
- }
- ).instructions.map((ix) => ({
- programIdIndex: ix.programIdIndex,
- accounts: ix.accounts,
- dataBuffer: Buffer.from(bs58.decode(ix.data)),
- }))
- }
- return []
- }
- export function parseTransaction(
- tx: VersionedTransactionResponse | TransactionResponse
- ): ParsedDlmmOperation[] {
- if (!tx.meta || tx.meta.err) return []
- const accountKeys = resolveAllAccountKeys(tx)
- const instructions = extractInstructions(tx)
- const operations: ParsedDlmmOperation[] = []
- for (const ix of instructions) {
- const programId = accountKeys[ix.programIdIndex]
- if (!programId || !programId.equals(DLMM_PROGRAM_ID)) continue
- try {
- const decoded = coder.decode(ix.dataBuffer)
- if (!decoded) continue
- const action = classifyAction(decoded.name)
- if (!action) continue
- const accountMap = ACCOUNT_MAPS[decoded.name]
- if (!accountMap) continue
- const positionAddress = accountKeys[ix.accounts[accountMap.position]]
- const lbPairAddress =
- accountMap.lbPair >= 0
- ? accountKeys[ix.accounts[accountMap.lbPair]]
- : undefined
- const senderAddress = accountKeys[ix.accounts[accountMap.sender]]
- if (!positionAddress || !senderAddress) continue
- const op: ParsedDlmmOperation = {
- action,
- instructionName: INSTRUCTION_NAME_MAP[decoded.name] ?? decoded.name,
- positionAddress,
- lbPairAddress: lbPairAddress ?? positionAddress,
- senderAddress,
- }
- // Extract add liquidity parameters
- if (action === 'ADD_LIQUIDITY') {
- const data = decoded.data as Record<string, unknown>
- const liqParam = data.liquidity_parameter as
- | Record<string, unknown>
- | undefined
- if (liqParam) {
- op.totalXAmount = new BN(liqParam.amount_x?.toString() ?? '0')
- op.totalYAmount = new BN(liqParam.amount_y?.toString() ?? '0')
- const strategyParams = liqParam.strategy_parameters as
- | Record<string, unknown>
- | undefined
- if (strategyParams) {
- op.minBinId = strategyParams.min_bin_id as number
- op.maxBinId = strategyParams.max_bin_id as number
- const strategyType = strategyParams.strategy_type as
- | Record<string, unknown>
- | undefined
- if (strategyType) {
- const strategyName = Object.keys(strategyType)[0]
- const strategyTypes = [
- 'SpotOneSide',
- 'CurveOneSide',
- 'BidAskOneSide',
- 'SpotBalanced',
- 'CurveBalanced',
- 'BidAskBalanced',
- 'SpotImBalanced',
- 'CurveImBalanced',
- 'BidAskImBalanced',
- ]
- op.strategyType = strategyTypes.indexOf(strategyName)
- }
- }
- }
- }
- // Extract remove liquidity parameters
- if (action === 'REMOVE_LIQUIDITY') {
- const data = decoded.data as Record<string, unknown>
- if (data.from_bin_id !== undefined) {
- op.fromBinId = data.from_bin_id as number
- op.toBinId = data.to_bin_id as number
- op.bpsToRemove = data.bps_to_remove as number
- }
- }
- // Extract rebalance liquidity parameters
- if (action === 'REBALANCE_LIQUIDITY') {
- const data = decoded.data as Record<string, unknown>
- const rebalanceParams = data.rebalance_liquidity_params as
- | Record<string, unknown>
- | undefined
- if (rebalanceParams) {
- op.rebalanceActiveId = rebalanceParams.active_id as number
- const adds = rebalanceParams.adds as
- | Record<string, unknown>[]
- | undefined
- if (adds && adds.length > 0) {
- op.rebalanceAdds = adds.map((add) => ({
- minDeltaId: add.min_delta_id as number,
- maxDeltaId: add.max_delta_id as number,
- }))
- // Compute absolute bin IDs from deltas for downstream use
- const activeId = op.rebalanceActiveId ?? 0
- const allMinBins = adds.map(
- (a) => activeId + (a.min_delta_id as number)
- )
- const allMaxBins = adds.map(
- (a) => activeId + (a.max_delta_id as number)
- )
- op.minBinId = Math.min(...allMinBins)
- op.maxBinId = Math.max(...allMaxBins)
- }
- const removes = rebalanceParams.removes as
- | Record<string, unknown>[]
- | undefined
- if (removes && removes.length > 0) {
- op.rebalanceRemoves = removes.map((rem) => ({
- fromBinId: rem.from_bin_id as number,
- toBinId: rem.to_bin_id as number,
- bpsToRemove: rem.bps_to_remove as number,
- }))
- }
- }
- }
- // Extract init position parameters
- if (action === 'INIT_POSITION') {
- const data = decoded.data as Record<string, unknown>
- if (data.lower_bin_id !== undefined) {
- op.minBinId = data.lower_bin_id as number
- const width = data.width as number
- if (width) {
- op.maxBinId = (data.lower_bin_id as number) + width - 1
- }
- }
- }
- operations.push(op)
- } catch {
- // Skip instructions that fail to decode
- continue
- }
- }
- return operations
- }
|