Compare commits

...

5 Commits

Author SHA1 Message Date
thloyi
8b608889cb fix pump wrapper buy and sell 2026-05-13 16:54:23 +08:00
cachalots
8d4aad1932 tip 2026-05-13 16:52:55 +08:00
thloyi
5cd3a97d81 pump usdc support 2026-05-08 11:21:30 +08:00
thloyi
0a4aabc67f raw tx binary 2026-04-30 17:56:35 +08:00
thloyi
d46e8b651c raw tx binary 2026-04-30 17:02:04 +08:00
13 changed files with 1776 additions and 185 deletions

View File

@@ -138,15 +138,20 @@ func addInnerInstructions(stats *sizeStats, values []pump_parser.InnerInstructio
func addInstructions(stats *sizeStats, prefix string, values []pump_parser.Instruction) {
stats.add(prefix+".count", 4)
for _, value := range values {
stats.add(prefix+".program_id_index", 2)
stats.add(prefix+".program_id_index", 1)
stats.add(prefix+".accounts.count", 4)
stats.add(prefix+".accounts.refs", uint64(len(value.Accounts))*2)
stats.add(prefix+".accounts.refs", uint64(len(value.Accounts)))
stats.add(prefix+".data.length", 4)
stats.add(prefix+".data.bytes", uint64(len(value.Data)))
stats.add(prefix+".stack_height.present", 1)
if value.StackHeight != nil {
stats.add(prefix+".stack_height.value", 4)
}
stats.add(prefix+".log_events.count", 4)
for _, event := range value.LogEvents {
stats.add(prefix+".log_events.length", 4)
stats.add(prefix+".log_events.bytes", uint64(len(event)))
}
}
}
@@ -194,13 +199,13 @@ func addLamportBalances(stats *sizeStats, preBalances []uint64, postBalances []u
func addTokenBalances(stats *sizeStats, prefix string, values []pump_parser.RawTxTokenBalanceBinary) {
stats.add(prefix+".count", 4)
for _, value := range values {
stats.add(prefix+".account_index", 2)
stats.add(prefix+".mint_ref", 2)
stats.add(prefix+".account_index", 1)
stats.add(prefix+".mint_ref", 1)
stats.add(prefix+".owner.present", 1)
if value.HasOwnerAccount {
stats.add(prefix+".owner_ref", 2)
stats.add(prefix+".owner_ref", 1)
}
stats.add(prefix+".program_id_ref", 2)
stats.add(prefix+".program_id_ref", 1)
stats.add(prefix+".decimals", 1)
stats.add(prefix+".pre_amount.present", 1)
if value.HasPreAmount {

View File

@@ -0,0 +1,302 @@
package main
import (
"context"
"flag"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"sort"
"syscall"
"time"
"github.com/gagliardetto/solana-go"
pump_parser "github.com/thloyi/pump-parser"
pb "go.onsig.ai/onsig/yellowstone-proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
)
type collector struct {
endpoint string
xToken string
plaintext bool
blocks map[uint64][]pump_parser.RawTx
seen map[string]struct{}
totalUpdates uint64
txUpdates uint64
savedNonVote uint64
duplicates uint64
voteFiltered uint64
convertErrs uint64
reconnects uint64
firstSlot uint64
lastSlot uint64
}
func main() {
var (
endpoint = flag.String("endpoint", "ams.rpc.orbitflare.com:10000", "Yellowstone gRPC endpoint")
xToken = flag.String("x-token", os.Getenv("YELLOWSTONE_X_TOKEN"), "Yellowstone x-token; defaults to YELLOWSTONE_X_TOKEN")
duration = flag.Duration("duration", 5*time.Minute, "collection duration")
output = flag.String("output", "", "output .prbs file path")
plaintext = flag.Bool("plaintext", true, "use plaintext gRPC instead of TLS")
)
flag.Parse()
if *xToken == "" {
exitf("missing -x-token or YELLOWSTONE_X_TOKEN")
}
if *duration <= 0 {
exitf("-duration must be positive")
}
if *output == "" {
*output = filepath.Join("testdata", "rawtx-binary", fmt.Sprintf("rawtx-yellowstone-%s.prbs", time.Now().Format("20060102-150405")))
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
ctx, cancel := context.WithTimeout(ctx, *duration)
defer cancel()
c := &collector{
endpoint: *endpoint,
xToken: *xToken,
plaintext: *plaintext,
blocks: make(map[uint64][]pump_parser.RawTx),
seen: make(map[string]struct{}),
}
started := time.Now()
done := make(chan error, 1)
go func() {
done <- c.run(ctx)
}()
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
loop:
for {
select {
case err := <-done:
if err != nil {
exitf("%v", err)
}
break loop
case <-ctx.Done():
if err := <-done; err != nil {
exitf("%v", err)
}
break loop
case <-ticker.C:
c.printProgress(started)
}
}
encoded, decodedCount, err := encodeAndVerify(c.blocks)
if err != nil {
exitf("raw tx binary encode/decode: %v", err)
}
if decodedCount != int(c.savedNonVote) {
exitf("decoded tx count mismatch: got=%d want=%d", decodedCount, c.savedNonVote)
}
if err := os.MkdirAll(filepath.Dir(*output), 0o755); err != nil {
exitf("mkdir output dir: %v", err)
}
if err := os.WriteFile(*output, encoded, 0o644); err != nil {
exitf("write output: %v", err)
}
fmt.Printf("output=%s\n", *output)
fmt.Printf("duration=%s elapsed=%s\n", *duration, time.Since(started).Truncate(time.Second))
fmt.Printf("updates=%d tx_updates=%d converted_nonvote=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
c.totalUpdates, c.txUpdates, c.savedNonVote, c.duplicates, c.voteFiltered, c.convertErrs, c.reconnects)
fmt.Printf("slots=%d first_slot=%d last_slot=%d decoded=%d\n", len(c.blocks), c.firstSlot, c.lastSlot, decodedCount)
fmt.Printf("bytes=%d bytes_per_tx=%.2f\n", len(encoded), float64(len(encoded))/float64(max(int(c.savedNonVote), 1)))
}
func (c *collector) run(ctx context.Context) error {
for ctx.Err() == nil {
if err := c.recvOnce(ctx); err != nil && ctx.Err() == nil {
c.reconnects++
fmt.Fprintf(os.Stderr, "stream_err reconnect=%d err=%v\n", c.reconnects, err)
time.Sleep(time.Second)
continue
}
if ctx.Err() == nil {
c.reconnects++
time.Sleep(time.Second)
}
}
return nil
}
func (c *collector) recvOnce(ctx context.Context) error {
conn, err := grpc.NewClient(
c.endpoint,
c.transportOption(),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,
Timeout: time.Second,
PermitWithoutStream: true,
}),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(64*1024*1024)),
)
if err != nil {
return err
}
defer conn.Close()
ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{"x-token": c.xToken}))
stream, err := pb.NewGeyserClient(conn).Subscribe(ctx)
if err != nil {
return err
}
vote := false
subscription := &pb.SubscribeRequest{
Transactions: map[string]*pb.SubscribeRequestFilterTransactions{
"nonvote": {Vote: &vote},
},
}
if err := stream.Send(subscription); err != nil {
return err
}
for {
resp, err := stream.Recv()
if err != nil {
if err == io.EOF || ctx.Err() != nil {
return nil
}
return err
}
c.totalUpdates++
txn := resp.GetTransaction()
if txn == nil {
continue
}
c.txUpdates++
created := time.Now().Unix()
if resp.GetCreatedAt() != nil {
created = resp.GetCreatedAt().Seconds
}
rawTx, err := pump_parser.ConvertYellowstoneGrpcTransactionToSolanaTransaction(txn, created)
if err != nil {
c.convertErrs++
continue
}
txHash := rawTx.TxHash()
if txHash != "" {
if _, exists := c.seen[txHash]; exists {
c.duplicates++
continue
}
c.seen[txHash] = struct{}{}
}
if c.firstSlot == 0 || rawTx.Slot < c.firstSlot {
c.firstSlot = rawTx.Slot
}
if rawTx.Slot > c.lastSlot {
c.lastSlot = rawTx.Slot
}
if isVoteTx(rawTx) {
c.voteFiltered++
continue
}
c.blocks[rawTx.Slot] = append(c.blocks[rawTx.Slot], *rawTx)
c.savedNonVote++
}
}
func (c *collector) transportOption() grpc.DialOption {
if c.plaintext {
return grpc.WithTransportCredentials(insecure.NewCredentials())
}
return grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))
}
func (c *collector) printProgress(started time.Time) {
fmt.Fprintf(os.Stderr,
"progress elapsed=%s updates=%d tx_updates=%d saved_nonvote=%d slots=%d duplicate=%d vote_filtered=%d convert_err=%d reconnects=%d\n",
time.Since(started).Truncate(time.Second),
c.totalUpdates,
c.txUpdates,
c.savedNonVote,
len(c.blocks),
c.duplicates,
c.voteFiltered,
c.convertErrs,
c.reconnects,
)
}
func encodeAndVerify(blocks map[uint64][]pump_parser.RawTx) ([]byte, int, error) {
slots := make([]uint64, 0, len(blocks))
for slot := range blocks {
slots = append(slots, slot)
}
sort.Slice(slots, func(i, j int) bool { return slots[i] < slots[j] })
ordered := make([][]pump_parser.RawTx, 0, len(slots))
for _, slot := range slots {
txs := blocks[slot]
blockTime := int64(0)
if len(txs) > 0 {
blockTime = txs[0].BlockTime
}
for i := range txs {
txs[i].BlockTime = blockTime
}
ordered = append(ordered, txs)
}
encoded, err := pump_parser.EncodeRawTxBlocksBinary(ordered)
if err != nil {
return nil, 0, err
}
decoded, err := pump_parser.DecodeRawTxBlocksBinary(encoded)
if err != nil {
return nil, 0, err
}
decodedCount := 0
for _, block := range decoded {
decodedCount += len(block)
}
return encoded, decodedCount, nil
}
func isVoteTx(tx *pump_parser.RawTx) bool {
if tx == nil {
return false
}
accountList := tx.GetAccountList()
for _, instr := range tx.Transaction.Message.Instructions {
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) && accountList[instr.ProgramIDIndex] == solana.VoteProgramID {
return true
}
}
return false
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func exitf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}

View File

