From 5cd3a97d8174e0cdeb14f89c7f0b5e0964f5aeb5 Mon Sep 17 00:00:00 2001 From: thloyi Date: Fri, 8 May 2026 11:21:30 +0800 Subject: [PATCH] pump usdc support --- cmd/rpc_parse/main.go | 2 +- meta.go | 6 +- pump.go | 650 ++++++++++++++++++++++++++++++++++++------ pump_test.go | 161 +++++++++++ 4 files changed, 729 insertions(+), 90 deletions(-) diff --git a/cmd/rpc_parse/main.go b/cmd/rpc_parse/main.go index 59814e4..18f21a6 100644 --- a/cmd/rpc_parse/main.go +++ b/cmd/rpc_parse/main.go @@ -14,7 +14,7 @@ func main() { const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d" txHash := os.Getenv("TX_HASH") if txHash == "" { - txHash = "2AhpL5KhVmG3D38CwMzrHuRyTucEQ43GzBXL2mo5WiugdZMVmK1dtX8brGe3sxvvFDY6iSSviJTvqCtr4UL3Pc7J" + txHash = "29v7u2ewLr3Se6cWYC2xwN8jszqMWwvVgPz7MqkctTveMo1csWWYDBcUsjuJwb5ciugc5so1jc9QcmR7syJTjEns" } if txHash == "" { diff --git a/meta.go b/meta.go index 0fa749f..4590e47 100644 --- a/meta.go +++ b/meta.go @@ -20,12 +20,16 @@ var mayhemFeeAccounts = []solana.PublicKey{ var pumpGetFeesDiscriminator = calculateDiscriminator("global:get_fees") var pumpBuyDiscriminator = calculateDiscriminator("global:buy") -var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_exact_sol_in") +var pumpBuyExactSolInDiscriminator = calculateDiscriminator("global:buy_exact_sol_in") +var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_v2") +var pumpBuyExactQuoteInV2Discriminator = calculateDiscriminator("global:buy_exact_quote_in_v2") var pumpSellDiscriminator = calculateDiscriminator("global:sell") +var pumpSellV2Discriminator = calculateDiscriminator("global:sell_v2") var pumpCreateDiscriminator = calculateDiscriminator("global:create") var pumpCreateV2Discriminator = calculateDiscriminator("global:create_v2") var pumpAdminSetCreatorDiscriminator = calculateDiscriminator("global:admin_set_creator") var pumpMigrateDiscriminator = calculateDiscriminator("global:migrate") +var pumpMigrateV2Discriminator = calculateDiscriminator("global:migrate_v2") var pumpEventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29} var pumpTradeEventDiscriminator = [16]byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238} diff --git a/pump.go b/pump.go index fa675d0..3867a01 100644 --- a/pump.go +++ b/pump.go @@ -33,7 +33,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct discriminator := *(*[8]byte)(decode[:8]) switch discriminator { - case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator: + case pumpBuyExactSolInDiscriminator, pumpBuyDiscriminator, pumpBuyV2Discriminator, pumpBuyExactQuoteInV2Discriminator, pumpSellDiscriminator, pumpSellV2Discriminator: if tx.Err != nil { return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset) } @@ -43,7 +43,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct return nil, increaseOffset(offset), InstructionIgnoredError } return CreateParser(tx, instruction, innerInstructions, offset) - case pumpMigrateDiscriminator: + case pumpMigrateDiscriminator, pumpMigrateV2Discriminator: if tx.Err != nil { return nil, increaseOffset(offset), InstructionIgnoredError } @@ -89,6 +89,56 @@ type PumpCreateEvent struct { TokenProgram solana.PublicKey IsMayhemMode bool IsCashbackEnabled bool + QuoteMint solana.PublicKey + VirtualQuoteReserves uint64 +} + +type pumpCreateEventLegacy struct { + Name string + Symbol string + Uri string + + Mint solana.PublicKey + BondingCurve solana.PublicKey + User solana.PublicKey + Creator solana.PublicKey + + Timestamp int64 + VirtualTokenReserves uint64 + VirtualSolReserves uint64 + RealTokenReserves uint64 + TokenTotalSupply uint64 + TokenProgram solana.PublicKey + IsMayhemMode bool + IsCashbackEnabled bool +} + +func decodePumpCreateEvent(data []byte) (PumpCreateEvent, error) { + var event PumpCreateEvent + if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil { + return event, nil + } + var legacy pumpCreateEventLegacy + if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err != nil { + return PumpCreateEvent{}, err + } + return PumpCreateEvent{ + Name: legacy.Name, + Symbol: legacy.Symbol, + Uri: legacy.Uri, + Mint: legacy.Mint, + BondingCurve: legacy.BondingCurve, + User: legacy.User, + Creator: legacy.Creator, + Timestamp: legacy.Timestamp, + VirtualTokenReserves: legacy.VirtualTokenReserves, + VirtualSolReserves: legacy.VirtualSolReserves, + RealTokenReserves: legacy.RealTokenReserves, + TokenTotalSupply: legacy.TokenTotalSupply, + TokenProgram: legacy.TokenProgram, + IsMayhemMode: legacy.IsMayhemMode, + IsCashbackEnabled: legacy.IsCashbackEnabled, + }, nil } func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { @@ -106,7 +156,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions } for innerIndex, innerInstr := range inners { if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], pumpCreateEventDiscriminator[:]) { - err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&createEvent) + createEvent, err = decodePumpCreateEvent(innerInstr.Data[16:]) if offset[1] == 0 { offset[0] += 1 } else { @@ -129,6 +179,11 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions } userBase := getAccountBalanceAfterTx(result, userIndex) userQuote, _ := GetSolAfterTx(result, userIndex) + quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, instr, createEvent) + userQuoteBalance := decimal.NewFromUint64(userQuote) + if !quoteMint.IsZero() && !quoteMint.Equals(wSolMint) { + userQuoteBalance = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint) + } totalSupply := decimal.NewFromUint64(createEvent.TokenTotalSupply).Div(decimal.New(1, 6)) tx.Token[createEvent.Mint] = TokenMeta{ @@ -146,12 +201,12 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions Event: "create", Pool: createEvent.BondingCurve, BaseMint: createEvent.Mint, - QuoteMint: solana.PublicKey{}, + QuoteMint: quoteMint, BaseTokenProgram: createEvent.TokenProgram, - QuoteTokenProgram: solana.PublicKey{}, + QuoteTokenProgram: quoteTokenProgram, Creator: createEvent.Creator, BaseMintDecimals: 6, - QuoteMintDecimals: 9, + QuoteMintDecimals: quoteDecimals, User: createEvent.User, BaseAmount: decimal.Zero, QuoteAmount: decimal.Zero, @@ -160,7 +215,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions Mayhem: createEvent.IsMayhemMode, Cashback: createEvent.IsCashbackEnabled, UserBaseBalance: userBase, - UserQuoteBalance: decimal.NewFromUint64(userQuote), + UserQuoteBalance: userQuoteBalance, EntryContract: entryContract, }, }, offset, nil @@ -197,6 +252,141 @@ type PumpTradeEvent struct { MayhemMode bool CashbackFeeBasisPoints uint64 Cashback uint64 + BuybackFeeBasisPoints uint64 + BuybackFee uint64 + Shareholders []PumpShareholder + QuoteMint solana.PublicKey + QuoteAmount uint64 + VirtualQuoteReserves uint64 + RealQuoteReserves uint64 +} + +type PumpShareholder struct { + Address solana.PublicKey + ShareBps uint16 +} + +type pumpTradeEventLegacy struct { + Mint solana.PublicKey + SolAmount uint64 + TokenAmount uint64 + IsBuy bool + User solana.PublicKey + Timestamp int64 + VirtualSolReserves uint64 + VirtualTokenReserves uint64 + + RealSolReserves uint64 + RealTokenReserves uint64 + + FeeRecipient solana.PublicKey + FeeBasisPoints uint64 + Fee uint64 + + Creator solana.PublicKey + + CreatorFeeBasisPoints uint64 + CreatorFee uint64 + + TrackVolume bool + TotalUnclaimedTokens uint64 + TotalClaimedTokens uint64 + CurrentSolVolume uint64 + LastUpdateTimestamp int64 + IxName string + MayhemMode bool + CashbackFeeBasisPoints uint64 + Cashback uint64 +} + +type pumpTradeEventLegacyV0 struct { + Mint solana.PublicKey + SolAmount uint64 + TokenAmount uint64 + IsBuy bool + User solana.PublicKey + Timestamp int64 + VirtualSolReserves uint64 + VirtualTokenReserves uint64 + RealSolReserves uint64 + RealTokenReserves uint64 + FeeRecipient solana.PublicKey + FeeBasisPoints uint64 + Fee uint64 + Creator solana.PublicKey + CreatorFeeBasisPoints uint64 + CreatorFee uint64 + TrackVolume bool + TotalUnclaimedTokens uint64 + TotalClaimedTokens uint64 + CurrentSolVolume uint64 + LastUpdateTimestamp int64 + IxName string +} + +func decodePumpTradeEvent(data []byte) (PumpTradeEvent, error) { + var event PumpTradeEvent + if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil { + return event, nil + } + var legacy pumpTradeEventLegacy + if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err == nil { + return PumpTradeEvent{ + Mint: legacy.Mint, + SolAmount: legacy.SolAmount, + TokenAmount: legacy.TokenAmount, + IsBuy: legacy.IsBuy, + User: legacy.User, + Timestamp: legacy.Timestamp, + VirtualSolReserves: legacy.VirtualSolReserves, + VirtualTokenReserves: legacy.VirtualTokenReserves, + RealSolReserves: legacy.RealSolReserves, + RealTokenReserves: legacy.RealTokenReserves, + FeeRecipient: legacy.FeeRecipient, + FeeBasisPoints: legacy.FeeBasisPoints, + Fee: legacy.Fee, + Creator: legacy.Creator, + CreatorFeeBasisPoints: legacy.CreatorFeeBasisPoints, + CreatorFee: legacy.CreatorFee, + TrackVolume: legacy.TrackVolume, + TotalUnclaimedTokens: legacy.TotalUnclaimedTokens, + TotalClaimedTokens: legacy.TotalClaimedTokens, + CurrentSolVolume: legacy.CurrentSolVolume, + LastUpdateTimestamp: legacy.LastUpdateTimestamp, + IxName: legacy.IxName, + MayhemMode: legacy.MayhemMode, + CashbackFeeBasisPoints: legacy.CashbackFeeBasisPoints, + Cashback: legacy.Cashback, + }, nil + } + var legacyV0 pumpTradeEventLegacyV0 + if err := agbinary.NewBorshDecoder(data).Decode(&legacyV0); err != nil { + return PumpTradeEvent{}, err + } + return PumpTradeEvent{ + Mint: legacyV0.Mint, + SolAmount: legacyV0.SolAmount, + TokenAmount: legacyV0.TokenAmount, + IsBuy: legacyV0.IsBuy, + User: legacyV0.User, + Timestamp: legacyV0.Timestamp, + VirtualSolReserves: legacyV0.VirtualSolReserves, + VirtualTokenReserves: legacyV0.VirtualTokenReserves, + RealSolReserves: legacyV0.RealSolReserves, + RealTokenReserves: legacyV0.RealTokenReserves, + FeeRecipient: legacyV0.FeeRecipient, + FeeBasisPoints: legacyV0.FeeBasisPoints, + Fee: legacyV0.Fee, + Creator: legacyV0.Creator, + CreatorFeeBasisPoints: legacyV0.CreatorFeeBasisPoints, + CreatorFee: legacyV0.CreatorFee, + TrackVolume: legacyV0.TrackVolume, + TotalUnclaimedTokens: legacyV0.TotalUnclaimedTokens, + TotalClaimedTokens: legacyV0.TotalClaimedTokens, + CurrentSolVolume: legacyV0.CurrentSolVolume, + LastUpdateTimestamp: legacyV0.LastUpdateTimestamp, + IxName: legacyV0.IxName, + }, nil } type PumpTradeFeeArg struct { @@ -220,17 +410,185 @@ type PumpTradeArgs struct { func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) { switch { - case bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]): + case bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]), + bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]): return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true - case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]): + case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]), + bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]): return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true - case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]): + case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]), + bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]): return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true default: return SwapModeUnknown, decimal.Zero, decimal.Zero, false } } +type pumpTradeAccountLayout struct { + IsV2 bool + FeeRecipient int + BaseMint int + QuoteMint int + BaseTokenProgram int + QuoteTokenProgram int + Pool int + BasePoolToken int + QuotePoolToken int + User int + BaseUserToken int + QuoteUserToken int +} + +func pumpTradeLayout(instr Instruction) (pumpTradeAccountLayout, bool) { + if len(instr.Data) < 8 { + return pumpTradeAccountLayout{}, false + } + discriminator := instr.Data[:8] + switch { + case bytes.Equal(discriminator, pumpBuyDiscriminator[:]), bytes.Equal(discriminator, pumpBuyExactSolInDiscriminator[:]): + if len(instr.Accounts) <= 8 { + return pumpTradeAccountLayout{}, false + } + return pumpTradeAccountLayout{ + FeeRecipient: 1, + BaseMint: 2, + QuoteMint: -1, + BaseTokenProgram: 8, + QuoteTokenProgram: -1, + Pool: 3, + BasePoolToken: 4, + QuotePoolToken: -1, + User: 6, + BaseUserToken: 5, + QuoteUserToken: -1, + }, true + case bytes.Equal(discriminator, pumpSellDiscriminator[:]): + if len(instr.Accounts) <= 9 { + return pumpTradeAccountLayout{}, false + } + return pumpTradeAccountLayout{ + FeeRecipient: 1, + BaseMint: 2, + QuoteMint: -1, + BaseTokenProgram: 9, + QuoteTokenProgram: -1, + Pool: 3, + BasePoolToken: 4, + QuotePoolToken: -1, + User: 6, + BaseUserToken: 5, + QuoteUserToken: -1, + }, true + case bytes.Equal(discriminator, pumpBuyV2Discriminator[:]), + bytes.Equal(discriminator, pumpBuyExactQuoteInV2Discriminator[:]), + bytes.Equal(discriminator, pumpSellV2Discriminator[:]): + if len(instr.Accounts) <= 15 { + return pumpTradeAccountLayout{}, false + } + return pumpTradeAccountLayout{ + IsV2: true, + FeeRecipient: 6, + BaseMint: 1, + QuoteMint: 2, + BaseTokenProgram: 3, + QuoteTokenProgram: 4, + Pool: 10, + BasePoolToken: 11, + QuotePoolToken: 12, + User: 13, + BaseUserToken: 14, + QuoteUserToken: 15, + }, true + default: + return pumpTradeAccountLayout{}, false + } +} + +func pumpInstructionIsSell(data []byte) bool { + return len(data) >= 8 && (bytes.Equal(data[:8], pumpSellDiscriminator[:]) || bytes.Equal(data[:8], pumpSellV2Discriminator[:])) +} + +func pumpInstructionIsExactQuoteIn(data []byte) bool { + return len(data) >= 8 && (bytes.Equal(data[:8], pumpBuyExactSolInDiscriminator[:]) || bytes.Equal(data[:8], pumpBuyExactQuoteInV2Discriminator[:])) +} + +func pumpAccount(result *RawTx, instr Instruction, accountIndex int) solana.PublicKey { + if accountIndex < 0 || accountIndex >= len(instr.Accounts) { + return solana.PublicKey{} + } + listIndex := instr.Accounts[accountIndex] + if listIndex < 0 || listIndex >= len(result.accountList) { + return solana.PublicKey{} + } + return result.accountList[listIndex] +} + +func pumpCreateQuoteAccounts(result *RawTx, instr Instruction, createEvent PumpCreateEvent) (solana.PublicKey, solana.PublicKey, uint8) { + quoteMint := createEvent.QuoteMint + quoteTokenProgram := solana.PublicKey{} + optionalStart := -1 + if len(instr.Data) >= 8 && bytes.Equal(instr.Data[:8], pumpCreateV2Discriminator[:]) { + optionalStart = 16 + } + if optionalStart >= 0 && len(instr.Accounts) > optionalStart { + accountQuoteMint := pumpAccount(result, instr, optionalStart) + if quoteMint.IsZero() && !accountQuoteMint.IsZero() && !accountQuoteMint.Equals(wSolMint) { + quoteMint = accountQuoteMint + } + if len(instr.Accounts) > optionalStart+2 && !quoteMint.IsZero() { + quoteTokenProgram = pumpAccount(result, instr, optionalStart+2) + } + } + if quoteMint.Equals(wSolMint) { + quoteTokenProgram = solana.TokenProgramID + } + if quoteTokenProgram.IsZero() && !quoteMint.IsZero() { + quoteTokenProgram = solana.TokenProgramID + } + return quoteMint, quoteTokenProgram, pumpQuoteDecimals(result, quoteMint) +} + +func pumpMintDecimalsFromBalances(result *RawTx, mint solana.PublicKey, fallback uint8) uint8 { + if mint.IsZero() { + return fallback + } + for _, balance := range result.Meta.PostTokenBalances { + balance.ParseAccount() + if balance.MintAccount.Equals(mint) { + return uint8(balance.UITokenAmount.Decimals) + } + } + for _, balance := range result.Meta.PreTokenBalances { + balance.ParseAccount() + if balance.MintAccount.Equals(mint) { + return uint8(balance.UITokenAmount.Decimals) + } + } + return fallback +} + +func pumpQuoteDecimals(result *RawTx, quoteMint solana.PublicKey) uint8 { + fallback := uint8(9) + if quoteMint.Equals(usdcMint) || quoteMint.Equals(usd1Mint) { + fallback = 6 + } + return pumpMintDecimalsFromBalances(result, quoteMint, fallback) +} + +func pumpQuoteAmount(tradeEvent PumpTradeEvent) uint64 { + if tradeEvent.QuoteAmount != 0 { + return tradeEvent.QuoteAmount + } + return tradeEvent.SolAmount +} + +func pumpQuoteReserve(tradeEvent PumpTradeEvent) uint64 { + if tradeEvent.RealQuoteReserves != 0 { + return tradeEvent.RealQuoteReserves + } + return tradeEvent.RealSolReserves +} + func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool { if completeEvent.Mint != tradeEvent.Mint { return false @@ -279,10 +637,16 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions result := tx.rawTx var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] - user := result.accountList[instruction.Accounts[6]] - ataUserIdx := instruction.Accounts[5] - userIndex := instruction.Accounts[6] - mint := result.accountList[instruction.Accounts[2]] + layout, ok := pumpTradeLayout(instruction) + if !ok { + return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1]) + } + user := pumpAccount(result, instruction, layout.User) + ataUserIdx := instruction.Accounts[layout.BaseUserToken] + userIndex := instruction.Accounts[layout.User] + mint := pumpAccount(result, instruction, layout.BaseMint) + quoteMint := pumpAccount(result, instruction, layout.QuoteMint) + quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram) var args PumpTradeArgs err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args) if err != nil { @@ -290,30 +654,27 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions } var event string var ( - solAmount, tokenAmount uint64 + quoteAmount, tokenAmount uint64 ) - if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) { + if bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]) || + bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]) { event = "buy_failed" - solAmount = args.Amount1 + quoteAmount = args.Amount1 tokenAmount = args.Amount2 - } else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) { + } else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) || + bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) { event = "buy_failed" - solAmount = args.Amount2 + quoteAmount = args.Amount2 tokenAmount = args.Amount1 - } else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) { + } else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) || + bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]) { event = "sell_failed" - solAmount = args.Amount2 + quoteAmount = args.Amount2 tokenAmount = args.Amount1 } else { return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1]) } - var baseTokenProgram solana.PublicKey - - if event == "buy_failed" { - baseTokenProgram = result.accountList[instruction.Accounts[8]] - } else { - baseTokenProgram = result.accountList[instruction.Accounts[9]] - } + baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram) if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) { userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint) //&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount @@ -325,31 +686,43 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions } userBase := getAccountBalanceAfterTx(result, ataUserIdx) - userQuote, _ := GetSolAfterTx(result, userIndex) + userQuote := decimal.Zero + if layout.IsV2 && !quoteMint.Equals(wSolMint) { + userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken]) + } else { + userQuoteLamports, _ := GetSolAfterTx(result, userIndex) + userQuote = decimal.NewFromUint64(userQuoteLamports) + } - bcIdx := instruction.Accounts[3] - bcAtaIndex := instruction.Accounts[4] - solReserves, _ := GetSolAfterTx(result, bcIdx) + bcIdx := instruction.Accounts[layout.Pool] + bcAtaIndex := instruction.Accounts[layout.BasePoolToken] + quoteReserves := decimal.Zero + if layout.IsV2 && !quoteMint.Equals(wSolMint) { + quoteReserves = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuotePoolToken]) + } else { + solReserves, _ := GetSolAfterTx(result, bcIdx) + quoteReserves = decimal.NewFromUint64(solReserves) + } tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex) swaps := []Swap{ { Program: SolProgramPump, Event: event, - Pool: result.accountList[instruction.Accounts[3]], + Pool: pumpAccount(result, instruction, layout.Pool), BaseMint: mint, - QuoteMint: solana.PublicKey{}, + QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, - QuoteTokenProgram: solana.PublicKey{}, + QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: 6, - QuoteMintDecimals: 9, + QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint), User: user, BaseAmount: decimal.NewFromUint64(tokenAmount), - QuoteAmount: decimal.NewFromUint64(solAmount), + QuoteAmount: decimal.NewFromUint64(quoteAmount), BaseReserve: tokenReserves, - QuoteReserve: decimal.NewFromUint64(solReserves), - Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), + QuoteReserve: quoteReserves, + Mayhem: isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)), UserBaseBalance: userBase, - UserQuoteBalance: decimal.NewFromUint64(userQuote), + UserQuoteBalance: userQuote, EntryContract: entryContract, }, } @@ -365,6 +738,10 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var err error var programIndex = instruction.ProgramIDIndex + layout, ok := pumpTradeLayout(instruction) + if !ok { + return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1]) + } feeEventProgramIndex := 0 for i, b := range result.accountList { @@ -411,7 +788,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns if tradeFound { break } - err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent) + tradeEvent, err = decodePumpTradeEvent(innerInstr.Data[16:]) if offset[1] == 0 { newoffset = [2]uint{offset[0] + 1, offset[1]} } else { @@ -420,7 +797,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns if err != nil { return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } - expectedIsBuy := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:]) + expectedIsBuy := !pumpInstructionIsSell(instruction.Data) if tradeEvent.IsBuy != expectedIsBuy { tradeEvent = PumpTradeEvent{} continue @@ -437,7 +814,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns if err != nil { return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } - if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, result.accountList[instruction.Accounts[3]]) { + if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, pumpAccount(result, instruction, layout.Pool)) { break } if offset[1] == 0 { @@ -451,7 +828,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns } } - if tradeEvent == (PumpTradeEvent{}) { + if !tradeFound { return nil, increaseOffset(offset), fmt.Errorf("pmp buy/sell event not found, offset, %d, %d", offset[0], offset[1]) } @@ -463,13 +840,16 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns } event := "" - baseTokenProgram := solana.TokenProgramID + baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram) + quoteMint := tradeEvent.QuoteMint + if quoteMint.IsZero() { + quoteMint = pumpAccount(result, instruction, layout.QuoteMint) + } + quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram) if tradeEvent.IsBuy { event = "buy" - baseTokenProgram = result.accountList[instruction.Accounts[8]] } else { event = "sell" - baseTokenProgram = result.accountList[instruction.Accounts[9]] } if _, exists := tx.Token[tradeEvent.Mint]; !exists { tx.Token[tradeEvent.Mint] = TokenMeta{ @@ -481,8 +861,8 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns var user = tradeEvent.User - ataUserIdx := instruction.Accounts[5] - userIndex := instruction.Accounts[6] + ataUserIdx := instruction.Accounts[layout.BaseUserToken] + userIndex := instruction.Accounts[layout.User] if !tradeEvent.User.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) { userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, tradeEvent.Mint) //&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount @@ -494,14 +874,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns } userBase := getAccountBalanceAfterTx(result, ataUserIdx) - userQuote, _ := GetSolAfterTx(result, userIndex) + userQuote := decimal.Zero + if layout.IsV2 && !quoteMint.Equals(wSolMint) { + userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken]) + } else { + userQuoteLamports, _ := GetSolAfterTx(result, userIndex) + userQuote = decimal.NewFromUint64(userQuoteLamports) + } - solAmount := tradeEvent.SolAmount - if tradeEvent.IsBuy && bytes.Equal(instruction.Data[:8], pumpBuyV2Discriminator[:]) { + quoteAmount := pumpQuoteAmount(tradeEvent) + if tradeEvent.IsBuy && pumpInstructionIsExactQuoteIn(instruction.Data) && !layout.IsV2 { fee := tradeEvent.Fee + tradeEvent.CreatorFee - solAmount = tradeFeeArg.TradeSize - if solAmount > fee { - solAmount = solAmount - fee + quoteAmount = tradeFeeArg.TradeSize + if quoteAmount > fee { + quoteAmount = quoteAmount - fee } } isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0 @@ -509,22 +895,22 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns { Program: SolProgramPump, Event: event, - Pool: result.accountList[instruction.Accounts[3]], + Pool: pumpAccount(result, instruction, layout.Pool), BaseMint: tradeEvent.Mint, - QuoteMint: solana.PublicKey{}, + QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, - QuoteTokenProgram: solana.PublicKey{}, + QuoteTokenProgram: quoteTokenProgram, Creator: tradeEvent.Creator, BaseMintDecimals: 6, - QuoteMintDecimals: 9, + QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint), User: user, BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount), - QuoteAmount: decimal.NewFromUint64(solAmount), + QuoteAmount: decimal.NewFromUint64(quoteAmount), BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves), - QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves), - Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), + QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)), + Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)), UserBaseBalance: userBase, - UserQuoteBalance: decimal.NewFromUint64(userQuote), + UserQuoteBalance: userQuote, EntryContract: entryContract, Cashback: isCashbackCoin, }, @@ -537,20 +923,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns swaps = append(swaps, Swap{ Program: SolProgramPump, Event: "complete", - Pool: result.accountList[instruction.Accounts[3]], + Pool: pumpAccount(result, instruction, layout.Pool), BaseMint: tradeEvent.Mint, - QuoteMint: solana.PublicKey{}, - BaseTokenProgram: result.accountList[instruction.Accounts[8]], - QuoteTokenProgram: solana.PublicKey{}, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, Creator: tradeEvent.Creator, BaseMintDecimals: 6, - QuoteMintDecimals: 9, + QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint), User: user, BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves), - QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves), - Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), + QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)), + Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)), UserBaseBalance: userBase, - UserQuoteBalance: decimal.NewFromUint64(userQuote), + UserQuoteBalance: userQuote, EntryContract: entryContract, }) } @@ -572,11 +958,74 @@ type MigrateEvent struct { Pool solana.PublicKey } +type pumpMigrateAccountLayout struct { + IsV2 bool + BaseMint int + QuoteMint int + Pool int + BasePoolToken int + QuotePoolToken int + User int + BaseTokenProgram int + QuoteTokenProgram int +} + +func pumpMigrateLayout(instr Instruction) (pumpMigrateAccountLayout, bool) { + if len(instr.Data) < 8 { + return pumpMigrateAccountLayout{}, false + } + discriminator := instr.Data[:8] + switch { + case bytes.Equal(discriminator, pumpMigrateDiscriminator[:]): + if len(instr.Accounts) <= 14 { + return pumpMigrateAccountLayout{}, false + } + return pumpMigrateAccountLayout{ + BaseMint: 2, + QuoteMint: 14, + Pool: 3, + BasePoolToken: 4, + QuotePoolToken: -1, + User: 5, + BaseTokenProgram: 7, + QuoteTokenProgram: -1, + }, true + case bytes.Equal(discriminator, pumpMigrateV2Discriminator[:]): + if len(instr.Accounts) <= 20 { + return pumpMigrateAccountLayout{}, false + } + return pumpMigrateAccountLayout{ + IsV2: true, + BaseMint: 2, + QuoteMint: 3, + Pool: 4, + BasePoolToken: 5, + QuotePoolToken: 6, + User: 7, + BaseTokenProgram: 19, + QuoteTokenProgram: 20, + }, true + default: + return pumpMigrateAccountLayout{}, false + } +} + +func decimalFromUint64WithFallback(primary, fallback uint64) decimal.Decimal { + if primary != 0 { + return decimal.NewFromUint64(primary) + } + return decimal.NewFromUint64(fallback) +} + func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { result := tx.rawTx var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var err error programIndex := instr.ProgramIDIndex + layout, ok := pumpMigrateLayout(instr) + if !ok { + return nil, increaseOffset(offset), fmt.Errorf("unknown pump migrate instruction account layout, offset, %d, %d", offset[0], offset[1]) + } ammprogramIdx := 0 for i, b := range result.accountList { if b.Equals(pumpAmmProgram) { @@ -633,20 +1082,45 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction offset = [2]uint{newoffset[0], newoffset[1]} // verify migrate by checking create pool and migrate event - userIndex := instr.Accounts[5] - ataBondingCurveAccountIndex := instr.Accounts[4] + userIndex := instr.Accounts[layout.User] + ataBondingCurveAccountIndex := instr.Accounts[layout.BasePoolToken] bc, err := getTokenBalanceAfterTx(result, ataBondingCurveAccountIndex) if err != nil || bc == nil { return nil, increaseOffset(offset), fmt.Errorf("pump migrate get bonding curve balance error: %v, offset, %d, %d", err, offset[0], offset[1]) } baseTokenProgram := bc.ProgramIDAccount + if layout.IsV2 { + baseTokenProgram = pumpAccount(result, instr, layout.BaseTokenProgram) + } + quoteMint := createEvent.QuoteMint + if quoteMint.IsZero() { + quoteMint = pumpAccount(result, instr, layout.QuoteMint) + } + quoteTokenProgram := pumpAccount(result, instr, layout.QuoteTokenProgram) + if quoteTokenProgram.IsZero() && !quoteMint.IsZero() { + quoteTokenProgram = solana.TokenProgramID + } + quoteDecimals := createEvent.QuoteMintDecimals + if quoteDecimals == 0 { + quoteDecimals = pumpQuoteDecimals(result, quoteMint) + } var userBase decimal.Decimal if result.accountList[userIndex].Equals(pumpMigrationAccount) { userBase = decimal.Zero } else { userBase = GetTokenBalanceAfterTx(result, userIndex, baseTokenProgram, migrateEvent.Mint) } - userQuote, _ := GetSolAfterTx(result, userIndex) + userQuote := decimal.Zero + if layout.IsV2 && !quoteMint.Equals(wSolMint) { + userQuote = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint) + } else { + userQuoteLamports, _ := GetSolAfterTx(result, userIndex) + userQuote = decimal.NewFromUint64(userQuoteLamports) + } + baseAmount := decimalFromUint64WithFallback(createEvent.BaseAmountIn, migrateEvent.MintAmount) + quoteAmount := decimalFromUint64WithFallback(createEvent.QuoteAmountIn, migrateEvent.SolAmount) + baseReserve := decimalFromUint64WithFallback(createEvent.PoolBaseAmount, migrateEvent.MintAmount) + quoteReserve := decimalFromUint64WithFallback(createEvent.PoolQuoteAmount, migrateEvent.SolAmount) if _, exists := tx.Token[migrateEvent.Mint]; !exists { tx.Token[migrateEvent.Mint] = TokenMeta{ @@ -661,22 +1135,22 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction Event: "migrate", Pool: migrateEvent.BondingCurve, BaseMint: migrateEvent.Mint, - QuoteMint: solana.PublicKey{}, + QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, - QuoteTokenProgram: solana.PublicKey{}, + QuoteTokenProgram: quoteTokenProgram, Creator: createEvent.Creator, BaseMintDecimals: 6, - QuoteMintDecimals: 9, + QuoteMintDecimals: quoteDecimals, User: migrateEvent.User, //BaseAmount: decimal.Decimal{}, //QuoteAmount: decimal.Decimal{}, - BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount), - QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, Mayhem: createEvent.IsMayhemMode, MigrateTopProgram: pumpAmmProgram, MigrateToPool: migrateEvent.Pool, UserBaseBalance: userBase, - UserQuoteBalance: decimal.NewFromUint64(userQuote), + UserQuoteBalance: userQuote, EntryContract: entryContract, }, } @@ -685,20 +1159,20 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction Event: "create", Pool: migrateEvent.Pool, BaseMint: migrateEvent.Mint, - QuoteMint: wSolMint, + QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram, - QuoteTokenProgram: solana.TokenProgramID, + QuoteTokenProgram: quoteTokenProgram, Creator: createEvent.Creator, BaseMintDecimals: 6, - QuoteMintDecimals: 9, + QuoteMintDecimals: quoteDecimals, User: migrateEvent.User, - BaseAmount: decimal.NewFromUint64(migrateEvent.MintAmount), - QuoteAmount: decimal.NewFromUint64(migrateEvent.SolAmount), - BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount), - QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount), + BaseAmount: baseAmount, + QuoteAmount: quoteAmount, + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, Mayhem: createEvent.IsMayhemMode, UserBaseBalance: userBase, - UserQuoteBalance: decimal.NewFromUint64(userQuote), + UserQuoteBalance: userQuote, EntryContract: entryContract, }) diff --git a/pump_test.go b/pump_test.go index dc89c8e..4e384a4 100644 --- a/pump_test.go +++ b/pump_test.go @@ -1,6 +1,7 @@ package pump_parser import ( + "bytes" "encoding/binary" "encoding/hex" "fmt" @@ -100,3 +101,163 @@ func TestPumpCompleteMatchesTradeEvent(t *testing.T) { t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user") } } + +func TestPumpV2Discriminators(t *testing.T) { + tests := []struct { + name string + got [8]byte + want [8]byte + }{ + {name: "buy_exact_sol_in", got: pumpBuyExactSolInDiscriminator, want: [8]byte{56, 252, 116, 8, 158, 223, 205, 95}}, + {name: "buy_v2", got: pumpBuyV2Discriminator, want: [8]byte{184, 23, 238, 97, 103, 197, 211, 61}}, + {name: "buy_exact_quote_in_v2", got: pumpBuyExactQuoteInV2Discriminator, want: [8]byte{194, 171, 28, 70, 104, 77, 91, 47}}, + {name: "sell_v2", got: pumpSellV2Discriminator, want: [8]byte{93, 246, 130, 60, 231, 233, 64, 178}}, + {name: "create_v2", got: pumpCreateV2Discriminator, want: [8]byte{214, 144, 76, 236, 95, 139, 49, 180}}, + {name: "migrate_v2", got: pumpMigrateV2Discriminator, want: [8]byte{187, 203, 18, 31, 206, 237, 254, 41}}, + } + for _, tt := range tests { + if tt.got != tt.want { + t.Fatalf("%s discriminator = %v, want %v", tt.name, tt.got, tt.want) + } + } +} + +func TestPumpMigrateLayoutV2(t *testing.T) { + accounts := make([]int, 27) + for i := range accounts { + accounts[i] = i + } + layout, ok := pumpMigrateLayout(Instruction{ + Data: pumpMigrateV2Discriminator[:], + Accounts: accounts, + }) + if !ok { + t.Fatal("migrate_v2 layout not recognized") + } + if !layout.IsV2 || + layout.BaseMint != 2 || + layout.QuoteMint != 3 || + layout.Pool != 4 || + layout.BasePoolToken != 5 || + layout.QuotePoolToken != 6 || + layout.User != 7 || + layout.BaseTokenProgram != 19 || + layout.QuoteTokenProgram != 20 { + t.Fatalf("migrate_v2 layout = %+v", layout) + } +} + +func TestPumpTradeAmountInfoV2(t *testing.T) { + tests := []struct { + name string + disc [8]byte + wantMode SwapMode + }{ + {name: "legacy exact quote in", disc: pumpBuyExactSolInDiscriminator, wantMode: SwapModeExactIn}, + {name: "v2 exact quote in", disc: pumpBuyExactQuoteInV2Discriminator, wantMode: SwapModeExactIn}, + {name: "v2 buy exact out", disc: pumpBuyV2Discriminator, wantMode: SwapModeExactOut}, + {name: "v2 sell exact in", disc: pumpSellV2Discriminator, wantMode: SwapModeExactIn}, + } + for _, tt := range tests { + mode, fixed, limit, ok := pumpTradeAmountInfoFromArgs(PumpTradeArgs{ + Discriminator: tt.disc, + Amount1: 11, + Amount2: 22, + }) + if !ok { + t.Fatalf("%s not recognized", tt.name) + } + if mode != tt.wantMode { + t.Fatalf("%s mode = %s, want %s", tt.name, mode.String(), tt.wantMode.String()) + } + if fixed.String() != "11" || limit.String() != "22" { + t.Fatalf("%s fixed/limit = %s/%s, want 11/22", tt.name, fixed, limit) + } + } +} + +func TestPumpCreateQuoteAccountsOptional(t *testing.T) { + createAccounts := make([]int, 17) + for i := range createAccounts { + createAccounts[i] = i + } + createV2Accounts := make([]int, 19) + for i := range createV2Accounts { + createV2Accounts[i] = i + } + createV2Accounts[16] = 14 + createV2Accounts[18] = 16 + accountList := make([]solana.PublicKey, 19) + accountList[14] = usdcMint + accountList[16] = solana.TokenProgramID + accountList[18] = solana.TokenProgramID + result := &RawTx{accountList: accountList} + + quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, Instruction{ + Data: pumpCreateDiscriminator[:], + Accounts: createAccounts, + }, PumpCreateEvent{}) + if !quoteMint.IsZero() || !quoteTokenProgram.IsZero() || quoteDecimals != 9 { + t.Fatalf("create quote accounts = %s/%s/%d, want zero/zero/9", quoteMint, quoteTokenProgram, quoteDecimals) + } + + quoteMint, quoteTokenProgram, quoteDecimals = pumpCreateQuoteAccounts(result, Instruction{ + Data: pumpCreateV2Discriminator[:], + Accounts: createV2Accounts, + }, PumpCreateEvent{}) + if !quoteMint.Equals(usdcMint) || !quoteTokenProgram.Equals(solana.TokenProgramID) || quoteDecimals != 6 { + t.Fatalf("create_v2 quote accounts = %s/%s/%d, want USDC/token/6", quoteMint, quoteTokenProgram, quoteDecimals) + } +} + +func TestDecodePumpTradeEventV2QuoteFields(t *testing.T) { + user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz") + mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump") + want := PumpTradeEvent{ + Mint: mint, + SolAmount: 1, + TokenAmount: 2, + IsBuy: true, + User: user, + VirtualTokenReserves: 3, + RealTokenReserves: 4, + IxName: "buy_v2", + Shareholders: []PumpShareholder{{Address: user, ShareBps: 250}}, + QuoteMint: usdcMint, + QuoteAmount: 5, + VirtualQuoteReserves: 6, + RealQuoteReserves: 7, + } + var buf bytes.Buffer + if err := agbinary.NewBorshEncoder(&buf).Encode(want); err != nil { + t.Fatalf("encode v2 trade event: %v", err) + } + got, err := decodePumpTradeEvent(buf.Bytes()) + if err != nil { + t.Fatalf("decodePumpTradeEvent() error = %v", err) + } + if !got.QuoteMint.Equals(usdcMint) || got.QuoteAmount != 5 || got.VirtualQuoteReserves != 6 || got.RealQuoteReserves != 7 { + t.Fatalf("decoded quote fields = %s/%d/%d/%d", got.QuoteMint, got.QuoteAmount, got.VirtualQuoteReserves, got.RealQuoteReserves) + } + if len(got.Shareholders) != 1 || got.Shareholders[0].ShareBps != 250 { + t.Fatalf("decoded shareholders = %+v", got.Shareholders) + } +} + +func TestDecodePumpTradeEventLegacyFallback(t *testing.T) { + hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e" + data, err := hex.DecodeString(hexData) + if err != nil { + t.Fatalf("decode hex: %v", err) + } + got, err := decodePumpTradeEvent(data[16:]) + if err != nil { + t.Fatalf("decodePumpTradeEvent() legacy error = %v", err) + } + if got.IxName != "buy_exact_sol_in" || got.SolAmount != 11725956 || !got.IsBuy { + t.Fatalf("legacy event = %+v", got) + } + if !got.QuoteMint.IsZero() || got.QuoteAmount != 0 || got.RealQuoteReserves != 0 { + t.Fatalf("legacy quote fields = %s/%d/%d, want zero", got.QuoteMint, got.QuoteAmount, got.RealQuoteReserves) + } +}