This commit is contained in:
2026-01-07 12:19:12 +08:00
13 changed files with 4069 additions and 84 deletions

62
cmd/debug_jupv6/main.go Normal file
View File

@@ -0,0 +1,62 @@
package main
import (
"encoding/hex"
"fmt"
"os"
)
func main() {
hexData := "bb64facc31c4af14be34e6edcc0000006f03a4df67000000b903320000000300000064342100024b00000000dc0500026310270203"
b, err := hex.DecodeString(hexData)
if err != nil {
panic(err)
}
payload := b[8:]
off := 0
read := func(n int) []byte {
if off+n > len(payload) {
fmt.Printf("OOB read: off=%d n=%d len=%d\n", off, n, len(payload))
os.Exit(1)
}
out := payload[off : off+n]
off += n
return out
}
u8 := func() uint8 { return read(1)[0] }
leU16 := func() uint16 {
b := read(2)
return uint16(b[0]) | uint16(b[1])<<8
}
leU32 := func() uint32 {
b := read(4)
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
leU64 := func() uint64 {
b := read(8)
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
}
fmt.Printf("payload len=%d\n", len(payload))
amountIn := leU64()
quotedOut := leU64()
slippage := leU16()
platform := leU16()
posSlip := leU16()
fmt.Printf("in=%d out=%d slip=%d plat=%d pos=%d\n", amountIn, quotedOut, slippage, platform, posSlip)
planLen := leU32()
fmt.Printf("planLen=%d\n", planLen)
for i := uint32(0); i < planLen; i++ {
swapTag := u8()
fmt.Printf("step[%d] swapTag=%d (0x%02x) off=%d\n", i, swapTag, swapTag, off)
// payload depends on swapTag; we don't know, so just print next few bytes and stop
bps := leU16()
inIdx := u8()
outIdx := u8()
fmt.Printf(" bps=%d inIdx=%d outIdx=%d off=%d\n", bps, inIdx, outIdx, off)
}
fmt.Printf("done off=%d\n", off)
}

View File

@@ -2,13 +2,15 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/gagliardetto/solana-go/rpc"
"github.com/samlior/libsam/pkg/shreder"
)
@@ -17,13 +19,25 @@ func main() {
if url == "" {
panic("URL is not set")
}
rpcUrl := os.Getenv("RPC_URL")
if rpcUrl == "" {
panic("RPC_URL is not set")
}
rpcClient := rpc.New(rpcUrl)
shreder.SetLogLevel(slog.LevelDebug)
shrederClient, cleanup, err := shreder.NewShrederClient(
url,
rpcClient,
map[string]*shreder.SubscribeRequestFilterTransactions{
"pumpfunamm": {
AccountRequired: []string{
//AccountRequired: []string{
// "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
//},
AccountInclude: []string{
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR", //Event Authority
"5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx", // Fee Config
"pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ", // pump fee program
},
},
"photon": {
@@ -31,6 +45,11 @@ func main() {
"BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW",
},
},
"jupiterV6": {
AccountRequired: []string{
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
},
},
// TODO: axiom, gmgn, etc.
})
if err != nil {
@@ -63,8 +82,13 @@ func main() {
case <-ctx.Done():
return
case txBatch := <-txCh:
jsonData, _ := json.MarshalIndent(txBatch, "", " ")
fmt.Println(string(jsonData))
//jsonData, _ := json.MarshalIndent(txBatch, "", " ")
for _, tx := range txBatch {
if tx.Label == "jupiterV6" {
fmt.Println("===============", tx.TxHash, tx.Token0Address, tx.Token0Amount)
}
}
//fmt.Println(txBatch[0].TxHash)
}
}
}

3
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/gagliardetto/solana-go v1.12.0
github.com/mr-tron/base58 v1.2.0
github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454
github.com/panjf2000/ants/v2 v2.11.4
github.com/shopspring/decimal v1.4.0
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.10
@@ -21,6 +22,7 @@ require (
github.com/gagliardetto/binary v0.8.0 // indirect
github.com/gagliardetto/treeout v0.1.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
@@ -38,6 +40,7 @@ require (
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect

9
go.sum
View File

@@ -36,6 +36,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
@@ -68,6 +70,8 @@ github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454 h1:lFN7TVecCMbCHVN
github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454/go.mod h1:NeMochZp7jN/pYFuxLkrZtmLqbADmnp/y1+/dL+AsyQ=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/panjf2000/ants/v2 v2.11.4 h1:UJQbtN1jIcI5CYNocTj0fuAUYvsLjPoYi0YuhqV/Y48=
github.com/panjf2000/ants/v2 v2.11.4/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -81,8 +85,9 @@ github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:Vl
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -140,6 +145,8 @@ golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,106 @@
package shreder
import (
"context"
"fmt"
"sync"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/panjf2000/ants/v2"
)
type AddressTables struct {
rpcClient *rpc.Client
mux sync.RWMutex
loadMux sync.Mutex
tables *lru.Cache[solana.PublicKey, []solana.PublicKey]
loading map[solana.PublicKey]struct{}
pool *ants.Pool
}
func NewAddressTables(rpcClient *rpc.Client) *AddressTables {
pool, _ := ants.NewPool(5, ants.WithPreAlloc(true), ants.WithNonblocking(true))
cache, _ := lru.New[solana.PublicKey, []solana.PublicKey](10000)
return &AddressTables{
rpcClient: rpcClient,
tables: cache,
loading: make(map[solana.PublicKey]struct{}),
pool: pool,
}
}
func (at *AddressTables) loadAddressTable(tablePubkey solana.PublicKey) ([]solana.PublicKey, error) {
// decode acc
acc, err := at.rpcClient.GetAccountInfoWithOpts(context.Background(), tablePubkey, &rpc.GetAccountInfoOpts{
Encoding: solana.EncodingBase64,
})
if err != nil {
return nil, err
}
data := acc.GetBinary()
if len(data) <= 56 {
return nil, fmt.Errorf("account data too short")
}
offset := 56
var addresses solana.PublicKeySlice = make([]solana.PublicKey, 0, (len(data)-offset)/32)
for offset+32 <= len(data) {
addresses = append(addresses, solana.PublicKeyFromBytes(data[offset:offset+32]))
offset += 32
}
// addresses = append(addresses, solana.PublicKeyFromBytes(data[start:start+32]))
return addresses, nil
}
func (at *AddressTables) GetAddressTable(tablePubkey solana.PublicKey, idx []uint8) []solana.PublicKey {
at.mux.RLock()
addresses, ok := at.tables.Get(tablePubkey)
if !ok {
at.mux.RUnlock()
_ = at.pool.Submit(func() {
at.loadMux.Lock()
_, loading := at.loading[tablePubkey]
if loading {
at.loadMux.Unlock()
return
}
at.loading[tablePubkey] = struct{}{}
at.loadMux.Unlock()
table, err := at.loadAddressTable(tablePubkey)
if err != nil {
logger.Error("loadAddressTable failed", "err", err, "table", tablePubkey)
at.loadMux.Lock()
delete(at.loading, tablePubkey)
at.loadMux.Unlock()
return
}
at.loadMux.Lock()
delete(at.loading, tablePubkey)
at.loadMux.Unlock()
at.mux.Lock()
at.tables.Add(tablePubkey, table)
total := at.tables.Len()
at.mux.Unlock()
logger.Info("loadAddressTable", "table", tablePubkey.String(), "table count:", total)
})
return nil
}
at.mux.RUnlock()
var result solana.PublicKeySlice = make([]solana.PublicKey, 0, len(idx))
for _, i := range idx {
if int(i) >= len(addresses) {
logger.Error("over loadAddressTable failed", "idx", i, "table", tablePubkey)
//todo... update table?
continue
}
result = append(result, addresses[i])
}
return result
}

View File

@@ -2,35 +2,39 @@ package shreder
import (
"context"
"log/slog"
"fmt"
"github.com/gagliardetto/solana-go/rpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type Client struct {
log *slog.Logger
conn *grpc.ClientConn
client ShrederServiceClient
tableLoader *AddressTables
subscription map[string]*SubscribeRequestFilterTransactions
}
func NewShrederClient(
url string,
rpcClient *rpc.Client,
subscription map[string]*SubscribeRequestFilterTransactions,
) (*Client, func(), error) {
if rpcClient == nil {
return nil, func() {}, fmt.Errorf("rpc client is nil")
}
conn, err := grpc.NewClient(url, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, func() {}, err
}
logger := slog.Default()
s := &Client{
log: logger,
conn: conn,
client: NewShrederServiceClient(conn),
subscription: subscription,
tableLoader: NewAddressTables(rpcClient),
}
return s, func() {
@@ -39,14 +43,14 @@ func NewShrederClient(
}
func (c *Client) Wait() {
c.log.Debug("waiting for shreder client to stop")
logger.Debug("waiting for shreder client to stop")
err := c.conn.Close()
if err != nil {
c.log.Error("failed to close connection: ", "err", err)
logger.Error("failed to close connection: ", "err", err)
}
c.log.Debug("shreder client stopped")
logger.Debug("shreder client stopped")
}
func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error {
@@ -68,7 +72,7 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error
return err
}
txBatch := ParseTransaction(response.Transaction)
txBatch := ParseTransaction(response.Transaction, c.tableLoader)
if len(txBatch) == 0 {
continue
}

File diff suppressed because it is too large Load Diff

1068
pkg/shreder/juptierv6.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
package shreder
import (
"encoding/hex"
"testing"
)
func TestDecodeRouteV2Arg(t *testing.T) {
tests := []struct {
name string
hexData string
}{
{
name: "Jupiter V6 RouteV2Arg Test 0",
hexData: "bb64facc31c4af14809fd500000000002222e8db1800000064000a000000020000005601fe102700016310270102",
},
{
name: "Jupiter V6 RouteV2Arg Test 1",
hexData: "bb64facc31c4af144ff91634b90000004e6c4d05000000002c013200000003000000520000000000000000102700014f102701024310270203",
},
{
name: "Jupiter V6 RouteV2Arg Test 2",
hexData: "bb64facc31c4af14ba2eafa02c1d0000777a9b2200000000f4010a0000000100000052000000000000000010270001",
},
{
name: "Jupiter V6 RouteV2Arg Test 3",
hexData: "bb64facc31c4af144a3521186b07000030508d0e00000000c201320000000300000052000000000000000010270001740110270102590010270203",
},
{
name: "Jupiter V6 RouteV2Arg Test 4",
hexData: "bb64facc31c4af14092d05050000000013701f198c0100008102380100000300000059011027000168001027010251000000000000000010270203",
},
{
name: "Jupiter V6 RouteV2Arg Test 5",
hexData: "bb64facc31c4af1480969800000000006f44ad39bd0000001202320000000200000068001027000151000000000000000010270102",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
instrData, err := hex.DecodeString(tt.hexData)
if err != nil {
t.Fatalf("failed to decode hex string: %v", err)
return
}
t.Logf("raw bytes: %x", instrData[8:])
args, err := decodeJupiterV6RouteV2Arg(instrData[8:])
if err != nil {
t.Fatalf("failed to decode jupiter arguments: %v", err)
return
}
t.Logf("decoded args: %+v", args)
})
}
}
func TestDecodeRouteArg(t *testing.T) {
tests := []struct {
name string
hexData string
}{
{
name: "Jupiter V6 RouteArg Test 0",
hexData: "e517cb977ae3ad2a030000004f6400014f64010251000000000000000064020340420f00000000005c1c81900e000000640000",
},
{
name: "Jupiter V6 RouteArg Test 1",
hexData: "e517cb977ae3ad2a0200000028640001510000000000000000640102c09ee605000000005e1bc48efa000000d00700",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
instrData, err := hex.DecodeString(tt.hexData)
if err != nil {
t.Fatalf("failed to decode hex string: %v", err)
return
}
t.Logf("raw bytes: %x", instrData[8:])
args, err := decodeJupiterV6RouteArg(instrData[8:])
if err != nil {
t.Fatalf("failed to decode jupiter arguments: %v", err)
return
}
t.Logf("decoded args: %+v", args)
})
}
}

View File

@@ -0,0 +1,5 @@
package shreder
//func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
//
//}

View File

@@ -1,6 +1,8 @@
package shreder
import (
"log/slog"
"os"
"time"
"github.com/shopspring/decimal"
@@ -11,8 +13,23 @@ const (
SolDecimals = 9
)
var (
logger *slog.Logger
)
func init() {
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})
logger = slog.New(handler)
}
func SetLogLevel(level slog.Level) {
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})
logger = slog.New(handler)
}
type TxSignal struct {
Source string `json:"source"`
Label string `json:"label"`
TxHash string `json:"tx_hash"`
Maker string `json:"maker"`
Token0Address string `json:"token0_address"`

View File

@@ -4,8 +4,8 @@ import (
"bytes"
"encoding/binary"
"fmt"
"log/slog"
"math/big"
"strings"
"github.com/gagliardetto/solana-go"
"github.com/mr-tron/base58"
@@ -43,8 +43,23 @@ var (
flasProgramID = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9")
terminalProgramID = solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3")
jupiterV6ProgramID = solana.MustPublicKeyFromBase58("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4")
)
type AccountNotFoundError struct {
Index int
Len int
}
func NewAccountNotFoundError(i, l int) error {
return &AccountNotFoundError{i, l}
}
func (e AccountNotFoundError) Error() string {
return fmt.Sprintf("account index %d out of range, len=%d", e.Index, e.Len)
}
// instruction discriminators
var (
pumpCreateCoinIX = []byte{24, 30, 200, 40, 5, 28, 7, 119}
@@ -81,11 +96,6 @@ var (
terminalAmmSellTokensIX = []byte{0x40, 0x64, 0x97, 0xb9, 0x16, 0xfa, 0xec, 0xb1}
)
// table lookups
const (
photonTableLookup = "3r6paeFSLpeUVmWtShb5uZtXYpcBE3729kUxkUS7xKi1"
)
type compiledInstruction struct {
ProgramIDIndex uint8
Accounts []uint8
@@ -183,7 +193,7 @@ type fjszBuyArgs struct {
}
// ParseTransaction mirrors the Rust parse_transaction entry point.
func ParseTransaction(update *SubscribeUpdateTransaction) []*TxSignal {
func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables) []*TxSignal {
versioned, err := toVersionedTransaction(update)
if err != nil || versioned == nil || len(versioned.Signatures) == 0 {
return nil
@@ -193,6 +203,35 @@ func ParseTransaction(update *SubscribeUpdateTransaction) []*TxSignal {
staticKeys := versioned.Message.StaticAccountKeys
instructions := versioned.Message.Instructions
if loader != nil && len(versioned.Message.AddressTableLookups) > 0 {
lookupTableOk := true
for _, lookup := range versioned.Message.AddressTableLookups {
if len(lookup.WritableIndexes) == 0 {
continue
}
accounts := loader.GetAddressTable(lookup.AccountKey, lookup.WritableIndexes)
if len(accounts) != len(lookup.WritableIndexes) {
lookupTableOk = false
break
}
staticKeys = append(staticKeys, accounts...)
}
if lookupTableOk {
for _, lookup := range versioned.Message.AddressTableLookups {
if len(lookup.ReadonlyIndexes) == 0 {
continue
}
accounts := loader.GetAddressTable(lookup.AccountKey, lookup.ReadonlyIndexes)
if len(accounts) != len(lookup.ReadonlyIndexes) {
break
}
staticKeys = append(staticKeys, accounts...)
}
}
versioned.Message.StaticAccountKeys = staticKeys
}
var parsed []*TxSignal
for i := range instructions {
@@ -233,6 +272,9 @@ func ParseTransaction(update *SubscribeUpdateTransaction) []*TxSignal {
case terminalProgramID:
txRes, err := parseTermInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "terminal", terminalProgramID.String())
case jupiterV6ProgramID:
txRes, err := parseJupiterV6Instruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "jupiterv6", jupiterV6ProgramID.String())
}
}
@@ -241,7 +283,9 @@ func ParseTransaction(update *SubscribeUpdateTransaction) []*TxSignal {
func appendParsed(list []*TxSignal, parsed *TxSignal, err error, txHash [64]byte, label string, entryContract string) []*TxSignal {
if err != nil {
slog.Error("txparser: failed to parse", "label", label, "instruction", err, "tx_hash", base58.Encode(txHash[:]))
if !strings.HasPrefix(err.Error(), "account index") {
logger.Debug("txparser: failed to parse", "label", label, "instruction", err, "tx_hash", base58.Encode(txHash[:]))
}
return list
}
if parsed != nil {
@@ -313,7 +357,7 @@ func formatSolAmount(lamports uint64) decimal.Decimal {
func getStaticKey(static []solana.PublicKey, index int) (solana.PublicKey, error) {
if index < 0 || index >= len(static) {
return solana.PublicKey{}, fmt.Errorf("account index %d out of bounds", index)
return solana.PublicKey{}, NewAccountNotFoundError(index, len(static))
}
return static[index], nil
}
@@ -359,6 +403,7 @@ func parsePumpCreate(tx *versionedTransaction, instruction *compiledInstruction)
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: creator.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -379,7 +424,7 @@ func parsePumpCreateV2(tx *versionedTransaction, instruction *compiledInstructio
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 8 {
return nil, fmt.Errorf("data too short for create v2 args")
return nil, fmt.Errorf("data too short for pump create v2 args, len=%d", len(instruction.Data))
}
staticKeys := tx.Message.StaticAccountKeys
@@ -399,6 +444,7 @@ func parsePumpCreateV2(tx *versionedTransaction, instruction *compiledInstructio
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: args.Creator.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -416,7 +462,7 @@ func parsePumpCreateV2(tx *versionedTransaction, instruction *compiledInstructio
func decodePumpBuyArgs(data []byte) (uint64, uint64, error) {
if len(data) < 9 {
return 0, 0, fmt.Errorf("data too short for buy args")
return 0, 0, fmt.Errorf("data too short for pump buy buy args, len=%d", len(data))
}
var args pumpBuyArgs
@@ -462,6 +508,7 @@ func parsePumpBuy(tx *versionedTransaction, instruction *compiledInstruction) (*
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: buyer.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -480,7 +527,7 @@ func parsePumpBuy(tx *versionedTransaction, instruction *compiledInstruction) (*
func decodePumpSellArgs(data []byte) (uint64, uint64, error) {
if len(data) < 9 {
return 0, 0, fmt.Errorf("data too short for sell args")
return 0, 0, fmt.Errorf("data too short for pump sell sell args, len=%d", len(data))
}
var args pumpExtendedSellArgs
@@ -519,6 +566,7 @@ func parsePumpSell(tx *versionedTransaction, instruction *compiledInstruction) (
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: seller.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -570,13 +618,14 @@ func parseAzczAmmBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal,
}
if len(instruction.Data) < 17 {
return nil, fmt.Errorf("data too short for buy args")
return nil, fmt.Errorf("data too short for azcz amm buy args, len=%d", len(instruction.Data))
}
solAmount := binary.LittleEndian.Uint64(instruction.Data[1:9])
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "azcz",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -610,7 +659,7 @@ func parseAzczBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, er
}
if len(instruction.Data) < 2 {
return nil, fmt.Errorf("data too short for buy args")
return nil, fmt.Errorf("data too short for azcz buy args len=%d", len(instruction.Data))
}
var args azczBuyArgs
@@ -620,6 +669,7 @@ func parseAzczBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, er
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "azcz",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -664,7 +714,7 @@ func parseF5tfInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
}
if len(instruction.Data) < 2 {
return nil, fmt.Errorf("data too short for buy args")
return nil, fmt.Errorf("data too short for f5tf buy args len=%d", len(instruction.Data))
}
var args f5tfBuyArgs
@@ -674,6 +724,7 @@ func parseF5tfInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "f5tf",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -699,8 +750,11 @@ func parseFlasInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if len(instruction.Data) == 10 && instruction.Data[0] == 1 {
return nil, nil
}
if len(instruction.Data) < 20 {
return nil, fmt.Errorf("data too short for args")
return nil, fmt.Errorf("data too short for args flas instruction, len: %d", len(instruction.Data))
}
methodData := instruction.Data[17:20]
if !matchMethod(methodData, flasBuyTokensIX) {
@@ -741,6 +795,7 @@ func parseFlasAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -780,6 +835,7 @@ func parseFlasAmmBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal,
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -819,6 +875,7 @@ func parseFlasSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, e
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -861,6 +918,7 @@ func parseFlasBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, er
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -906,7 +964,7 @@ func parsePhotonBuy(tx *versionedTransaction, instruction *compiledInstruction)
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 16 {
return nil, fmt.Errorf("data too short for buy args")
return nil, fmt.Errorf("data too short for photon buy args, len=%d", len(instruction.Data))
}
staticKeys := tx.Message.StaticAccountKeys
@@ -927,6 +985,7 @@ func parsePhotonBuy(tx *versionedTransaction, instruction *compiledInstruction)
solAmount := args.SolAmount * (100000000 - 1234568) / 100000000
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "photon",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -947,7 +1006,7 @@ func parsePhotonSwap(tx *versionedTransaction, instruction *compiledInstruction)
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 16 {
return nil, fmt.Errorf("data too short for swap args")
return nil, fmt.Errorf("data too short for swap args for photon. len=%d", len(instruction.Data))
}
staticKeys := tx.Message.StaticAccountKeys
@@ -956,12 +1015,11 @@ func parsePhotonSwap(tx *versionedTransaction, instruction *compiledInstruction)
return nil, err
}
quoteIndex := int(instruction.Accounts[4])
quote, err := resolveQuoteAccount(tx, quoteIndex, []string{photonTableLookup}, 0)
quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
if quote != wsolMint {
if !quote.Equals(solana.WrappedSol) {
return nil, nil
}
@@ -983,6 +1041,7 @@ func parsePhotonSwap(tx *versionedTransaction, instruction *compiledInstruction)
solAmount := args.FromAmount * (100000000 - 1234568) / 100000000
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "photon",
Maker: buyer.String(),
Token0Address: base.String(),
Token1Address: wsolMint,
@@ -1064,6 +1123,7 @@ func parseTermAmmSell(tx *versionedTransaction, instruction *compiledInstruction
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "term",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -1099,6 +1159,7 @@ func parseTermBuy(tx *versionedTransaction, instruction *compiledInstruction) (*
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "term",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -1133,6 +1194,7 @@ func parseTermSell(tx *versionedTransaction, instruction *compiledInstruction) (
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "term",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -1150,7 +1212,7 @@ func parseTermSell(tx *versionedTransaction, instruction *compiledInstruction) (
func decodePumpAmmBuyArgs(data []byte) (uint64, uint64, error) {
if len(data) < 9 {
return 0, 0, fmt.Errorf("data too short for buy args")
return 0, 0, fmt.Errorf("data too short for pump amm buy args, len=%d", len(data))
}
var args pumpAmmBuyArgs
@@ -1189,12 +1251,11 @@ func parsePumpAmmBuy(tx *versionedTransaction, instruction *compiledInstruction)
if err != nil {
return nil, err
}
quoteIndex := int(instruction.Accounts[4])
quote, err := resolveQuoteAccount(tx, quoteIndex, nil, 0)
quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
if quote != wsolMint {
if !quote.Equals(solana.WrappedSol) {
return nil, nil
}
@@ -1205,6 +1266,7 @@ func parsePumpAmmBuy(tx *versionedTransaction, instruction *compiledInstruction)
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pumpamm",
Maker: buyer.String(),
Token0Address: base.String(),
Token1Address: wsolMint,
@@ -1236,12 +1298,11 @@ func parsePumpAmmSell(tx *versionedTransaction, instruction *compiledInstruction
if err != nil {
return nil, err
}
quoteIndex := int(instruction.Accounts[4])
quote, err := resolveQuoteAccount(tx, quoteIndex, nil, 0)
quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
if quote != wsolMint {
if !quote.Equals(solana.WrappedSol) {
return nil, nil
}
@@ -1252,6 +1313,7 @@ func parsePumpAmmSell(tx *versionedTransaction, instruction *compiledInstruction
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pumpamm",
Maker: buyer.String(),
Token0Address: base.String(),
Token1Address: wsolMint,
@@ -1285,7 +1347,7 @@ func parseBoboInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 16 {
return nil, fmt.Errorf("data too short for buy args")
return nil, fmt.Errorf("data too short for bobo buy args, len=%d", len(instruction.Data))
}
staticKeys := tx.Message.StaticAccountKeys
@@ -1305,6 +1367,7 @@ func parseBoboInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "bobo",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -1348,7 +1411,7 @@ func parseQtkvSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, e
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 24 {
return nil, fmt.Errorf("data too short for sell args")
return nil, fmt.Errorf("data too short for qtkv sell args, len=%d", len(instruction.Data))
}
staticKeys := tx.Message.StaticAccountKeys
@@ -1365,6 +1428,7 @@ func parseQtkvSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, e
tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25])
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -1386,7 +1450,7 @@ func parseQtkvAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 24 {
return nil, fmt.Errorf("data too short for sell args")
return nil, fmt.Errorf("data too short for qtkv amm sell args, len=%d", len(instruction.Data))
}
staticKeys := tx.Message.StaticAccountKeys
@@ -1403,6 +1467,7 @@ func parseQtkvAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal
tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25])
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -1441,6 +1506,7 @@ func parseQtkvBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, er
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -1474,7 +1540,7 @@ func parseFjszInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 16 {
return nil, fmt.Errorf("data too short for buy args")
return nil, fmt.Errorf("data too short for fjzs buy args, len=%d", len(instruction.Data))
}
staticKeys := tx.Message.StaticAccountKeys
@@ -1494,6 +1560,7 @@ func parseFjszInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "fjsz",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
@@ -1530,43 +1597,6 @@ func parseTerminalInstruction(tx *versionedTransaction, instructionIndex int) (*
return nil, nil
}
func resolveQuoteAccount(tx *versionedTransaction, quoteIndex int, expectedTableKeys []string, targetIndex uint8) (string, error) {
staticKeys := tx.Message.StaticAccountKeys
if quoteIndex < len(staticKeys) {
quoteKey := staticKeys[quoteIndex].String()
return quoteKey, nil
}
// attempt to load from address table lookup
if len(expectedTableKeys) == 0 || len(tx.Message.AddressTableLookups) != 1 {
return "", fmt.Errorf("parse quote from table lookup failed")
}
table := tx.Message.AddressTableLookups[0]
match := false
for _, key := range expectedTableKeys {
if table.AccountKey.String() == key {
match = true
break
}
}
if !match {
return "", fmt.Errorf("parse quote from table lookup failed")
}
indexOfTarget := indexOf(table.ReadonlyIndexes, targetIndex)
if indexOfTarget < 0 {
return "", fmt.Errorf("parse quote from table lookup failed")
}
expectedIndex := len(staticKeys) + len(table.WritableIndexes) + indexOfTarget
if quoteIndex != expectedIndex {
return "", fmt.Errorf("parse quote from table lookup failed")
}
return wsolMint, nil
}
func indexOf(haystack []uint8, needle uint8) int {
for i, v := range haystack {
if v == needle {