@@ -14,7 +14,7 @@ func main() {
const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
txHash := os.Getenv("TX_HASH")
if txHash == "" {
txHash = "2AhpL5KhVmG3D38CwMzrHuRyTucEQ43GzBXL2mo5WiugdZMVmK1dtX8brGe3sxvvFDY6iSSviJTvqCtr4UL3Pc7J"
txHash = "24wP3rk2ZfSVDB5YGyEQbhuy1jRXKZYkzXDRrDwwoKgzD6G4Kyyh4vmnir9ye98uLVKA5bBMj5Fq4cwgbDxp2Gie"
}
if txHash == "" {

View File

@@ -186,6 +186,12 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
@@ -200,6 +206,8 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("ASY4mvCtrACKFK8Jiuvqcu8fad9gGTzvfm5zp4megRes"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMsWjc4SUVVuLyCu1ig2RdomQnHKgMzBMfmSo3DK"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMZfUJmAvppzc5cr7eYG8Cenig4FbQGBytr4DGCh"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("b1ooMDLjzz4QqecNsJ8bBXzJTzfAPDCP3CxijTS2K93"): MevAgentAstralane,
@@ -381,6 +389,16 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("7HkiWXe5deJvzn4D6kgMUFCADwX9Z4DMrdjNSSxN6bPp"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanrUknLZXzT9JPj968A7RfgCjp77Lx1W1xKRAtfshb"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanHbk2UsiT3jKsKjD7UuEqS5Vgpmcd4pG9HycAAV8g"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanNazKCXNRoKnPS9BBbFTELTpNwUDJxeKEb1JtZJer"): MevAgentZan,
solana.MustPublicKeyFromBase58("zan3gbFhXCjGLHhRe2vaXRDta5fCrYiYr3Dq4RLvpfU"): MevAgentZan,
solana.MustPublicKeyFromBase58("zan6WoE3DX5aK7FMQT1vSGsGrgZG1ngns3oCsFMnBHU"): MevAgentZan,
solana.MustPublicKeyFromBase58("zan8Nb9fB4zMDsuTRP9R65QZbc9v2Cjn5a4Hjwnj8D3"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanJgoR7ALJAJ6ohoKs6aS9T71D9ZkNN9gYM5xUsi3u"): MevAgentZan,
solana.MustPublicKeyFromBase58("zanAtYifQP7Bo6kStB97mJvzqSDW1toKNibWibwcKDd"): MevAgentZan,
solana.MustPublicKeyFromBase58("GWT5UjDheZzoqinLavJkYvSRH5sakW8vDRdAgrUS5ZcS"): MevAgentTunneling,
}
var entryContractAddresses = map[solana.PublicKey]string{

View File

@@ -21,6 +21,8 @@ const (
MevAgentSpeedlanding = "speedlanding"
MevAgentAllenhark = "allenhark"
MevAgentRaiden = "raiden"
MevAgentZan = "zan"
MevAgentTunneling = "tunneling"
)
const (

View File

@@ -20,12 +20,16 @@ var mayhemFeeAccounts = []solana.PublicKey{
var pumpGetFeesDiscriminator = calculateDiscriminator("global:get_fees")
var pumpBuyDiscriminator = calculateDiscriminator("global:buy")
var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_exact_sol_in")
var pumpBuyExactSolInDiscriminator = calculateDiscriminator("global:buy_exact_sol_in")
var pumpBuyV2Discriminator = calculateDiscriminator("global:buy_v2")
var pumpBuyExactQuoteInV2Discriminator = calculateDiscriminator("global:buy_exact_quote_in_v2")
var pumpSellDiscriminator = calculateDiscriminator("global:sell")
var pumpSellV2Discriminator = calculateDiscriminator("global:sell_v2")
var pumpCreateDiscriminator = calculateDiscriminator("global:create")
var pumpCreateV2Discriminator = calculateDiscriminator("global:create_v2")
var pumpAdminSetCreatorDiscriminator = calculateDiscriminator("global:admin_set_creator")
var pumpMigrateDiscriminator = calculateDiscriminator("global:migrate")
var pumpMigrateV2Discriminator = calculateDiscriminator("global:migrate_v2")
var pumpEventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29}
var pumpTradeEventDiscriminator = [16]byte{228, 69, 165, 46, 81, 203, 154, 29, 189, 219, 127, 211, 78, 230, 97, 238}
@@ -132,6 +136,7 @@ var (
metaoraPoolRemoveBalanceLiquidityDiscriminator = calculateDiscriminator("global:remove_balance_liquidity")
metaoraPoolClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
metaoraPoolBootstrapLiquidityDiscriminator = calculateDiscriminator("global:bootstrap_liquidity")
metaoraPoolSwapEventDiscriminator = calculateDiscriminator("event:Swap")
)
var (

View File

@@ -2,8 +2,10 @@ package pump_parser
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"strings"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
@@ -20,6 +22,14 @@ type metaoraPoolSwapArgs struct {
MinimumOutAmount uint64
}
type metaoraPoolSwapEvent struct {
InAmount uint64
OutAmount uint64
TradeFee uint64
ProtocolFee uint64
HostFee uint64
}
var (
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6}
@@ -731,6 +741,7 @@ func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructio
}
func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
swapOffset := offset
var args metaoraPoolSwapArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode meteora pools swap args: %w", err)
@@ -886,10 +897,195 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
EntryContract: entryContract,
},
}
swaps[0].SetSwapAmountInfo(
fixedSide := fixedSwapAmountSide(event, SwapModeExactIn)
limitSide := oppositeSwapAmountSide(fixedSide)
if fixedSide == SwapAmountSideUnknown || limitSide == SwapAmountSideUnknown {
swaps[0].SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(args.InAmount),
decimal.NewFromUint64(args.MinimumOutAmount),
)
return swaps, offset, nil
}
actualLimitAmount := swapAmountForSide(baseAmount, quoteAmount, limitSide)
if swapEvent, ok := metaoraPoolSwapEventFromInstruction(instruction); ok {
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
} else if swapEvent, ok := metaoraPoolSwapEventForOffset(tx, swapOffset); ok {
actualLimitAmount = decimal.NewFromUint64(swapEvent.OutAmount)
}
swaps[0].SetSwapAmountInfoDetailed(
SwapModeExactIn,
decimal.NewFromUint64(args.InAmount),
fixedSide,
swapMintForSide(baseMint, quoteMint, fixedSide),
SwapLimitTypeMinOut,
decimal.NewFromUint64(args.MinimumOutAmount),
limitSide,
swapMintForSide(baseMint, quoteMint, limitSide),
actualLimitAmount,
)
return swaps, offset, nil
}
func metaoraPoolSwapEventFromInstruction(instruction Instruction) (metaoraPoolSwapEvent, bool) {
for _, event := range instruction.LogEvents {
if swapEvent, ok := metaoraPoolDecodeSwapEventData(event); ok {
return swapEvent, true
}
}
return metaoraPoolSwapEvent{}, false
}
func metaoraPoolSwapEventForOffset(tx *Tx, offset [2]uint) (metaoraPoolSwapEvent, bool) {
if tx == nil || tx.rawTx == nil {
return metaoraPoolSwapEvent{}, false
}
occurrence := metaoraPoolSwapInstructionOccurrence(tx.rawTx, offset)
if occurrence == 0 {
return metaoraPoolSwapEvent{}, false
}
return metaoraPoolSwapEventFromLogs(tx.rawTx.Meta.LogMessages, occurrence)
}
func metaoraPoolSwapInstructionOccurrence(rawTx *RawTx, offset [2]uint) int {
if rawTx == nil {
return 0
}
accountList := rawTx.getAccountList()
innerByOuter := make(map[int]InnerInstructions, len(rawTx.Meta.InnerInstructions))
for _, inner := range rawTx.Meta.InnerInstructions {
innerByOuter[inner.Index] = inner
}
occurrence := 0
for i, instruction := range rawTx.Transaction.Message.Instructions {
if uint(i) == offset[0] && offset[1] == 0 {
if metaoraPoolIsSwapInstruction(accountList, instruction) {
return occurrence + 1
}
return 0
}
if metaoraPoolIsSwapInstruction(accountList, instruction) {
occurrence++
}
inner := innerByOuter[i]
for j, instruction := range inner.Instructions {
innerOffset := uint(j + 1)
if uint(i) == offset[0] && offset[1] == innerOffset {
if metaoraPoolIsSwapInstruction(accountList, instruction) {
return occurrence + 1
}
return 0
}
if metaoraPoolIsSwapInstruction(accountList, instruction) {
occurrence++
}
}
}
return 0
}
func metaoraPoolIsSwapInstruction(accountList []solana.PublicKey, instruction Instruction) bool {
if instruction.ProgramIDIndex < 0 || instruction.ProgramIDIndex >= len(accountList) {
return false
}
if !accountList[instruction.ProgramIDIndex].Equals(metaoraPoolProgramID) {
return false
}
return len(instruction.Data) >= 8 && bytes.Equal(instruction.Data[:8], metaoraPoolSwapDiscriminator[:])
}
func metaoraPoolSwapEventFromLogs(logMessages []string, occurrence int) (metaoraPoolSwapEvent, bool) {
if occurrence <= 0 {
return metaoraPoolSwapEvent{}, false
}
type frame struct {
program string
sawSwap bool
}
targetProgram := metaoraPoolProgramID.String()
var stack []frame
seen := 0
for _, logMessage := range logMessages {
if program, ok := metaoraPoolLogInvokeProgram(logMessage); ok {
stack = append(stack, frame{program: program})
continue
}
if len(stack) > 0 && stack[len(stack)-1].program == targetProgram {
if logMessage == "Program log: Instruction: Swap" {
stack[len(stack)-1].sawSwap = true
continue
}
if stack[len(stack)-1].sawSwap && strings.HasPrefix(logMessage, "Program data: ") {
event, ok := metaoraPoolDecodeSwapEventLog(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
if ok {
seen++
if seen == occurrence {
return event, true
}
}
}
}
if program, ok := metaoraPoolLogFinishedProgram(logMessage); ok {
for i := len(stack) - 1; i >= 0; i-- {
if stack[i].program == program {
stack = stack[:i]
break
}
}
}
}
return metaoraPoolSwapEvent{}, false
}
func metaoraPoolLogInvokeProgram(logMessage string) (string, bool) {
if !strings.HasPrefix(logMessage, "Program ") || !strings.Contains(logMessage, " invoke [") {
return "", false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, _, ok := strings.Cut(rest, " ")
return program, ok
}
func metaoraPoolLogFinishedProgram(logMessage string) (string, bool) {
if !strings.HasPrefix(logMessage, "Program ") {
return "", false
}
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
return "", false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, _, ok := strings.Cut(rest, " ")
return program, ok
}
func metaoraPoolDecodeSwapEventLog(encoded string) (metaoraPoolSwapEvent, bool) {
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return metaoraPoolSwapEvent{}, false
}
return metaoraPoolDecodeSwapEventData(data)
}
func metaoraPoolDecodeSwapEventData(data []byte) (metaoraPoolSwapEvent, bool) {
if len(data) < 48 {
return metaoraPoolSwapEvent{}, false
}
if !bytes.Equal(data[:8], metaoraPoolSwapEventDiscriminator[:]) {
return metaoraPoolSwapEvent{}, false
}
body := data[8:]
return metaoraPoolSwapEvent{
InAmount: binary.LittleEndian.Uint64(body[0:8]),
OutAmount: binary.LittleEndian.Uint64(body[8:16]),
TradeFee: binary.LittleEndian.Uint64(body[16:24]),
ProtocolFee: binary.LittleEndian.Uint64(body[24:32]),
HostFee: binary.LittleEndian.Uint64(body[32:40]),
}, true
}

152
metaorapool_test.go Normal file
View File

@@ -0,0 +1,152 @@
package pump_parser
import (
"encoding/base64"
"encoding/binary"
"testing"
"github.com/gagliardetto/solana-go"
)
func TestMetaoraPoolSwapEventFromLogsUsesMatchingInvocation(t *testing.T) {
firstEvent := metaoraPoolSwapEventLogForTest(10, 9, 1, 0, 0)
secondEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
logs := []string{
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
"Program log: Instruction: Swap",
"Program data: " + firstEvent,
"Program " + metaoraPoolProgramID.String() + " success",
"Program " + solana.TokenProgramID.String() + " invoke [1]",
"Program data: " + secondEvent,
"Program " + solana.TokenProgramID.String() + " success",
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
"Program log: Instruction: Swap",
"Program data: " + secondEvent,
"Program " + metaoraPoolProgramID.String() + " success",
}
event, ok := metaoraPoolSwapEventFromLogs(logs, 2)
if !ok {
t.Fatal("expected second swap event")
}
if event.InAmount != 4013522650 {
t.Fatalf("in amount = %d, want 4013522650", event.InAmount)
}
if event.OutAmount != 135 {
t.Fatalf("out amount = %d, want 135", event.OutAmount)
}
if event.TradeFee != 8043041 {
t.Fatalf("trade fee = %d, want 8043041", event.TradeFee)
}
if event.ProtocolFee != 2010760 {
t.Fatalf("protocol fee = %d, want 2010760", event.ProtocolFee)
}
}
func TestMetaoraPoolSwapInstructionOccurrenceIncludesInnerInstructions(t *testing.T) {
rawTx := &RawTx{
Transaction: Transaction{
Message: Message{
AccountKeys: solana.PublicKeySlice{
metaoraPoolProgramID,
solana.MustPublicKeyFromBase58("BASDaPs2cdVTsvgPRfESDLZgek8tKRTfqbR2ksdgptsn"),
},
Instructions: []Instruction{
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
{ProgramIDIndex: 1, Data: []byte{1}},
},
},
},
Meta: Meta{
InnerInstructions: []InnerInstructions{
{
Index: 1,
Instructions: []Instruction{
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
},
},
},
},
}
if got := metaoraPoolSwapInstructionOccurrence(rawTx, [2]uint{1, 1}); got != 2 {
t.Fatalf("occurrence = %d, want 2", got)
}
}
func TestAttachLogEventsToInstructions(t *testing.T) {
swapEvent := metaoraPoolSwapEventLogForTest(4013522650, 135, 8043041, 2010760, 0)
rawTx := &RawTx{
Transaction: Transaction{
Message: Message{
AccountKeys: solana.PublicKeySlice{
metaoraPoolProgramID,
solana.TokenProgramID,
},
Instructions: []Instruction{
{ProgramIDIndex: 0, Data: metaoraPoolSwapInstructionDataForTest()},
},
},
},
Meta: Meta{
InnerInstructions: []InnerInstructions{
{
Index: 0,
Instructions: []Instruction{
{ProgramIDIndex: 1, Data: []byte{3}, StackHeight: intPtrForTest(2)},
},
},
},
LogMessages: []string{
"Program " + metaoraPoolProgramID.String() + " invoke [1]",
"Program log: Instruction: Swap",
"Program " + solana.TokenProgramID.String() + " invoke [2]",
"Program " + solana.TokenProgramID.String() + " success",
"Program data: " + swapEvent,
"Program " + metaoraPoolProgramID.String() + " success",
},
},
}
applyRawTxConvertLogOptions(rawTx, RawTxConvertOptions{ParseLogEvents: true, IgnoreLogMessages: true})
if len(rawTx.Meta.LogMessages) != 0 {
t.Fatalf("log messages length = %d, want 0", len(rawTx.Meta.LogMessages))
}
if len(rawTx.Transaction.Message.Instructions[0].LogEvents) != 1 {
t.Fatalf("outer log events length = %d, want 1", len(rawTx.Transaction.Message.Instructions[0].LogEvents))
}
if len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents) != 0 {
t.Fatalf("inner log events length = %d, want 0", len(rawTx.Meta.InnerInstructions[0].Instructions[0].LogEvents))
}
event, ok := metaoraPoolSwapEventFromInstruction(rawTx.Transaction.Message.Instructions[0])
if !ok {
t.Fatal("expected swap event from outer instruction")
}
if event.OutAmount != 135 {
t.Fatalf("out amount = %d, want 135", event.OutAmount)
}
}
func metaoraPoolSwapInstructionDataForTest() []byte {
data := make([]byte, 8+16)
copy(data, metaoraPoolSwapDiscriminator[:])
return data
}
func metaoraPoolSwapEventLogForTest(inAmount, outAmount, tradeFee, protocolFee, hostFee uint64) string {
data := make([]byte, 8+40)
copy(data, metaoraPoolSwapEventDiscriminator[:])
binary.LittleEndian.PutUint64(data[8:16], inAmount)
binary.LittleEndian.PutUint64(data[16:24], outAmount)
binary.LittleEndian.PutUint64(data[24:32], tradeFee)
binary.LittleEndian.PutUint64(data[32:40], protocolFee)
binary.LittleEndian.PutUint64(data[40:48], hostFee)
return base64.StdEncoding.EncodeToString(data)
}
func intPtrForTest(value int) *int {
return &value
}

653
pump.go
View File

@@ -33,7 +33,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
discriminator := *(*[8]byte)(decode[:8])
switch discriminator {
case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator:
case pumpBuyExactSolInDiscriminator, pumpBuyDiscriminator, pumpBuyV2Discriminator, pumpBuyExactQuoteInV2Discriminator, pumpSellDiscriminator, pumpSellV2Discriminator:
if tx.Err != nil {
return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset)
}
@@ -43,7 +43,7 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct
return nil, increaseOffset(offset), InstructionIgnoredError
}
return CreateParser(tx, instruction, innerInstructions, offset)
case pumpMigrateDiscriminator:
case pumpMigrateDiscriminator, pumpMigrateV2Discriminator:
if tx.Err != nil {
return nil, increaseOffset(offset), InstructionIgnoredError
}
@@ -89,6 +89,56 @@ type PumpCreateEvent struct {
TokenProgram solana.PublicKey
IsMayhemMode bool
IsCashbackEnabled bool
QuoteMint solana.PublicKey
VirtualQuoteReserves uint64
}
type pumpCreateEventLegacy struct {
Name string
Symbol string
Uri string
Mint solana.PublicKey
BondingCurve solana.PublicKey
User solana.PublicKey
Creator solana.PublicKey
Timestamp int64
VirtualTokenReserves uint64
VirtualSolReserves uint64
RealTokenReserves uint64
TokenTotalSupply uint64
TokenProgram solana.PublicKey
IsMayhemMode bool
IsCashbackEnabled bool
}
func decodePumpCreateEvent(data []byte) (PumpCreateEvent, error) {
var event PumpCreateEvent
if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil {
return event, nil
}
var legacy pumpCreateEventLegacy
if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err != nil {
return PumpCreateEvent{}, err
}
return PumpCreateEvent{
Name: legacy.Name,
Symbol: legacy.Symbol,
Uri: legacy.Uri,
Mint: legacy.Mint,
BondingCurve: legacy.BondingCurve,
User: legacy.User,
Creator: legacy.Creator,
Timestamp: legacy.Timestamp,
VirtualTokenReserves: legacy.VirtualTokenReserves,
VirtualSolReserves: legacy.VirtualSolReserves,
RealTokenReserves: legacy.RealTokenReserves,
TokenTotalSupply: legacy.TokenTotalSupply,
TokenProgram: legacy.TokenProgram,
IsMayhemMode: legacy.IsMayhemMode,
IsCashbackEnabled: legacy.IsCashbackEnabled,
}, nil
}
func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -106,7 +156,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
}
for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], pumpCreateEventDiscriminator[:]) {
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&createEvent)
createEvent, err = decodePumpCreateEvent(innerInstr.Data[16:])
if offset[1] == 0 {
offset[0] += 1
} else {
@@ -129,6 +179,11 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
}
userBase := getAccountBalanceAfterTx(result, userIndex)
userQuote, _ := GetSolAfterTx(result, userIndex)
quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, instr, createEvent)
userQuoteBalance := decimal.NewFromUint64(userQuote)
if !quoteMint.IsZero() && !quoteMint.Equals(wSolMint) {
userQuoteBalance = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
}
totalSupply := decimal.NewFromUint64(createEvent.TokenTotalSupply).Div(decimal.New(1, 6))
tx.Token[createEvent.Mint] = TokenMeta{
@@ -146,12 +201,12 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
Event: "create",
Pool: createEvent.BondingCurve,
BaseMint: createEvent.Mint,
QuoteMint: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: createEvent.TokenProgram,
QuoteTokenProgram: solana.PublicKey{},
QuoteTokenProgram: quoteTokenProgram,
Creator: createEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: quoteDecimals,
User: createEvent.User,
BaseAmount: decimal.Zero,
QuoteAmount: decimal.Zero,
@@ -160,7 +215,7 @@ func CreateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions
Mayhem: createEvent.IsMayhemMode,
Cashback: createEvent.IsCashbackEnabled,
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuoteBalance,
EntryContract: entryContract,
},
}, offset, nil
@@ -197,6 +252,141 @@ type PumpTradeEvent struct {
MayhemMode bool
CashbackFeeBasisPoints uint64
Cashback uint64
BuybackFeeBasisPoints uint64
BuybackFee uint64
Shareholders []PumpShareholder
QuoteMint solana.PublicKey
QuoteAmount uint64
VirtualQuoteReserves uint64
RealQuoteReserves uint64
}
type PumpShareholder struct {
Address solana.PublicKey
ShareBps uint16
}
type pumpTradeEventLegacy struct {
Mint solana.PublicKey
SolAmount uint64
TokenAmount uint64
IsBuy bool
User solana.PublicKey
Timestamp int64
VirtualSolReserves uint64
VirtualTokenReserves uint64
RealSolReserves uint64
RealTokenReserves uint64
FeeRecipient solana.PublicKey
FeeBasisPoints uint64
Fee uint64
Creator solana.PublicKey
CreatorFeeBasisPoints uint64
CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
MayhemMode bool
CashbackFeeBasisPoints uint64
Cashback uint64
}
type pumpTradeEventLegacyV0 struct {
Mint solana.PublicKey
SolAmount uint64
TokenAmount uint64
IsBuy bool
User solana.PublicKey
Timestamp int64
VirtualSolReserves uint64
VirtualTokenReserves uint64
RealSolReserves uint64
RealTokenReserves uint64
FeeRecipient solana.PublicKey
FeeBasisPoints uint64
Fee uint64
Creator solana.PublicKey
CreatorFeeBasisPoints uint64
CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
}
func decodePumpTradeEvent(data []byte) (PumpTradeEvent, error) {
var event PumpTradeEvent
if err := agbinary.NewBorshDecoder(data).Decode(&event); err == nil {
return event, nil
}
var legacy pumpTradeEventLegacy
if err := agbinary.NewBorshDecoder(data).Decode(&legacy); err == nil {
return PumpTradeEvent{
Mint: legacy.Mint,
SolAmount: legacy.SolAmount,
TokenAmount: legacy.TokenAmount,
IsBuy: legacy.IsBuy,
User: legacy.User,
Timestamp: legacy.Timestamp,
VirtualSolReserves: legacy.VirtualSolReserves,
VirtualTokenReserves: legacy.VirtualTokenReserves,
RealSolReserves: legacy.RealSolReserves,
RealTokenReserves: legacy.RealTokenReserves,
FeeRecipient: legacy.FeeRecipient,
FeeBasisPoints: legacy.FeeBasisPoints,
Fee: legacy.Fee,
Creator: legacy.Creator,
CreatorFeeBasisPoints: legacy.CreatorFeeBasisPoints,
CreatorFee: legacy.CreatorFee,
TrackVolume: legacy.TrackVolume,
TotalUnclaimedTokens: legacy.TotalUnclaimedTokens,
TotalClaimedTokens: legacy.TotalClaimedTokens,
CurrentSolVolume: legacy.CurrentSolVolume,
LastUpdateTimestamp: legacy.LastUpdateTimestamp,
IxName: legacy.IxName,
MayhemMode: legacy.MayhemMode,
CashbackFeeBasisPoints: legacy.CashbackFeeBasisPoints,
Cashback: legacy.Cashback,
}, nil
}
var legacyV0 pumpTradeEventLegacyV0
if err := agbinary.NewBorshDecoder(data).Decode(&legacyV0); err != nil {
return PumpTradeEvent{}, err
}
return PumpTradeEvent{
Mint: legacyV0.Mint,
SolAmount: legacyV0.SolAmount,
TokenAmount: legacyV0.TokenAmount,
IsBuy: legacyV0.IsBuy,
User: legacyV0.User,
Timestamp: legacyV0.Timestamp,
VirtualSolReserves: legacyV0.VirtualSolReserves,
VirtualTokenReserves: legacyV0.VirtualTokenReserves,
RealSolReserves: legacyV0.RealSolReserves,
RealTokenReserves: legacyV0.RealTokenReserves,
FeeRecipient: legacyV0.FeeRecipient,
FeeBasisPoints: legacyV0.FeeBasisPoints,
Fee: legacyV0.Fee,
Creator: legacyV0.Creator,
CreatorFeeBasisPoints: legacyV0.CreatorFeeBasisPoints,
CreatorFee: legacyV0.CreatorFee,
TrackVolume: legacyV0.TrackVolume,
TotalUnclaimedTokens: legacyV0.TotalUnclaimedTokens,
TotalClaimedTokens: legacyV0.TotalClaimedTokens,
CurrentSolVolume: legacyV0.CurrentSolVolume,
LastUpdateTimestamp: legacyV0.LastUpdateTimestamp,
IxName: legacyV0.IxName,
}, nil
}
type PumpTradeFeeArg struct {
@@ -220,17 +410,185 @@ type PumpTradeArgs struct {
func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
switch {
case bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
case bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]),
bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]):
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]),
bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]):
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]),
bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
type pumpTradeAccountLayout struct {
IsV2 bool
FeeRecipient int
BaseMint int
QuoteMint int
BaseTokenProgram int
QuoteTokenProgram int
Pool int
BasePoolToken int
QuotePoolToken int
User int
BaseUserToken int
QuoteUserToken int
}
func pumpTradeLayout(instr Instruction) (pumpTradeAccountLayout, bool) {
if len(instr.Data) < 8 {
return pumpTradeAccountLayout{}, false
}
discriminator := instr.Data[:8]
switch {
case bytes.Equal(discriminator, pumpBuyDiscriminator[:]), bytes.Equal(discriminator, pumpBuyExactSolInDiscriminator[:]):
if len(instr.Accounts) <= 8 {
return pumpTradeAccountLayout{}, false
}
return pumpTradeAccountLayout{
FeeRecipient: 1,
BaseMint: 2,
QuoteMint: -1,
BaseTokenProgram: 8,
QuoteTokenProgram: -1,
Pool: 3,
BasePoolToken: 4,
QuotePoolToken: -1,
User: 6,
BaseUserToken: 5,
QuoteUserToken: -1,
}, true
case bytes.Equal(discriminator, pumpSellDiscriminator[:]):
if len(instr.Accounts) <= 9 {
return pumpTradeAccountLayout{}, false
}
return pumpTradeAccountLayout{
FeeRecipient: 1,
BaseMint: 2,
QuoteMint: -1,
BaseTokenProgram: 9,
QuoteTokenProgram: -1,
Pool: 3,
BasePoolToken: 4,
QuotePoolToken: -1,
User: 6,
BaseUserToken: 5,
QuoteUserToken: -1,
}, true
case bytes.Equal(discriminator, pumpBuyV2Discriminator[:]),
bytes.Equal(discriminator, pumpBuyExactQuoteInV2Discriminator[:]),
bytes.Equal(discriminator, pumpSellV2Discriminator[:]):
if len(instr.Accounts) <= 15 {
return pumpTradeAccountLayout{}, false
}
return pumpTradeAccountLayout{
IsV2: true,
FeeRecipient: 6,
BaseMint: 1,
QuoteMint: 2,
BaseTokenProgram: 3,
QuoteTokenProgram: 4,
Pool: 10,
BasePoolToken: 11,
QuotePoolToken: 12,
User: 13,
BaseUserToken: 14,
QuoteUserToken: 15,
}, true
default:
return pumpTradeAccountLayout{}, false
}
}
func pumpInstructionIsSell(data []byte) bool {
return len(data) >= 8 && (bytes.Equal(data[:8], pumpSellDiscriminator[:]) || bytes.Equal(data[:8], pumpSellV2Discriminator[:]))
}
func pumpInstructionIsExactQuoteIn(data []byte) bool {
return len(data) >= 8 && (bytes.Equal(data[:8], pumpBuyExactSolInDiscriminator[:]) || bytes.Equal(data[:8], pumpBuyExactQuoteInV2Discriminator[:]))
}
func pumpAccount(result *RawTx, instr Instruction, accountIndex int) solana.PublicKey {
if accountIndex < 0 || accountIndex >= len(instr.Accounts) {
return solana.PublicKey{}
}
listIndex := instr.Accounts[accountIndex]
if listIndex < 0 || listIndex >= len(result.accountList) {
return solana.PublicKey{}
}
return result.accountList[listIndex]
}
func pumpCreateQuoteAccounts(result *RawTx, instr Instruction, createEvent PumpCreateEvent) (solana.PublicKey, solana.PublicKey, uint8) {
quoteMint := createEvent.QuoteMint
quoteTokenProgram := solana.PublicKey{}
optionalStart := -1
if len(instr.Data) >= 8 && bytes.Equal(instr.Data[:8], pumpCreateV2Discriminator[:]) {
optionalStart = 16
}
if optionalStart >= 0 && len(instr.Accounts) > optionalStart {
accountQuoteMint := pumpAccount(result, instr, optionalStart)
if quoteMint.IsZero() && !accountQuoteMint.IsZero() && !accountQuoteMint.Equals(wSolMint) {
quoteMint = accountQuoteMint
}
if len(instr.Accounts) > optionalStart+2 && !quoteMint.IsZero() {
quoteTokenProgram = pumpAccount(result, instr, optionalStart+2)
}
}
if quoteMint.Equals(wSolMint) {
quoteTokenProgram = solana.TokenProgramID
}
if quoteTokenProgram.IsZero() && !quoteMint.IsZero() {
quoteTokenProgram = solana.TokenProgramID
}
return quoteMint, quoteTokenProgram, pumpQuoteDecimals(result, quoteMint)
}
func pumpMintDecimalsFromBalances(result *RawTx, mint solana.PublicKey, fallback uint8) uint8 {
if mint.IsZero() {
return fallback
}
for _, balance := range result.Meta.PostTokenBalances {
balance.ParseAccount()
if balance.MintAccount.Equals(mint) {
return uint8(balance.UITokenAmount.Decimals)
}
}
for _, balance := range result.Meta.PreTokenBalances {
balance.ParseAccount()
if balance.MintAccount.Equals(mint) {
return uint8(balance.UITokenAmount.Decimals)
}
}
return fallback
}
func pumpQuoteDecimals(result *RawTx, quoteMint solana.PublicKey) uint8 {
fallback := uint8(9)
if quoteMint.Equals(usdcMint) || quoteMint.Equals(usd1Mint) {
fallback = 6
}
return pumpMintDecimalsFromBalances(result, quoteMint, fallback)
}
func pumpQuoteAmount(tradeEvent PumpTradeEvent) uint64 {
if tradeEvent.QuoteAmount != 0 {
return tradeEvent.QuoteAmount
}
return tradeEvent.SolAmount
}
func pumpQuoteReserve(tradeEvent PumpTradeEvent) uint64 {
if tradeEvent.RealQuoteReserves != 0 {
return tradeEvent.RealQuoteReserves
}
return tradeEvent.RealSolReserves
}
func pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool {
if completeEvent.Mint != tradeEvent.Mint {
return false
@@ -279,10 +637,16 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
user := result.accountList[instruction.Accounts[6]]
ataUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[6]
mint := result.accountList[instruction.Accounts[2]]
layout, ok := pumpTradeLayout(instruction)
if !ok {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1])
}
user := pumpAccount(result, instruction, layout.User)
ataUserIdx := instruction.Accounts[layout.BaseUserToken]
userIndex := instruction.Accounts[layout.User]
mint := pumpAccount(result, instruction, layout.BaseMint)
quoteMint := pumpAccount(result, instruction, layout.QuoteMint)
quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram)
var args PumpTradeArgs
err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args)
if err != nil {
@@ -290,30 +654,27 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
}
var event string
var (
solAmount, tokenAmount uint64
quoteAmount, tokenAmount uint64
)
if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
if bytes.Equal(args.Discriminator[:], pumpBuyExactSolInDiscriminator[:]) ||
bytes.Equal(args.Discriminator[:], pumpBuyExactQuoteInV2Discriminator[:]) {
event = "buy_failed"
solAmount = args.Amount1
quoteAmount = args.Amount1
tokenAmount = args.Amount2
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) {
} else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) ||
bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) {
event = "buy_failed"
solAmount = args.Amount2
quoteAmount = args.Amount2
tokenAmount = args.Amount1
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) {
} else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) ||
bytes.Equal(args.Discriminator[:], pumpSellV2Discriminator[:]) {
event = "sell_failed"
solAmount = args.Amount2
quoteAmount = args.Amount2
tokenAmount = args.Amount1
} else {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1])
}
var baseTokenProgram solana.PublicKey
if event == "buy_failed" {
baseTokenProgram = result.accountList[instruction.Accounts[8]]
} else {
baseTokenProgram = result.accountList[instruction.Accounts[9]]
}
baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram)
if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint)
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
@@ -325,31 +686,43 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
}
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
userQuote, _ := GetSolAfterTx(result, userIndex)
userQuote := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken])
} else {
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
userQuote = decimal.NewFromUint64(userQuoteLamports)
}
bcIdx := instruction.Accounts[3]
bcAtaIndex := instruction.Accounts[4]
solReserves, _ := GetSolAfterTx(result, bcIdx)
bcIdx := instruction.Accounts[layout.Pool]
bcAtaIndex := instruction.Accounts[layout.BasePoolToken]
quoteReserves := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
quoteReserves = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuotePoolToken])
} else {
solReserves, _ := GetSolAfterTx(result, bcIdx)
quoteReserves = decimal.NewFromUint64(solReserves)
}
tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex)
swaps := []Swap{
{
Program: SolProgramPump,
Event: event,
Pool: result.accountList[instruction.Accounts[3]],
Pool: pumpAccount(result, instruction, layout.Pool),
BaseMint: mint,
QuoteMint: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{},
QuoteTokenProgram: quoteTokenProgram,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
User: user,
BaseAmount: decimal.NewFromUint64(tokenAmount),
QuoteAmount: decimal.NewFromUint64(solAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: tokenReserves,
QuoteReserve: decimal.NewFromUint64(solReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
QuoteReserve: quoteReserves,
Mayhem: isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}
@@ -365,6 +738,10 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error
var programIndex = instruction.ProgramIDIndex
layout, ok := pumpTradeLayout(instruction)
if !ok {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction account layout, offset, %d, %d", offset[0], offset[1])
}
feeEventProgramIndex := 0
for i, b := range result.accountList {
@@ -400,6 +777,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex == feeEventProgramIndex && bytes.Equal(innerInstr.Data[:8], pumpGetFeesDiscriminator[:]) {
if tradeFound {
continue
}
err = agbinary.NewBorshDecoder(innerInstr.Data[8:]).Decode(&tradeFeeArg)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump get fees event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
@@ -411,7 +791,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
if tradeFound {
break
}
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
tradeEvent, err = decodePumpTradeEvent(innerInstr.Data[16:])
if offset[1] == 0 {
newoffset = [2]uint{offset[0] + 1, offset[1]}
} else {
@@ -420,7 +800,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
if err != nil {
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
expectedIsBuy := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:])
expectedIsBuy := !pumpInstructionIsSell(instruction.Data)
if tradeEvent.IsBuy != expectedIsBuy {
tradeEvent = PumpTradeEvent{}
continue
@@ -437,7 +817,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, result.accountList[instruction.Accounts[3]]) {
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, pumpAccount(result, instruction, layout.Pool)) {
break
}
if offset[1] == 0 {
@@ -451,7 +831,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
}
}
if tradeEvent == (PumpTradeEvent{}) {
if !tradeFound {
return nil, increaseOffset(offset), fmt.Errorf("pmp buy/sell event not found, offset, %d, %d", offset[0], offset[1])
}
@@ -463,13 +843,16 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
}
event := ""
baseTokenProgram := solana.TokenProgramID
baseTokenProgram := pumpAccount(result, instruction, layout.BaseTokenProgram)
quoteMint := tradeEvent.QuoteMint
if quoteMint.IsZero() {
quoteMint = pumpAccount(result, instruction, layout.QuoteMint)
}
quoteTokenProgram := pumpAccount(result, instruction, layout.QuoteTokenProgram)
if tradeEvent.IsBuy {
event = "buy"
baseTokenProgram = result.accountList[instruction.Accounts[8]]
} else {
event = "sell"
baseTokenProgram = result.accountList[instruction.Accounts[9]]
}
if _, exists := tx.Token[tradeEvent.Mint]; !exists {
tx.Token[tradeEvent.Mint] = TokenMeta{
@@ -481,8 +864,8 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
var user = tradeEvent.User
ataUserIdx := instruction.Accounts[5]
userIndex := instruction.Accounts[6]
ataUserIdx := instruction.Accounts[layout.BaseUserToken]
userIndex := instruction.Accounts[layout.User]
if !tradeEvent.User.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) {
userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, tradeEvent.Mint)
//&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount
@@ -494,14 +877,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
}
userBase := getAccountBalanceAfterTx(result, ataUserIdx)
userQuote, _ := GetSolAfterTx(result, userIndex)
userQuote := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
userQuote = getAccountBalanceAfterTx(result, instruction.Accounts[layout.QuoteUserToken])
} else {
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
userQuote = decimal.NewFromUint64(userQuoteLamports)
}
solAmount := tradeEvent.SolAmount
if tradeEvent.IsBuy && bytes.Equal(instruction.Data[:8], pumpBuyV2Discriminator[:]) {
quoteAmount := pumpQuoteAmount(tradeEvent)
if tradeEvent.IsBuy && pumpInstructionIsExactQuoteIn(instruction.Data) && !layout.IsV2 {
fee := tradeEvent.Fee + tradeEvent.CreatorFee
solAmount = tradeFeeArg.TradeSize
if solAmount > fee {
solAmount = solAmount - fee
quoteAmount = tradeFeeArg.TradeSize
if quoteAmount > fee {
quoteAmount = quoteAmount - fee
}
}
isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0
@@ -509,22 +898,22 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
{
Program: SolProgramPump,
Event: event,
Pool: result.accountList[instruction.Accounts[3]],
Pool: pumpAccount(result, instruction, layout.Pool),
BaseMint: tradeEvent.Mint,
QuoteMint: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{},
QuoteTokenProgram: quoteTokenProgram,
Creator: tradeEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
User: user,
BaseAmount: decimal.NewFromUint64(tradeEvent.TokenAmount),
QuoteAmount: decimal.NewFromUint64(solAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount),
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)),
Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
Cashback: isCashbackCoin,
},
@@ -537,20 +926,20 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
swaps = append(swaps, Swap{
Program: SolProgramPump,
Event: "complete",
Pool: result.accountList[instruction.Accounts[3]],
Pool: pumpAccount(result, instruction, layout.Pool),
BaseMint: tradeEvent.Mint,
QuoteMint: solana.PublicKey{},
BaseTokenProgram: result.accountList[instruction.Accounts[8]],
QuoteTokenProgram: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: quoteTokenProgram,
Creator: tradeEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: pumpQuoteDecimals(result, quoteMint),
User: user,
BaseReserve: decimal.NewFromUint64(tradeEvent.RealTokenReserves),
QuoteReserve: decimal.NewFromUint64(tradeEvent.RealSolReserves),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]),
QuoteReserve: decimal.NewFromUint64(pumpQuoteReserve(tradeEvent)),
Mayhem: tradeEvent.MayhemMode || isMayhemPump(pumpAccount(result, instruction, layout.FeeRecipient)),
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
})
}
@@ -572,11 +961,74 @@ type MigrateEvent struct {
Pool solana.PublicKey
}
type pumpMigrateAccountLayout struct {
IsV2 bool
BaseMint int
QuoteMint int
Pool int
BasePoolToken int
QuotePoolToken int
User int
BaseTokenProgram int
QuoteTokenProgram int
}
func pumpMigrateLayout(instr Instruction) (pumpMigrateAccountLayout, bool) {
if len(instr.Data) < 8 {
return pumpMigrateAccountLayout{}, false
}
discriminator := instr.Data[:8]
switch {
case bytes.Equal(discriminator, pumpMigrateDiscriminator[:]):
if len(instr.Accounts) <= 14 {
return pumpMigrateAccountLayout{}, false
}
return pumpMigrateAccountLayout{
BaseMint: 2,
QuoteMint: 14,
Pool: 3,
BasePoolToken: 4,
QuotePoolToken: -1,
User: 5,
BaseTokenProgram: 7,
QuoteTokenProgram: -1,
}, true
case bytes.Equal(discriminator, pumpMigrateV2Discriminator[:]):
if len(instr.Accounts) <= 20 {
return pumpMigrateAccountLayout{}, false
}
return pumpMigrateAccountLayout{
IsV2: true,
BaseMint: 2,
QuoteMint: 3,
Pool: 4,
BasePoolToken: 5,
QuotePoolToken: 6,
User: 7,
BaseTokenProgram: 19,
QuoteTokenProgram: 20,
}, true
default:
return pumpMigrateAccountLayout{}, false
}
}
func decimalFromUint64WithFallback(primary, fallback uint64) decimal.Decimal {
if primary != 0 {
return decimal.NewFromUint64(primary)
}
return decimal.NewFromUint64(fallback)
}
func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
result := tx.rawTx
var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var err error
programIndex := instr.ProgramIDIndex
layout, ok := pumpMigrateLayout(instr)
if !ok {
return nil, increaseOffset(offset), fmt.Errorf("unknown pump migrate instruction account layout, offset, %d, %d", offset[0], offset[1])
}
ammprogramIdx := 0
for i, b := range result.accountList {
if b.Equals(pumpAmmProgram) {
@@ -633,20 +1085,45 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
offset = [2]uint{newoffset[0], newoffset[1]}
// verify migrate by checking create pool and migrate event
userIndex := instr.Accounts[5]
ataBondingCurveAccountIndex := instr.Accounts[4]
userIndex := instr.Accounts[layout.User]
ataBondingCurveAccountIndex := instr.Accounts[layout.BasePoolToken]
bc, err := getTokenBalanceAfterTx(result, ataBondingCurveAccountIndex)
if err != nil || bc == nil {
return nil, increaseOffset(offset), fmt.Errorf("pump migrate get bonding curve balance error: %v, offset, %d, %d", err, offset[0], offset[1])
}
baseTokenProgram := bc.ProgramIDAccount
if layout.IsV2 {
baseTokenProgram = pumpAccount(result, instr, layout.BaseTokenProgram)
}
quoteMint := createEvent.QuoteMint
if quoteMint.IsZero() {
quoteMint = pumpAccount(result, instr, layout.QuoteMint)
}
quoteTokenProgram := pumpAccount(result, instr, layout.QuoteTokenProgram)
if quoteTokenProgram.IsZero() && !quoteMint.IsZero() {
quoteTokenProgram = solana.TokenProgramID
}
quoteDecimals := createEvent.QuoteMintDecimals
if quoteDecimals == 0 {
quoteDecimals = pumpQuoteDecimals(result, quoteMint)
}
var userBase decimal.Decimal
if result.accountList[userIndex].Equals(pumpMigrationAccount) {
userBase = decimal.Zero
} else {
userBase = GetTokenBalanceAfterTx(result, userIndex, baseTokenProgram, migrateEvent.Mint)
}
userQuote, _ := GetSolAfterTx(result, userIndex)
userQuote := decimal.Zero
if layout.IsV2 && !quoteMint.Equals(wSolMint) {
userQuote = GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint)
} else {
userQuoteLamports, _ := GetSolAfterTx(result, userIndex)
userQuote = decimal.NewFromUint64(userQuoteLamports)
}
baseAmount := decimalFromUint64WithFallback(createEvent.BaseAmountIn, migrateEvent.MintAmount)
quoteAmount := decimalFromUint64WithFallback(createEvent.QuoteAmountIn, migrateEvent.SolAmount)
baseReserve := decimalFromUint64WithFallback(createEvent.PoolBaseAmount, migrateEvent.MintAmount)
quoteReserve := decimalFromUint64WithFallback(createEvent.PoolQuoteAmount, migrateEvent.SolAmount)
if _, exists := tx.Token[migrateEvent.Mint]; !exists {
tx.Token[migrateEvent.Mint] = TokenMeta{
@@ -661,22 +1138,22 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
Event: "migrate",
Pool: migrateEvent.BondingCurve,
BaseMint: migrateEvent.Mint,
QuoteMint: solana.PublicKey{},
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.PublicKey{},
QuoteTokenProgram: quoteTokenProgram,
Creator: createEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: quoteDecimals,
User: migrateEvent.User,
//BaseAmount: decimal.Decimal{},
//QuoteAmount: decimal.Decimal{},
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: createEvent.IsMayhemMode,
MigrateTopProgram: pumpAmmProgram,
MigrateToPool: migrateEvent.Pool,
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}
@@ -685,20 +1162,20 @@ func MigrateParser(tx *Tx, instr Instruction, innerInstructions InnerInstruction
Event: "create",
Pool: migrateEvent.Pool,
BaseMint: migrateEvent.Mint,
QuoteMint: wSolMint,
QuoteMint: quoteMint,
BaseTokenProgram: baseTokenProgram,
QuoteTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: quoteTokenProgram,
Creator: createEvent.Creator,
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
QuoteMintDecimals: quoteDecimals,
User: migrateEvent.User,
BaseAmount: decimal.NewFromUint64(migrateEvent.MintAmount),
QuoteAmount: decimal.NewFromUint64(migrateEvent.SolAmount),
BaseReserve: decimal.NewFromUint64(migrateEvent.MintAmount),
QuoteReserve: decimal.NewFromUint64(migrateEvent.SolAmount),
BaseAmount: baseAmount,
QuoteAmount: quoteAmount,
BaseReserve: baseReserve,
QuoteReserve: quoteReserve,
Mayhem: createEvent.IsMayhemMode,
UserBaseBalance: userBase,
UserQuoteBalance: decimal.NewFromUint64(userQuote),
UserQuoteBalance: userQuote,
EntryContract: entryContract,
})

View File

@@ -1,6 +1,7 @@
package pump_parser
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
@@ -100,3 +101,188 @@ func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user")
}
}
func TestPumpExactQuoteInKeepsFeeArgBeforeMatchedTrade(t *testing.T) {
EnableAllParsers()
tx := mustParseRPCFixtureTx(t, "3jugr2KthX3cUHzPrMpaFKM7RtxXM6Gcxi8eFjDL7aZGLXpc6f1RaVdnAoB4ye5bRVYsP2fFs3aLaP19Utz91ewv")
if len(tx.Swaps) != 4 {
t.Fatalf("swaps len = %d, want 4", len(tx.Swaps))
}
for i := 0; i < 3; i++ {
swap := tx.Swaps[i]
if swap.Program != SolProgramPump || swap.Event != "buy" {
t.Fatalf("swap[%d] = %s/%s, want Pump/buy", i, swap.Program, swap.Event)
}
assertDecimalString(t, fmt.Sprintf("swap[%d].quote_amount", i), swap.QuoteAmount, "329217")
assertDecimalString(t, fmt.Sprintf("swap[%d].fixed_amount", i), swap.FixedAmount, "333333")
}
sell := tx.Swaps[3]
if sell.Program != SolProgramPump || sell.Event != "sell" {
t.Fatalf("swap[3] = %s/%s, want Pump/sell", sell.Program, sell.Event)
}
assertDecimalString(t, "swap[3].base_amount", sell.BaseAmount, "12282189230")
assertDecimalString(t, "swap[3].quote_amount", sell.QuoteAmount, "987647")
}
func TestPumpV2Discriminators(t *testing.T) {
tests := []struct {
name string
got [8]byte
want [8]byte
}{
{name: "buy_exact_sol_in", got: pumpBuyExactSolInDiscriminator, want: [8]byte{56, 252, 116, 8, 158, 223, 205, 95}},
{name: "buy_v2", got: pumpBuyV2Discriminator, want: [8]byte{184, 23, 238, 97, 103, 197, 211, 61}},
{name: "buy_exact_quote_in_v2", got: pumpBuyExactQuoteInV2Discriminator, want: [8]byte{194, 171, 28, 70, 104, 77, 91, 47}},
{name: "sell_v2", got: pumpSellV2Discriminator, want: [8]byte{93, 246, 130, 60, 231, 233, 64, 178}},
{name: "create_v2", got: pumpCreateV2Discriminator, want: [8]byte{214, 144, 76, 236, 95, 139, 49, 180}},
{name: "migrate_v2", got: pumpMigrateV2Discriminator, want: [8]byte{187, 203, 18, 31, 206, 237, 254, 41}},
}
for _, tt := range tests {
if tt.got != tt.want {
t.Fatalf("%s discriminator = %v, want %v", tt.name, tt.got, tt.want)
}
}
}
func TestPumpMigrateLayoutV2(t *testing.T) {
accounts := make([]int, 27)
for i := range accounts {
accounts[i] = i
}
layout, ok := pumpMigrateLayout(Instruction{
Data: pumpMigrateV2Discriminator[:],
Accounts: accounts,
})
if !ok {
t.Fatal("migrate_v2 layout not recognized")
}
if !layout.IsV2 ||
layout.BaseMint != 2 ||
layout.QuoteMint != 3 ||
layout.Pool != 4 ||
layout.BasePoolToken != 5 ||
layout.QuotePoolToken != 6 ||
layout.User != 7 ||
layout.BaseTokenProgram != 19 ||
layout.QuoteTokenProgram != 20 {
t.Fatalf("migrate_v2 layout = %+v", layout)
}
}
func TestPumpTradeAmountInfoV2(t *testing.T) {
tests := []struct {
name string
disc [8]byte
wantMode SwapMode
}{
{name: "legacy exact quote in", disc: pumpBuyExactSolInDiscriminator, wantMode: SwapModeExactIn},
{name: "v2 exact quote in", disc: pumpBuyExactQuoteInV2Discriminator, wantMode: SwapModeExactIn},
{name: "v2 buy exact out", disc: pumpBuyV2Discriminator, wantMode: SwapModeExactOut},
{name: "v2 sell exact in", disc: pumpSellV2Discriminator, wantMode: SwapModeExactIn},
}
for _, tt := range tests {
mode, fixed, limit, ok := pumpTradeAmountInfoFromArgs(PumpTradeArgs{
Discriminator: tt.disc,
Amount1: 11,
Amount2: 22,
})
if !ok {
t.Fatalf("%s not recognized", tt.name)
}
if mode != tt.wantMode {
t.Fatalf("%s mode = %s, want %s", tt.name, mode.String(), tt.wantMode.String())
}
if fixed.String() != "11" || limit.String() != "22" {
t.Fatalf("%s fixed/limit = %s/%s, want 11/22", tt.name, fixed, limit)
}
}
}
func TestPumpCreateQuoteAccountsOptional(t *testing.T) {
createAccounts := make([]int, 17)
for i := range createAccounts {
createAccounts[i] = i
}
createV2Accounts := make([]int, 19)
for i := range createV2Accounts {
createV2Accounts[i] = i
}
createV2Accounts[16] = 14
createV2Accounts[18] = 16
accountList := make([]solana.PublicKey, 19)
accountList[14] = usdcMint
accountList[16] = solana.TokenProgramID
accountList[18] = solana.TokenProgramID
result := &RawTx{accountList: accountList}
quoteMint, quoteTokenProgram, quoteDecimals := pumpCreateQuoteAccounts(result, Instruction{
Data: pumpCreateDiscriminator[:],
Accounts: createAccounts,
}, PumpCreateEvent{})
if !quoteMint.IsZero() || !quoteTokenProgram.IsZero() || quoteDecimals != 9 {
t.Fatalf("create quote accounts = %s/%s/%d, want zero/zero/9", quoteMint, quoteTokenProgram, quoteDecimals)
}
quoteMint, quoteTokenProgram, quoteDecimals = pumpCreateQuoteAccounts(result, Instruction{
Data: pumpCreateV2Discriminator[:],
Accounts: createV2Accounts,
}, PumpCreateEvent{})
if !quoteMint.Equals(usdcMint) || !quoteTokenProgram.Equals(solana.TokenProgramID) || quoteDecimals != 6 {
t.Fatalf("create_v2 quote accounts = %s/%s/%d, want USDC/token/6", quoteMint, quoteTokenProgram, quoteDecimals)
}
}
func TestDecodePumpTradeEventV2QuoteFields(t *testing.T) {
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
want := PumpTradeEvent{
Mint: mint,
SolAmount: 1,
TokenAmount: 2,
IsBuy: true,
User: user,
VirtualTokenReserves: 3,
RealTokenReserves: 4,
IxName: "buy_v2",
Shareholders: []PumpShareholder{{Address: user, ShareBps: 250}},
QuoteMint: usdcMint,
QuoteAmount: 5,
VirtualQuoteReserves: 6,
RealQuoteReserves: 7,
}
var buf bytes.Buffer
if err := agbinary.NewBorshEncoder(&buf).Encode(want); err != nil {
t.Fatalf("encode v2 trade event: %v", err)
}
got, err := decodePumpTradeEvent(buf.Bytes())
if err != nil {
t.Fatalf("decodePumpTradeEvent() error = %v", err)
}
if !got.QuoteMint.Equals(usdcMint) || got.QuoteAmount != 5 || got.VirtualQuoteReserves != 6 || got.RealQuoteReserves != 7 {
t.Fatalf("decoded quote fields = %s/%d/%d/%d", got.QuoteMint, got.QuoteAmount, got.VirtualQuoteReserves, got.RealQuoteReserves)
}
if len(got.Shareholders) != 1 || got.Shareholders[0].ShareBps != 250 {
t.Fatalf("decoded shareholders = %+v", got.Shareholders)
}
}
func TestDecodePumpTradeEventLegacyFallback(t *testing.T) {
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
data, err := hex.DecodeString(hexData)
if err != nil {
t.Fatalf("decode hex: %v", err)
}
got, err := decodePumpTradeEvent(data[16:])
if err != nil {
t.Fatalf("decodePumpTradeEvent() legacy error = %v", err)
}
if got.IxName != "buy_exact_sol_in" || got.SolAmount != 11725956 || !got.IsBuy {
t.Fatalf("legacy event = %+v", got)
}
if !got.QuoteMint.IsZero() || got.QuoteAmount != 0 || got.RealQuoteReserves != 0 {
t.Fatalf("legacy quote fields = %s/%d/%d, want zero", got.QuoteMint, got.QuoteAmount, got.RealQuoteReserves)
}
}

