Compare commits

...

23 Commits

Author SHA1 Message Date
bijianing97
7a82990770 Update meteoradlmm remove and add enum 2026-03-16 10:14:50 +08:00
bijianing97
e82bcb3c07 Merge remote-tracking branch 'origin' 2026-03-12 16:22:09 +08:00
bijianing97
a74f769064 Add raydiumv4 swapv2 2026-03-12 16:21:38 +08:00
thloyi
1e276e8bd2 [tx] swap inner idx 2026-03-12 13:49:34 +08:00
thloyi
eb2bde98ac update parsed tx: add swap at instr idx 2026-03-12 13:49:34 +08:00
66f0d247f5 chore: add astralane fee address 2026-03-12 12:08:06 +08:00
879b7fefad chore: add PlatformDexScreener 2026-03-12 11:52:58 +08:00
149dfae378 chore: add trojan fee address 2026-03-10 11:23:15 +08:00
8c4b43747c fix EntryContractAxiom 2026-03-02 20:04:38 +08:00
thloyi
e9ba16766f chain link parser 2026-03-02 18:01:32 +08:00
thloyi
cd1d681621 record ProgramFailedToComplete failed tx 2026-03-02 15:47:11 +08:00
thloyi
920c5ba25b fix errTx do not updte sol transfer 2026-02-27 17:41:58 +08:00
thloyi
3d447ef2e8 fix errTx mev and cu 2026-02-27 17:07:49 +08:00
thloyi
b0d4342fa2 pump and pump swap errTx parser 2026-02-27 16:37:15 +08:00
cachalots
972ddc7960 IsCashbackCoin 2026-02-27 02:10:12 +08:00
bcd442195c chore: support IsCashbackEnabled 2026-02-27 01:43:07 +08:00
0633707142 chore: add trojan fee addresses 2026-02-26 15:19:57 +08:00
8e49f01054 must to next inner at least 2026-02-21 09:29:28 +08:00
thloyi
62cc64a90a fix from rpc ComputeUnitsConsumed 2026-02-12 10:43:30 +08:00
thloyi
629ffe2ea7 fix pump complete parse error 2026-02-12 10:38:56 +08:00
cachalots
56dac04a2a fix culimit 2026-02-12 10:11:29 +08:00
cachalots
852ad4b382 cu limit 2026-02-11 17:49:43 +08:00
thloyi
3fdd4c4490 fix parse error 2026-02-11 14:58:12 +08:00
26 changed files with 1867 additions and 772 deletions

View File

@@ -27,10 +27,11 @@ func budgetParser(tx *Tx, instr Instruction, _ InnerInstructions, offset [2]uint
} }
} }
func computeUnitLimitParser(offset [2]uint, _ *Tx, decodedData []byte) ([2]uint, error) { func computeUnitLimitParser(offset [2]uint, tx *Tx, decodedData []byte) ([2]uint, error) {
if len(decodedData) < 8 { if len(decodedData) < 4 {
return increaseOffset(offset), nil return increaseOffset(offset), nil
} }
tx.CuLimit = binary.LittleEndian.Uint32(decodedData[:4])
return increaseOffset(offset), nil return increaseOffset(offset), nil
} }

120
chainlink.go Normal file
View File

@@ -0,0 +1,120 @@
package pump_parser
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"math/big"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var (
chainlinkSOLUSDFeedAccount = solana.MustPublicKeyFromBase58("CH31Xns5z3M1cTAbKW34jcxPPciazARpijcHj9rxtemt")
chainlinkSubmitDiscriminator = calculateDiscriminator("global:submit")
)
func chainLinkParser(tx *Tx, instruction Instruction, inners InnerInstructions, offset [2]uint) ([2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(chainLinkProgram) {
return increaseOffset(offset), fmt.Errorf("system program instruction not found, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.rawTx.Slot, tx.rawTx.TxHash(), offset[0], offset[1])
}
if tx.rawTx.Meta.Err != nil {
return increaseOffset(offset), nil
}
decode := instruction.Data
discriminator := binary.LittleEndian.Uint32(decode[0:4])
switch discriminator {
case transferDiscriminator:
return chainLinkSubmitParser(instruction, inners, offset, tx, decode[4:])
default:
return increaseOffset(offset), nil
}
}
func chainLinkSubmitParser(instruction Instruction, inners InnerInstructions, offset [2]uint, tx *Tx, decodeData []byte) ([2]uint, error) {
if len(instruction.Accounts) < 6 {
return increaseOffset(offset), InstructionIgnoredError
}
inner, err := getInnerInstructions(inners, offset[1])
if err != nil {
return increaseOffset(offset), err
}
if len(inner) < 1 {
return increaseOffset(offset), InstructionIgnoredError
}
storeInstruction := inner[0]
if len(storeInstruction.Accounts) < 2 {
return increaseOffset(offset), InstructionIgnoredError
}
if storeInstruction.Accounts[0] >= len(tx.rawTx.accountList) || tx.rawTx.accountList[storeInstruction.Accounts[0]] != chainlinkSOLUSDFeedAccount {
return increaseOffset(offset), InstructionIgnoredError
}
if !bytes.Equal(storeInstruction.Data[0:8], chainlinkSubmitDiscriminator[:]) {
return increaseOffset(offset), InstructionIgnoredError
}
data, err := parseChainLinkSubmitData(storeInstruction.Data)
if err != nil {
return increaseOffset(offset), InstructionIgnoredError
}
tx.ChainLink.Timestamp = int64(data.Timestamp)
tx.ChainLink.Price = decimal.NewFromBigInt(data.Price(), -8)
return increaseOffset(offset), nil
}
type SubmitData struct {
Discriminator [8]byte
Timestamp uint64
Answer [16]byte
}
func parseChainLinkSubmitData(data []byte) (*SubmitData, error) {
if len(data) != 32 {
return nil, errors.New("invalid submit data length")
}
var submitData SubmitData
copy(submitData.Discriminator[:], data[:8])
submitData.Timestamp = binary.LittleEndian.Uint64(data[8:16])
copy(submitData.Answer[:], data[16:32])
return &submitData, nil
}
func (s *SubmitData) Price() *big.Int {
return int128LEBytesToBigInt(s.Answer)
}
func int128LEBytesToBigInt(bytes [16]byte) *big.Int {
// Create new big.Int
bigInt := new(big.Int)
// Reverse bytes for little-endian to big-endian conversion
reversed := make([]byte, 16)
for i := 0; i < 16; i++ {
reversed[15-i] = bytes[i]
}
// Check if negative (first byte in little-endian is highest byte)
isNegative := bytes[15]&0x80 != 0
if isNegative {
// If negative, flip all bits
for i := range reversed {
reversed[i] = ^reversed[i]
}
// Convert to big.Int
bigInt.SetBytes(reversed)
// Add 1 and negate
bigInt.Add(bigInt, big.NewInt(1))
bigInt.Neg(bigInt)
} else {
// If positive, convert directly
bigInt.SetBytes(reversed)
}
return bigInt
}

View File

@@ -1,576 +0,0 @@
package pump_parser
import (
"log"
"strings"
"github.com/gagliardetto/solana-go"
)
func checkBonkGmgnBuy(rawTx *RawTx) bool {
// 检查交易版本
var version, _ = rawTx.Version.(solana.MessageVersion)
if version != solana.MessageVersionLegacy {
return false
}
// 检查交易指令数量
if len(rawTx.Transaction.Message.Instructions) != 10 && len(rawTx.Transaction.Message.Instructions) != 9 {
return false
}
accountList := rawTx.getAccountList()
// 检查 cu limit
{
instruction := rawTx.Transaction.Message.Instructions[0]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
return false
}
if len(instruction.Accounts) != 1 {
return false
}
accountId := accountList[instruction.Accounts[0]].String()
if !strings.HasPrefix(accountId, "jitodontfront1111111111151111111111111655") {
return false
}
}
// 检查 cu price
{
instruction := rawTx.Transaction.Message.Instructions[1]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
return false
}
}
// 检查 ata.createIdempotent
{
instruction := rawTx.Transaction.Message.Instructions[2]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
return false
}
if instruction.Data.String() != "2" {
return false
}
if len(instruction.Accounts) < 4 {
return false
}
// gmgn 会先创建 wsol 账户, 而不是 token 账户
accountId := accountList[instruction.Accounts[3]]
if accountId != solana.WrappedSol {
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[3]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
// 检查 token.syncNative
{
instruction := rawTx.Transaction.Message.Instructions[4]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.TokenProgramID {
return false
}
if instruction.Data.String() != "J" {
return false
}
}
offset := 5
if len(rawTx.Transaction.Message.Instructions) == 10 {
// 检查 ata.create
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
return false
}
if instruction.Data.String() != "1" {
return false
}
}
offset++
}
// 检查 bonk.buy
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != raydiumLaunchLabProgramID {
return false
}
}
offset++
// 检查 token.closeAccount
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.TokenProgramID {
return false
}
if instruction.Data.String() != "A" {
return false
}
}
offset++
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
offset++
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[offset]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
return true
}
var (
axiomTxLoopupTable = solana.MustPublicKeyFromBase58("7RKtfATWCe98ChuwecNq8XCzAzfoK3DtZTprFsPMGtio")
axiomProgramID = solana.MustPublicKeyFromBase58("AxiomfHaWDemCFBLBayqnEnNwE6b7B2Qz3UmzMpgbMG6")
gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb")
)
func checkBonkAxiomBuy(rawTx *RawTx) bool {
// 检查交易版本
var version, _ = rawTx.Version.(solana.MessageVersion)
if version == solana.MessageVersionLegacy || len(rawTx.Transaction.Message.AddressTableLookups) != 1 {
log.Printf("CheckBonkAxiomBuy: Invalid transaction version or address table lookups: %v %v %v", rawTx.Transaction.Signatures[0].String(), version, len(rawTx.Transaction.Message.AddressTableLookups))
return false
}
// 检查 addressLookupTable 是否是 Axiom 的
if rawTx.Transaction.Message.AddressTableLookups[0].AccountKey != axiomTxLoopupTable {
log.Printf("CheckBonkAxiomBuy: Invalid address lookup table: %v", rawTx.Transaction.Signatures[0].String())
return false
}
// 检查交易指令数量
if len(rawTx.Transaction.Message.Instructions) != 10 {
return false
}
accountList := rawTx.getAccountList()
// 检查 cu limit
{
instruction := rawTx.Transaction.Message.Instructions[0]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for ComputeBudget: %v", programId)
return false
}
if len(instruction.Accounts) != 1 {
log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for ComputeBudget: %v", len(instruction.Accounts))
return false
}
accountId := accountList[instruction.Accounts[0]].String()
if !strings.HasPrefix(accountId, "jitodontfront") || !strings.HasSuffix(accountId, "TradeWithAxiomDotTrade") {
log.Printf("CheckBonkAxiomBuy: Invalid account ID for ComputeBudget: %v", accountId)
return false
}
}
// 检查 cu price
{
instruction := rawTx.Transaction.Message.Instructions[1]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for ComputeBudget: %v", programId)
return false
}
}
// 检查 ata.createIdempotent
{
instruction := rawTx.Transaction.Message.Instructions[2]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SPLAssociatedTokenAccount: %v", programId)
return false
}
if instruction.Data.String() != "2" {
log.Printf("CheckBonkAxiomBuy: Invalid data for SPLAssociatedTokenAccount: %v", instruction.Data.String())
return false
}
if len(instruction.Accounts) < 4 {
log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for SPLAssociatedTokenAccount: %v", len(instruction.Accounts))
return false
}
// axiom 会先创建 token 账户, 而不是 wsol 账户
accountId := accountList[instruction.Accounts[3]]
if accountId == solana.WrappedSol {
log.Printf("CheckBonkAxiomBuy: Invalid account ID for SPLAssociatedTokenAccount, expected token account but got wsol: %v", accountId)
return false
}
}
// 检查 ata.createIdempotent
{
instruction := rawTx.Transaction.Message.Instructions[3]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SPLAssociatedTokenAccount2: %v", programId)
return false
}
if instruction.Data.String() != "2" {
log.Printf("CheckBonkAxiomBuy: Invalid data for SPLAssociatedTokenAccount2: %v", instruction.Data.String())
return false
}
if len(instruction.Accounts) < 4 {
log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for SPLAssociatedTokenAccount2: %v", len(instruction.Accounts))
return false
}
// axiom 会先创建 token 账户, 而不是 wsol 账户
accountId := accountList[instruction.Accounts[3]]
if accountId != solana.WrappedSol {
log.Printf("CheckBonkAxiomBuy: Invalid account ID for SPLAssociatedTokenAccount2, expected wsol but got: %v", accountId)
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[4]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram3: %v", programId)
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer3: %v", instruction.Data)
return false
}
}
// 检查 token.syncNative
{
instruction := rawTx.Transaction.Message.Instructions[5]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.TokenProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for TokenProgram3: %v", programId)
return false
}
if instruction.Data.String() != "J" {
log.Printf("CheckBonkAxiomBuy: Invalid data for TokenProgram syncNative3: %v", instruction.Data.String())
return false
}
}
// 检查 bonk.buy
{
instruction := rawTx.Transaction.Message.Instructions[6]
programId := accountList[instruction.ProgramIDIndex]
if programId != raydiumLaunchLabProgramID {
return false
}
}
// 检查 token.closeAccount
{
instruction := rawTx.Transaction.Message.Instructions[7]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.TokenProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for TokenProgram closeAccount: %v", programId)
return false
}
if instruction.Data.String() != "A" {
log.Printf("CheckBonkAxiomBuy: Invalid data for TokenProgram closeAccount: %v", instruction.Data.String())
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[8]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram transfer4: %v", programId)
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer4: %v", instruction.Data)
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[9]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram transfer5: %v", programId)
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer5: %v", instruction.Data)
return false
}
}
return true
}
func checkPumpFunAxiomBuy(rawTx *RawTx) bool {
// 检查交易版本
if rawTx.Version == "legacy" || len(rawTx.Transaction.Message.AddressTableLookups) != 1 {
return false
}
// 检查 addressLookupTable 是否是 Axiom 的
if rawTx.Transaction.Message.AddressTableLookups[0].AccountKey != axiomTxLoopupTable {
return false
}
// 检查交易指令数量
if len(rawTx.Transaction.Message.Instructions) != 6 {
return false
}
accountList := rawTx.getAccountList()
// 检查 cu limit
{
instruction := rawTx.Transaction.Message.Instructions[0]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
return false
}
if len(instruction.Accounts) != 1 {
return false
}
accountId := accountList[instruction.Accounts[0]].String()
if !strings.HasPrefix(accountId, "jitodontfront") || !strings.HasSuffix(accountId, "TradeWithAxiomDotTrade") {
return false
}
}
// 检查 cu price
{
instruction := rawTx.Transaction.Message.Instructions[1]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
return false
}
}
// 检查 ata.createIdempotent
{
instruction := rawTx.Transaction.Message.Instructions[2]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
return false
}
if instruction.Data.String() != "2" {
return false
}
if len(instruction.Accounts) < 4 {
return false
}
// axiom 会先创建 token 账户, 而不是 wsol 账户
accountId := accountList[instruction.Accounts[3]]
if accountId == solana.WrappedSol {
return false
}
}
// 检查调用axiom合约
{
instruction := rawTx.Transaction.Message.Instructions[3]
programId := accountList[instruction.ProgramIDIndex]
if programId != axiomProgramID {
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[4]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[5]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
return true
}
func checkPumpFunGmgnBuy(rawTx *RawTx) bool {
// 检查交易版本
if rawTx.Version != "legacy" {
return false
}
// 检查交易指令数量
if len(rawTx.Transaction.Message.Instructions) != 6 {
return false
}
accountList := rawTx.getAccountList()
// 检查 cu limit
{
instruction := rawTx.Transaction.Message.Instructions[0]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
return false
}
if len(instruction.Accounts) != 1 {
return false
}
accountId := accountList[instruction.Accounts[0]].String()
if !strings.HasPrefix(accountId, "jitodontfront1111111111151111111111111655") {
return false
}
}
// 检查 cu price
{
instruction := rawTx.Transaction.Message.Instructions[1]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.ComputeBudget {
return false
}
}
// 检查 ata.createIdempotent
{
instruction := rawTx.Transaction.Message.Instructions[2]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SPLAssociatedTokenAccountProgramID {
return false
}
if instruction.Data.String() != "2" {
return false
}
if len(instruction.Accounts) < 4 {
return false
}
accountId := accountList[instruction.Accounts[3]]
if accountId == solana.WrappedSol {
return false
}
}
// 检查调用 gmgn 合约
{
instruction := rawTx.Transaction.Message.Instructions[3]
programId := accountList[instruction.ProgramIDIndex]
if programId != gmgnProgramID {
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[4]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
// 检查 transfer
{
instruction := rawTx.Transaction.Message.Instructions[5]
programId := accountList[instruction.ProgramIDIndex]
if programId != solana.SystemProgramID {
return false
}
if len(instruction.Data) == 0 || instruction.Data[0] != 2 {
return false
}
}
return true
}

View File

@@ -39,6 +39,14 @@ var platformFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("F4hJ3Ee3c5UuaorKAMfELBjYCjiiLH75haZTKqTywRP3"): PlatformBullX, solana.MustPublicKeyFromBase58("F4hJ3Ee3c5UuaorKAMfELBjYCjiiLH75haZTKqTywRP3"): PlatformBullX,
solana.MustPublicKeyFromBase58("47hEzz83VFR23rLTEeVm9A7eFzjJwjvdupPPmX3cePqF"): PlatformBanana, solana.MustPublicKeyFromBase58("47hEzz83VFR23rLTEeVm9A7eFzjJwjvdupPPmX3cePqF"): PlatformBanana,
solana.MustPublicKeyFromBase58("9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco"): PlatformTrojan, solana.MustPublicKeyFromBase58("9yMwSPk9mrXSN7yDHUuZurAh1sjbJsfpUqjZ7SvVtdco"): PlatformTrojan,
solana.MustPublicKeyFromBase58("92Med3qeK7duC5iiYsHX38H2f2twJfRsSx93oNrza2VH"): PlatformTrojan,
solana.MustPublicKeyFromBase58("65gDv7pZQCZELsNpNYSFEBtNFpWZAbxmRFB6BGMqFkHH"): PlatformTrojan,
solana.MustPublicKeyFromBase58("8jgg7moFJkHyTtAv9M6RBSPMp2oXeXhuiUMKW8YbYCWn"): PlatformTrojan,
solana.MustPublicKeyFromBase58("BJgbYMZgqm79gNrmm31tV3L8GQorw91XFm4m7evyfPjr"): PlatformTrojan,
solana.MustPublicKeyFromBase58("BWgb8wR1FEGiu1jCDSKuHKf752W27b4iN6SvoNCiK4qp"): PlatformTrojan,
solana.MustPublicKeyFromBase58("GV4Bt6ehW5x5dqtaWAJBSnz8uum5Z2Rp9P2Tr5iVuQn5"): PlatformTrojan,
solana.MustPublicKeyFromBase58("2jwHNxavSoMZMEDbT1eV9PcPt5dDcayCqM6MkgaPpmWQ"): PlatformTrojan,
solana.MustPublicKeyFromBase58("66N1M2aaDSdJFZ1d7YoVN4EU45ju6XiscapLVHn5FLms"): PlatformTrojan,
solana.MustPublicKeyFromBase58("4mih95RmBqfHYvEfqq6uGGLp1Fr3gVS3VNSEa3JVRfQK"): PlatformRaybot, solana.MustPublicKeyFromBase58("4mih95RmBqfHYvEfqq6uGGLp1Fr3gVS3VNSEa3JVRfQK"): PlatformRaybot,
solana.MustPublicKeyFromBase58("3udvfL24waJcLhskRAsStNMoNUvtyXdxrWQz4hgi953N"): PlatformMoonshot, solana.MustPublicKeyFromBase58("3udvfL24waJcLhskRAsStNMoNUvtyXdxrWQz4hgi953N"): PlatformMoonshot,
solana.MustPublicKeyFromBase58("3kxSQybWEeQZsMuNWMRJH4TxrhwoDwfv41TNMLRzFP5A"): PlatformMEVX, solana.MustPublicKeyFromBase58("3kxSQybWEeQZsMuNWMRJH4TxrhwoDwfv41TNMLRzFP5A"): PlatformMEVX,
@@ -59,6 +67,7 @@ var platformFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("HkJYryz2BNeMQfuuSWDYktWt5fZLV26eK6nqu7EJycoG"): PlatformAxiom, solana.MustPublicKeyFromBase58("HkJYryz2BNeMQfuuSWDYktWt5fZLV26eK6nqu7EJycoG"): PlatformAxiom,
solana.MustPublicKeyFromBase58("BfFX9rUm8qTZiZjmeq9BktWVTNuG3YWMc5AvkrCKJike"): PlatformAxiom, solana.MustPublicKeyFromBase58("BfFX9rUm8qTZiZjmeq9BktWVTNuG3YWMc5AvkrCKJike"): PlatformAxiom,
solana.MustPublicKeyFromBase58("2ApLdwLrGayEmxgpLX9BTR47Q2QprfMg5SpjrLeaK8s7"): PlatformAxiom, solana.MustPublicKeyFromBase58("2ApLdwLrGayEmxgpLX9BTR47Q2QprfMg5SpjrLeaK8s7"): PlatformAxiom,
solana.MustPublicKeyFromBase58("AVahywMVNRYzdgWrufSWrtdGXAeNEvfpJFxhVFK516mT"): PlatformDexScreener,
} }
var mevAgentFeeAddresses = map[solana.PublicKey]string{ var mevAgentFeeAddresses = map[solana.PublicKey]string{
@@ -188,6 +197,7 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("ASde6y8pBCU1aityWHRpqT7pEAcEonjCgFUMeh5egRes"): MevAgentAstralane, solana.MustPublicKeyFromBase58("ASde6y8pBCU1aityWHRpqT7pEAcEonjCgFUMeh5egRes"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASUv6G8Cj6zt71UAqD1aVtDC3CRn6FFddqF17ZiegrES"): MevAgentAstralane, solana.MustPublicKeyFromBase58("ASUv6G8Cj6zt71UAqD1aVtDC3CRn6FFddqF17ZiegrES"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane, solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane, solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane, solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane, solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
@@ -358,6 +368,7 @@ var entryContractAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("B3111yJCeHBcA1bizdJjUFPALfhAfSRnAbJzGUtnt56A"): EntryContractBinanceWallet, solana.MustPublicKeyFromBase58("B3111yJCeHBcA1bizdJjUFPALfhAfSRnAbJzGUtnt56A"): EntryContractBinanceWallet,
solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9"): EntryContractAxiom, solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9"): EntryContractAxiom,
solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq"): EntryContractAxiom, solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq"): EntryContractAxiom,
solana.MustPublicKeyFromBase58("Gz9VPiSLQYbvKyb3jZPjNfyA6n4T4qVFUuAukgL964nL"): EntryContractAxiom,
solana.MustPublicKeyFromBase58("B3jytJa6Tzpn4Ly7GNnDm3dMGqUin5aMRm5aPsJGU5G7"): EntryContractTradewiz, solana.MustPublicKeyFromBase58("B3jytJa6Tzpn4Ly7GNnDm3dMGqUin5aMRm5aPsJGU5G7"): EntryContractTradewiz,
solana.MustPublicKeyFromBase58("DBotWvSso9oD1ZB3aHx2LiD2ZoFpF8PbKjaT4uHKLLVs"): EntryContractDbot, solana.MustPublicKeyFromBase58("DBotWvSso9oD1ZB3aHx2LiD2ZoFpF8PbKjaT4uHKLLVs"): EntryContractDbot,
solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3"): EntryContractPadre, solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3"): EntryContractPadre,
@@ -365,3 +376,5 @@ var entryContractAddresses = map[solana.PublicKey]string{
var okxDexRoutersV2 = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u") var okxDexRoutersV2 = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u")
var okxAggregatorV2 = solana.MustPublicKeyFromBase58("6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma") var okxAggregatorV2 = solana.MustPublicKeyFromBase58("6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma")
var axiomOuterContract = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9")

