From 6bc84ce126e77786bbb6325056840488df4d96fd Mon Sep 17 00:00:00 2001 From: bijianing97 <826015751@qq.com> Date: Wed, 7 Jan 2026 16:41:49 +0800 Subject: [PATCH] Add MetaOra DLMM parser --- meta.go | 19 +- metaoradlmm.go | 468 +++++++++++++++++++++++++++++++++++++++++++++++++ parser.go | 5 +- 3 files changed, 488 insertions(+), 4 deletions(-) create mode 100644 metaoradlmm.go diff --git a/meta.go b/meta.go index 6d8a588..f0aee3b 100644 --- a/meta.go +++ b/meta.go @@ -35,8 +35,11 @@ var pumpMigrateEventDiscriminator = calculateDiscriminator("event:CompletePumpAm var pumpBuyEventDiscriminator = [8]byte{189, 219, 127, 211, 78, 230, 97, 238} var ( - pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") - wSolMint = solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112") + pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") + wSolMint = solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112") + usdcMint = solana.MustPublicKeyFromBase58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") + usd1Mint = solana.MustPublicKeyFromBase58("USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB") + meteoraDlmmProgram = solana.MustPublicKeyFromBase58("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo") ) var ( @@ -63,6 +66,16 @@ var ( pumpAmmDepositEventDiscriminator = calculateDiscriminator("event:DepositEvent") ) +var ( + meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap") + meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2") + meteoraDlmmSwapExactOutDiscriminator = calculateDiscriminator("global:swap_exact_out") + meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2") + meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact") + meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2") + meteoraDlmmSwapEventDiscriminator = calculateDiscriminator("event:Swap") +) + // Program PumpAmm program ID var budgGetProgram = solana.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111") @@ -75,3 +88,5 @@ var createAccountWithSeedDiscriminator = uint32(3) var systemProgram = solana.MustPublicKeyFromBase58("11111111111111111111111111111111") var raydiumLaunchLabProgramID = solana.MustPublicKeyFromBase58("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") + +var eventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29} diff --git a/metaoradlmm.go b/metaoradlmm.go new file mode 100644 index 0000000..a013ca5 --- /dev/null +++ b/metaoradlmm.go @@ -0,0 +1,468 @@ +package pump_parser + +import ( + "bytes" + "fmt" + + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +type meteoraDlmmSwapArgs struct { + AmountIn uint64 + MinAmountOut uint64 +} + +type meteoraDlmmSwapExactOutArgs struct { + MaxInAmount uint64 + OutAmount uint64 +} + +type meteoraDlmmSwapWithPriceImpactArgs struct { + AmountIn uint64 + ActiveID *int32 `bin:"optional"` + MaxPriceImpactBps uint16 +} + +type dlmmSwapEvent struct { + LbPair solana.PublicKey + From solana.PublicKey + StartBinId int32 + EndBinId int32 + AmountIn uint64 + AmountOut uint64 + SwapForY bool + Fee uint64 + ProtocolFee uint64 + FeeBps agbinary.Uint128 + HostFee uint64 +} + +type dlmmSwapAccounts struct { + poolIdx int + reserveXIdx int + reserveYIdx int + userTokenInIdx int + userTokenOutIdx int + tokenXMintIdx int + tokenYMintIdx int + oracleIdx int + userIdx int + tokenXProgramIdx int + tokenYProgramIdx int +} + +var meteoraDlmmEventAuthority = func() solana.PublicKey { + key, _, err := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, meteoraDlmmProgram) + if err != nil { + return solana.PublicKey{} + } + return key +}() + +func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(meteoraDlmmProgram) { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm program instruction not found, offset, %d, %d", offset[0], offset[1]) + } + decode := instruction.Data + if len(decode) < 8 { + offset[1] += 1 + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm program instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + + discriminator := *(*[8]byte)(decode[:8]) + switch discriminator { + case meteoraDlmmSwapDiscriminator, meteoraDlmmSwapExactOutDiscriminator, meteoraDlmmSwapWithPriceImpactDiscriminator: + return metaoradlmmSwapParser(tx, instruction, innerInstructions, offset) + case meteoraDlmmSwap2Discriminator, meteoraDlmmSwapExactOut2Discriminator, meteoraDlmmSwapWithPriceImpact2Discriminator: + return metaoradlmmSwap2Parser(tx, instruction, innerInstructions, offset) + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } +} + +func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + result := tx.rawTx + + entryContract := result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + if instruction.StackHeight != nil && *instruction.StackHeight > 2 { + for _, innerInstr := range innerInstructions.Instructions { + if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 { + entryContract = result.accountList[innerInstr.ProgramIDIndex] + } + } + } + + decode := instruction.Data + if len(decode) < 8 { + offset[1] += 1 + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap instruction data too short, offset, %d, %d", offset[0], offset[1]) + } + + discriminator := *(*[8]byte)(decode[:8]) + switch discriminator { + case meteoraDlmmSwapDiscriminator, meteoraDlmmSwap2Discriminator: + var args meteoraDlmmSwapArgs + if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + case meteoraDlmmSwapExactOutDiscriminator, meteoraDlmmSwapExactOut2Discriminator: + var args meteoraDlmmSwapExactOutArgs + if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap_exact_out decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + case meteoraDlmmSwapWithPriceImpactDiscriminator, meteoraDlmmSwapWithPriceImpact2Discriminator: + var args meteoraDlmmSwapWithPriceImpactArgs + if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap_with_price_impact decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + default: + return nil, increaseOffset(offset), InstructionIgnoredError + } + + accounts, err := resolveDlmmSwapAccounts(result, instruction.Accounts) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + pool := result.accountList[accounts.poolIdx] + reserveXIdx := accounts.reserveXIdx + reserveYIdx := accounts.reserveYIdx + userTokenInIdx := accounts.userTokenInIdx + userTokenOutIdx := accounts.userTokenOutIdx + tokenXMint := result.accountList[accounts.tokenXMintIdx] + tokenYMint := result.accountList[accounts.tokenYMintIdx] + userIdx := accounts.userIdx + tokenXProgram := result.accountList[accounts.tokenXProgramIdx] + tokenYProgram := result.accountList[accounts.tokenYProgramIdx] + + swapEvent, nextOffset, err := dlmmSwapEventFromInnerInstructions(innerInstructions, instruction, offset) + if err != nil { + return nil, nextOffset, err + } + offset = nextOffset + + baseMint, quoteMint, baseIsX := dlmmSelectBaseQuote(tokenXMint, tokenYMint) + baseTokenProgram := tokenXProgram + quoteTokenProgram := tokenYProgram + baseReserveIdx := reserveXIdx + quoteReserveIdx := reserveYIdx + if !baseIsX { + baseTokenProgram = tokenYProgram + quoteTokenProgram = tokenXProgram + baseReserveIdx = reserveYIdx + quoteReserveIdx = reserveXIdx + } + + swapForY := swapEvent.SwapForY + inputIsX := swapForY + amountIn := decimal.NewFromUint64(swapEvent.AmountIn) + amountOut := decimal.NewFromUint64(swapEvent.AmountOut) + + event := "buy" + if baseIsX == inputIsX { + event = "sell" + } + + userBaseIdx := userTokenOutIdx + userQuoteIdx := userTokenInIdx + if baseIsX == inputIsX { + userBaseIdx = userTokenInIdx + userQuoteIdx = userTokenOutIdx + } + + baseAmount := amountOut + quoteAmount := amountIn + if baseIsX { + if swapForY { + baseAmount = amountIn + quoteAmount = amountOut + } + } else { + if !swapForY { + baseAmount = amountIn + quoteAmount = amountOut + } + } + + eventUser := result.accountList[userIdx] + if !swapEvent.From.IsZero() { + eventUser = swapEvent.From + } + if !eventUser.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) { + userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, baseMint) + if !userBaseAmount.IsZero() { + eventUser = result.accountList[0] + userIdx = 0 + if ataIndex > 0 { + userBaseIdx = ataIndex + } + } + } + + baseDecimals, ok := dlmmTokenDecimals(result, baseReserveIdx) + if !ok { + baseDecimals, _ = dlmmTokenDecimals(result, userBaseIdx) + } + quoteDecimals, ok := dlmmTokenDecimals(result, quoteReserveIdx) + if !ok { + quoteDecimals, _ = dlmmTokenDecimals(result, userQuoteIdx) + } + + if _, exists := tx.Token[baseMint]; !exists && !baseMint.Equals(wSolMint) { + tx.Token[baseMint] = TokenMeta{ + Mint: baseMint, + Decimals: baseDecimals, + TokenProgram: baseTokenProgram, + } + } + if _, exists := tx.Token[quoteMint]; !exists && !quoteMint.Equals(wSolMint) { + tx.Token[quoteMint] = TokenMeta{ + Mint: quoteMint, + Decimals: quoteDecimals, + TokenProgram: quoteTokenProgram, + } + } + + baseReserve := getAccountBalanceAfterTx(result, baseReserveIdx) + quoteReserve := getAccountBalanceAfterTx(result, quoteReserveIdx) + userBase := getAccountBalanceAfterTx(result, userBaseIdx) + userQuote := GetTokenBalanceAfterTx(result, userIdx, quoteTokenProgram, quoteMint) + if quoteMint.Equals(wSolMint) { + if solAmount, err := GetSolAfterTx(result, userIdx); err == nil { + userQuote = userQuote.Add(decimal.NewFromUint64(solAmount)) + } + } + + swap := Swap{ + Program: SolProgramMeteoraDLMM, + Event: event, + Pool: pool, + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + Creator: solana.PublicKey{}, + BaseMintDecimals: baseDecimals, + QuoteMintDecimals: quoteDecimals, + User: eventUser, + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + } + + return []Swap{swap}, offset, nil +} + +func metaoradlmmSwap2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + return metaoradlmmSwapParser(tx, instruction, innerInstructions, offset) +} + +func dlmmSelectBaseQuote(tokenX, tokenY solana.PublicKey) (baseMint solana.PublicKey, quoteMint solana.PublicKey, baseIsX bool) { + priority := []solana.PublicKey{wSolMint, usdcMint, usd1Mint} + for _, mint := range priority { + if tokenX.Equals(mint) { + return tokenY, tokenX, false + } + if tokenY.Equals(mint) { + return tokenX, tokenY, true + } + } + return tokenX, tokenY, true +} + +func dlmmSwapEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmSwapEvent, [2]uint, error) { + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return dlmmSwapEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm swap get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen) + } + for innerIndex, innerInstr := range inners { + if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex { + continue + } + event, ok := dlmmDecodeSwapEvent(innerInstr.Data) + if !ok { + continue + } + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] += uint(innerIndex) + 1 + prefixLen + } + return event, offset, nil + } + return dlmmSwapEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm swap event not found, offset, %d, %d", offset[0], prefixLen) +} + +func dlmmDecodeSwapEvent(data []byte) (dlmmSwapEvent, bool) { + switch { + case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmSwapEventDiscriminator[:]): + var event dlmmSwapEvent + if err := agbinary.NewBorshDecoder(data[8:]).Decode(&event); err != nil { + return dlmmSwapEvent{}, false + } + return event, true + case len(data) >= 16 && + bytes.Equal(data[:8], eventDiscriminator[:]) && + bytes.Equal(data[8:16], meteoraDlmmSwapEventDiscriminator[:]): + var event dlmmSwapEvent + if err := agbinary.NewBorshDecoder(data[16:]).Decode(&event); err != nil { + return dlmmSwapEvent{}, false + } + return event, true + default: + return dlmmSwapEvent{}, false + } +} + +func resolveDlmmSwapAccounts(result *RawTx, accounts []int) (dlmmSwapAccounts, error) { + if len(accounts) < 13 { + return dlmmSwapAccounts{}, fmt.Errorf("accounts too short, expected at least 13") + } + accountList := result.accountList + + basePosCandidates := []int{1, 2} + for _, basePos := range basePosCandidates { + if basePos+6 >= len(accounts) { + continue + } + oraclePos := basePos + 6 + + userPos := oraclePos + 1 + hostFeePresent := true + if userPos < len(accounts) && dlmmIsSigner(result, accounts[userPos]) { + hostFeePresent = false + } else { + userPos = oraclePos + 2 + } + if userPos+2 >= len(accounts) { + continue + } + tokenXProgramPos := userPos + 1 + tokenYProgramPos := userPos + 2 + + eventAuthorityPos := tokenYProgramPos + 1 + if eventAuthorityPos < len(accounts) && accountList[accounts[eventAuthorityPos]].Equals(solana.MemoProgramID) { + eventAuthorityPos++ + } + programPos := eventAuthorityPos + 1 + if programPos >= len(accounts) { + continue + } + if !accountList[accounts[eventAuthorityPos]].Equals(meteoraDlmmEventAuthority) { + continue + } + if !accountList[accounts[programPos]].Equals(meteoraDlmmProgram) { + continue + } + + if hostFeePresent && oraclePos+1 < len(accounts) && dlmmIsSigner(result, accounts[oraclePos+1]) { + continue + } + + return dlmmSwapAccounts{ + poolIdx: accounts[0], + reserveXIdx: accounts[oraclePos-6], + reserveYIdx: accounts[oraclePos-5], + userTokenInIdx: accounts[oraclePos-4], + userTokenOutIdx: accounts[oraclePos-3], + tokenXMintIdx: accounts[oraclePos-2], + tokenYMintIdx: accounts[oraclePos-1], + oracleIdx: accounts[oraclePos], + userIdx: accounts[userPos], + tokenXProgramIdx: accounts[tokenXProgramPos], + tokenYProgramIdx: accounts[tokenYProgramPos], + }, nil + } + + return dlmmSwapAccounts{}, fmt.Errorf("accounts layout invalid") +} + +func dlmmTokenDecimals(result *RawTx, accountIndex int) (uint8, bool) { + for _, meta := range result.Meta.PostTokenBalances { + if meta.AccountIndex == accountIndex { + return uint8(meta.UITokenAmount.Decimals), true + } + } + for _, meta := range result.Meta.PreTokenBalances { + if meta.AccountIndex == accountIndex { + return uint8(meta.UITokenAmount.Decimals), true + } + } + return 0, false +} + +func dlmmTokenDelta(result *RawTx, accountIndex int) (decimal.Decimal, bool) { + before, okBefore := dlmmTokenAmount(result, accountIndex, false) + after, okAfter := dlmmTokenAmount(result, accountIndex, true) + if !okBefore && !okAfter { + return decimal.Zero, false + } + if !okBefore { + before = decimal.Zero + } + if !okAfter { + after = decimal.Zero + } + return after.Sub(before).Abs(), true +} + +func dlmmTokenDeltaSigned(result *RawTx, accountIndex int) (decimal.Decimal, bool) { + before, okBefore := dlmmTokenAmount(result, accountIndex, false) + after, okAfter := dlmmTokenAmount(result, accountIndex, true) + if !okBefore && !okAfter { + return decimal.Zero, false + } + if !okBefore { + before = decimal.Zero + } + if !okAfter { + after = decimal.Zero + } + return after.Sub(before), true +} + +func dlmmTokenAmount(result *RawTx, accountIndex int, post bool) (decimal.Decimal, bool) { + var balances []TokenBalance + if post { + balances = result.Meta.PostTokenBalances + } else { + balances = result.Meta.PreTokenBalances + } + for _, meta := range balances { + if meta.AccountIndex == accountIndex { + amount, err := decimal.NewFromString(meta.UITokenAmount.Amount) + if err != nil { + return decimal.Zero, false + } + return amount, true + } + } + return decimal.Zero, false +} + +func dlmmIsSigner(result *RawTx, accountIndex int) bool { + if accountIndex < 0 || accountIndex >= len(result.Transaction.Message.AccountKeys) { + return false + } + return accountIndex < result.Transaction.Message.Header.NumRequiredSignatures +} + +func dlmmTokenBalanceMeta(result *RawTx, accountIndex int) (TokenBalance, bool) { + for _, meta := range result.Meta.PostTokenBalances { + if meta.AccountIndex == accountIndex { + return meta, true + } + } + for _, meta := range result.Meta.PreTokenBalances { + if meta.AccountIndex == accountIndex { + return meta, true + } + } + return TokenBalance{}, false +} diff --git a/parser.go b/parser.go index 75d6bfc..138ef00 100644 --- a/parser.go +++ b/parser.go @@ -9,8 +9,9 @@ import ( ) var swapPrograms = map[solana.PublicKey]swapParser{ - pumpAmmProgram: pumpAmmParser, - pumpProgram: pumpParser, + pumpAmmProgram: pumpAmmParser, + pumpProgram: pumpParser, + meteoraDlmmProgram: metaoradlmmParser, } var actionPrograms = map[solana.PublicKey]actionParser{