189
rawtx.go
View File

@@ -1,8 +1,11 @@
package pump_parser
import (
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
bin "github.com/gagliardetto/binary"
@@ -105,10 +108,11 @@ func (tx *RawTx) GetBlockTime() *pgtype.Timestamptz {
}
type Instruction struct {
Accounts []int `json:"accounts"`
Data solana.Base58 `json:"data"`
ProgramIDIndex int `json:"programIdIndex"`
StackHeight *int `json:"stackHeight"`
Accounts []int `json:"accounts"`
Data solana.Base58 `json:"data"`
ProgramIDIndex int `json:"programIdIndex"`
StackHeight *int `json:"stackHeight"`
LogEvents []solana.Base64 `json:"logEvents,omitempty"`
}
type InnerInstructions struct {
Index int `json:"index"`
@@ -180,6 +184,11 @@ type Transaction struct {
Signatures []solana.Signature `json:"signatures"`
}
type RawTxConvertOptions struct {
IgnoreLogMessages bool
ParseLogEvents bool
}
func (tx *Transaction) UnmarshalJSON(data []byte) error {
if len(data) == 0 || (len(data) == 4 && string(data) == "null") {
// TODO: is this an error?
@@ -308,7 +317,8 @@ func marshalRpcTransactionErr(err any) string {
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, options ...RawTxConvertOptions) (*RawTx, error) {
option := rawTxConvertOption(options)
created := int64(0)
if blockTime != nil {
created = int64(*blockTime)
@@ -523,6 +533,8 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s
})
}
applyRawTxConvertLogOptions(sTx, option)
return sTx, nil
}
@@ -833,7 +845,8 @@ func isAccountOwner(account, owner, mint solana.PublicKey) (bool, error) {
return account == ata, nil
}
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64) (*RawTx, error) {
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64, options ...RawTxConvertOptions) (*RawTx, error) {
option := rawTxConvertOption(options)
sTx := &RawTx{
BlockTime: created,
Slot: y.Slot,
@@ -1002,6 +1015,8 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
})
}
applyRawTxConvertLogOptions(sTx, option)
// resolve the lookups
//{
// if sTx.Transaction.Message.IsVersioned() {
@@ -1021,6 +1036,168 @@ func newInt16(x uint16) *int {
return &y
}
func rawTxConvertOption(options []RawTxConvertOptions) RawTxConvertOptions {
out := RawTxConvertOptions{ParseLogEvents: true}
if len(options) > 0 {
out = options[0]
}
return out
}
func applyRawTxConvertLogOptions(tx *RawTx, option RawTxConvertOptions) {
if tx == nil {
return
}
if option.ParseLogEvents {
attachLogEventsToInstructions(tx, tx.Meta.LogMessages)
}
if option.IgnoreLogMessages {
tx.Meta.LogMessages = nil
}
}
type instructionLogFrame struct {
program string
instr *Instruction
}
type instructionLogTarget struct {
program string
stackHeight int
instr *Instruction
}
func attachLogEventsToInstructions(tx *RawTx, logMessages []string) {
if tx == nil || len(logMessages) == 0 {
return
}
targets := rawTxInstructionLogTargets(tx)
if len(targets) == 0 {
return
}
nextTarget := 0
var stack []instructionLogFrame
for _, logMessage := range logMessages {
if program, stackHeight, ok := parseProgramInvokeLog(logMessage); ok {
var instr *Instruction
for nextTarget < len(targets) {
target := targets[nextTarget]
nextTarget++
if target.program != program {
continue
}
if target.stackHeight != 0 && target.stackHeight != stackHeight {
continue
}
instr = target.instr
break
}
stack = append(stack, instructionLogFrame{program: program, instr: instr})
continue
}
if data, ok := parseProgramDataLog(logMessage); ok {
if len(stack) == 0 {
continue
}
top := stack[len(stack)-1]
if top.instr != nil {
top.instr.LogEvents = append(top.instr.LogEvents, data)
}
continue
}
if program, ok := parseProgramFinishedLog(logMessage); ok {
for i := len(stack) - 1; i >= 0; i-- {
if stack[i].program == program {
stack = stack[:i]
break
}
}
}
}
}
func rawTxInstructionLogTargets(tx *RawTx) []instructionLogTarget {
accountList := tx.getAccountList()
innerByOuter := make(map[int]*InnerInstructions, len(tx.Meta.InnerInstructions))
for i := range tx.Meta.InnerInstructions {
inner := &tx.Meta.InnerInstructions[i]
innerByOuter[inner.Index] = inner
}
out := make([]instructionLogTarget, 0, len(tx.Transaction.Message.Instructions))
for i := range tx.Transaction.Message.Instructions {
instr := &tx.Transaction.Message.Instructions[i]
if instr.ProgramIDIndex >= 0 && instr.ProgramIDIndex < len(accountList) {
out = append(out, instructionLogTarget{
program: accountList[instr.ProgramIDIndex].String(),
stackHeight: 1,
instr: instr,
})
}
if inner := innerByOuter[i]; inner != nil {
for j := range inner.Instructions {
innerInstr := &inner.Instructions[j]
if innerInstr.ProgramIDIndex < 0 || innerInstr.ProgramIDIndex >= len(accountList) {
continue
}
stackHeight := 0
if innerInstr.StackHeight != nil {
stackHeight = *innerInstr.StackHeight
}
out = append(out, instructionLogTarget{
program: accountList[innerInstr.ProgramIDIndex].String(),
stackHeight: stackHeight,
instr: innerInstr,
})
}
}
}
return out
}
func parseProgramInvokeLog(logMessage string) (string, int, bool) {
if !strings.HasPrefix(logMessage, "Program ") {
return "", 0, false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, suffix, ok := strings.Cut(rest, " invoke [")
if !ok {
return "", 0, false
}
suffix = strings.TrimSuffix(suffix, "]")
stackHeight, err := strconv.Atoi(suffix)
if err != nil {
return "", 0, false
}
return program, stackHeight, true
}
func parseProgramDataLog(logMessage string) (solana.Base64, bool) {
if !strings.HasPrefix(logMessage, "Program data: ") {
return nil, false
}
data, err := base64.StdEncoding.DecodeString(strings.TrimSpace(strings.TrimPrefix(logMessage, "Program data: ")))
if err != nil {
return nil, false
}
return solana.Base64(data), true
}
func parseProgramFinishedLog(logMessage string) (string, bool) {
if !strings.HasPrefix(logMessage, "Program ") {
return "", false
}
if !strings.HasSuffix(logMessage, " success") && !strings.Contains(logMessage, " failed:") {
return "", false
}
rest := strings.TrimPrefix(logMessage, "Program ")
program, _, ok := strings.Cut(rest, " ")
return program, ok
}
func newInt(x *uint32) *int {
if x == nil {
return nil

View File

@@ -13,7 +13,7 @@ import (
"github.com/shopspring/decimal"
)
const rawTxBinarySchemaVersionCurrent uint16 = 7
const rawTxBinarySchemaVersionCurrent uint16 = 10
var rawTxBinaryMagic = [4]byte{'P', 'R', 'T', 'X'}
var rawTxsBinaryMagic = [4]byte{'P', 'R', 'T', 'S'}
@@ -61,11 +61,11 @@ type RawTxMetaBinary struct {
}
type RawTxTokenBalanceBinary struct {
AccountIndex uint16
MintAccount uint16
OwnerAccount uint16
AccountIndex uint8
MintAccount uint8
OwnerAccount uint8
HasOwnerAccount bool
ProgramIDAccount uint16
ProgramIDAccount uint8
Decimals uint8
HasPreAmount bool
PreAmount string
@@ -270,10 +270,17 @@ func DecodeRawTxsBinaryReader(r io.Reader) iter.Seq2[*RawTx, error] {
}
func newRawTxBinaryWithAddressTable(tx *RawTx, addressTable []solana.PublicKey, addressIndex *txBinaryAddressIndex) (*RawTxBinary, error) {
accountList := tx.getAccountList()
accountList, err := rawTxBinaryEffectiveAccountList(tx)
if err != nil {
return nil, err
}
if uint64(len(accountList)) > uint64(math.MaxUint32) {
return nil, fmt.Errorf("account list exceeds uint32 capacity")
}
accountListIndex, err := newRawTxBinaryAccountListIndex(accountList)
if err != nil {
return nil, err
}
if uint64(len(tx.Transaction.Message.AccountKeys)) > uint64(math.MaxUint32) {
return nil, fmt.Errorf("message account key count exceeds uint32 capacity")
}
@@ -299,7 +306,7 @@ func newRawTxBinaryWithAddressTable(tx *RawTx, addressTable []solana.PublicKey,
out.AccountList = append(out.AccountList, ref)
}
meta, err := rawTxMetaToBinary(&tx.Meta, addressIndex)
meta, err := rawTxMetaToBinary(&tx.Meta, accountListIndex)
if err != nil {
return nil, err
}
@@ -520,7 +527,7 @@ func (tx *RawTxBinary) ToRawTx() (*RawTx, error) {
IndexWithinBlock: int64(tx.IndexWithinBlock),
Slot: tx.Slot,
Version: rawTxBinaryVersionValue(tx.Version),
Meta: rawTxMetaFromBinary(tx.Meta, tx.AddressTable),
Meta: rawTxMetaFromBinary(tx.Meta, accountList),
Transaction: rawTxTransactionFromBinary(tx.Transaction, tx.AddressTable),
}
if tx.AccountKeyCount > 0 && tx.AccountKeyCount <= uint32(len(accountList)) {
@@ -678,7 +685,7 @@ func rawTxBinaryReadTxBody(dec txBinaryBodyReader, tx *RawTxBinary, addressTable
return nil
}
func rawTxMetaToBinary(meta *Meta, addressIndex *txBinaryAddressIndex) (RawTxMetaBinary, error) {
func rawTxMetaToBinary(meta *Meta, accountListIndex map[solana.PublicKey]uint8) (RawTxMetaBinary, error) {
out := RawTxMetaBinary{
Err: cloneTransactionParsedError(meta.Err),
Fee: meta.Fee,
@@ -688,7 +695,7 @@ func rawTxMetaToBinary(meta *Meta, addressIndex *txBinaryAddressIndex) (RawTxMet
ComputeUnitsConsumed: meta.ComputeUnitsConsumed,
}
var err error
out.TokenBalances, err = rawTxTokenBalancesToBinary(meta.PreTokenBalances, meta.PostTokenBalances, addressIndex)
out.TokenBalances, err = rawTxTokenBalancesToBinary(meta.PreTokenBalances, meta.PostTokenBalances, accountListIndex)
if err != nil {
return out, fmt.Errorf("token_balances: %w", err)
}
@@ -715,86 +722,75 @@ func rawTxMessageToBinary(message *Message, addressIndex *txBinaryAddressIndex)
return out, nil
}
func rawTxTokenBalancesToBinary(preBalances []TokenBalance, postBalances []TokenBalance, addressIndex *txBinaryAddressIndex) ([]RawTxTokenBalanceBinary, error) {
func rawTxTokenBalancesToBinary(preBalances []TokenBalance, postBalances []TokenBalance, accountListIndex map[solana.PublicKey]uint8) ([]RawTxTokenBalanceBinary, error) {
out := make([]RawTxTokenBalanceBinary, 0, len(preBalances)+len(postBalances))
byAccountIndex := make(map[uint16]int, len(preBalances)+len(postBalances))
preByAccountIndex := make(map[uint8]int, len(preBalances))
postSeenByAccountIndex := make(map[uint8]struct{}, len(postBalances))
for i, balance := range preBalances {
encoded, err := rawTxTokenBalanceToBinary(balance, addressIndex)
encoded, err := rawTxTokenBalanceToBinary(balance, accountListIndex)
if err != nil {
return nil, fmt.Errorf("pre[%d]: %w", i, err)
}
if _, exists := byAccountIndex[encoded.AccountIndex]; exists {
if _, exists := preByAccountIndex[encoded.AccountIndex]; exists {
return nil, fmt.Errorf("pre[%d].account_index duplicate: %d", i, encoded.AccountIndex)
}
encoded.HasPreAmount = true
encoded.PreAmount = balance.UITokenAmount.Amount
byAccountIndex[encoded.AccountIndex] = len(out)
preByAccountIndex[encoded.AccountIndex] = len(out)
out = append(out, encoded)
}
for i, balance := range postBalances {
encoded, err := rawTxTokenBalanceToBinary(balance, addressIndex)
encoded, err := rawTxTokenBalanceToBinary(balance, accountListIndex)
if err != nil {
return nil, fmt.Errorf("post[%d]: %w", i, err)
}
if existingIndex, exists := byAccountIndex[encoded.AccountIndex]; exists {
if out[existingIndex].HasPostAmount {
return nil, fmt.Errorf("post[%d].account_index duplicate: %d", i, encoded.AccountIndex)
}
if !rawTxTokenBalanceBinarySameIdentity(out[existingIndex], encoded) {
return nil, fmt.Errorf("post[%d].account_index %d identity mismatch", i, encoded.AccountIndex)
}
if _, exists := postSeenByAccountIndex[encoded.AccountIndex]; exists {
return nil, fmt.Errorf("post[%d].account_index duplicate: %d", i, encoded.AccountIndex)
}
postSeenByAccountIndex[encoded.AccountIndex] = struct{}{}
if existingIndex, exists := preByAccountIndex[encoded.AccountIndex]; exists && rawTxTokenBalanceBinarySameIdentity(out[existingIndex], encoded) {
out[existingIndex].HasPostAmount = true
out[existingIndex].PostAmount = balance.UITokenAmount.Amount
continue
}
encoded.HasPostAmount = true
encoded.PostAmount = balance.UITokenAmount.Amount
byAccountIndex[encoded.AccountIndex] = len(out)
out = append(out, encoded)
}
return out, nil
}
func rawTxTokenBalanceToBinary(balance TokenBalance, addressIndex *txBinaryAddressIndex) (RawTxTokenBalanceBinary, error) {
func rawTxTokenBalanceToBinary(balance TokenBalance, accountListIndex map[solana.PublicKey]uint8) (RawTxTokenBalanceBinary, error) {
mintAccount, ownerAccount, programIDAccount, err := rawTxBinaryTokenBalanceAccounts(balance)
if err != nil {
return RawTxTokenBalanceBinary{}, err
}
mint, err := addressIndex.id(mintAccount)
if err != nil {
return RawTxTokenBalanceBinary{}, fmt.Errorf("mint: %w", err)
mint, ok := accountListIndex[mintAccount]
if !ok {
return RawTxTokenBalanceBinary{}, fmt.Errorf("mint account not found in account_list: %s", mintAccount)
}
programID, err := addressIndex.id(programIDAccount)
if err != nil {
return RawTxTokenBalanceBinary{}, fmt.Errorf("program_id: %w", err)
}
if mint > math.MaxUint16 {
return RawTxTokenBalanceBinary{}, fmt.Errorf("mint ref overflows uint16: %d", mint)
}
if programID > math.MaxUint16 {
return RawTxTokenBalanceBinary{}, fmt.Errorf("program_id ref overflows uint16: %d", programID)
programID, ok := accountListIndex[programIDAccount]
if !ok {
return RawTxTokenBalanceBinary{}, fmt.Errorf("program_id account not found in account_list: %s", programIDAccount)
}
if balance.UITokenAmount.Decimals > math.MaxUint8 {
return RawTxTokenBalanceBinary{}, fmt.Errorf("decimals overflows uint8: %d", balance.UITokenAmount.Decimals)
}
if balance.AccountIndex < 0 || balance.AccountIndex > math.MaxUint16 {
return RawTxTokenBalanceBinary{}, fmt.Errorf("account_index overflows uint16: %d", balance.AccountIndex)
if balance.AccountIndex < 0 || balance.AccountIndex > math.MaxUint8 {
return RawTxTokenBalanceBinary{}, fmt.Errorf("account_index overflows uint8: %d", balance.AccountIndex)
}
encoded := RawTxTokenBalanceBinary{
AccountIndex: uint16(balance.AccountIndex),
MintAccount: uint16(mint),
ProgramIDAccount: uint16(programID),
AccountIndex: uint8(balance.AccountIndex),
MintAccount: mint,
ProgramIDAccount: programID,
Decimals: uint8(balance.UITokenAmount.Decimals),
}
if ownerAccount != nil {
owner, err := addressIndex.id(*ownerAccount)
if err != nil {
return RawTxTokenBalanceBinary{}, fmt.Errorf("owner: %w", err)
owner, ok := accountListIndex[*ownerAccount]
if !ok {
return RawTxTokenBalanceBinary{}, fmt.Errorf("owner account not found in account_list: %s", *ownerAccount)
}
if owner > math.MaxUint16 {
return RawTxTokenBalanceBinary{}, fmt.Errorf("owner ref overflows uint16: %d", owner)
}
encoded.OwnerAccount = uint16(owner)
encoded.OwnerAccount = owner
encoded.HasOwnerAccount = true
}
return encoded, nil
@@ -809,8 +805,8 @@ func rawTxTokenBalanceBinarySameIdentity(a, b RawTxTokenBalanceBinary) bool {
a.Decimals == b.Decimals
}
func rawTxMetaFromBinary(meta RawTxMetaBinary, addressTable []solana.PublicKey) Meta {
preTokenBalances, postTokenBalances := rawTxTokenBalancesFromBinary(meta.TokenBalances, addressTable)
func rawTxMetaFromBinary(meta RawTxMetaBinary, accountList []solana.PublicKey) Meta {
preTokenBalances, postTokenBalances := rawTxTokenBalancesFromBinary(meta.TokenBalances, accountList)
return Meta{
Err: cloneTransactionParsedError(meta.Err),
Fee: meta.Fee,
@@ -838,15 +834,15 @@ func rawTxTransactionFromBinary(tx RawTxTransactionBinary, addressTable []solana
return out
}
func rawTxTokenBalancesFromBinary(balances []RawTxTokenBalanceBinary, addressTable []solana.PublicKey) ([]TokenBalance, []TokenBalance) {
func rawTxTokenBalancesFromBinary(balances []RawTxTokenBalanceBinary, accountList []solana.PublicKey) ([]TokenBalance, []TokenBalance) {
pre := make([]TokenBalance, 0, len(balances))
post := make([]TokenBalance, 0, len(balances))
for _, balance := range balances {
mint, _ := txBinaryAddressAt(addressTable, uint32(balance.MintAccount), "token_balance.mint")
programID, _ := txBinaryAddressAt(addressTable, uint32(balance.ProgramIDAccount), "token_balance.program_id")
mint, _ := txBinaryAddressAt(accountList, uint32(balance.MintAccount), "token_balance.mint")
programID, _ := txBinaryAddressAt(accountList, uint32(balance.ProgramIDAccount), "token_balance.program_id")
var owner *solana.PublicKey
if balance.HasOwnerAccount {
ownerKey, _ := txBinaryAddressAt(addressTable, uint32(balance.OwnerAccount), "token_balance.owner")
ownerKey, _ := txBinaryAddressAt(accountList, uint32(balance.OwnerAccount), "token_balance.owner")
owner = &ownerKey
}
if balance.HasPreAmount {
@@ -1065,10 +1061,10 @@ func readInnerInstructions(dec txBinaryBodyReader) ([]InnerInstructions, error)
func writeInstructions(enc *txBinaryEncoder, values []Instruction) error {
enc.writeUint32(uint32(len(values)))
for i, value := range values {
if value.ProgramIDIndex < 0 || value.ProgramIDIndex > math.MaxUint16 {
return fmt.Errorf("[%d].program_id_index overflows uint16: %d", i, value.ProgramIDIndex)
if value.ProgramIDIndex < 0 || value.ProgramIDIndex > math.MaxUint8 {
return fmt.Errorf("[%d].program_id_index overflows uint8: %d", i, value.ProgramIDIndex)
}
enc.writeUint16(uint16(value.ProgramIDIndex))
enc.writeUint8(uint8(value.ProgramIDIndex))
if err := writeAccountIndexSlice(enc, value.Accounts); err != nil {
return fmt.Errorf("[%d].accounts: %w", i, err)
}
@@ -1077,6 +1073,10 @@ func writeInstructions(enc *txBinaryEncoder, values []Instruction) error {
if value.StackHeight != nil {
enc.writeUint32(uint32(*value.StackHeight))
}
enc.writeUint32(uint32(len(value.LogEvents)))
for _, event := range value.LogEvents {
writeByteSlice(enc, event)
}
}
return nil
}
@@ -1088,7 +1088,7 @@ func readInstructions(dec txBinaryBodyReader) ([]Instruction, error) {
}
out := make([]Instruction, 0, count)
for i := uint32(0); i < count; i++ {
programIDIndex, err := dec.readUint16()
programIDIndex, err := dec.readUint8()
if err != nil {
return nil, err
}
@@ -1113,11 +1113,24 @@ func readInstructions(dec txBinaryBodyReader) ([]Instruction, error) {
sh := int(rawStackHeight)
stackHeight = &sh
}
logEventCount, err := dec.readUint32()
if err != nil {
return nil, err
}
logEvents := make([]solana.Base64, 0, logEventCount)
for j := uint32(0); j < logEventCount; j++ {
eventData, err := readByteSlice(dec)
if err != nil {
return nil, err
}
logEvents = append(logEvents, solana.Base64(eventData))
}
out = append(out, Instruction{
ProgramIDIndex: int(programIDIndex),
Accounts: accounts,
Data: solana.Base58(data),
StackHeight: stackHeight,
LogEvents: logEvents,
})
}
return out, nil
@@ -1126,13 +1139,13 @@ func readInstructions(dec txBinaryBodyReader) ([]Instruction, error) {
func writeTokenBalances(enc *txBinaryEncoder, values []RawTxTokenBalanceBinary) error {
enc.writeUint32(uint32(len(values)))
for i, value := range values {
enc.writeUint16(value.AccountIndex)
enc.writeUint16(value.MintAccount)
enc.writeUint8(value.AccountIndex)
enc.writeUint8(value.MintAccount)
enc.writeBool(value.HasOwnerAccount)
if value.HasOwnerAccount {
enc.writeUint16(value.OwnerAccount)
enc.writeUint8(value.OwnerAccount)
}
enc.writeUint16(value.ProgramIDAccount)
enc.writeUint8(value.ProgramIDAccount)
enc.writeUint8(value.Decimals)
enc.writeBool(value.HasPreAmount)
if value.HasPreAmount {
@@ -1158,21 +1171,21 @@ func readTokenBalances(dec txBinaryBodyReader) ([]RawTxTokenBalanceBinary, error
out := make([]RawTxTokenBalanceBinary, 0, count)
for i := uint32(0); i < count; i++ {
value := RawTxTokenBalanceBinary{}
if value.AccountIndex, err = dec.readUint16(); err != nil {
if value.AccountIndex, err = dec.readUint8(); err != nil {
return nil, err
}
if value.MintAccount, err = dec.readUint16(); err != nil {
if value.MintAccount, err = dec.readUint8(); err != nil {
return nil, err
}
if value.HasOwnerAccount, err = dec.readBool(); err != nil {
return nil, err
}
if value.HasOwnerAccount {
if value.OwnerAccount, err = dec.readUint16(); err != nil {
if value.OwnerAccount, err = dec.readUint8(); err != nil {
return nil, err
}
}
if value.ProgramIDAccount, err = dec.readUint16(); err != nil {
if value.ProgramIDAccount, err = dec.readUint8(); err != nil {
return nil, err
}
if value.Decimals, err = dec.readUint8(); err != nil {
@@ -1336,10 +1349,10 @@ func readUint32Slice(dec txBinaryBodyReader) ([]uint32, error) {
func writeAccountIndexSlice(enc *txBinaryEncoder, values []int) error {
enc.writeUint32(uint32(len(values)))
for i, value := range values {
if value < 0 || value > math.MaxUint16 {
return fmt.Errorf("[%d] overflows uint16: %d", i, value)
if value < 0 || value > math.MaxUint8 {
return fmt.Errorf("[%d] overflows uint8: %d", i, value)
}
enc.writeUint16(uint16(value))
enc.writeUint8(uint8(value))
}
return nil
}
@@ -1351,7 +1364,7 @@ func readAccountIndexSlice(dec txBinaryBodyReader) ([]int, error) {
}
out := make([]int, 0, count)
for i := uint32(0); i < count; i++ {
value, err := dec.readUint16()
value, err := dec.readUint8()
if err != nil {
return nil, err
}
@@ -1547,7 +1560,11 @@ func rawTxBinaryBuildAddressTable(txs []*RawTx) ([]solana.PublicKey, error) {
if tx == nil {
return nil, fmt.Errorf("tx[%d] is nil", txIndex)
}
for accountIndex, account := range tx.getAccountList() {
accountList, err := rawTxBinaryEffectiveAccountList(tx)
if err != nil {
return nil, fmt.Errorf("tx[%d].account_list: %w", txIndex, err)
}
for accountIndex, account := range accountList {
if err := builder.add(account); err != nil {
return nil, fmt.Errorf("tx[%d].account_list[%d]: %w", txIndex, accountIndex, err)
}
@@ -1557,20 +1574,64 @@ func rawTxBinaryBuildAddressTable(txs []*RawTx) ([]solana.PublicKey, error) {
return nil, fmt.Errorf("tx[%d].address_table_lookups[%d].account_key: %w", txIndex, lookupIndex, err)
}
}
for balanceIndex, balance := range tx.Meta.PreTokenBalances {
if err := rawTxBinaryAddTokenBalanceAddresses(&builder, balance); err != nil {
return nil, fmt.Errorf("tx[%d].pre_token_balances[%d]: %w", txIndex, balanceIndex, err)
}
}
for balanceIndex, balance := range tx.Meta.PostTokenBalances {
if err := rawTxBinaryAddTokenBalanceAddresses(&builder, balance); err != nil {
return nil, fmt.Errorf("tx[%d].post_token_balances[%d]: %w", txIndex, balanceIndex, err)
}
}
}
return builder.addresses, nil
}
func rawTxBinaryEffectiveAccountList(tx *RawTx) ([]solana.PublicKey, error) {
accountList := append([]solana.PublicKey(nil), tx.getAccountList()...)
seen := make(map[solana.PublicKey]struct{}, len(accountList))
for _, account := range accountList {
seen[account] = struct{}{}
}
for balanceIndex, balance := range tx.Meta.PreTokenBalances {
if err := rawTxBinaryAppendTokenBalanceAccounts(&accountList, seen, balance); err != nil {
return nil, fmt.Errorf("pre_token_balances[%d]: %w", balanceIndex, err)
}
}
for balanceIndex, balance := range tx.Meta.PostTokenBalances {
if err := rawTxBinaryAppendTokenBalanceAccounts(&accountList, seen, balance); err != nil {
return nil, fmt.Errorf("post_token_balances[%d]: %w", balanceIndex, err)
}
}
return accountList, nil
}
func rawTxBinaryAppendTokenBalanceAccounts(accountList *[]solana.PublicKey, seen map[solana.PublicKey]struct{}, balance TokenBalance) error {
mintAccount, ownerAccount, programIDAccount, err := rawTxBinaryTokenBalanceAccounts(balance)
if err != nil {
return err
}
rawTxBinaryAppendAccountIfMissing(accountList, seen, mintAccount)
if ownerAccount != nil {
rawTxBinaryAppendAccountIfMissing(accountList, seen, *ownerAccount)
}
rawTxBinaryAppendAccountIfMissing(accountList, seen, programIDAccount)
return nil
}
func rawTxBinaryAppendAccountIfMissing(accountList *[]solana.PublicKey, seen map[solana.PublicKey]struct{}, account solana.PublicKey) {
if _, exists := seen[account]; exists {
return
}
seen[account] = struct{}{}
*accountList = append(*accountList, account)
}
func newRawTxBinaryAccountListIndex(accountList []solana.PublicKey) (map[solana.PublicKey]uint8, error) {
out := make(map[solana.PublicKey]uint8, len(accountList))
for i, account := range accountList {
if i > math.MaxUint8 {
return nil, fmt.Errorf("account_list index overflows uint8: %d", i)
}
if _, exists := out[account]; exists {
continue
}
out[account] = uint8(i)
}
return out, nil
}
func rawTxBinarySharedBlockTime(txs []*RawTx, field string) (int64, error) {
if len(txs) == 0 {
return 0, nil
@@ -1672,6 +1733,7 @@ func cloneInstructions(values []Instruction) []Instruction {
Accounts: append([]int(nil), value.Accounts...),
Data: append(solana.Base58(nil), value.Data...),
ProgramIDIndex: value.ProgramIDIndex,
LogEvents: cloneBase64Slice(value.LogEvents),
}
if value.StackHeight != nil {
stackHeight := *value.StackHeight
@@ -1682,6 +1744,14 @@ func cloneInstructions(values []Instruction) []Instruction {
return out
}
func cloneBase64Slice(values []solana.Base64) []solana.Base64 {
out := make([]solana.Base64, 0, len(values))
for _, value := range values {
out = append(out, append(solana.Base64(nil), value...))
}
return out
}
func rawTxBinaryVersionID(version interface{}) uint8 {
switch value := version.(type) {
case solana.MessageVersion:

File diff suppressed because one or more lines are too long