View File

@@ -75,6 +75,7 @@ const (
PlatformMaestro = "maestro" PlatformMaestro = "maestro"
PlatformBonkBot = "bonkbot" PlatformBonkBot = "bonkbot"
PlatformPadre = "padre" PlatformPadre = "padre"
PlatformDexScreener = "dexscreener"
// used to flag transactions impersonating platform users // used to flag transactions impersonating platform users
PlatformFake = "fake" PlatformFake = "fake"

154
error.go
View File

@@ -79,54 +79,54 @@ const (
const ( const (
GenericError InstructionErrorVariant = iota GenericError InstructionErrorVariant = iota
/// The arguments provided to a program were invalid // InvalidArgument / The arguments provided to a program were invalid
InvalidArgument InvalidArgument
/// An instruction's data contents were invalid // InvalidInstructionData / An instruction's data contents were invalid
InvalidInstructionData InvalidInstructionData
/// An account's data contents was invalid // InvalidAccountData / An account's data contents was invalid
InvalidAccountData InvalidAccountData
/// An account's data was too small // AccountDataTooSmall / An account's data was too small
AccountDataTooSmall AccountDataTooSmall
/// An account's balance was too small to complete the instruction // InsufficientFunds / An account's balance was too small to complete the instruction
InsufficientFunds InsufficientFunds
/// The account did not have the expected program id // IncorrectProgramId / The account did not have the expected program id
IncorrectProgramId IncorrectProgramId
/// A signature was required but not found // MissingRequiredSignature / A signature was required but not found
MissingRequiredSignature MissingRequiredSignature
/// An initialize instruction was sent to an account that has already been initialized. // AccountAlreadyInitialized / An initialize instruction was sent to an account that has already been initialized.
AccountAlreadyInitialized AccountAlreadyInitialized
/// An attempt to operate on an account that hasn't been initialized. // UninitializedAccount / An attempt to operate on an account that hasn't been initialized.
UninitializedAccount UninitializedAccount
/// Program's instruction lamport balance does not equal the balance after the instruction // UnbalancedInstruction / Program's instruction lamport balance does not equal the balance after the instruction
UnbalancedInstruction UnbalancedInstruction
/// Program illegally modified an account's program id // ModifiedProgramId / Program illegally modified an account's program id
ModifiedProgramId ModifiedProgramId
/// Program spent the lamports of an account that doesn't belong to it // ExternalAccountLamportSpend / Program spent the lamports of an account that doesn't belong to it
ExternalAccountLamportSpend ExternalAccountLamportSpend
/// Program modified the data of an account that doesn't belong to it // ExternalAccountDataModified / Program modified the data of an account that doesn't belong to it
ExternalAccountDataModified ExternalAccountDataModified
/// Read-only account's lamports modified // ReadonlyLamportChange / Read-only account's lamports modified
ReadonlyLamportChange ReadonlyLamportChange
/// Read-only account's data was modified // ReadonlyDataModified / Read-only account's data was modified
ReadonlyDataModified ReadonlyDataModified
/// An account was referenced more than once in a single instruction // DuplicateAccountIndex / An account was referenced more than once in a single instruction
// Deprecated, instructions can now contain duplicate accounts // Deprecated, instructions can now contain duplicate accounts
DuplicateAccountIndex DuplicateAccountIndex
/// Executable bit on account changed, but shouldn't have // ExecutableModified / Executable bit on account changed, but shouldn't have
ExecutableModified ExecutableModified
/// Rent_epoch account changed, but shouldn't have // RentEpochModified / Rent_epoch account changed, but shouldn't have
RentEpochModified RentEpochModified
/// The instruction expected additional account keys // NotEnoughAccountKeys / The instruction expected additional account keys
NotEnoughAccountKeys NotEnoughAccountKeys
/// Program other than the account's owner changed the size of the account data // AccountDataSizeChanged / Program other than the account's owner changed the size of the account data
AccountDataSizeChanged AccountDataSizeChanged
/// The instruction expected an executable account // AccountNotExecutable / The instruction expected an executable account
AccountNotExecutable AccountNotExecutable
/// Failed to borrow a reference to account data, already borrowed // AccountBorrowFailed / Failed to borrow a reference to account data, already borrowed
AccountBorrowFailed AccountBorrowFailed
/// Account data has an outstanding reference after a program's execution // InstructionAccountBorrowOutstanding / Account data has an outstanding reference after a program's execution
InstructionAccountBorrowOutstanding InstructionAccountBorrowOutstanding
/// The same account was multiply passed to an on-chain program's entrypoint, but the program // DuplicateAccountOutOfSync / The same account was multiply passed to an on-chain program's entrypoint, but the program
/// modified them differently. A program can only modify one instance of the account because /// modified them differently. A program can only modify one instance of the account because
/// the runtime cannot determine which changes to pick or how to merge them if both are modified /// the runtime cannot determine which changes to pick or how to merge them if both are modified
DuplicateAccountOutOfSync DuplicateAccountOutOfSync
@@ -136,42 +136,42 @@ const (
Custom // Custom(u32), Custom // Custom(u32),
/// The return value from the program was invalid. Valid errors are either a defined builtin // InvalidError / The return value from the program was invalid. Valid errors are either a defined builtin
/// error value or a user-defined error in the lower 32 bits. /// error value or a user-defined error in the lower 32 bits.
InvalidError InvalidError
/// Executable account's data was modified // ExecutableDataModified / Executable account's data was modified
ExecutableDataModified ExecutableDataModified
/// Executable account's lamports modified // ExecutableLamportChange / Executable account's lamports modified
ExecutableLamportChange ExecutableLamportChange
/// Executable accounts must be rent exempt // ExecutableAccountNotRentExempt / Executable accounts must be rent exempt
ExecutableAccountNotRentExempt ExecutableAccountNotRentExempt
/// Unsupported program id // UnsupportedProgramId / Unsupported program id
UnsupportedProgramId UnsupportedProgramId
/// Cross-program invocation call depth too deep // CallDepth / Cross-program invocation call depth too deep
CallDepth CallDepth
/// An account required by the instruction is missing // MissingAccount / An account required by the instruction is missing
MissingAccount MissingAccount
/// Cross-program invocation reentrancy not allowed for this instruction // ReentrancyNotAllowed / Cross-program invocation reentrancy not allowed for this instruction
ReentrancyNotAllowed ReentrancyNotAllowed
/// Length of the seed is too long for address generation // MaxSeedLengthExceeded / Length of the seed is too long for address generation
MaxSeedLengthExceeded MaxSeedLengthExceeded
/// Provided seeds do not result in a valid address // InvalidSeeds / Provided seeds do not result in a valid address
InvalidSeeds InvalidSeeds
/// Failed to reallocate account data of this length // InvalidRealloc / Failed to reallocate account data of this length
InvalidRealloc InvalidRealloc
/// Computational budget exceeded // ComputationalBudgetExceeded / Computational budget exceeded
ComputationalBudgetExceeded ComputationalBudgetExceeded
/// Cross-program invocation with unauthorized signer or writable account // PrivilegeEscalation / Cross-program invocation with unauthorized signer or writable account
PrivilegeEscalation PrivilegeEscalation
/// Failed to create program execution environment // ProgramEnvironmentSetupFailure / Failed to create program execution environment
ProgramEnvironmentSetupFailure ProgramEnvironmentSetupFailure
/// Program failed to complete // ProgramFailedToComplete / Program failed to complete
ProgramFailedToComplete ProgramFailedToComplete
/// Program failed to compile // ProgramFailedToCompile / Program failed to compile
ProgramFailedToCompile ProgramFailedToCompile
/// Account is immutable // Immutable / Account is immutable
Immutable Immutable
/// Incorrect authority provided // IncorrectAuthority / Incorrect authority provided
IncorrectAuthority IncorrectAuthority
/// Failed to serialize or deserialize account data /// Failed to serialize or deserialize account data
/// ///
@@ -185,23 +185,23 @@ const (
BorshIoError // BorshIoError(String) BorshIoError // BorshIoError(String)
// An account does not have enough lamports to be rent-exempt // AccountNotRentExempt An account does not have enough lamports to be rent-exempt
AccountNotRentExempt AccountNotRentExempt
/// Invalid account owner // InvalidAccountOwner Invalid account owner
InvalidAccountOwner InvalidAccountOwner
/// Program arithmetic overflowed // ArithmeticOverflow Program arithmetic overflowed
ArithmeticOverflow ArithmeticOverflow
/// Unsupported sysvar // UnsupportedSysvar Unsupported sysvar
UnsupportedSysvar UnsupportedSysvar
/// Illegal account owner // IllegalOwner Illegal account owner
IllegalOwner IllegalOwner
/// Accounts data allocations exceeded the maximum allowed per transaction // MaxAccountsDataAllocationsExceeded / Accounts data allocations exceeded the maximum allowed per transaction
MaxAccountsDataAllocationsExceeded MaxAccountsDataAllocationsExceeded
/// Max accounts exceeded // MaxAccountsExceeded Max accounts exceeded
MaxAccountsExceeded MaxAccountsExceeded
/// Max instruction trace length exceeded // MaxInstructionTraceLengthExceeded Max instruction trace length exceeded
MaxInstructionTraceLengthExceeded MaxInstructionTraceLengthExceeded
/// Builtin programs must consume compute units // BuiltinProgramsMustConsumeComputeUnits Builtin programs must consume compute units
BuiltinProgramsMustConsumeComputeUnits BuiltinProgramsMustConsumeComputeUnits
) )
@@ -210,6 +210,15 @@ type TransactionError struct {
rest []byte rest []byte
} }
type TransactionParsedError struct {
Index uint8
Variant TransactionErrorVariant
Enum InstructionErrorVariant
CustomCode uint32
UnKnown string
}
var ( var (
ErrInvalidTransactionError = errors.New("invalid transaction error") ErrInvalidTransactionError = errors.New("invalid transaction error")
NotAnInstructionError = errors.New("not an instruction error") NotAnInstructionError = errors.New("not an instruction error")
@@ -233,6 +242,49 @@ func DecodeTransactionError(data []byte) (*TransactionError, error) {
return &err, nil return &err, nil
} }
func ParseTransactionErrorFromGeyser(data []byte) *TransactionParsedError {
if len(data) == 0 {
return nil
}
transactionError, err := DecodeTransactionError(data)
if err != nil {
return &TransactionParsedError{
UnKnown: string(data),
}
}
enumErr, err := transactionError.GetInstructionError()
if err != nil {
return &TransactionParsedError{
Variant: transactionError.Variant,
UnKnown: string(data),
}
}
if enumErr.Variant != Custom {
return &TransactionParsedError{
Index: enumErr.Index,
Variant: transactionError.Variant,
Enum: enumErr.Variant,
}
}
customCode, err := enumErr.Custom()
if err != nil {
return &TransactionParsedError{
Index: enumErr.Index,
Variant: transactionError.Variant,
Enum: enumErr.Variant,
UnKnown: string(data),
}
}
return &TransactionParsedError{
Index: enumErr.Index,
Variant: transactionError.Variant,
Enum: enumErr.Variant,
CustomCode: customCode.Code,
}
}
func (e *TransactionError) GetCustomErrorCode() (uint8, uint32, error) { func (e *TransactionError) GetCustomErrorCode() (uint8, uint32, error) {
instr, err := e.GetInstructionError() instr, err := e.GetInstructionError()
if err != nil { if err != nil {

47
error_test.go Normal file
View File

@@ -0,0 +1,47 @@
package pump_parser
import (
"encoding/base64"
"testing"
)
func TestDecodeTransactionError(t *testing.T) {
var testCases = []struct {
name string
data string
}{
{
name: "ComputationalBudgetExceeded",
data: "CAAAAAMlAAAA",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
bytesData, err := base64.StdEncoding.DecodeString(tc.data)
if err != nil {
t.Fatalf("Failed to decode base64 data for %s: %v", tc.name, err)
return
}
te, err := DecodeTransactionError(bytesData)
if err != nil {
t.Errorf("Decoded error for %s: %v", tc.name, err)
return
}
if te.Variant != InstructionError {
t.Errorf("Expected Variant to be InstructionError for %s, got %d", tc.name, te.Variant)
return
}
errName, err := te.GetInstructionError()
if err != nil {
t.Errorf("Failed to get instruction error for %s: %v", tc.name, err)
return
}
if errName.Variant != ComputationalBudgetExceeded {
t.Errorf("Expected instruction error variant to be Custom for %s, got %d", tc.name, errName.Variant)
return
}
})
}
}

View File

@@ -5,10 +5,17 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
parser "github.com/thloyi/pump-parser" parser "github.com/thloyi/pump-parser"
example "github.com/thloyi/pump-parser/internal/example" example "github.com/thloyi/pump-parser/internal/example"
) )
var (
pumpProgram = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
)
func main() { func main() {
//pool, err := ants.NewPool(100, ants.WithPreAlloc(true), ants.WithNonblocking(true)) //pool, err := ants.NewPool(100, ants.WithPreAlloc(true), ants.WithNonblocking(true))
//if err != nil { //if err != nil {
@@ -31,6 +38,7 @@ func main() {
continue continue
} }
ptx := msg.Tx ptx := msg.Tx
// fmt.Println("consume", ptx.ComputeUnitsConsumed, "limit", ptx.CuLimit, "hash", ptx.GetTxHash())
//data, _ := json.Marshal(tx) //data, _ := json.Marshal(tx)
//fmt.Println(string(data)) //fmt.Println(string(data))
//continue //continue
@@ -43,43 +51,9 @@ func main() {
//} //}
// 处理交易 // 处理交易
txErr, ok := ptx.Err.(*parser.TransactionError) if len(ptx.Swaps) > 0 && (ptx.Swaps[0].Program == parser.SolProgramPump || ptx.Swaps[0].Program == parser.SolProgramPumpAMM) {
var customerErrCode uint32 fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Swaps[0].Program, ptx.Swaps[0].Event, ptx.Block, ptx.GetTxHash(),
var instructorErrIndex uint8 ptx.Swaps[0].BaseAmount.Div(decimal.NewFromInt(1e6)), ptx.Swaps[0].QuoteAmount.Div(decimal.NewFromInt(1e9)))
if ok {
instructorErrIndex, customerErrCode, _ = txErr.GetCustomErrorCode()
fmt.Printf("now: %s, block: %d, tx: %s, errInstr Code: %d, errInstrIndex: %d, err: %v\n", time.Now().Format("2006-01-02 15:04:05"), ptx.Block, ptx.GetTxHash(), customerErrCode, instructorErrIndex, ptx.Err)
} else {
txs := example.FromTx(ptx)
if len(txs) == 0 {
fmt.Printf("tx is empty, block: %d, tx %s \n", ptx.Block, ptx.GetTxHash())
continue
}
// printed := false
for _, tx := range txs {
if tx.Program != parser.SolProgramPumpAMM {
continue
}
if tx.EntryContract == "" || tx.EntryContract == parser.SolProgramPumpAMM || tx.EntryContract == parser.EntryContractOKXDexRouterV2 || tx.EntryContract == "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" {
continue
}
//if tx.Token1Amount.GreaterThanOrEqual(decimal.NewFromFloat(0.1)) || tx.Event != "buy" {
// continue
//}
// printed = true
fmt.Printf("t: %s, block: %d, hash: %s, maker: %s, program: %s, event: %s, token0: %s, entryContract: %s, token balance: %s, EntryContract: %s\n",
time.Now().Format(time.RFC3339Nano),
tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token0Amount, tx.EntryContract, tx.AfterSignerToken0Balance, tx.EntryContract)
//break
}
//if !printed {
// continue
//}
//fmt.Printf("t: %s, block: %d, hash: %s, signer: %s, program: %s, event: %s, token0: %s, token1: %s, signer before sol :%s, after sol: %s, after token: %s, tokencreator: %s, tokenprogram: %s, mayhem: %t\n",
// time.Now().Format(time.RFC3339Nano),
// tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token0Amount.String(), tx.Token1Amount.String(),
// tx.BeforeSolBalance, tx.AfterSOLBalance, tx.AfterSignerToken0Balance, tx.TokenCreator, tx.Token0Program, tx.Mayhem)
} }
// currentBlock = ptx.Block // currentBlock = ptx.Block
// //

View File

@@ -3,7 +3,6 @@ package parser
import ( import (
"fmt" "fmt"
"github.com/shopspring/decimal"
types "github.com/thloyi/pump-parser" types "github.com/thloyi/pump-parser"
) )
@@ -23,24 +22,14 @@ func NewPumpHandler(cb func(*types.Tx)) *PumpHandler {
func (h *PumpHandler) HandleMessage(rawTx *types.RawTx) { func (h *PumpHandler) HandleMessage(rawTx *types.RawTx) {
if rawTx.Meta.Err != nil { if rawTx.Meta.Err != nil {
// Notify the channel about the failed transaction // Notify the channel about the failed transaction
beforeSolBalance := decimal.Zero var parsedTx = &types.Tx{}
afterSolBalance := decimal.Zero parsedTx.SetRawTx(rawTx)
if rawTx.Meta.PreBalances != nil && len(rawTx.Meta.PreBalances) > 0 { err := parsedTx.Parser()
beforeSolBalance = decimal.NewFromUint64(rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9)) if err != nil {
fmt.Printf("parser failed tx error: %s, block: %d tx: %s\n", err, rawTx.Slot, rawTx.TxHash())
return
} }
if rawTx.Meta.PostBalances != nil && len(rawTx.Meta.PostBalances) > 0 { h.callback(parsedTx)
afterSolBalance = decimal.NewFromUint64(rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9))
}
h.callback(&types.Tx{
TxHash: (*[64]byte)((rawTx.Transaction.Signatures[0][:])),
Err: rawTx.Meta.Err,
Signer: rawTx.GetSigner(),
Block: rawTx.Slot,
BlockIndex: uint64(rawTx.IndexWithinBlock),
BeforeSolBalance: beforeSolBalance,
AfterSOLBalance: afterSolBalance,
})
return return
} }

View File

@@ -50,7 +50,8 @@ type Tx struct {
AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"` AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"`
EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"` EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"`
Mayhem bool Mayhem bool
Cashback bool `json:"is_cashback_coin"`
} }
func (tx *Tx) GetTxHash() string { func (tx *Tx) GetTxHash() string {
@@ -121,6 +122,7 @@ func FromTx(tx *parser.Tx) []*Tx {
EntryContract: s.CheckEntryContract(), EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem, Mayhem: s.Mayhem,
Cashback: s.Cashback,
} }
} else if s.Program == "PumpAMM" { } else if s.Program == "PumpAMM" {
if s.BaseMint.Equals(solana.WrappedSol) { if s.BaseMint.Equals(solana.WrappedSol) {
@@ -175,6 +177,7 @@ func FromTx(tx *parser.Tx) []*Tx {
EntryContract: s.CheckEntryContract(), EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem, Mayhem: s.Mayhem,
Cashback: s.Cashback,
} }
} else { } else {
newTx = &Tx{ newTx = &Tx{
@@ -219,6 +222,7 @@ func FromTx(tx *parser.Tx) []*Tx {
EntryContract: s.CheckEntryContract(), EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem, Mayhem: s.Mayhem,
Cashback: s.Cashback,
} }
} }
} }

View File

@@ -50,21 +50,18 @@ type Client struct {
func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client { func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client {
var subscription pb.SubscribeRequest var subscription pb.SubscribeRequest
var failed = false //var failed = true
var vote = false var vote = false
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions) subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{ subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
Failed: &failed, //Failed: &failed,
Vote: &vote, Vote: &vote,
} }
subscription.Transactions["transactions_sub"].AccountInclude = []string{ subscription.Transactions["transactions_sub"].AccountInclude = []string{
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
} }
subscription.Transactions["transactions_sub"].AccountRequired = []string{
"ARu4n5mFdZogZAravu7CcizaojWnS6oqka37gdLT5SZn",
}
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta) subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{} subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
@@ -85,12 +82,12 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Client { func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Client {
var subscription pb.SubscribeRequest var subscription pb.SubscribeRequest
var failed = false //var failed = false
var vote = false var vote = false
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions) subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{ subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
Failed: &failed, //Failed: &failed,
Vote: &vote, Vote: &vote,
} }
subscription.Transactions["transactions_sub"].AccountInclude = []string{ subscription.Transactions["transactions_sub"].AccountInclude = []string{

View File

@@ -23,7 +23,7 @@ var ()
func main() { func main() {
var slot uint64 = 399230674 var slot uint64 = 403021435
var data = NewBlockData(decimal.NewFromFloat(100.0)) var data = NewBlockData(decimal.NewFromFloat(100.0))
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d") client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
var rewards = false var rewards = false
@@ -53,9 +53,9 @@ func main() {
fmt.Println("from rpc tx error:", i, err) fmt.Println("from rpc tx error:", i, err)
break break
} }
if rawTx.Meta.Err != nil { //if rawTx.Meta.Err != nil {
continue // continue
} //}
parsedTx, err := solana_parser.ParseRawTx(rawTx) parsedTx, err := solana_parser.ParseRawTx(rawTx)
if err != nil { if err != nil {
fmt.Println("parse tx error:", i, rawTx.TxHash(), err) fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
@@ -91,9 +91,7 @@ func main() {
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err) fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)
} }
} }
if result.GetTxHash() == "3vMp5Lqm7PxS9MXfXgmptKA9xLkyfKfJpuFs2mUfhRuqTWjGj7DcvSb65NsHQH5RF9JXVLbxrpUHV4LXrgmjXmft" {
fmt.Println("xxx")
}
} }
fmt.Println("slot", slot, "tx count: ", len(data.Txs)) fmt.Println("slot", slot, "tx count: ", len(data.Txs))

817
internal/test3/test.go Normal file
View File

@@ -0,0 +1,817 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"log/slog"
"strings"
"time"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/jackc/pgtype"
"github.com/shopspring/decimal"
solana_parser "github.com/thloyi/pump-parser"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var ()
func main() {
var data = NewBlockData(decimal.NewFromFloat(100.0))
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
var version uint64 = 0
txSig, _ := solana.SignatureFromBase58("2LCw5yZy6sGTWKpJNxpFxR11M66cXPsrGmJXnQmWW9QVv6SDWRmu1aevc6yE9NeUz78mFb4T8TEx9w5781NHnz2T")
tx, err := client.GetTransaction(context.Background(), txSig, &rpc.GetTransactionOpts{
Commitment: rpc.CommitmentFinalized,
Encoding: solana.EncodingBase64,
MaxSupportedTransactionVersion: &version,
})
if err != nil {
fmt.Println("get block error:", err)
return
}
solana_parser.EnableAllParsers()
var blockTime uint64
rawTx, err := solana_parser.FromRpcTransactionWithMeta(rpc.TransactionWithMeta{
Slot: 0,
BlockTime: nil,
Transaction: rpc.DataBytesOrJSONFromBytes(tx.Transaction.GetBinary()),
Meta: tx.Meta,
Version: tx.Version,
}, &blockTime, 0, int64(0))
if err != nil {
fmt.Println("from rpc tx error:", err)
return
}
result, err := solana_parser.ParseRawTx(rawTx)
if err != nil {
fmt.Println("parse tx error:", rawTx.TxHash(), err)
return
}
swapsLen := len(result.Swaps)
for i := 0; i < swapsLen; i++ {
action := result.Swaps[i]
var actions []solana_parser.Swap = make([]solana_parser.Swap, 0, 2)
actions = append(actions, action)
if i+1 < swapsLen {
nextAction := result.Swaps[i+1]
if action.Event == "buy" && nextAction.Event == "complete" &&
action.Program == solana_parser.SolProgramPump &&
nextAction.Program == solana_parser.SolProgramPump &&
action.BaseMint == nextAction.BaseMint {
actions = append(actions, nextAction)
i++
}
if action.Event == "migrate" && nextAction.Event == "create" &&
action.Program == solana_parser.SolProgramPump &&
nextAction.Program == solana_parser.SolProgramPumpAMM &&
action.BaseMint == nextAction.BaseMint {
actions = append(actions, nextAction)
i++
}
}
if err = HandleAction(context.Background(), result, actions, data); err != nil {
//h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err)
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)
}
}
fmt.Println("tx count: ", len(data.Txs))
}
var (
meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG")
raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C")
)
func HandleAction(ctx context.Context, tx *solana_parser.Tx, swaps []solana_parser.Swap, data *BlockData) error {
swapLen := len(swaps)
if len(swaps) == 0 {
return nil
}
if swaps[0].BaseMint != solana_parser.WSOL && swaps[0].QuoteMint != solana_parser.WSOL && !swaps[0].QuoteMint.IsZero() {
return nil
}
if len(swaps) == 0 {
return nil
}
event := swaps[0].Event
swap := swaps[0]
action := SwapGetter{swap}
switch event {
case "buy", "sell":
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
if swap.Program == solana_parser.SolProgramPump {
if swapLen == 2 && swaps[1].Event == "complete" {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
data.AppendAction(Action{
Maker: swaps[1].User.String(),
Token: swaps[1].BaseMint.String(),
Pair: swaps[1].Pool.String(),
Action: "pump-migrate",
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
}
}
return data.SetPair(action, tx.Block, "")
case "create":
pair, err := action.GetPair(tx.Block, "")
if err != nil {
return err
}
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
data.Pairs[pair.Address] = *pair
case "add_liquidity", "remove_liquidity", "deposit", "withdraw", "add", "remove":
liquidityTx, err := action.GetLiquidityTx(tx, uint64(swap.TxIndex))
if liquidityTx == nil {
return err
}
data.AppendTx(*liquidityTx)
return data.SetPair(action, tx.Block, "")
}
if event != "migrate" {
return nil
}
if swap.Program == solana_parser.SolProgramPump {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
if swapLen == 2 && swaps[1].Event == "create" && swaps[1].Program == solana_parser.SolProgramPumpAMM && swaps[1].BaseMint == swap.BaseMint {
tokenMint := swap.BaseMint.String()
data.AppendAction(Action{
Maker: swap.User.String(),
Token: tokenMint,
Pair: swaps[1].Pool.String(),
Action: "on-pumpswap",
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
data.NewRaydium = append(data.NewRaydium, tokenMint)
}
} else if swap.Program == solana_parser.SolProgramRaydiumLaunchLab || swap.Program == solana_parser.SolProgramRaydiumLaunchLabBonk {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
var actionType string
if action.MigrateTopProgram == raydiumCPmmProgramID {
actionType = "on-raydium-cpmm"
} else {
actionType = "on-raydium-amm"
}
data.AppendAction(Action{
Maker: action.User.String(),
Token: action.BaseMint.String(),
Pair: action.MigrateToPool.String(),
Action: actionType,
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
} else if swap.Program == solana_parser.SolProgramMeteoraBondingCurve {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
var actionType string
if swap.MigrateTopProgram == meteoraDammV2Program {
actionType = "on-meteora-amm-v2"
} else {
actionType = "on-meteora-amm-v1"
}
data.AppendAction(Action{
Maker: action.User.String(),
Token: action.BaseMint.String(),
Pair: action.MigrateToPool.String(),
Action: actionType,
Block: uint64(tx.Block),
BlockAt: t,
TxHash: tx.GetTxHash(),
})
}
return nil
}
type Pair struct {
Id string `gorm:"column:id;primaryKey;default:uuid_generate_v4()"`
Address string
Name string
Token0 string
Token1 string
LpToken string
ChainId int64
Reserve0 decimal.Decimal
Reserve1 decimal.Decimal
Block uint64
BlockAt *pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at,omitempty"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"created_at,omitempty"`
SortId uint64
Program string
IsCreate bool `gorm:"-"`
//TokenObj *Token `gorm:"-" json:"token_obj,omitempty"`
UpdateSlot uint64 `gorm:"-"`
InDB bool `gorm:"-"`
}
type Tx struct {
Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"`
PairAddress string `json:"pair_address"`
Maker string `json:"maker"`
Token0Address string `json:"token0_address"`
Token1Address string `json:"token1_address"`
Token0Amount decimal.Decimal `json:"token0Amount" gorm:"column:token0_amount;type:numeric"`
Token1Amount decimal.Decimal `json:"token1Amount" gorm:"column:token1_amount;type:numeric"`
PriceUsd decimal.Decimal `json:"price_usd" gorm:"column:price_usd;type:numeric"`
AmountUsd decimal.Decimal `json:"amount_usd" gorm:"column:amount_usd;type:numeric"`
Block uint64 `json:"block"`
BlockIndex uint64 `json:"index"`
Event string `json:"event"`
TxHash string `json:"tx_hash"`
TxIndex uint64 `json:"topic_index"`
Program string `json:"program"`
BlockAt pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
TotalSupply string `gorm:"total_supply"`
AfterReserve0 string `gorm:"after_reserve0"`
AfterReserve1 string `gorm:"after_reserve1"`
PositionChange int64 `gorm:"position_change"`
Platform string `gorm:"column:tx_platform;type:platform;default:'none'" json:"tx_platform"`
PlatformFee decimal.Decimal `gorm:"-" json:"-"` // TODO: save to db
CUPrice decimal.Decimal `gorm:"column:tx_cu_price;type:numeric" json:"tx_cu_price"`
MevAgent string `gorm:"column:tx_mev_agent;type:mev_agent;default:'none'" json:"tx_mev_agent"`
MevAgentFee decimal.Decimal `gorm:"column:tx_mev_agent_fee;type:numeric" json:"tx_mev_agent_fee"`
AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"`
EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"`
}
type Action struct {
Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"`
Maker string `json:"maker"`
Token string `json:"token"`
Pair string `json:"pair"`
Action string `json:"action"`
Block uint64 `json:"block"`
BlockAt pgtype.Timestamptz `json:"block_at"`
TxHash string `json:"tx_hash"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
}
type BlockData struct {
Pairs map[string]Pair
Txs []Tx
Actions []Action
Price decimal.Decimal
NewRaydium []string
}
func NewBlockData(price decimal.Decimal) *BlockData {
return &BlockData{
Pairs: make(map[string]Pair),
Txs: make([]Tx, 0),
Actions: make([]Action, 0),
Price: price,
NewRaydium: make([]string, 0),
}
}
func (bd *BlockData) AppendTx(tx Tx) {
bd.Txs = append(bd.Txs, tx)
}
func (bd *BlockData) AppendAction(action Action) {
bd.Actions = append(bd.Actions, action)
}
func (bd *BlockData) SetPair(action SwapGetter, block uint64, _ string) error {
pair, err := action.GetPair(block, "")
if err != nil {
return err
}
bd.Pairs[pair.Address] = *pair
return nil
}
type SwapGetter struct {
solana_parser.Swap
}
const (
PositionChangeNone = int64(iota)
PositionChangeNewBuy
PositionChangeBuyMore
PositionChangeSellPart
PositionChangeSellAll
)
func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, error) {
if spg.BaseMint != solana.WrappedSol && spg.QuoteMint != solana.WrappedSol {
return nil, nil
}
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
pool0 decimal.Decimal
pool1 decimal.Decimal
event string
)
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
token0 = spg.QuoteMint.String()
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
} else {
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
token0 = spg.BaseMint.String()
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
}
if spg.Event == "deposit" || spg.Event == "add" || spg.Event == "add_liquidity" || spg.Event == "add_liquidity_one_side" {
event = "add"
} else if spg.Event == "withdraw" || spg.Event == "remove" || spg.Event == "remove_liquidity" || spg.Event == "remove_liquidity_one_side" {
event = "remove"
}
if event == "" {
return nil, nil
}
mevName, mevFee := tx.CheckMevAgent()
platformName, platformFee := tx.CheckPlatform(spg.Swap)
pairString := ""
if spg.Program == solana_parser.SolProgramPump {
pairString = spg.BaseMint.String()
} else {
pairString = spg.Pool.String()
}
t := pgtype.Timestamptz{}
_ = t.Set(time.Unix(tx.BlockAt, 0))
return &Tx{
PairAddress: pairString,
Maker: spg.User.String(),
Token0Address: token0,
Token1Address: "So11111111111111111111111111111111111111112",
Token0Amount: amount0,
Token1Amount: amount1,
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: event,
TxHash: tx.GetTxHash(),
TxIndex: index,
BlockAt: t,
Program: spg.Program,
AfterReserve0: pool0.String(),
AfterReserve1: pool1.String(),
Platform: platformName,
PlatformFee: platformFee,
CUPrice: tx.CUPrice,
MevAgent: mevName,
MevAgentFee: mevFee,
AfterSOLBalance: spg.AfterSOLBalance,
EntryContract: spg.CheckEntryContract(),
}, nil
}
func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.Decimal) Tx {
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
pool0 decimal.Decimal
pool1 decimal.Decimal
event string
)
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
token0 = spg.QuoteMint.String()
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
if spg.Event == "buy" {
event = "sell"
} else if spg.Event == "sell" {
event = "buy"
}
} else {
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
token0 = spg.BaseMint.String()
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
event = spg.Event
}
priceUsd := decimal.Zero
if amount0.GreaterThan(priceUsd) {
priceUsd = amount1.Div(amount0).Mul(price)
}
pc := PositionChangeNone
if event == "buy" {
pc = PositionChangeNewBuy
if spg.BaseMint == solana.WrappedSol {
if spg.UserQuoteBalance.GreaterThan(spg.QuoteAmount) {
pc = PositionChangeBuyMore
}
} else {
if spg.UserBaseBalance.GreaterThan(spg.BaseAmount) {
pc = PositionChangeBuyMore
}
}
} else if event == "sell" {
pc = PositionChangeSellPart
if spg.BaseMint == solana.WrappedSol {
if spg.UserQuoteBalance.Div(decimal.New(1, int32(spg.QuoteMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
pc = PositionChangeSellAll
}
} else {
if spg.UserBaseBalance.Div(decimal.New(1, int32(spg.BaseMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
pc = PositionChangeSellAll
}
}
}
mevName, mevFee := tx.CheckMevAgent()
platformName, platformFee := tx.CheckPlatform(spg.Swap)
if mevName == "" {
mevName = "none"
}
if mevName == "unknown" {
mevName = "none"
mevFee = decimal.Zero
}
pairString := ""
if spg.Program == solana_parser.SolProgramPump {
pairString = spg.BaseMint.String()
} else {
pairString = spg.Pool.String()
}
t := pgtype.Timestamptz{}
_ = t.Set(time.Unix(tx.BlockAt, 0))
return Tx{
PairAddress: pairString,
Maker: spg.User.String(),
Token0Address: token0,
Token1Address: "So11111111111111111111111111111111111111112",
Token0Amount: amount0,
Token1Amount: amount1,
PriceUsd: priceUsd,
AmountUsd: amount1.Mul(price),
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: event,
TxHash: tx.GetTxHash(),
TxIndex: index,
BlockAt: t,
Program: spg.Program,
AfterReserve0: pool0.String(),
AfterReserve1: pool1.String(),
PositionChange: pc,
Platform: platformName,
PlatformFee: platformFee,
CUPrice: tx.CUPrice,
MevAgent: mevName,
MevAgentFee: mevFee,
AfterSOLBalance: spg.AfterSOLBalance,
EntryContract: spg.CheckEntryContract(),
}
}
func (spg SwapGetter) GetPair(slot uint64, _ string) (*Pair, error) {
//pump amm
if spg.Program == solana_parser.SolProgramPump {
tokenMint := spg.BaseMint.String()
return &Pair{
Address: tokenMint,
Token0: tokenMint,
Token1: "So11111111111111111111111111111111111111112",
ChainId: 900,
Reserve0: spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))),
Reserve1: spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))),
IsCreate: spg.Event == "create",
Program: spg.Program,
UpdateSlot: slot,
}, nil
} else {
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
)
if spg.BaseMint.IsZero() || spg.QuoteMint.IsZero() {
return nil, errors.New("base mint or quote mint is empty")
}
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
//decimal0 = spg.QuoteMintDecimals
token0 = spg.QuoteMint.String()
} else {
amount0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
//decimal0 = a.BaseDecimals
token0 = spg.BaseMint.String()
}
return &Pair{
Address: spg.Pool.String(),
LpToken: spg.LpMint.String(),
Token0: token0,
Token1: "So11111111111111111111111111111111111111112",
ChainId: 900,
Reserve0: amount0,
Reserve1: amount1,
IsCreate: false,
Program: spg.Program,
UpdateSlot: slot,
}, nil
}
}
func getBlockTxsFromDb(db *gorm.DB, block uint64) ([]Tx, error) {
var txs []Tx
result := db.Table("tx").Where("block = ?", block).Find(&txs)
return txs, result.Error
}
func getBlockActionsFromDb(db *gorm.DB, block uint64) ([]Action, error) {
var txs []Action
result := db.Table("action").Where("block = ?", block).Find(&txs)
return txs, result.Error
}
type dbLog struct {
logger *slog.Logger
}
func (l *dbLog) Printf(format string, args ...interface{}) {
l.logger.Info(fmt.Sprintf(format, args...))
}
func newDbLog() *dbLog {
return &dbLog{logger: slog.Default()}
}
func NewGorm(dsn string) *gorm.DB {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.New(newDbLog(), logger.Config{
Colorful: false,
LogLevel: logger.Warn,
SlowThreshold: time.Second * 10,
IgnoreRecordNotFoundError: true,
}),
})
if err != nil {
panic(err)
}
return db
}
func compareTxs(dbTxs []Tx, dataTxs []Tx) (diff int, missing int) {
dataByHash := make(map[string][]Tx, len(dataTxs))
for _, tx := range dataTxs {
dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)] = append(dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)], tx)
}
for _, dbTx := range dbTxs {
candidates := dataByHash[fmt.Sprintf("%s-%d", dbTx.TxHash, dbTx.TxIndex)]
if len(candidates) == 0 {
missing++
log.Printf("missing tx: %s", txCompareString(dbTx))
continue
}
matched := false
for _, dataTx := range candidates {
if txEqualWithoutHash(dbTx, dataTx) {
matched = true
break
}
}
if !matched {
diff++
log.Printf("tx diff hash=%s, program=%s, event:%s: %s, ", dbTx.TxHash, dbTx.Program, dbTx.Event, txCompareDiffString(dbTx, candidates[0]))
}
}
log.Printf("compare txs done: db=%d parsed=%d missing=%d diff=%d", len(dbTxs), len(dataTxs), missing, diff)
return diff, missing
}
func withinOnePercentDecimal(a decimal.Decimal, b decimal.Decimal) bool {
if a.IsZero() {
return b.IsZero()
}
diff := a.Sub(b).Abs()
threshold := a.Abs().Mul(decimal.NewFromFloat(0.03))
return diff.LessThanOrEqual(threshold)
}
func withinOnePercentStringDecimal(a string, b string) bool {
ad, errA := decimal.NewFromString(a)
bd, errB := decimal.NewFromString(b)
if errA != nil || errB != nil {
return a == b
}
return withinOnePercentDecimal(ad, bd)
}
func txEqualWithoutHash(a Tx, b Tx) bool {
//mevMatch := a.MevAgent == b.MevAgent || (a.MevAgent == "none" && b.MevAgent == "unknown") || (a.MevAgent == "unknown" && b.MevAgent == "none")
//mevNone := a.MevAgent == "none" || a.MevAgent == "unknown"
return a.PairAddress == b.PairAddress &&
a.Token1Address == b.Token1Address &&
(a.Token0Address == "" || a.Token0Address == b.Token0Address) &&
//a.Maker == b.Maker &&
(a.Token0Address == "" || withinOnePercentDecimal(a.Token0Amount, b.Token0Amount)) &&
withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) &&
a.Block == b.Block &&
a.BlockIndex == b.BlockIndex &&
a.Event == b.Event &&
a.TxIndex == b.TxIndex &&
a.Program == b.Program &&
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0)) &&
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1)) &&
// a.PositionChange == b.PositionChange &&
(a.Platform == b.Platform || (a.Platform == "photon" && b.Platform == "fake") || (a.Platform == "trojan" && b.Platform == "fake")) &&
a.CUPrice.String() == b.CUPrice.String() // &&
//mevMatch &&
//(mevNone || a.MevAgentFee.String() == b.MevAgentFee.String()) &&
//(a.Program == solana_parser.SolProgramRaydiumV4 || a.AfterSOLBalance.String() == b.AfterSOLBalance.String())
//&&
// a.EntryContract == b.EntryContract
}
func txCompareDiffString(a Tx, b Tx) string {
var diffs []string
if a.PairAddress != b.PairAddress {
diffs = append(diffs, fmt.Sprintf("PairAddress db=%s data=%s", a.PairAddress, b.PairAddress))
}
//if a.Maker != b.Maker {
// diffs = append(diffs, fmt.Sprintf("Maker db=%s, data=%s", a.Maker, b.Maker))
//}
if a.Token1Address != b.Token1Address {
diffs = append(diffs, fmt.Sprintf("Token1Address db=%s data=%s", a.Token1Address, b.Token1Address))
}
if a.Token0Address != b.Token0Address {
diffs = append(diffs, fmt.Sprintf("Token0Address db=%s data=%s", a.Token0Address, b.Token0Address))
}
if !withinOnePercentDecimal(a.Token0Amount, b.Token0Amount) {
diffs = append(diffs, fmt.Sprintf("Token0Amount db=%s data=%s", a.Token0Amount.String(), b.Token0Amount.String()))
}
if !withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) {
diffs = append(diffs, fmt.Sprintf("Token1Amount db=%s data=%s", a.Token1Amount.String(), b.Token1Amount.String()))
}
if a.Block != b.Block {
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
}
if a.BlockIndex != b.BlockIndex {
diffs = append(diffs, fmt.Sprintf("BlockIndex db=%d data=%d", a.BlockIndex, b.BlockIndex))
}
if a.Event != b.Event {
diffs = append(diffs, fmt.Sprintf("Event db=%s data=%s", a.Event, b.Event))
}
if a.TxIndex != b.TxIndex {
diffs = append(diffs, fmt.Sprintf("TxIndex db=%d data=%d", a.TxIndex, b.TxIndex))
}
if a.Program != b.Program {
diffs = append(diffs, fmt.Sprintf("Program db=%s data=%s", a.Program, b.Program))
}
if !withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0) {
diffs = append(diffs, fmt.Sprintf("AfterReserve0 db=%s data=%s", a.AfterReserve0, b.AfterReserve0))
}
if !withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1) {
diffs = append(diffs, fmt.Sprintf("AfterReserve1 db=%s data=%s", a.AfterReserve1, b.AfterReserve1))
}
//if a.PositionChange != b.PositionChange {
// diffs = append(diffs, fmt.Sprintf("PositionChange db=%d data=%d", a.PositionChange, b.PositionChange))
//}
if a.Platform != b.Platform {
diffs = append(diffs, fmt.Sprintf("Platform db=%s data=%s", a.Platform, b.Platform))
}
if a.CUPrice.String() != b.CUPrice.String() {
diffs = append(diffs, fmt.Sprintf("CUPrice db=%s data=%s", a.CUPrice.String(), b.CUPrice.String()))
}
//if a.MevAgent != b.MevAgent {
// diffs = append(diffs, fmt.Sprintf("MevAgent db=%s data=%s", a.MevAgent, b.MevAgent))
//}
//if a.MevAgentFee.String() != b.MevAgentFee.String() {
// diffs = append(diffs, fmt.Sprintf("MevAgentFee db=%s data=%s", a.MevAgentFee.String(), b.MevAgentFee.String()))
//}
//if a.AfterSOLBalance.String() != b.AfterSOLBalance.String() {
// diffs = append(diffs, fmt.Sprintf("AfterSOLBalance db=%s data=%s", a.AfterSOLBalance.String(), b.AfterSOLBalance.String()))
//}
//if a.EntryContract != b.EntryContract {
// diffs = append(diffs, fmt.Sprintf("EntryContract db=%s data=%s", a.EntryContract, b.EntryContract))
//}
return strings.Join(diffs, "; ")
}
func compareActions(dbActions []Action, dataActions []Action) (diff, missing int) {
dataByHash := make(map[string][]Action, len(dataActions))
for _, action := range dataActions {
dataByHash[action.TxHash] = append(dataByHash[action.TxHash], action)
}
for _, dbAction := range dbActions {
candidates := dataByHash[dbAction.TxHash]
if len(candidates) == 0 {
missing++
log.Printf("missing action: %s", actionCompareString(dbAction))
continue
}
matched := false
for _, dataAction := range candidates {
if actionEqualWithoutHash(dbAction, dataAction) {
matched = true
break
}
}
if !matched {
diff++
log.Printf("action diff hash=%s: %s", dbAction.TxHash, actionCompareDiffString(dbAction, candidates[0]))
}
}
log.Printf("compare actions done: db=%d parsed=%d missing=%d diff=%d", len(dbActions), len(dataActions), missing, diff)
return diff, missing
}
func actionEqualWithoutHash(a Action, b Action) bool {
return a.Maker == b.Maker &&
a.Token == b.Token &&
a.Pair == b.Pair &&
a.Action == b.Action &&
a.Block == b.Block
}
func actionCompareDiffString(a Action, b Action) string {
var diffs []string
if a.Maker != b.Maker {
diffs = append(diffs, fmt.Sprintf("Maker db=%s data=%s", a.Maker, b.Maker))
}
if a.Token != b.Token {
diffs = append(diffs, fmt.Sprintf("Token db=%s data=%s", a.Token, b.Token))
}
if a.Pair != b.Pair {
diffs = append(diffs, fmt.Sprintf("Pair db=%s data=%s", a.Pair, b.Pair))
}
if a.Action != b.Action {
diffs = append(diffs, fmt.Sprintf("Action db=%s data=%s", a.Action, b.Action))
}
if a.Block != b.Block {
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
}
return strings.Join(diffs, "; ")
}
func actionCompareString(action Action) string {
return fmt.Sprintf("Maker=%s Token=%s Pair=%s Action=%s Block=%d TxHash=%s", action.Maker, action.Token, action.Pair, action.Action, action.Block, action.TxHash)
}
func txCompareString(tx Tx) string {
return fmt.Sprintf(
"tx.Program=%s TxHash=%s PairAddress=%s Token1Address=%s Token0Amount=%s Token1Amount=%s Block=%d BlockIndex=%d Event=%s TxIndex=%d AfterReserve0=%s AfterReserve1=%s PositionChange=%d Platform=%s CUPrice=%s MevAgent=%s MevAgentFee=%s AfterSOLBalance=%s EntryContract=%s",
tx.Program,
tx.TxHash,
tx.PairAddress,
tx.Token1Address,
tx.Token0Amount.String(),
tx.Token1Amount.String(),
tx.Block,
tx.BlockIndex,
tx.Event,
tx.TxIndex,
tx.AfterReserve0,
tx.AfterReserve1,
tx.PositionChange,
tx.Platform,
tx.CUPrice.String(),
tx.MevAgent,
tx.MevAgentFee.String(),
tx.AfterSOLBalance.String(),
tx.EntryContract,
)
}

View File

@@ -200,8 +200,10 @@ var (
const ( const (
raydiumV4InitializePoolDiscriminator = uint8(1) raydiumV4InitializePoolDiscriminator = uint8(1)
raydiumV4SwapBaseInDiscriminator = uint8(9) raydiumV4SwapBaseInDiscriminator = uint8(9)
raydiumV4SwapBaseOutDiscriminator = uint8(11) raydiumV4SwapBaseOutDiscriminator = uint8(11)
raydiumV4SwapBaseInV2Discriminator = uint8(16)
raydiumV4SwapBaseOutV2Discriminator = uint8(17)
raydiumV4AddLiquidityDiscriminator = uint8(3) raydiumV4AddLiquidityDiscriminator = uint8(3)
raydiumV4RemoveLiquidityDiscriminator = uint8(4) raydiumV4RemoveLiquidityDiscriminator = uint8(4)
@@ -234,5 +236,8 @@ var transferDiscriminator = uint32(2)
var createAccountWithSeedDiscriminator = uint32(3) var createAccountWithSeedDiscriminator = uint32(3)
var systemProgram = solana.MustPublicKeyFromBase58("11111111111111111111111111111111") var systemProgram = solana.MustPublicKeyFromBase58("11111111111111111111111111111111")
var momoProgram = solana.MustPublicKeyFromBase58("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
var chainLinkProgram = solana.MustPublicKeyFromBase58("cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ")
var eventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29} var eventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}

View File

@@ -634,7 +634,7 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
swap := Swap{ swap := Swap{
Program: SolProgramMeteoraDLMM, Program: SolProgramMeteoraDLMM,
Event: "add_liquidity", Event: "add",
Pool: pool, Pool: pool,
BaseMint: baseMint, BaseMint: baseMint,
QuoteMint: quoteMint, QuoteMint: quoteMint,
@@ -802,7 +802,7 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
swap := Swap{ swap := Swap{
Program: SolProgramMeteoraDLMM, Program: SolProgramMeteoraDLMM,
Event: "remove_liquidity", Event: "remove",
Pool: pool, Pool: pool,
BaseMint: baseMint, BaseMint: baseMint,
QuoteMint: quoteMint, QuoteMint: quoteMint,

View File

@@ -90,7 +90,7 @@ func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerIns
var prefixLen = offset[1] var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("meta damm initial get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
var loadedEvent bool var loadedEvent bool
var initializePoolEvent MetaoraDammInitializePoolEvent var initializePoolEvent MetaoraDammInitializePoolEvent
@@ -119,6 +119,13 @@ func meteoraDammV2InitializePoolParser(tx *Tx, instruction Instruction, innerIns
} }
baseVaultAccountIndex := instruction.Accounts[10] baseVaultAccountIndex := instruction.Accounts[10]
quoteVaultAccountIndex := instruction.Accounts[11] quoteVaultAccountIndex := instruction.Accounts[11]
if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializePoolWithDynamicConfig[:]) {
baseVaultAccountIndex = instruction.Accounts[11]
quoteVaultAccountIndex = instruction.Accounts[12]
} else if bytes.Equal(instruction.Data[:8], meteoraDammV2InitializeCustomizablePoolDiscriminator[:]) {
baseVaultAccountIndex = instruction.Accounts[9]
quoteVaultAccountIndex = instruction.Accounts[10]
}
baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex) baseVaultTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultAccountIndex)
if err != nil { if err != nil {

View File

@@ -848,11 +848,11 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
var baseFound, quoteFound bool var baseFound, quoteFound bool
var skipOffset = 0 var skipOffset = 0
for i, inner := range inners { for i, inner := range inners {
if !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.Token2022ProgramID) && !tx.rawTx.accountList[inner.ProgramIDIndex].Equals(solana.TokenProgramID) {
continue
}
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner) from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil { if err != nil {
if i <= 1 { //maybe momo??
continue
}
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) { if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
@@ -1010,11 +1010,11 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
{ {
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase) baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token0 vault balance after tx: %v", err)
} }
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote) quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token1 vault balance after tx: %v", err)
} }
userBase := getAccountBalanceAfterTx(tx.rawTx, pool2UserBase) userBase := getAccountBalanceAfterTx(tx.rawTx, pool2UserBase)
@@ -1105,8 +1105,8 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
//pool2UserBase := instruction.Accounts[8] //pool2UserBase := instruction.Accounts[8]
pool2VaultBase := instruction.Accounts[11] pool2VaultBase := instruction.Accounts[11]
pool2UserQuote := instruction.Accounts[12] pool2VaultQuote := instruction.Accounts[12]
pool2VaultQuote := instruction.Accounts[13] pool2UserQuote := instruction.Accounts[13]
swaps := make([]Swap, 2) swaps := make([]Swap, 2)
{ {
@@ -1186,11 +1186,11 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
{ {
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase) baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultBase)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token0 vault balance after tx: %v", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token0 vault balance after tx: %v", err)
} }
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote) quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, pool2VaultQuote)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get pool1 token1 vault balance after tx: %v", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get pool2 token1 vault balance after tx: %v", err)
} }
userBase := decimal.Zero userBase := decimal.Zero

View File

@@ -2,7 +2,6 @@ package pump_parser
import ( import (
"errors" "errors"
"log"
"slices" "slices"
"github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go"
@@ -14,6 +13,11 @@ var defaultSwapPrograms = map[solana.PublicKey]swapParser{
pumpProgram: pumpParser, pumpProgram: pumpParser,
} }
var errTxParserPrograms = map[solana.PublicKey]swapParser{
pumpAmmProgram: pumpAmmParser,
pumpProgram: pumpParser,
}
var swapPrograms = cloneSwapPrograms(defaultSwapPrograms) var swapPrograms = cloneSwapPrograms(defaultSwapPrograms)
type ParserOption func(*parserConfig) type ParserOption func(*parserConfig)
@@ -56,8 +60,9 @@ func WithMeteoraDlmm() ParserOption {
} }
var actionPrograms = map[solana.PublicKey]actionParser{ var actionPrograms = map[solana.PublicKey]actionParser{
systemProgram: systemParser, systemProgram: systemParser,
budgGetProgram: budgetParser, budgGetProgram: budgetParser,
chainLinkProgram: chainLinkParser,
} }
func ParseRawTx(rawTx *RawTx) (*Tx, error) { func ParseRawTx(rawTx *RawTx) (*Tx, error) {
@@ -91,11 +96,55 @@ func (tx *Tx) Parser() error {
tx.BlockAt = tx.rawTx.BlockTime tx.BlockAt = tx.rawTx.BlockTime
tx.CuFee = decimal.NewFromUint64(tx.rawTx.Meta.Fee) tx.CuFee = decimal.NewFromUint64(tx.rawTx.Meta.Fee)
tx.ComputeUnitsConsumed = tx.rawTx.Meta.ComputeUnitsConsumed
tx.BeforeSolBalance = decimal.NewFromUint64(tx.rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9)) tx.BeforeSolBalance = decimal.NewFromUint64(tx.rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9))
tx.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9)) tx.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9))
tx.Token = make(map[solana.PublicKey]TokenMeta) tx.Token = make(map[solana.PublicKey]TokenMeta)
if tx.rawTx.Meta.Err != nil {
tx.Err = tx.rawTx.Meta.Err
if tx.Err.UnKnown != "" {
return nil
}
if len(tx.rawTx.Transaction.Message.Instructions) <= int(tx.Err.Index) {
return nil
}
programIdx := tx.rawTx.Transaction.Message.Instructions[tx.Err.Index].ProgramIDIndex
if len(accountList) <= programIdx {
return nil
}
programAccount := accountList[programIdx]
parserFunc, exists := errTxParserPrograms[programAccount]
if !exists {
return nil
}
// parse failed tx
swaps, _, err := parserFunc(tx, tx.rawTx.Transaction.Message.Instructions[tx.Err.Index], InnerInstructions{}, [2]uint{uint(tx.Err.Index), uint(0)})
if err != nil {
return nil
//fmt.Printf("parser failed tx error: %s, block: %d tx: %s\n", err, tx.Block, tx.GetTxHash())
}
if len(swaps) > 0 {
for i := range swaps {
swaps[i].InstrIdx = tx.Err.Index
}
tx.Swaps = swaps
}
for i, instr := range tx.rawTx.Transaction.Message.Instructions {
if p, exists := actionPrograms[accountList[instr.ProgramIDIndex]]; exists {
_, err := p(tx, instr, InnerInstructions{}, [2]uint{uint(i), uint(0)})
if err != nil {
if errors.Is(err, InstructionIgnoredError) {
continue
}
continue
}
}
}
return nil
}
var innersMap = make(map[int]InnerInstructions) var innersMap = make(map[int]InnerInstructions)
for _, inner := range tx.rawTx.Meta.InnerInstructions { for _, inner := range tx.rawTx.Meta.InnerInstructions {
innersMap[inner.Index] = inner innersMap[inner.Index] = inner
@@ -126,6 +175,7 @@ func (tx *Tx) Parser() error {
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9)) swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
} }
} }
swap.InstrIdx = uint8(i)
tx.Swaps = append(tx.Swaps, swap) tx.Swaps = append(tx.Swaps, swap)
} }
@@ -143,7 +193,7 @@ func (tx *Tx) Parser() error {
innerLength := len(innersMap[i].Instructions) innerLength := len(innersMap[i].Instructions)
for j := 1; j <= innerLength; { for j := 1; j <= innerLength; {
if j <= 0 || j > innerLength { if j <= 0 || j > innerLength {
log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j) //log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j)
break break
} }
innerInstr := innersMap[i].Instructions[j-1] innerInstr := innersMap[i].Instructions[j-1]
@@ -173,11 +223,18 @@ func (tx *Tx) Parser() error {
swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9)) swap.AfterSOLBalance = decimal.NewFromUint64(tx.rawTx.Meta.PostBalances[userIdx]).Div(decimal.NewFromInt(1e9))
} }
} }
swap.InstrIdx = uint8(i)
swap.InnerIdx = uint8(j)
tx.Swaps = append(tx.Swaps, swap) tx.Swaps = append(tx.Swaps, swap)
} }
// tx.Swaps = append(tx.Swaps, swaps...) // tx.Swaps = append(tx.Swaps, swaps...)
j = int(offset[1]) if ii == int(offset[0]) && j == int(offset[1]) {
ii = int(offset[0]) j = j + 1
} else {
j = int(offset[1])
ii = int(offset[0])
}
} else if p, exists := actionPrograms[innerProgramAccount]; exists { } else if p, exists := actionPrograms[innerProgramAccount]; exists {
offset, err := p(tx, innerInstr, innersMap[i], [2]uint{uint(i), uint(j)}) offset, err := p(tx, innerInstr, innersMap[i], [2]uint{uint(i), uint(j)})
if err != nil { if err != nil {

143
pump.go
View File

@@ -34,10 +34,19 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
switch discriminator { switch discriminator {
case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator: case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator:
if tx.Err != nil {
return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset)
}
return BuyOrSellParser(tx, instruction, innerInstructions, offset) return BuyOrSellParser(tx, instruction, innerInstructions, offset)
case pumpCreateDiscriminator, pumpCreateV2Discriminator: case pumpCreateDiscriminator, pumpCreateV2Discriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return CreateParser(tx, instruction, innerInstructions, offset) return CreateParser(tx, instruction, innerInstructions, offset)
case pumpMigrateDiscriminator: case pumpMigrateDiscriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return MigrateParser(tx, instruction, innerInstructions, offset) return MigrateParser(tx, instruction, innerInstructions, offset)
default: default:
return nil, increaseOffset(offset), InstructionIgnoredError return nil, increaseOffset(offset), InstructionIgnoredError
@@ -79,6 +88,7 @@ type PumpCreateEvent struct {
TokenTotalSupply uint64 TokenTotalSupply uint64
TokenProgram solana.PublicKey TokenProgram solana.PublicKey
IsMayhemMode bool IsMayhemMode bool
IsCashbackEnabled bool
} }
func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -148,6 +158,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
BaseReserve: decimal.NewFromUint64(createEvent.RealTokenReserves), BaseReserve: decimal.NewFromUint64(createEvent.RealTokenReserves),
QuoteReserve: decimal.Zero, QuoteReserve: decimal.Zero,
Mayhem: createEvent.IsMayhemMode, Mayhem: createEvent.IsMayhemMode,
Cashback: createEvent.IsCashbackEnabled,
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote), UserQuoteBalance: decimal.NewFromUint64(userQuote),
EntryContract: entryContract, EntryContract: entryContract,
@@ -176,6 +187,16 @@ type PumpTradeEvent struct {
CreatorFeeBasisPoints uint64 CreatorFeeBasisPoints uint64
CreatorFee uint64 CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
MayhemMode bool
CashbackFeeBasisPoints uint64
Cashback uint64
} }
type PumpTradeFeeArg struct { type PumpTradeFeeArg struct {
@@ -191,6 +212,112 @@ type CompleteEvent struct {
Timestamp int64 Timestamp int64
} }
type PumpTradeArgs struct {
Discriminator [8]byte
Amount1 uint64
Amount2 uint64
}
func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump failed but error is nil, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Variant != InstructionError {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded && tx.Err.Enum != ProgramFailedToComplete {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum == Custom {
if !(tx.Err.CustomCode == 1 ||
tx.Err.CustomCode == 6042 ||
tx.Err.CustomCode == 6041 ||
tx.Err.CustomCode == 6040 ||
tx.Err.CustomCode == 6023 || tx.Err.CustomCode == 6021 || tx.Err.CustomCode == 6003 || tx.Err.CustomCode == 6002) {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode)
}
}
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]]
var args PumpTradeArgs
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var event string
var (
solAmount, tokenAmount uint64
)
if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
event = "buy_failed"
solAmount = args.Amount1
tokenAmount = args.Amount2
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) {
event = "buy_failed"
solAmount = args.Amount2
tokenAmount = args.Amount1
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) {
event = "sell_failed"
solAmount = 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]]
}
if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint)
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
if !userBaseAmount.IsZero() {
user = result.accountList[0]
userIndex = 0
ataUserIdx = ataIndex
}
}
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
userQuote, _ := GetSolAfterTx(result, userIndex)
bcIdx := instruction.Accounts[3]
bcAtaIndex := instruction.Accounts[4]
solReserves, _ := GetSolAfterTx(result, bcIdx)
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
swaps := []Swap{
{
Program: SolProgramPump,
Event: event,
Pool: result.accountList[instruction.Accounts[3]],
BaseMint: mint,
QuoteMint: solana.PublicKey{},
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{},
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: user,
BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(solAmount),
BaseReserve: tokenReserves,
QuoteReserve: decimal.NewFromUint64(solReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
EntryContract: entryContract,
},
}
return swaps, offset, nil
}
func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
result := tx.rawTx result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
@@ -217,11 +344,13 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v,offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v,offset, %d, %d", err, offset[0], offset[1])
} }
if instruction.StackHeight != nil && *instruction.StackHeight > 2 { if !entryContract.Equals(axiomOuterContract) {
for _, innerInstr := range innerInstructions.Instructions { if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 { for _, innerInstr := range innerInstructions.Instructions {
entryContract = result.accountList[innerInstr.ProgramIDIndex] if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
break entryContract = result.accountList[innerInstr.ProgramIDIndex]
break
}
} }
} }
} }
@@ -312,6 +441,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
solAmount = solAmount - fee solAmount = solAmount - fee
} }
} }
isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0
swaps := []Swap{ swaps := []Swap{
{ {
Program: SolProgramPump, Program: SolProgramPump,
@@ -333,6 +463,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote), UserQuoteBalance: decimal.NewFromUint64(userQuote),
EntryContract: entryContract, EntryContract: entryContract,
Cashback: isCashbackCoin,
}, },
} }
if completed { if completed {
@@ -348,6 +479,8 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
BaseMintDecimals: 6, BaseMintDecimals: 6,
QuoteMintDecimals: 9, QuoteMintDecimals: 9,
User: user, User: user,
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote), UserQuoteBalance: decimal.NewFromUint64(userQuote),

View File

@@ -41,6 +41,8 @@ type ammBuyEvent struct {
LastUpdateTimestamp int64 LastUpdateTimestamp int64
MinBaseAmountOut uint64 MinBaseAmountOut uint64
IxName string IxName string
CashbackFeeBasisPoints uint64
Cashback uint64
} }
type ammCreatePoolEvent struct { type ammCreatePoolEvent struct {
@@ -113,6 +115,8 @@ type ammSellEvent struct {
CoinCreator solana.PublicKey CoinCreator solana.PublicKey
CoinCreatorFeeBasisPoints uint64 CoinCreatorFeeBasisPoints uint64
CoinCreatorFee uint64 CoinCreatorFee uint64
CashbackFeeBasisPoints uint64
Cashback uint64
} }
type ammWithdrawEvent struct { type ammWithdrawEvent struct {
@@ -148,14 +152,29 @@ func pumpAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
discriminator := *(*[8]byte)(decode[:8]) discriminator := *(*[8]byte)(decode[:8])
switch discriminator { switch discriminator {
case pumpAmmCreateDiscriminator: case pumpAmmCreateDiscriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return ammCreatePoolParser(tx, instruction, innerInstructions, offset) return ammCreatePoolParser(tx, instruction, innerInstructions, offset)
case pumpAmmBuyDiscriminator, pumpAmmBuyV2Discriminator: case pumpAmmBuyDiscriminator, pumpAmmBuyV2Discriminator:
if tx.Err != nil {
return failedTxAmmBuyParser(tx, instruction, innerInstructions, offset)
}
return ammBuyParser(tx, instruction, innerInstructions, offset) return ammBuyParser(tx, instruction, innerInstructions, offset)
case pumpAmmSellDiscriminator: case pumpAmmSellDiscriminator:
if tx.Err != nil {
return failedTxAmmSellParser(tx, instruction, innerInstructions, offset)
}
return ammSellParser(tx, instruction, innerInstructions, offset) return ammSellParser(tx, instruction, innerInstructions, offset)
case pumpAmmDepositDiscriminator: case pumpAmmDepositDiscriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return depositParse(tx, instruction, innerInstructions, offset) return depositParse(tx, instruction, innerInstructions, offset)
case pumpAmmWithdrawDiscriminator: case pumpAmmWithdrawDiscriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
return withdrawParse(tx, instruction, innerInstructions, offset) return withdrawParse(tx, instruction, innerInstructions, offset)
default: default:
return nil, increaseOffset(offset), InstructionIgnoredError return nil, increaseOffset(offset), InstructionIgnoredError
@@ -236,6 +255,254 @@ func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions Inne
} }
type PumpSwapArgs struct {
Discriminator [8]byte
Amount1 uint64
Amount2 uint64
}
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Variant != InstructionError {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded && tx.Err.Enum != ProgramFailedToComplete {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum == Custom {
if !(tx.Err.CustomCode == 1 || tx.Err.CustomCode == 6004 ||
tx.Err.CustomCode == 6040 ||
tx.Err.CustomCode == 6039 ||
tx.Err.CustomCode == 6016 ||
tx.Err.CustomCode == 6014) {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode)
}
}
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var args PumpSwapArgs
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm buy failed decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var event string
var (
quoteAmount, tokenAmount uint64
)
if bytes.Equal(args.Discriminator[:], pumpAmmBuyV2Discriminator[:]) {
event = "buy_failed"
quoteAmount = args.Amount1
tokenAmount = args.Amount2
} else if bytes.Equal(args.Discriminator[:], pumpAmmBuyDiscriminator[:]) {
event = "buy_failed"
quoteAmount = args.Amount2
tokenAmount = args.Amount1
} else {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump amm trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
}
baseMint := result.accountList[instruction.Accounts[3]]
quoteMint := result.accountList[instruction.Accounts[4]]
baseTokenProgram := result.accountList[instruction.Accounts[11]]
quoteTokenProgram := result.accountList[instruction.Accounts[12]]
poolBaseAccountIdx := instruction.Accounts[7]
poolQuoteAccountIdx := instruction.Accounts[8]
var (
baseMintDecimals uint8
quoteMintDecimals uint8
)
for _, meta := range result.Meta.PostTokenBalances {
if meta.AccountIndex == poolBaseAccountIdx {
baseMintDecimals = uint8(meta.UITokenAmount.Decimals)
} else if meta.AccountIndex == poolQuoteAccountIdx {
quoteMintDecimals = uint8(meta.UITokenAmount.Decimals)
}
}
if _, exists := tx.Token[baseMint]; !exists && !baseMint.Equals(wSolMint) {
tx.Token[baseMint] = TokenMeta{
Mint: baseMint,
Decimals: baseMintDecimals,
TokenProgram: baseTokenProgram,
}
}
if _, exists := tx.Token[quoteMint]; !exists && !quoteMint.Equals(wSolMint) {
tx.Token[quoteMint] = TokenMeta{
Mint: quoteMint,
Decimals: quoteMintDecimals,
TokenProgram: quoteTokenProgram,
}
}
var eventUser = tx.rawTx.accountList[instruction.Accounts[1]]
baseMintAtaUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[1]
if !eventUser.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, baseMint)
// && userBaseAmount.BigInt().Uint64() == event.BaseAmountOut
if !userBaseAmount.IsZero() {
eventUser = result.accountList[0]
userIndex = 0
baseMintAtaUserIdx = ataIndex
}
}
userBase := getAccountBalanceAfterTx(result, baseMintAtaUserIdx)
userQuote := GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
if quoteMint.Equals(wSolMint) {
userBalance, _ := GetSolAfterTx(result, userIndex)
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
return []Swap{
{
Program: SolProgramPumpAMM,
Event: event,
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: eventUser,
BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Variant != InstructionError {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded && tx.Err.Enum != ProgramFailedToComplete {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1])
}
if tx.Err.Enum == Custom {
if !(tx.Err.CustomCode == 1 || tx.Err.CustomCode == 6004 ||
tx.Err.CustomCode == 6040 ||
tx.Err.CustomCode == 6039 ||
tx.Err.CustomCode == 6016 ||
tx.Err.CustomCode == 6014) {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode)
}
}
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var args PumpSwapArgs
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm buy failed decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
var event string
var (
quoteAmount, tokenAmount uint64
)
if bytes.Equal(args.Discriminator[:], pumpAmmSellDiscriminator[:]) {
event = "sell_failed"
tokenAmount = args.Amount1
quoteAmount = args.Amount2
} else {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump amm trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
}
baseMint := result.accountList[instruction.Accounts[3]]
quoteMint := result.accountList[instruction.Accounts[4]]
baseTokenProgram := result.accountList[instruction.Accounts[11]]
quoteTokenProgram := result.accountList[instruction.Accounts[12]]
poolBaseAccountIdx := instruction.Accounts[7]
poolQuoteAccountIdx := instruction.Accounts[8]
var (
baseMintDecimals uint8
quoteMintDecimals uint8
)
for _, meta := range result.Meta.PostTokenBalances {
if meta.AccountIndex == poolBaseAccountIdx {
baseMintDecimals = uint8(meta.UITokenAmount.Decimals)
} else if meta.AccountIndex == poolQuoteAccountIdx {
quoteMintDecimals = uint8(meta.UITokenAmount.Decimals)
}
}
if _, exists := tx.Token[baseMint]; !exists && !baseMint.Equals(wSolMint) {
tx.Token[baseMint] = TokenMeta{
Mint: baseMint,
Decimals: baseMintDecimals,
TokenProgram: baseTokenProgram,
}
}
if _, exists := tx.Token[quoteMint]; !exists && !quoteMint.Equals(wSolMint) {
tx.Token[quoteMint] = TokenMeta{
Mint: quoteMint,
Decimals: quoteMintDecimals,
TokenProgram: quoteTokenProgram,
}
}
var eventUser = tx.rawTx.accountList[instruction.Accounts[1]]
baseMintAtaUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[1]
if !eventUser.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, baseMint)
// && userBaseAmount.BigInt().Uint64() == event.BaseAmountIn
if !userBaseAmount.IsZero() {
eventUser = result.accountList[0]
userIndex = 0
baseMintAtaUserIdx = ataIndex
}
}
userBase := getAccountBalanceAfterTx(result, baseMintAtaUserIdx)
userQuote := GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
if quoteMint.Equals(wSolMint) {
userBalance, _ := GetSolAfterTx(result, userIndex)
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
return []Swap{
{
Program: SolProgramPumpAMM,
Event: event,
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
BaseMint: baseMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: baseMintDecimals,
QuoteMintDecimals: quoteMintDecimals,
User: eventUser,
BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
result := tx.rawTx result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
@@ -245,11 +512,13 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen) return nil, increaseOffset(offset), fmt.Errorf("pumpamm create get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
} }
if instruction.StackHeight != nil && *instruction.StackHeight > 2 { if !entryContract.Equals(axiomOuterContract) {
for _, innerInstr := range innerInstructions.Instructions { if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 { for _, innerInstr := range innerInstructions.Instructions {
entryContract = result.accountList[innerInstr.ProgramIDIndex] if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
break entryContract = result.accountList[innerInstr.ProgramIDIndex]
break
}
} }
} }
} }
@@ -329,6 +598,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
userBalance, _ := GetSolAfterTx(result, userIndex) userBalance, _ := GetSolAfterTx(result, userIndex)
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance)) userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
} }
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
return []Swap{ return []Swap{
{ {
Program: SolProgramPumpAMM, Program: SolProgramPumpAMM,
@@ -347,6 +617,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut), BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn), QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Cashback: isCashbackCoin,
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: userQuote, UserQuoteBalance: userQuote,
EntryContract: entryContract, EntryContract: entryContract,
@@ -364,11 +635,13 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
return nil, increaseOffset(offset), fmt.Errorf("pumpamm sell get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen) return nil, increaseOffset(offset), fmt.Errorf("pumpamm sell get inner instructions error: %v, offset: %d, %d", err, offset[0], prefixLen)
} }
if instruction.StackHeight != nil && *instruction.StackHeight > 2 { if !entryContract.Equals(axiomOuterContract) {
for _, innerInstr := range innerInstructions.Instructions { if instruction.StackHeight != nil && *instruction.StackHeight > 2 {
if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 { for _, innerInstr := range innerInstructions.Instructions {
entryContract = result.accountList[innerInstr.ProgramIDIndex] if innerInstr.StackHeight != nil && *innerInstr.StackHeight == *instruction.StackHeight-1 {
break entryContract = result.accountList[innerInstr.ProgramIDIndex]
break
}
} }
} }
} }
@@ -448,6 +721,7 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
userBalance, _ := GetSolAfterTx(result, userIndex) userBalance, _ := GetSolAfterTx(result, userIndex)
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance)) userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
} }
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
return []Swap{ return []Swap{
{ {
Program: SolProgramPumpAMM, Program: SolProgramPumpAMM,
@@ -466,6 +740,7 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn), BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut), QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Cashback: isCashbackCoin,
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: userQuote, UserQuoteBalance: userQuote,
EntryContract: entryContract, EntryContract: entryContract,

107
rawtx.go
View File

@@ -73,6 +73,10 @@ func (tx *RawTx) GetAccountLust() []solana.PublicKey {
return tx.getAccountList() return tx.getAccountList()
} }
func (tx *RawTx) GetAccountList() []solana.PublicKey {
return tx.getAccountList()
}
func (tx *RawTx) TxHash() string { func (tx *RawTx) TxHash() string {
if len(tx.Transaction.Signatures) > 0 { if len(tx.Transaction.Signatures) > 0 {
return tx.Transaction.Signatures[0].String() return tx.Transaction.Signatures[0].String()
@@ -146,16 +150,17 @@ func (tb *TokenBalance) ParseAccount() {
} }
type Meta struct { type Meta struct {
Err interface{} `json:"err"` Err *TransactionParsedError `json:"err"`
Fee uint64 `json:"fee"` Fee uint64 `json:"fee"`
InnerInstructions []InnerInstructions `json:"innerInstructions"` InnerInstructions []InnerInstructions `json:"innerInstructions"`
LoadedAddresses LoadedAddresses `json:"loadedAddresses"` LoadedAddresses LoadedAddresses `json:"loadedAddresses"`
LogMessages []string `json:"logMessages"` LogMessages []string `json:"logMessages"`
PostBalances []uint64 `json:"postBalances"` PostBalances []uint64 `json:"postBalances"`
PostTokenBalances []TokenBalance `json:"postTokenBalances"` PostTokenBalances []TokenBalance `json:"postTokenBalances"`
PreBalances []uint64 `json:"preBalances"` PreBalances []uint64 `json:"preBalances"`
PreTokenBalances []TokenBalance `json:"preTokenBalances"` PreTokenBalances []TokenBalance `json:"preTokenBalances"`
Rewards []interface{} `json:"rewards"` Rewards []interface{} `json:"rewards"`
ComputeUnitsConsumed uint64 `json:"computeUnitsConsumed"`
} }
type Header struct { type Header struct {
NumReadonlySignedAccounts int `json:"numReadonlySignedAccounts"` NumReadonlySignedAccounts int `json:"numReadonlySignedAccounts"`
@@ -293,6 +298,16 @@ func InstructionsFromRpc(instructions []solana.CompiledInstruction) []Instructio
return instrs return instrs
} }
type RpcTransactionErr []interface{}
func marshalRpcTransactionErr(err any) string {
e, _ := json.Marshal(err)
if len(e) == 0 {
return "UnKnown"
}
return string(e)
}
func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) { func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) {
created := int64(0) created := int64(0)
if blockTime != nil { if blockTime != nil {
@@ -320,12 +335,68 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s
yTx, _ := tx.GetTransaction() yTx, _ := tx.GetTransaction()
if meta.Err != nil { if meta.Err != nil {
e, _ := json.Marshal(meta.Err) if iErr, ok := meta.Err.(map[string]any); ok {
sTx.Meta.Err = string(e) instructionError := iErr["InstructionError"]
if instructionError == nil {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
} else {
if oErr, ok := instructionError.([]any); ok {
if len(oErr) <= 1 {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
} else if instrIdx, ok := oErr[0].(float64); ok {
sTx.Meta.Err = &TransactionParsedError{
Index: uint8(instrIdx),
Variant: InstructionError,
}
errDetail, ok := oErr[1].(string)
if ok {
if errDetail == "ComputationalBudgetExceeded" || errDetail == "ProgramFailedToComplete" {
sTx.Meta.Err.Enum = ComputationalBudgetExceeded
} else {
sTx.Meta.Err.UnKnown = errDetail
}
} else {
errDetail2, ok := oErr[1].(map[string]any)
if ok && len(errDetail2) > 0 && errDetail2["Custom"] != nil {
custom, ok := errDetail2["Custom"].(float64)
if ok {
sTx.Meta.Err.Enum = Custom
sTx.Meta.Err.CustomCode = uint32(custom)
} else {
sTx.Meta.Err.UnKnown = marshalRpcTransactionErr(meta.Err)
}
} else {
sTx.Meta.Err.UnKnown = marshalRpcTransactionErr(meta.Err)
}
}
} else {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
}
} else {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
}
}
} else {
sTx.Meta.Err = &TransactionParsedError{
UnKnown: marshalRpcTransactionErr(meta.Err),
}
}
} }
sTx.Meta.Fee = meta.Fee sTx.Meta.Fee = meta.Fee
//sTx.Meta.InnerInstructions = meta.InnerInstructions //sTx.Meta.InnerInstructions = meta.InnerInstructions
if meta.ComputeUnitsConsumed != nil {
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
}
for _, innerInstr := range meta.InnerInstructions { for _, innerInstr := range meta.InnerInstructions {
var instrs []Instruction var instrs []Instruction
for _, instr := range innerInstr.Instructions { for _, instr := range innerInstr.Instructions {
@@ -513,6 +584,7 @@ func intSliceFromUint16Slice(in []uint16) []int {
} }
return out return out
} }
func getAtaIdxByOwner(result *RawTx, owner solana.PublicKey, mint solana.PublicKey) (int, error) { func getAtaIdxByOwner(result *RawTx, owner solana.PublicKey, mint solana.PublicKey) (int, error) {
var preBalance *TokenBalance var preBalance *TokenBalance
for _, pre := range result.Meta.PreTokenBalances { for _, pre := range result.Meta.PreTokenBalances {
@@ -795,17 +867,12 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
if meta.Err != nil && len(meta.Err.GetErr()) > 0 { if meta.Err != nil && len(meta.Err.GetErr()) > 0 {
// If the transaction has an error, we set the error in the Meta // If the transaction has an error, we set the error in the Meta
transError, err := DecodeTransactionError(meta.Err.GetErr()) sTx.Meta.Err = ParseTransactionErrorFromGeyser(meta.Err.GetErr())
if err != nil {
sTx.Meta.Err = err
} else {
sTx.Meta.Err = transError
}
// sTx.Meta.Err = meta.Err.GetErr() // sTx.Meta.Err = meta.Err.GetErr()
} }
sTx.Meta.Fee = meta.Fee sTx.Meta.Fee = meta.Fee
//sTx.Meta.InnerInstructions = meta.InnerInstructions //sTx.Meta.InnerInstructions = meta.InnerInstructions
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
for _, innerInstr := range meta.InnerInstructions { for _, innerInstr := range meta.InnerInstructions {
var instrs []Instruction var instrs []Instruction
for _, instr := range innerInstr.Instructions { for _, instr := range innerInstr.Instructions {

View File

@@ -54,12 +54,12 @@ func raydiumCPmmCreatePoolParser(tx *Tx, instruction Instruction, innerInstructi
vault1 = instruction.Accounts[12] vault1 = instruction.Accounts[12]
} }
baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[vault0]) baseTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault0)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get token0 vault balance after tx: %v", err)
} }
quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, instruction.Accounts[vault1]) quoteTokenBalance, err := getTokenBalanceAfterTx(tx.rawTx, vault1)
if err != nil { if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err) return nil, increaseOffset(offset), fmt.Errorf("failed to get token1 vault balance after tx: %v", err)
} }

View File

@@ -143,7 +143,7 @@ type RaydiumLaunchLabCreateEvent struct {
} }
func raydiumLaunchLabInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func raydiumLaunchLabInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 16 { if len(instruction.Accounts) < 15 {
return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction") return nil, increaseOffset(offset), fmt.Errorf("not enough accounts for initialize instruction")
} }
user := tx.rawTx.accountList[instruction.Accounts[0]] user := tx.rawTx.accountList[instruction.Accounts[0]]

View File

@@ -29,6 +29,8 @@ func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerIns
return raydiumv4WithdrawPNLParser(tx, instruction, innerInstructions, offset) return raydiumv4WithdrawPNLParser(tx, instruction, innerInstructions, offset)
case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseOutDiscriminator: case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseOutDiscriminator:
return raydiumv4SwapParser(tx, instruction, innerInstructions, offset) return raydiumv4SwapParser(tx, instruction, innerInstructions, offset)
case raydiumV4SwapBaseInV2Discriminator, raydiumV4SwapBaseOutV2Discriminator:
return raydiumv4SwapV2Parser(tx, instruction, innerInstructions, offset)
default: default:
return nil, increaseOffset(offset), InstructionIgnoredError return nil, increaseOffset(offset), InstructionIgnoredError
@@ -397,3 +399,99 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
}, },
}, offset, nil }, offset, nil
} }
func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
if accountsLen != 8 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swapv2 instruction, offset %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
user := tx.rawTx.accountList[instruction.Accounts[7]]
userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]]
userDestinationTokenAccount := tx.rawTx.accountList[instruction.Accounts[6]]
baseVaultIdx := instruction.Accounts[3]
quoteVaultIdx := instruction.Accounts[4]
baseTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, baseVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get base vault balance after tx: %v", err)
}
quoteTokenbalance, err := getTokenBalanceAfterTx(tx.rawTx, quoteVaultIdx)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to get quote vault balance after tx: %v", err)
}
inners, err := getInnerInstructions(innerInstructions, offset[1])
if err != nil {
return nil, increaseOffset(offset), err
}
var nextIndex int
var srcFound, destFound bool
var baseAmount, quoteAmount decimal.Decimal
var event string
for i, inner := range inners {
from, to, amount, err := parseTokenTransfer(tx.rawTx, inner)
if err != nil {
continue
}
if from.Equals(userSourceTokenAccount) && !srcFound {
if to.Equals(tx.rawTx.accountList[baseVaultIdx]) {
event = "sell"
baseAmount = decimal.NewFromUint64(amount)
srcFound = true
} else if to.Equals(tx.rawTx.accountList[quoteVaultIdx]) {
event = "buy"
quoteAmount = decimal.NewFromUint64(amount)
srcFound = true
}
} else if to.Equals(userDestinationTokenAccount) && !destFound {
if from.Equals(tx.rawTx.accountList[quoteVaultIdx]) {
event = "sell"
quoteAmount = decimal.NewFromUint64(amount)
destFound = true
} else if from.Equals(tx.rawTx.accountList[baseVaultIdx]) {
event = "buy"
baseAmount = decimal.NewFromUint64(amount)
destFound = true
}
}
if srcFound && destFound {
nextIndex = i + 1
break
}
}
if !srcFound || !destFound {
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 failed to find token transfer inner instruction for swapv2, offset %d, %d", offset[0], offset[1])
}
offset[1] += uint(nextIndex + 1)
userBase := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[5])
userQuote := getAccountBalanceAfterTx(tx.rawTx, instruction.Accounts[6])
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{
{
Program: SolProgramRaydiumV4,
Event: event,
Pool: ammAccount,
BaseMint: baseTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
User: user,
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: false,
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}

View File

@@ -34,12 +34,14 @@ func TransferParser(result *RawTx, instruction Instruction, offset [2]uint, tx *
from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]] from := result.accountList[result.Transaction.Message.Instructions[offset[0]].Accounts[0]]
to := result.accountList[instruction.Accounts[1]] to := result.accountList[instruction.Accounts[1]]
if offset[1] == 0 { if result.Meta.Err == nil {
tx.SolTransfer = append(tx.SolTransfer, SolTransfer{ if offset[1] == 0 {
From: from, tx.SolTransfer = append(tx.SolTransfer, SolTransfer{
To: to, From: from,
Amount: decimal.NewFromInt(int64(lamports)), // solana decimals To: to,
}) Amount: decimal.NewFromInt(int64(lamports)), // solana decimals
})
}
} }
// load platform by to address // load platform by to address
platform, ok := platformFeeAddresses[to] platform, ok := platformFeeAddresses[to]

28
tx.go
View File

@@ -12,6 +12,9 @@ type Swap struct {
TxIndex int TxIndex int
InstrIdx uint8
InnerIdx uint8
Pool solana.PublicKey Pool solana.PublicKey
BaseMint solana.PublicKey BaseMint solana.PublicKey
QuoteMint solana.PublicKey QuoteMint solana.PublicKey
@@ -31,6 +34,7 @@ type Swap struct {
BaseReserve decimal.Decimal BaseReserve decimal.Decimal
QuoteReserve decimal.Decimal QuoteReserve decimal.Decimal
Mayhem bool Mayhem bool
Cashback bool
UserBaseBalance decimal.Decimal UserBaseBalance decimal.Decimal
UserQuoteBalance decimal.Decimal UserQuoteBalance decimal.Decimal
@@ -47,6 +51,8 @@ type Swap struct {
StartBinId int32 StartBinId int32
EndBinId int32 EndBinId int32
BinChanges []DlmmBinLiquidityChange BinChanges []DlmmBinLiquidityChange
ConsumeUnit uint64
} }
type DlmmBinLiquidityChange struct { type DlmmBinLiquidityChange struct {
@@ -71,18 +77,23 @@ type SolTransfer struct {
To solana.PublicKey To solana.PublicKey
Amount decimal.Decimal Amount decimal.Decimal
} }
type ChainLink struct {
Timestamp int64
Price decimal.Decimal
}
type Tx struct { type Tx struct {
rawTx *RawTx rawTx *RawTx
Vote bool Vote bool
Signer solana.PublicKey Signer solana.PublicKey
Err interface{} `json:"err,omitempty"` Err *TransactionParsedError `json:"err,omitempty"`
Swaps []Swap `json:"swaps,omitempty"` Swaps []Swap `json:"swaps,omitempty"`
SolTransfer []SolTransfer `json:"sol_transfer,omitempty"` SolTransfer []SolTransfer `json:"sol_transfer,omitempty"`
Block uint64 `json:"block"` Block uint64 `json:"block"`
BlockIndex uint64 `json:"index"` ChainLink ChainLink `json:"chain_link"`
TxHash *[64]byte `json:"-"` BlockIndex uint64 `json:"index"`
BlockAt int64 `json:"block_at"` TxHash *[64]byte `json:"-"`
BlockAt int64 `json:"block_at"`
CuFee decimal.Decimal `json:"cu_fee"` CuFee decimal.Decimal `json:"cu_fee"`
@@ -98,6 +109,9 @@ type Tx struct {
// update tokenInfo // update tokenInfo
Token map[solana.PublicKey]TokenMeta `gorm:"-"` Token map[solana.PublicKey]TokenMeta `gorm:"-"`
ComputeUnitsConsumed uint64 `json:"compute_units_consumed"`
CuLimit uint32 `json:"cu_limit"`
// todo pool info ?? // todo pool info ??
} }