pump usdc support

This commit is contained in:
thloyi
2026-05-08 11:21:30 +08:00
parent 0a4aabc67f
commit 5cd3a97d81
4 changed files with 729 additions and 90 deletions

650
pump.go
View File

@@ -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,
})