14 Commits

Author SHA1 Message Date
6bab10866b fix: photon buy 2026-01-08 16:25:34 +08:00
83aa772710 chore: improve test 2026-01-08 16:20:17 +08:00
da51b19b50 chore: add bonk parser 2026-01-08 16:07:57 +08:00
f39b89b497 chore: add simple test 2026-01-08 15:20:50 +08:00
26e07ec52e chore: add gmgn and remove entry contract 2026-01-08 12:54:21 +08:00
35c57c3c7a chore: remove useless 2026-01-08 12:46:58 +08:00
3e58b62e1f chore: enable term 2026-01-08 12:44:47 +08:00
thloyi
4c0abc5c34 stats 2026-01-08 11:57:57 +08:00
thloyi
d9aea3e8d7 parallel parsing 2026-01-07 21:15:54 +08:00
thloyi
b82b7d9b0e dflow parser 2026-01-07 18:03:30 +08:00
d9bc106eb1 chore: add SWQoSAgentSoyas address 2026-01-07 17:59:21 +08:00
871dac8bd3 chore: improve clients, add soyas and other regions 2026-01-07 17:26:00 +08:00
thloyi
156fd9b0bf okxv2 parser 2026-01-07 15:39:32 +08:00
thloyi
2504636fb0 fix axiom 2026-01-07 13:24:23 +08:00
17 changed files with 1808 additions and 162 deletions

187
README.md
View File

@@ -15,6 +15,8 @@ go get github.com/samlior/libsam
| fra | fra1.shreder.xyz:9991 |
| ams | ams1.shreder.xyz:9991 |
| ewr | ny1.shreder.xyz:9991 |
| uk | lon.shreder.xyz:9991 |
| jp | tyo.shreder.xyz:9991 |
### Usage
@@ -105,6 +107,13 @@ See [example](./cmd/shreder/main.go).
"keepAliveUrl": "http://germany.solana.dex.blxrbdn.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "fra.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
}
```
@@ -191,6 +200,13 @@ See [example](./cmd/shreder/main.go).
"keepAliveUrl": "http://amsterdam.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "ams.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
}
```
@@ -278,6 +294,177 @@ See [example](./cmd/shreder/main.go).
"keepAliveUrl": "http://ny.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "nyc.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
}
```
</details>
<details><summary> London </summary>
```json
{
"name": "helius",
"sendTxUrl": "http://lon-sender.helius-rpc.com/fast",
"sendBundleUrl": "",
"keepAliveUrl": "http://lon-sender.helius-rpc.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "blockrazor",
"sendTxUrl": "london.solana-grpc.blockrazor.xyz:80",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "node1",
"sendTxUrl": "http://lon.node1.me",
"sendBundleUrl": "",
"keepAliveUrl": "http://lon.node1.me/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "nextblock",
"sendTxUrl": "http://london.nextblock.io/api/v2/submit",
"sendBundleUrl": "",
"keepAliveUrl": "http://london.nextblock.io/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "flashBlock",
"sendTxUrl": "http://london.flashblock.trade/api/v2/submit-batch",
"sendBundleUrl": "http://london.flashblock.trade/api/v2/submit-batch",
"keepAliveUrl": "http://london.flashblock.trade/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "stellium",
"sendTxUrl": "http://lhr1.flashrpc.com/be95e80d-afc2-4a48-b017-db021fc4c19e",
"sendBundleUrl": "",
"keepAliveUrl": "http://lhr1.flashrpc.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "blocxroute",
"sendTxUrl": "http://uk.solana.dex.blxrbdn.com/api/v2/submit",
"sendBundleUrl": "",
"keepAliveUrl": "http://uk.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "lon.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
}
```
</details>
<details><summary> Japan </summary>
```json
{
"name": "helius",
"sendTxUrl": "http://tyo-sender.helius-rpc.com/fast",
"sendBundleUrl": "",
"keepAliveUrl": "http://tyo-sender.helius-rpc.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "0slot",
"sendTxUrl": "http://jp1.0slot.trade?api-key=3fec78a0d361418a8eff95be9ed85cc3&anti-mev=true",
"sendBundleUrl": "",
"keepAliveUrl": "http://jp1.0slot.trade/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "blockrazor",
"sendTxUrl": "tokyo.solana-grpc.blockrazor.xyz:80",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "node1",
"sendTxUrl": "http://tk.node1.me",
"sendBundleUrl": "",
"keepAliveUrl": "http://tk.node1.me/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "nextblock",
"sendTxUrl": "http://tokyo.nextblock.io/api/v2/submit",
"sendBundleUrl": "",
"keepAliveUrl": "http://tokyo.nextblock.io/api/v2/ping",
"tips": "0.001",
"rateLimit": 4
},
{
"name": "flashBlock",
"sendTxUrl": "http://tokyo.flashblock.trade/api/v2/submit-batch",
"sendBundleUrl": "http://tokyo.flashblock.trade/api/v2/submit-batch",
"keepAliveUrl": "http://tokyo.flashblock.trade/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "astralane",
"sendTxUrl": "http://jp.gateway.astralane.io/iris?api-key=zhaozNc5OIadLPI3r9nUVVPpCZcQAUjngO6Tgr5XUJcmBrIisFaaZF81Ijn01Ytn",
"sendBundleUrl": "",
"keepAliveUrl": "http://jp.gateway.astralane.io/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "nozomi",
"sendTxUrl": "http://tyo1.nozomi.temporal.xyz/?c=34cff37e-f1a5-446a-98bb-66aa1b62cb74",
"sendBundleUrl": "",
"keepAliveUrl": "http://tyo1.nozomi.temporal.xyz/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "stellium",
"sendTxUrl": "http://tyo1.flashrpc.com/be95e80d-afc2-4a48-b017-db021fc4c19e",
"sendBundleUrl": "",
"keepAliveUrl": "http://tyo1.flashrpc.com/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "blocxroute",
"sendTxUrl": "http://tokyo.solana.dex.blxrbdn.com/api/v2/submit",
"sendBundleUrl": "",
"keepAliveUrl": "http://tokyo.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001",
"rateLimit": 0
},
{
"name": "soyas",
"sendTxUrl": "tyo.landing.soyas.xyz:9000",
"sendBundleUrl": "",
"tips": "0.001",
"rateLimit": 0
}
```

View File

@@ -50,8 +50,18 @@ func main() {
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
},
},
"okxdexroutev2": {
AccountRequired: []string{
"proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u",
},
},
"dflow": {
AccountRequired: []string{
"DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH",
},
},
// TODO: axiom, gmgn, etc.
})
}, shreder.BlocksStats(false), shreder.LogParsedStats(true), shreder.ShowTableLoaded(false))
if err != nil {
panic(err)
}
@@ -65,7 +75,6 @@ func main() {
<-exitSignal
cancel()
}()
// async read from shreder
txCh := make(chan shreder.TxSignalBatch, 1000)
go func() {
@@ -84,13 +93,8 @@ func main() {
case txBatch := <-txCh:
//jsonData, _ := json.MarshalIndent(txBatch, "", " ")
for _, tx := range txBatch {
if tx.Label == "flas" {
if tx.Event == "buy" {
fmt.Println("===============", tx.TxHash, tx.Program, tx.Event, tx.Token0Address, "token:", tx.Token0Amount, "sol:", tx.Token1Amount)
} else if tx.Event == "sell" {
fmt.Println("===============", tx.TxHash, tx.Program, tx.Event, tx.Token0Address, "token:", tx.Token0Amount)
}
if tx.Label == "okxdexroutev2" || tx.Label == "jupiterv6" || tx.Label == "dflow" {
fmt.Println("===============", tx.TxHash, tx.Label, tx.Event, tx.Token0Address, "token:", tx.Token0Amount, "parse time:", tx.ParseEnd.Sub(tx.ParseStart))
}
}
//fmt.Println(txBatch[0].TxHash)

1
go.mod
View File

@@ -32,6 +32,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
github.com/quic-go/quic-go v0.58.0 // indirect
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect
go.mongodb.org/mongo-driver v1.12.2 // indirect
go.uber.org/atomic v1.7.0 // indirect

3
go.sum
View File

@@ -77,6 +77,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
@@ -88,6 +90,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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=

View File

@@ -121,4 +121,5 @@ var SWQoSFeeAddresses = map[string]string{
"ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb": enum.SWQoSAgentStellium,
"ste11p7e2KLYou5bwtt35H7BM6uMdo4pvioGjJXKFcN": enum.SWQoSAgentStellium,
"ste11TMV68LMi1BguM4RQujtbNCZvf1sjsASpqgAvSX": enum.SWQoSAgentStellium,
"soyas4s6L8KWZ8rsSk1mF3d1mQScoTGGAgjk98bF8nP": enum.SWQoSAgentSoyas,
}

View File

@@ -12,4 +12,5 @@ const (
SWQoSAgentBlockRazor = "blockrazor"
SWQoSAgentAstralane = "astralane"
SWQoSAgentStellium = "stellium"
SWQoSAgentSoyas = "soyas"
)

View File

@@ -11,24 +11,33 @@ import (
"github.com/panjf2000/ants/v2"
)
type TableInfo struct {
overErrCount int
addresses []solana.PublicKey
}
type AddressTables struct {
showTableLoaded bool
rpcClient *rpc.Client
mux sync.RWMutex
loadMux sync.Mutex
tables *lru.Cache[solana.PublicKey, []solana.PublicKey]
tables *lru.Cache[solana.PublicKey, *TableInfo]
loading map[solana.PublicKey]struct{}
pool *ants.Pool
}
func NewAddressTables(rpcClient *rpc.Client) *AddressTables {
func NewAddressTables(rpcClient *rpc.Client, showTableLoaded bool) *AddressTables {
pool, _ := ants.NewPool(5, ants.WithPreAlloc(true), ants.WithNonblocking(true))
cache, _ := lru.New[solana.PublicKey, []solana.PublicKey](10000)
cache, _ := lru.New[solana.PublicKey, *TableInfo](10000)
return &AddressTables{
rpcClient: rpcClient,
tables: cache,
loading: make(map[solana.PublicKey]struct{}),
pool: pool,
showTableLoaded: showTableLoaded,
}
}
@@ -54,53 +63,78 @@ func (at *AddressTables) loadAddressTable(tablePubkey solana.PublicKey) ([]solan
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{}{}
func (at *AddressTables) load(tablePubkey solana.PublicKey) {
_ = 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
}
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)
at.tables.Add(tablePubkey, &TableInfo{
addresses: table,
})
if at.showTableLoaded {
total := at.tables.Len()
logger.Info("loadAddressTable", "table", tablePubkey.String(), "table count:", total)
}
})
}
func (at *AddressTables) FillToTx(tx *versionedTransaction, tablePubkey solana.PublicKey, idx []uint8) bool {
addresses, ok := at.tables.Get(tablePubkey)
if !ok {
at.load(tablePubkey)
return false
}
for _, i := range idx {
if int(i) >= len(addresses.addresses) {
logger.Error("over loadAddressTable failed", "idx", i, "table", tablePubkey)
addresses.overErrCount++
if addresses.overErrCount > 10 {
at.load(tablePubkey)
}
return false
}
tx.Message.StaticAccountKeys = append(tx.Message.StaticAccountKeys, addresses.addresses[i])
}
return true
}
func (at *AddressTables) GetAddressTable(tablePubkey solana.PublicKey, idx []uint8) []solana.PublicKey {
addresses, ok := at.tables.Get(tablePubkey)
if !ok {
at.load(tablePubkey)
return nil
}
at.mux.RUnlock()
var result solana.PublicKeySlice = make([]solana.PublicKey, 0, len(idx))
for _, i := range idx {
if int(i) >= len(addresses) {
if int(i) >= len(addresses.addresses) {
logger.Error("over loadAddressTable failed", "idx", i, "table", tablePubkey)
//todo... update table?
continue
addresses.overErrCount++
if addresses.overErrCount > 10 {
at.load(tablePubkey)
}
break
}
result = append(result, addresses[i])
result = append(result, addresses.addresses[i])
}
return result
}

View File

@@ -3,23 +3,61 @@ package shreder
import (
"context"
"fmt"
"runtime"
"time"
"github.com/gagliardetto/solana-go/rpc"
"github.com/panjf2000/ants/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type Client struct {
enableBlockStats bool
enableParseStats bool
conn *grpc.ClientConn
client ShrederServiceClient
tableLoader *AddressTables
subscription map[string]*SubscribeRequestFilterTransactions
pool *ants.Pool
lastSlot uint64
lastSlotTime time.Time
}
type ClientOpts struct {
blockStats bool
showTableLoaded bool
logParseStats bool
}
type ClientOption func(*ClientOpts)
func ShowTableLoaded(enable bool) ClientOption {
return func(opts *ClientOpts) {
opts.showTableLoaded = enable
}
}
func BlocksStats(enable bool) ClientOption {
return func(opts *ClientOpts) {
opts.blockStats = enable
}
}
func LogParsedStats(enable bool) ClientOption {
return func(opts *ClientOpts) {
opts.logParseStats = enable
}
}
func NewShrederClient(
url string,
rpcClient *rpc.Client,
subscription map[string]*SubscribeRequestFilterTransactions,
options ...ClientOption,
) (*Client, func(), error) {
if rpcClient == nil {
return nil, func() {}, fmt.Errorf("rpc client is nil")
@@ -30,11 +68,29 @@ func NewShrederClient(
return nil, func() {}, err
}
poolSize := runtime.NumCPU()*2 + 2
logger.Info("creating shreder client", "url", url, "pool_size", poolSize)
pool, err := ants.NewPool(poolSize, ants.WithNonblocking(false))
if err != nil {
return nil, func() {}, err
}
o := &ClientOpts{
blockStats: false,
showTableLoaded: true,
logParseStats: false,
}
for _, option := range options {
option(o)
}
s := &Client{
conn: conn,
client: NewShrederServiceClient(conn),
subscription: subscription,
tableLoader: NewAddressTables(rpcClient),
tableLoader: NewAddressTables(rpcClient, o.showTableLoaded),
pool: pool,
enableBlockStats: o.blockStats,
enableParseStats: o.logParseStats,
}
return s, func() {
@@ -45,6 +101,10 @@ func NewShrederClient(
func (c *Client) Wait() {
logger.Debug("waiting for shreder client to stop")
if c.pool != nil {
c.pool.Release()
}
err := c.conn.Close()
if err != nil {
logger.Error("failed to close connection: ", "err", err)
@@ -59,6 +119,8 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error
return err
}
logger.Debug("subscribing to transactions")
err = stream.Send(&SubscribeTransactionsRequest{
Transactions: c.subscription,
})
@@ -72,20 +134,38 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error
return err
}
txBatch := ParseTransaction(response.Transaction, c.tableLoader)
if len(txBatch) == 0 {
continue
if c.enableBlockStats {
slot := response.Transaction.Slot
now := time.Now()
if c.lastSlotTime.IsZero() || slot > c.lastSlot {
if !c.lastSlotTime.IsZero() {
logger.Info("block processed", "running", c.pool.Running(), "slot", slot, "prev_slot", c.lastSlot, "delta_ms", now.Sub(c.lastSlotTime).Milliseconds())
}
c.lastSlot = slot
c.lastSlotTime = now
}
}
// set fixed source for tx signals
for _, tx := range txBatch {
tx.Source = "shreder"
}
txData := response.Transaction
select {
case <-ctx.Done():
return ctx.Err()
case txCh <- txBatch:
err = c.pool.Submit(func() {
txBatch := ParseTransaction(txData, c.tableLoader, c.enableParseStats)
if len(txBatch) == 0 {
return
}
for _, tx := range txBatch {
tx.Source = "shreder"
}
select {
case <-ctx.Done():
return
case txCh <- txBatch:
}
})
if err != nil {
return err
}
}
}

324
pkg/shreder/dflow.go Normal file
View File

@@ -0,0 +1,324 @@
package shreder
import (
"bytes"
"encoding/binary"
"fmt"
bin "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var (
dflowProgramID = solana.MustPublicKeyFromBase58("DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH")
dflowProgramString = dflowProgramID.String()
dflowSwapDisc = []byte{248, 198, 158, 145, 225, 117, 135, 200}
dflowSwap2Disc = []byte{65, 75, 63, 76, 235, 91, 91, 136}
dflowSwapWithDestinationDisc = []byte{168, 172, 24, 77, 197, 156, 135, 101}
dflowSwapWithDestinationNative = []byte{205, 77, 127, 108, 241, 32, 196, 195}
dflowSwap2WithDestinationDisc = []byte{95, 123, 213, 246, 122, 1, 86, 231}
dflowSwap2WithDestinationNative = []byte{222, 100, 184, 146, 186, 196, 105, 165}
wrappedSOL = solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112")
)
// Action enum tags (0-based, per dflow_idl Action variants)
const (
ActWhirlpoolsSwap uint8 = iota
ActClearpoolsSwap
ActRaydiumAmmSwap
ActLifinityV2Swap
ActMeteoraDlmmSwap
ActRaydiumClmmSwap
ActRaydiumClmmSwapV2
ActPhoenixSwap
ActPumpFunBuy
ActPumpFunSell
ActGammaSwap
ActObricV2Swap
ActPumpFunAmmBuy
ActPumpFunAmmSell
ActSolFiSwap
ActRubiconSwap
ActMeteoraDammV1Swap
ActRaydiumCpSwap
ActStabbleStableSwap
ActTesseraVSwap
ActMeteoraDammV2Swap
ActRaydiumLaunchlabSwap
ActMeteoraDbcSwap
ActHumidiFiSwap
ActWhirlpoolsSwapV2
ActMeteoraDlmmSwapV2
ActZeroFiSwap
ActAlphaQSwap
ActTokenSwap
ActSolFiV2Swap
ActMozartSwap
ActDFlowDynamicRouteV1
ActHeavenSwap
ActNexusSwap
ActSarosDlmmSwap
ActTransferFee
ActTransferFeeWithMint
ActRecordId
ActRecordId2
ActManifestSwap
ActBisonFiSwap
ActSanctumInfinitySwap
ActSanctumInfinityLiquidity
ActOpenPredictionsOrder
ActScorchSwap
ActIncludeAccount
)
// DynamicRouteV1CandidateAction tags
const (
drv1SolFi uint8 = iota
drv1Rubicon
drv1TesseraV
drv1HumidiFi
drv1SolFiV2
drv1Mozart
drv1ObricV2
drv1Nexus
)
// PumpFunAmmSellOptions { amount: u64, orchestrator_flags: OrchestratorFlags{flags u8} }
type pumpFunAmm struct {
Amount uint64
Flags uint8
}
type dflowAction struct {
Tag uint8
Pump *pumpFunAmm
}
type dflowSwapParams struct {
Actions []dflowAction
}
// bytes to skip for Action variants before/after PumpFunAmmSell; only PumpFunAmmSell is decoded.
func skipDflowAction(dec *bin.Decoder, tag uint8) (*pumpFunAmm, error) {
switch tag {
case ActWhirlpoolsSwap, ActClearpoolsSwap, ActWhirlpoolsSwapV2:
// amount u64 + bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActRaydiumAmmSwap, ActLifinityV2Swap, ActPumpFunBuy, ActPumpFunSell, ActObricV2Swap,
ActSolFiSwap, ActRubiconSwap, ActMeteoraDammV1Swap, ActRaydiumCpSwap,
ActStabbleStableSwap, ActTesseraVSwap, ActMeteoraDammV2Swap, ActRaydiumLaunchlabSwap,
ActZeroFiSwap, ActAlphaQSwap, ActTokenSwap, ActSolFiV2Swap, ActMozartSwap, ActHeavenSwap,
ActNexusSwap, ActSarosDlmmSwap, ActManifestSwap, ActBisonFiSwap:
// amount u64 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1)
case ActMeteoraDlmmSwap, ActRaydiumClmmSwap, ActRaydiumClmmSwapV2, ActMeteoraDlmmSwapV2:
// amount u64 + u8 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActPhoenixSwap:
// amount u64 + side u8 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActGammaSwap:
// amount u64 + endorsed bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActPumpFunAmmSell, ActPumpFunAmmBuy:
amt, err := dec.ReadUint64(binary.LittleEndian)
if err != nil {
return nil, err
}
flg, err := dec.ReadUint8()
if err != nil {
return nil, err
}
return &pumpFunAmm{Amount: amt, Flags: flg}, nil
case ActMeteoraDbcSwap:
// amount u64 + is_rate_limiter_applied bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActHumidiFiSwap:
// amount u64 + swap_id u64 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 8 + 1)
case ActDFlowDynamicRouteV1:
// candidate_actions Vec<DynamicRouteV1CandidateAction> + amount u64 + orchestrator_flags u8
ln, err := dec.ReadUint32(binary.LittleEndian)
if err != nil {
return nil, err
}
for j := uint32(0); j < ln; j++ {
t, err := dec.ReadUint8()
if err != nil {
return nil, err
}
if t == drv1HumidiFi {
if err := dec.SkipBytes(8); err != nil {
return nil, err
}
}
// other variants carry no payload
}
if err := dec.SkipBytes(8); err != nil { // amount
return nil, err
}
return nil, dec.SkipBytes(1) // orchestrator_flags
case ActTransferFee, ActTransferFeeWithMint:
return nil, dec.SkipBytes(8)
case ActRecordId:
return nil, dec.SkipBytes(76)
case ActRecordId2:
return nil, dec.SkipBytes(4)
case ActSanctumInfinitySwap:
// amount u64 + src_lst_value_calc_accs u8 + dst_lst_value_calc_accs u8 + src_lst_index u32 + dst_lst_index u32 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1 + 4 + 4 + 1)
case ActSanctumInfinityLiquidity:
// amount u64 + lst_value_calc_accs u8 + lst_index u32 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 4 + 1)
case ActOpenPredictionsOrder:
// nonce u64 + order_outcome u8 + quoted_out_amount u64 + slippage_bps u16 + platform_fee_recipient_vault pubkey(32) + platform_fee_scale u16
return nil, dec.SkipBytes(8 + 1 + 8 + 2 + 32 + 2)
case ActScorchSwap:
// amount u64 + id u128 + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 16 + 1)
case ActIncludeAccount:
return nil, nil
default:
return nil, fmt.Errorf("unsupported action tag %d", tag)
}
}
// SwapParams: actions Vec<Action>, quoted_out_amount u64, slippage_bps u16, platform_fee_bps u16
func decodeSwapParams(data []byte) (*dflowSwapParams, error) {
dec := bin.NewBorshDecoder(data)
out := &dflowSwapParams{}
ln, err := dec.ReadUint32(binary.LittleEndian)
if err != nil {
return nil, err
}
out.Actions = make([]dflowAction, 0, ln)
for i := uint32(0); i < ln; i++ {
tag, err := dec.ReadUint8()
if err != nil {
return nil, fmt.Errorf("actions[%d] tag: %w", i, err)
}
pump, err := skipDflowAction(dec, tag)
if err != nil {
return nil, fmt.Errorf("actions[%d]: %w", i, err)
}
out.Actions = append(out.Actions, dflowAction{Tag: tag, Pump: pump})
}
return out, nil
}
// Swap2Params: actions Vec<Action>, quoted_out_amount u64, slippage_bps u16, platform_fee_bps u16, positive_slippage_fee_limit_pct u8
func decodeSwap2Params(data []byte) (*dflowSwapParams, error) {
dec := bin.NewBorshDecoder(data)
out := &dflowSwapParams{}
ln, err := dec.ReadUint32(binary.LittleEndian)
if err != nil {
return nil, err
}
out.Actions = make([]dflowAction, 0, ln)
for i := uint32(0); i < ln; i++ {
tag, err := dec.ReadUint8()
if err != nil {
return nil, fmt.Errorf("actions[%d] tag: %w", i, err)
}
pump, err := skipDflowAction(dec, tag)
if err != nil {
return nil, fmt.Errorf("actions[%d]: %w", i, err)
}
out.Actions = append(out.Actions, dflowAction{Tag: tag, Pump: pump})
}
return out, nil
}
func parseDFlowInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
msg := tx.Message
if instructionIndex >= len(msg.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
ix := msg.Instructions[instructionIndex]
if len(ix.Data) < 8 {
return nil, nil
}
var err error
disc := ix.Data[:8]
payload := ix.Data[8:]
var params *dflowSwapParams
switch {
case bytes.Equal(disc, dflowSwapDisc), bytes.Equal(disc, dflowSwapWithDestinationDisc), bytes.Equal(disc, dflowSwapWithDestinationNative):
params, err = decodeSwapParams(payload)
case bytes.Equal(disc, dflowSwap2Disc), bytes.Equal(disc, dflowSwap2WithDestinationDisc), bytes.Equal(disc, dflowSwap2WithDestinationNative):
params, err = decodeSwap2Params(payload)
default:
return nil, nil
}
if err != nil {
return nil, err
}
if params == nil {
return nil, nil
}
var pump *pumpFunAmm
for _, act := range params.Actions {
if act.Tag == ActPumpFunAmmSell && act.Pump != nil {
pump = act.Pump
break
}
}
if pump == nil {
return nil, nil // only care about PumpFunAmmSell
}
// Require WSOL pair when destination mint is provided.
var (
srcIdx uint8
)
if len(ix.Accounts) <= 6 {
return nil, nil
}
accounts := ix.Accounts[5:]
for i, acctIdx := range accounts {
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
if err != nil {
return nil, err
}
if key.Equals(pumpAmmProgramID) {
srcIdx = uint8(i + 4)
break
}
}
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
return nil, nil
}
baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
if err != nil {
return nil, err
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
if err != nil {
return nil, err
}
if !quoteMint.Equals(solana.WrappedSol) {
return nil, nil
}
// Build TxSignal
sig := &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Program: "PumpAMM",
Event: "sell",
Token0Address: baseMint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(pump.Amount),
Token1Amount: decimal.Zero,
Token0AmountUint64: uint64(pump.Amount),
Token1AmountUint64: 0,
}
return sig, nil
}

File diff suppressed because one or more lines are too long

View File

@@ -935,10 +935,11 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
var (
srcIdx uint8
)
for i, acctIdx := range instruction.Accounts {
if i < 9 {
continue
}
if len(instruction.Accounts) <= 9 {
return nil, nil
}
accounts := instruction.Accounts[8:]
for i, acctIdx := range accounts {
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
if err != nil {
return nil, err
@@ -948,16 +949,18 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
break
}
}
if srcIdx == 0 {
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
return nil, nil
}
sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx]))
baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
if err != nil {
return nil, err
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx+1]))
if !sourceMint.Equals(baseMint) {
return nil, nil
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
if err != nil {
return nil, err
}
@@ -976,10 +979,11 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
var (
srcIdx uint8
)
for i, acctIdx := range instruction.Accounts {
if i < 12 {
continue
}
if len(instruction.Accounts) <= 12 {
return nil, nil
}
accounts := instruction.Accounts[11:]
for i, acctIdx := range accounts {
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
if err != nil {
return nil, err
@@ -989,16 +993,19 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
break
}
}
if srcIdx == 0 {
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
return nil, nil
}
sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx]))
baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
if err != nil {
return nil, err
}
if !sourceMint.Equals(baseMint) {
return nil, nil
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx+1]))
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
if err != nil {
return nil, err
}
@@ -1013,10 +1020,8 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
srcIdx uint8
)
for i, acctIdx := range instruction.Accounts {
if i < 9 {
continue
}
accounts := instruction.Accounts[9:]
for i, acctIdx := range accounts {
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
if err != nil {
return nil, err
@@ -1026,15 +1031,15 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
break
}
}
if srcIdx == 0 {
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
return nil, nil
}
sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx]))
sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
if err != nil {
return nil, err
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[srcIdx+1]))
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
if err != nil {
return nil, err
}
@@ -1044,7 +1049,6 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
}
signal := &TxSignal{
Label: "jupiterV6",
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: sourceMint.String(),

View File

@@ -1,5 +1,368 @@
package shreder
//func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
//
//}
import (
"bytes"
"encoding/binary"
"fmt"
bin "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var (
okxDexRouteV2ProgramID = solana.MustPublicKeyFromBase58("proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u")
okxDexRouteV2ProgramIDString = okxDexRouteV2ProgramID.String()
okxSwapTobDisc = []byte{170, 41, 85, 177, 132, 80, 31, 53}
okxSwapTobWithReceiverDisc = []byte{223, 170, 216, 234, 204, 6, 241, 25}
okxSwapTocDisc = []byte{187, 201, 212, 51, 16, 155, 236, 60}
okxSwapTocV2Disc = []byte{127, 214, 107, 189, 23, 90, 47, 104}
)
// IDL: SwapArgs { order_id:u64, amount_in:u64, expect_amount_out:u64, slippage:u16, routes: Vec<Route> }
// IDL: Route { dex: Dex(enum), weight:u16, index:u8 }
type OkxV2Route struct {
Dex OkxV2SwapKind
Weight uint16
Index uint8
}
type OkxV2SwapArgs struct {
OrderID uint64
AmountIn uint64
ExpectAmountOut uint64
Slippage uint16
Routes []OkxV2Route
}
type OkxV2SwapKind uint8
const (
OKCV2_SplTokenSwap OkxV2SwapKind = iota
OKCV2_StableSwap
OKCV2_Whirlpool
OKCV2_MeteoraDynamicpool
OKCV2_RaydiumSwap
OKCV2_RaydiumStableSwap
OKCV2_RaydiumClmmSwap
OKCV2_AldrinExchangeV1
OKCV2_AldrinExchangeV2
OKCV2_LifinityV1
OKCV2_LifinityV2
OKCV2_RaydiumClmmSwapV2
OKCV2_FluxBeam
OKCV2_MeteoraDlmm
OKCV2_RaydiumCpmmSwap
OKCV2_OpenBookV2
OKCV2_WhirlpoolV2
OKCV2_Phoenix
OKCV2_ObricV2
OKCV2_SanctumAddLiq
OKCV2_SanctumRemoveLiq
OKCV2_SanctumNonWsolSwap
OKCV2_SanctumWsolSwap
OKCV2_PumpfunBuy
OKCV2_PumpfunSell
OKCV2_StabbleSwap
OKCV2_SanctumRouter
OKCV2_MeteoraVaultDeposit
OKCV2_MeteoraVaultWithdraw
OKCV2_Saros
OKCV2_MeteoraLst
OKCV2_Solfi
OKCV2_QualiaSwap
OKCV2_Zerofi
OKCV2_PumpfunammBuy
OKCV2_PumpfunammSell
OKCV2_Virtuals
OKCV2_VertigoBuy
OKCV2_VertigoSell
OKCV2_PerpetualsAddLiq
OKCV2_PerpetualsRemoveLiq
OKCV2_PerpetualsSwap
OKCV2_RaydiumLaunchpad
OKCV2_LetsBonkFun
OKCV2_Woofi
OKCV2_MeteoraDbc
OKCV2_MeteoraDlmmSwap2
OKCV2_MeteoraDAMMV2
OKCV2_Gavel
OKCV2_BoopfunBuy
OKCV2_BoopfunSell
OKCV2_MeteoraDbc2
OKCV2_GooseFX
OKCV2_Dooar
OKCV2_Numeraire
OKCV2_SaberDecimalWrapperDeposit
OKCV2_SaberDecimalWrapperWithdraw
OKCV2_SarosDlmm
OKCV2_OneDexSwap
OKCV2_Manifest
OKCV2_ByrealClmm
OKCV2_PancakeSwapV3Swap
OKCV2_PancakeSwapV3SwapV2
OKCV2_Tessera
OKCV2_SolRfq
OKCV2_Humidifi
OKCV2_HeavenBuy
OKCV2_HeavenSell
OKCV2_SolfiV2
OKCV2_Goonfi
OKCV2_MoonitBuy
OKCV2_MoonitSell
OKCV2_RaydiumSwapV2
OKCV2_Whalestreet
OKCV2_SugarMoneyBuy
OKCV2_SugarMoneySell
OKCV2_MeteoraDAMMV2Swap2
OKCV2_AlphaQ
OKCV2_FutarchyAmm
OKCV2_PumpfunBuy2
OKCV2_PumpfunSell2
OKCV2_HumidifiSwap2
OKCV2_Scorch
OKCV2_JupiterLendDeposit
OKCV2_JupiterLendRedeem
OKCV2_TokkaAmm
)
func decodeOkxSwapTobSwapArgs(data []byte) (*OkxV2SwapArgs, error) {
dec := bin.NewBorshDecoder(data)
return decodeOkxV2SwapArgs(dec)
}
func decodeOkxSwapTobWithReceiverSwapArgs(data []byte) (*OkxV2SwapArgs, error) {
dec := bin.NewBorshDecoder(data)
return decodeOkxV2SwapArgs(dec)
}
func decodeOkxSwapTocSwapArgs(data []byte) (*OkxV2SwapArgs, error) {
dec := bin.NewBorshDecoder(data)
return decodeOkxV2SwapArgs(dec)
}
func decodeOkxSwapTocV2SwapArgs(data []byte) (*OkxV2SwapArgs, error) {
dec := bin.NewBorshDecoder(data)
return decodeOkxV2SwapArgs(dec)
}
func skipOkxV2DexPayload(dec *bin.Decoder, dex OkxV2SwapKind) error {
// IMPORTANT: In IDL, Dex is an enum. Most variants have no fields, but some carry payload.
// We only need to keep decoding aligned for SwapArgs.routes.
switch dex {
case OKCV2_SolRfq:
// fields: 6*u64 + 2*bool
// rfq_id, expected_maker_amount, expected_taker_amount, maker_send_amount,
// taker_send_amount, expiry, maker_use_native_sol, taker_use_native_sol
if err := dec.SkipBytes(8 * 6); err != nil {
return err
}
return dec.SkipBytes(2)
case OKCV2_SugarMoneyBuy, OKCV2_SugarMoneySell:
// fields: u8 + u8
return dec.SkipBytes(2)
case OKCV2_HumidifiSwap2:
// fields: u64
return dec.SkipBytes(8)
case OKCV2_Scorch:
// fields: u128 => 16 bytes
return dec.SkipBytes(16)
default:
return nil
}
}
func decodeOkxV2SwapArgs(dec *bin.Decoder) (*OkxV2SwapArgs, error) {
out := &OkxV2SwapArgs{}
var err error
if out.OrderID, err = dec.ReadUint64(binary.LittleEndian); err != nil {
return nil, fmt.Errorf("read order_id: %w", err)
}
if out.AmountIn, err = dec.ReadUint64(binary.LittleEndian); err != nil {
return nil, fmt.Errorf("read amount_in: %w", err)
}
if out.ExpectAmountOut, err = dec.ReadUint64(binary.LittleEndian); err != nil {
return nil, fmt.Errorf("read expect_amount_out: %w", err)
}
if out.Slippage, err = dec.ReadUint16(binary.LittleEndian); err != nil {
return nil, fmt.Errorf("read slippage: %w", err)
}
// routes: Vec<Route>
routesLen, err := dec.ReadUint32(binary.LittleEndian)
if err != nil {
return nil, fmt.Errorf("read routes len: %w", err)
}
out.Routes = make([]OkxV2Route, 0, routesLen)
for i := uint32(0); i < routesLen; i++ {
// Route { dex: Dex(enum tag u8 [+ payload]), weight: u16, index: u8 }
tag, err := dec.ReadUint8()
if err != nil {
return nil, fmt.Errorf("read routes[%d].dex: %w", i, err)
}
dex := OkxV2SwapKind(tag)
if err := skipOkxV2DexPayload(dec, dex); err != nil {
return nil, fmt.Errorf("skip routes[%d].dex payload (%d): %w", i, tag, err)
}
weight, err := dec.ReadUint16(binary.LittleEndian)
if err != nil {
return nil, fmt.Errorf("read routes[%d].weight: %w", i, err)
}
idx, err := dec.ReadUint8()
if err != nil {
return nil, fmt.Errorf("read routes[%d].index: %w", i, err)
}
out.Routes = append(out.Routes, OkxV2Route{Dex: dex, Weight: weight, Index: idx})
}
return out, nil
}
type OkxV2SwapSolRfq struct {
RfqId uint64
expectedMakerAmount uint64
expectedTakerAmount uint64
makerSendAmount uint64
takerSendAmount uint64
expiry uint64
makerUseNativeSol bool
takerUseNativeSol bool
}
type OkxV2SwapSugarMoney struct {
BondingCurveBump uint8
BondingCurveSolAssociatedAccountBump uint8
}
type OkxV2SwapHumidifiSwap2 struct {
SwapId uint64
}
type OkxV2SwapScorch struct {
Id [16]byte
}
func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
msg := tx.Message
if instructionIndex >= len(msg.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
ix := msg.Instructions[instructionIndex]
if len(ix.Data) < 8 {
return nil, nil
}
disc := ix.Data[:8]
data := ix.Data[8:]
var (
args *OkxV2SwapArgs
err error
)
switch {
case bytes.Equal(disc, okxSwapTobDisc):
args, err = decodeOkxSwapTobSwapArgs(data)
if err != nil {
return nil, fmt.Errorf("decode swap_tob args: %w", err)
}
case bytes.Equal(disc, okxSwapTobWithReceiverDisc):
args, err = decodeOkxSwapTobWithReceiverSwapArgs(data)
if err != nil {
return nil, fmt.Errorf("decode swap_tob_with_receiver args: %w", err)
}
case bytes.Equal(disc, okxSwapTocDisc):
args, err = decodeOkxSwapTocSwapArgs(data)
if err != nil {
return nil, fmt.Errorf("decode swap_toc args: %w", err)
}
case bytes.Equal(disc, okxSwapTocV2Disc):
args, err = decodeOkxSwapTocV2SwapArgs(data)
if err != nil {
return nil, fmt.Errorf("decode swap_toc_v2 args: %w", err)
}
default:
return nil, nil
}
if len(ix.Accounts) < 15 {
return nil, fmt.Errorf("invalid account count: %d", len(ix.Accounts))
}
var (
inputAmount uint64
routeCount int
)
for _, route := range args.Routes {
if route.Index == 1 && (route.Dex == OKCV2_PumpfunammSell ||
route.Dex == OKCV2_PumpfunSell2) {
routeCount++
inputAmount = args.AmountIn * uint64(route.Weight) / 10000
}
}
if routeCount > 1 {
logger.Warn("pumpSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "routeCount", routeCount)
return nil, nil
}
if inputAmount == 0 {
return nil, nil
}
srcMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[3]))
var (
srcIdx uint8
)
if len(ix.Accounts) <= 15 {
return nil, nil
}
accounts := ix.Accounts[14:]
for i, acctIdx := range accounts {
key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx))
if err != nil {
return nil, err
}
if key.Equals(pumpAmmProgramID) {
srcIdx = uint8(i + 6)
break
}
}
if srcIdx == 0 || int(srcIdx+1) >= len(accounts) {
return nil, nil
}
baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
if err != nil {
return nil, err
}
if !baseMint.Equals(srcMint) {
return nil, nil
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
if err != nil {
return nil, err
}
if !quoteMint.Equals(solana.WrappedSol) {
return nil, nil
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: baseMint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(inputAmount),
Token1Amount: decimal.Zero,
Event: "sell",
Program: "PumpAMM",
IsProcessed: false,
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: false,
Token0AmountUint64: inputAmount,
Token1AmountUint64: 0,
}, nil
}

View File

@@ -45,13 +45,15 @@ type TxSignal struct {
IsToken2022 bool `json:"is_token2022"`
IsMayhemMode bool `json:"is_mayhem_mode"`
TxFee decimal.Decimal `json:"tx_fee"`
EntryContract string `json:"entry_contract"`
ExactSOL bool `json:"exact_in"`
// parsed values
Token0AmountUint64 uint64 `json:"-"`
Token1AmountUint64 uint64 `json:"-"`
ParseStart time.Time `json:"parse_start"`
ParseEnd time.Time `json:"parse_end"`
}
func (t *TxSignal) Parse() *TxSignal {

View File

@@ -6,6 +6,8 @@ import (
"fmt"
"math/big"
"strings"
"sync"
"time"
"github.com/gagliardetto/solana-go"
"github.com/mr-tron/base58"
@@ -21,13 +23,11 @@ const (
// program ids
var (
pumpProgramID = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
// has no sell function with pump and pump.amm program
azczProgramID = solana.MustPublicKeyFromBase58("AzcZqCRUQgKEg5FTAgY7JacATABEYCEfMbjXEzspLYFB")
// only buy function with pump program
f5tfProgramID = solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq")
// only pump.fun function
photonProgramID = solana.MustPublicKeyFromBase58("BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW")
@@ -45,6 +45,10 @@ var (
terminalProgramID = solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3")
jupiterV6ProgramID = solana.MustPublicKeyFromBase58("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4")
gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb")
bonkProgramID = solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD")
)
type AccountNotFoundError struct {
@@ -94,6 +98,10 @@ var (
terminalBuyTokensIX = []byte{0xa6, 0x54, 0x14, 0x96, 0x9f, 0x77, 0x59, 0xca}
terminalSellTokensIX = []byte{0xbe, 0x84, 0xa2, 0x96, 0x93, 0x7c, 0xf8, 0x6b}
terminalAmmSellTokensIX = []byte{0x40, 0x64, 0x97, 0xb9, 0x16, 0xfa, 0xec, 0xb1}
gmgnBuyTokensIX = []byte{0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea}
bonkBuyAndSellTokensIX = []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a}
)
type compiledInstruction struct {
@@ -118,6 +126,7 @@ type versionedTransaction struct {
Signatures []solana.Signature
Message versionedMessage
Block uint64
Time time.Time
}
type pumpExtendedSellArgs struct {
@@ -192,15 +201,72 @@ type fjszBuyArgs struct {
TokenAmount uint64
}
var (
versionedPool = sync.Pool{}
accIdxPool = sync.Pool{}
)
func requireAccIdxSlice() []uint8 {
v := accIdxPool.Get()
if v == nil {
return make([]uint8, 0, 16)
}
return v.([]uint8)
}
func releaseAccIdxSlice(s []uint8) {
if s == nil {
return
}
s = s[:0]
accIdxPool.Put(s)
}
func requireVersionedPool() *versionedTransaction {
v := versionedPool.Get()
if v == nil {
return &versionedTransaction{
Signatures: make([]solana.Signature, 0, 10),
Message: versionedMessage{
StaticAccountKeys: make([]solana.PublicKey, 0, 256),
Instructions: make([]compiledInstruction, 0, 16),
AddressTableLookups: make([]addressTableLookup, 0, 10),
},
}
}
return v.(*versionedTransaction)
}
func releaseVersionedPool(v *versionedTransaction) {
if v == nil {
return
}
for i := range v.Message.Instructions {
releaseAccIdxSlice(v.Message.Instructions[i].Accounts)
}
for i := range v.Message.AddressTableLookups {
releaseAccIdxSlice(v.Message.AddressTableLookups[i].WritableIndexes)
releaseAccIdxSlice(v.Message.AddressTableLookups[i].ReadonlyIndexes)
}
versionedPool.Put(v)
}
// ParseTransaction mirrors the Rust parse_transaction entry point.
func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables) []*TxSignal {
func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables, stats bool) []*TxSignal {
var now time.Time
if stats {
now = time.Now()
}
versioned, err := toVersionedTransaction(update)
if err != nil || versioned == nil || len(versioned.Signatures) == 0 {
return nil
}
defer func() {
releaseVersionedPool(versioned)
}()
txHash := versioned.Signatures[0]
staticKeys := versioned.Message.StaticAccountKeys
// staticKeys := versioned.Message.StaticAccountKeys
instructions := versioned.Message.Instructions
if loader != nil && len(versioned.Message.AddressTableLookups) > 0 {
@@ -209,79 +275,87 @@ func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables)
if len(lookup.WritableIndexes) == 0 {
continue
}
accounts := loader.GetAddressTable(lookup.AccountKey, lookup.WritableIndexes)
if len(accounts) != len(lookup.WritableIndexes) {
lookupTableOk = false
lookupTableOk = loader.FillToTx(versioned, lookup.AccountKey, lookup.WritableIndexes)
if !lookupTableOk {
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) {
lookupTableOk = loader.FillToTx(versioned, lookup.AccountKey, lookup.ReadonlyIndexes)
if !lookupTableOk {
break
}
staticKeys = append(staticKeys, accounts...)
}
}
versioned.Message.StaticAccountKeys = staticKeys
// versioned.Message.StaticAccountKeys = staticKeys
}
var parsed []*TxSignal
var parsed []*TxSignal = make([]*TxSignal, 0, 3)
for i := range instructions {
inst := instructions[i]
if int(inst.ProgramIDIndex) >= len(staticKeys) {
if int(inst.ProgramIDIndex) >= len(versioned.Message.StaticAccountKeys) {
continue
}
programID := staticKeys[inst.ProgramIDIndex]
programID := versioned.Message.StaticAccountKeys[inst.ProgramIDIndex]
switch programID {
case pumpProgramID:
txRes, err := parsePumpInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "pump", pumpProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "pump")
case azczProgramID:
txRes, err := parseAzczInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "azcz", azczProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "azcz")
case f5tfProgramID:
txRes, err := parseF5tfInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "f5tf", f5tfProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "f5tf")
case flasProgramID:
txRes, err := parseFlasInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "flas", flasProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "flas")
case photonProgramID:
txRes, err := parsePhotonInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "photon", photonProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "photon")
case pumpAmmProgramID:
txRes, err := parsePumpAmmInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "pumpamm", pumpAmmProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "pumpamm")
case boboProgramID:
txRes, err := parseBoboInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "bobo", boboProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "bobo")
case qtkvProgramID:
txRes, err := parseQtkvInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "qtkv", qtkvProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "qtkv")
case fjszProgramID:
txRes, err := parseFjszInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "fjsz", fjszProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "fjsz")
case terminalProgramID:
txRes, err := parseTermInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "terminal", terminalProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "terminal")
case jupiterV6ProgramID:
txRes, err := parseJupiterV6Instruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "jupiterv6", jupiterV6ProgramID.String())
parsed = appendParsed(now, parsed, txRes, err, txHash, "jupiterv6")
case okxDexRouteV2ProgramID:
txRes, err := parseOkxDexRouteV2Instruction(versioned, i)
parsed = appendParsed(now, parsed, txRes, err, txHash, "okxdexroutev2")
case dflowProgramID:
txRes, err := parseDFlowInstruction(versioned, i)
parsed = appendParsed(now, parsed, txRes, err, txHash, "dflow")
case gmgnProgramID:
txRes, err := parseGMGNInstruction(versioned, i)
parsed = appendParsed(now, parsed, txRes, err, txHash, "gmgn")
case bonkProgramID:
txRes, err := parseBonkInstruction(versioned, i)
parsed = appendParsed(now, parsed, txRes, err, txHash, "bonk")
}
}
return parsed
}
func appendParsed(list []*TxSignal, parsed *TxSignal, err error, txHash [64]byte, label string, entryContract string) []*TxSignal {
func appendParsed(start time.Time, list []*TxSignal, parsed *TxSignal, err error, txHash [64]byte, label string) []*TxSignal {
if err != nil {
if !strings.HasPrefix(err.Error(), "account index") {
logger.Debug("txparser: failed to parse", "label", label, "instruction", err, "tx_hash", base58.Encode(txHash[:]))
@@ -289,7 +363,11 @@ func appendParsed(list []*TxSignal, parsed *TxSignal, err error, txHash [64]byte
return list
}
if parsed != nil {
parsed.EntryContract = entryContract
parsed.Label = label
if !start.IsZero() {
parsed.ParseEnd = time.Now()
parsed.ParseStart = start
}
list = append(list, parsed)
}
return list
@@ -302,47 +380,42 @@ func toVersionedTransaction(update *SubscribeUpdateTransaction) (*versionedTrans
protoTx := update.Transaction
msg := protoTx.Message
signatures := make([]solana.Signature, len(protoTx.Signatures))
for i, rawSig := range protoTx.Signatures {
signatures[i] = solana.SignatureFromBytes(rawSig)
versioned := requireVersionedPool()
versioned.Signatures = versioned.Signatures[:0]
for _, rawSig := range protoTx.Signatures {
versioned.Signatures = append(versioned.Signatures, solana.SignatureFromBytes(rawSig))
}
versioned.Message.StaticAccountKeys = versioned.Message.StaticAccountKeys[:0]
for _, key := range msg.AccountKeys {
versioned.Message.StaticAccountKeys = append(versioned.Message.StaticAccountKeys, solana.PublicKeyFromBytes(key))
}
versioned.Message.Instructions = versioned.Message.Instructions[:0]
for _, instr := range msg.Instructions {
accounts := requireAccIdxSlice()
accounts = append(accounts, instr.Accounts...)
versioned.Message.Instructions = append(versioned.Message.Instructions,
compiledInstruction{
ProgramIDIndex: uint8(instr.ProgramIdIndex),
Accounts: accounts,
Data: instr.Data,
})
}
staticKeys := make([]solana.PublicKey, len(msg.AccountKeys))
for i, key := range msg.AccountKeys {
staticKeys[i] = solana.PublicKeyFromBytes(key)
}
instructions := make([]compiledInstruction, len(msg.Instructions))
for i, instr := range msg.Instructions {
accounts := append([]uint8(nil), instr.Accounts...)
instructions[i] = compiledInstruction{
ProgramIDIndex: uint8(instr.ProgramIdIndex),
Accounts: accounts,
Data: instr.Data,
}
}
lookups := make([]addressTableLookup, len(msg.AddressTableLookups))
for i, lookup := range msg.AddressTableLookups {
writable := append([]uint8(nil), lookup.WritableIndexes...)
readonly := append([]uint8(nil), lookup.ReadonlyIndexes...)
lookups[i] = addressTableLookup{
versioned.Message.AddressTableLookups = versioned.Message.AddressTableLookups[:0]
for _, lookup := range msg.AddressTableLookups {
writable := requireAccIdxSlice()
writable = append(writable, lookup.WritableIndexes...)
readonly := requireAccIdxSlice()
readonly = append(readonly, lookup.ReadonlyIndexes...)
versioned.Message.AddressTableLookups = append(versioned.Message.AddressTableLookups, addressTableLookup{
AccountKey: solana.PublicKeyFromBytes(lookup.AccountKey),
WritableIndexes: writable,
ReadonlyIndexes: readonly,
}
})
}
return &versionedTransaction{
Signatures: signatures,
Message: versionedMessage{
StaticAccountKeys: staticKeys,
Instructions: instructions,
AddressTableLookups: lookups,
},
Block: update.GetSlot(),
}, nil
versioned.Block = update.GetSlot()
return versioned, nil
}
func formatTokenAmount(amount uint64) decimal.Decimal {
@@ -799,7 +872,7 @@ func parseFlasAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatSolAmount(args.Amount1),
Token0Amount: formatTokenAmount(args.Amount1),
Token1Amount: formatSolAmount(args.Amount2),
Program: "PumpAMM",
Event: "sell",
@@ -933,6 +1006,66 @@ func parseFlasBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, er
}, nil
}
func parseGMGNInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
msg := tx.Message
if instructionIndex >= len(msg.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := msg.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if len(instruction.Data) < 8 {
return nil, nil
}
if matchMethod(instruction.Data, gmgnBuyTokensIX) {
return parseGMGNBuy(tx, &instruction)
}
return nil, nil
}
func parseGMGNBuy(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 24 {
return nil, fmt.Errorf("data too short for gmgn buy args, len=%d", len(instruction.Data))
}
staticKeys := tx.Message.StaticAccountKeys
mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
user, err := getStaticKey(staticKeys, int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
solAmount := binary.LittleEndian.Uint64(instruction.Data[8:16])
tokenAmount := binary.LittleEndian.Uint64(instruction.Data[16:24])
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "gmgn",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(tokenAmount),
Token1Amount: formatSolAmount(solAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: tokenAmount,
Token1AmountUint64: solAmount,
}, nil
}
func parsePhotonInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
msg := tx.Message
if instructionIndex >= len(msg.Instructions) {
@@ -993,6 +1126,7 @@ func parsePhotonBuy(tx *versionedTransaction, instruction *compiledInstruction)
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: args.TokenAmount,
Token1AmountUint64: solAmount,
@@ -1167,6 +1301,7 @@ func parseTermBuy(tx *versionedTransaction, instruction *compiledInstruction) (*
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: tokenAmount,
Token1AmountUint64: solAmount,
@@ -1202,6 +1337,7 @@ func parseTermSell(tx *versionedTransaction, instruction *compiledInstruction) (
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: false,
Block: tx.Block,
Token0AmountUint64: tokenAmount,
Token1AmountUint64: solAmount,
@@ -1574,7 +1710,7 @@ func parseFjszInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
}, nil
}
func parseTerminalInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
func parseBonkInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
msg := tx.Message
if instructionIndex >= len(msg.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
@@ -1584,24 +1720,87 @@ func parseTerminalInstruction(tx *versionedTransaction, instructionIndex int) (*
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if matchMethod(instruction.Data, terminalBuyTokensIX) {
return parseTermBuy(tx, &instruction)
} else if matchMethod(instruction.Data, terminalSellTokensIX) {
return parseTermSell(tx, &instruction)
} else if matchMethod(instruction.Data, terminalAmmSellTokensIX) {
return parseTermAmmSell(tx, &instruction)
if matchMethod(instruction.Data, bonkBuyAndSellTokensIX) {
return parseBonkBuyAndSell(tx, &instruction)
}
return nil, nil
}
func indexOf(haystack []uint8, needle uint8) int {
for i, v := range haystack {
if v == needle {
return i
}
func parseBonkBuyAndSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
staticKeys := tx.Message.StaticAccountKeys
programId, err := getStaticKey(staticKeys, int(instruction.Accounts[7]))
if err != nil {
return nil, err
}
if programId != pumpProgramID {
return nil, nil
}
user, err := getStaticKey(staticKeys, int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
flagAccount, err := getStaticKey(staticKeys, int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
amount1 := binary.LittleEndian.Uint64(instruction.Data[17:25])
amount2 := binary.LittleEndian.Uint64(instruction.Data[25:33])
if user == flagAccount {
mint, err := getStaticKey(staticKeys, int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "bonk",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(amount2),
Token1Amount: formatSolAmount(amount1),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: amount2,
Token1AmountUint64: amount1,
}, nil
} else {
mint, err := getStaticKey(staticKeys, int(instruction.Accounts[5]))
if err != nil {
return nil, err
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "bonk",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(amount1),
Token1Amount: formatSolAmount(amount2),
Program: "Pump",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: false,
Block: tx.Block,
Token0AmountUint64: amount1,
Token1AmountUint64: amount2,
}, nil
}
return -1
}
func matchMethod(data []byte, methods []byte) bool {

View File

@@ -1,9 +1,13 @@
package shreder
import (
"context"
"encoding/hex"
"os"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/near/borsh-go"
)
@@ -54,3 +58,234 @@ func TestDecodeAxiomArgs(t *testing.T) {
})
}
}
func toUpdata(slot uint64, tx *solana.Transaction) *SubscribeUpdateTransaction {
signatures := make([][]byte, len(tx.Signatures))
for i, sig := range tx.Signatures {
signatures[i] = sig[:]
}
accountKeys := make([][]byte, len(tx.Message.AccountKeys))
for i, key := range tx.Message.AccountKeys {
accountKeys[i] = key[:]
}
instructions := make([]*CompiledInstruction, len(tx.Message.Instructions))
for i, instr := range tx.Message.Instructions {
accounts := make([]byte, len(instr.Accounts))
for j, acc := range instr.Accounts {
accounts[j] = byte(acc)
}
instructions[i] = &CompiledInstruction{
ProgramIdIndex: uint32(instr.ProgramIDIndex),
Accounts: accounts,
Data: instr.Data[:],
}
}
addressTableLookups := make([]*MessageAddressTableLookup, len(tx.Message.AddressTableLookups))
for i, lookup := range tx.Message.AddressTableLookups {
writable := make([]byte, len(lookup.WritableIndexes))
for j, idx := range lookup.WritableIndexes {
writable[j] = byte(idx)
}
readonly := make([]byte, len(lookup.ReadonlyIndexes))
for j, idx := range lookup.ReadonlyIndexes {
readonly[j] = byte(idx)
}
addressTableLookups[i] = &MessageAddressTableLookup{
AccountKey: lookup.AccountKey[:],
WritableIndexes: writable,
ReadonlyIndexes: readonly,
}
}
return &SubscribeUpdateTransaction{
Transaction: &Transaction{
Signatures: signatures,
Message: &Message{
Header: &MessageHeader{
NumRequiredSignatures: uint32(tx.Message.Header.NumRequiredSignatures),
NumReadonlySignedAccounts: uint32(tx.Message.Header.NumReadonlySignedAccounts),
NumReadonlyUnsignedAccounts: uint32(tx.Message.Header.NumReadonlyUnsignedAccounts),
},
AccountKeys: accountKeys,
RecentBlockhash: nil, // TODO
Instructions: instructions,
Versioned: false, // TODO
AddressTableLookups: addressTableLookups,
},
},
Slot: slot,
}
}
func getTransaction(t *testing.T, client *rpc.Client, signature string) *SubscribeUpdateTransaction {
version := uint64(0)
tx, err := client.GetTransaction(
context.Background(),
solana.MustSignatureFromBase58(signature),
&rpc.GetTransactionOpts{
Commitment: rpc.CommitmentFinalized,
MaxSupportedTransactionVersion: &version,
},
)
if err != nil {
t.Fatalf("failed to get transaction: %v", err)
}
_tx, err := tx.Transaction.GetTransaction()
if err != nil {
t.Fatalf("failed to get transaction: %v", err)
}
return toUpdata(tx.Slot, _tx)
}
func TestParseTermBuy(t *testing.T) {
rpcUrl := os.Getenv("SOL_RPC_URL")
if rpcUrl == "" {
t.Fatalf("SOL_RPC_URL is not set")
}
client := rpc.New(rpcUrl)
signals := ParseTransaction(
getTransaction(t, client, "5Gz1fa4Qhb35bkg9QCMXpxCX5uuNr7WcjcmrwajGZA7kXsvNS9pDnYe12ggWeSqf1nwZbVPob6DkX6fcwbE9ofBR"),
nil,
false,
)
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "terminal" {
t.Fatalf("expected terminal signal, got %s", signal.Label)
}
if signal.Event != "buy" {
t.Fatalf("expected buy event, got %s", signal.Event)
}
if signal.Maker != "BaLxyjXzATAnfm7cc5AFhWBpiwnsb71THcnofDLTWAPK" {
t.Fatalf("expected maker BaLxyjXzATAnfm7cc5AFhWBpiwnsb71THcnofDLTWAPK, got %s", signal.Maker)
}
if signal.Token0Address != "5Wgv54peXRKDHYHapAELzgNKEPEh9E5Bf3hUR3sTpump" {
t.Fatalf("expected token0 address 5Wgv54peXRKDHYHapAELzgNKEPEh9E5Bf3hUR3sTpump, got %s", signal.Token0Address)
}
if signal.Token0AmountUint64 != 6952026214256 {
t.Fatalf("expected token0 amount 6952026214256, got %d", signal.Token0AmountUint64)
}
if signal.Token1AmountUint64 != 653333333 {
t.Fatalf("expected token1 amount 653333333, got %d", signal.Token1AmountUint64)
}
}
func TestParseBonkBuy(t *testing.T) {
rpcUrl := os.Getenv("SOL_RPC_URL")
if rpcUrl == "" {
t.Fatalf("SOL_RPC_URL is not set")
}
client := rpc.New(rpcUrl)
signals := ParseTransaction(
getTransaction(t, client, "3gHF3TA2aA8rpjdmoEs2vA89vrq9J9NnTTUSXHfE6uXcaYP9cJgLtEUjCmsK9EWAyHEg7cEiepehQf4GFv1272jW"),
nil,
false,
)
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "bonk" {
t.Fatalf("expected bonk signal, got %s", signal.Label)
}
if signal.Event != "buy" {
t.Fatalf("expected buy event, got %s", signal.Event)
}
if signal.Maker != "BFobdhAbdBteBuDvHUdBthsQqJyMuWnG9SGUheW1Ni2C" {
t.Fatalf("expected maker BFobdhAbdBteBuDvHUdBthsQqJyMuWnG9SGUheW1Ni2C, got %s", signal.Maker)
}
if signal.Token0Address != "Awupo9Jxe1fsc7eEtCEcN9D3PoyReQhc9WEuEAHXpump" {
t.Fatalf("expected token0 address Awupo9Jxe1fsc7eEtCEcN9D3PoyReQhc9WEuEAHXpump, got %s", signal.Token0Address)
}
if signal.Token0AmountUint64 != 8616799656436 {
t.Fatalf("expected token0 amount 8616799656436, got %d", signal.Token0AmountUint64)
}
if signal.Token1AmountUint64 != 495000000 {
t.Fatalf("expected token1 amount 495000000, got %d", signal.Token1AmountUint64)
}
}
func TestParseBonkSell(t *testing.T) {
rpcUrl := os.Getenv("SOL_RPC_URL")
if rpcUrl == "" {
t.Fatalf("SOL_RPC_URL is not set")
}
client := rpc.New(rpcUrl)
signals := ParseTransaction(
getTransaction(t, client, "3XNi6b3j69SSStqLLRQVH5BNGVfEoFxGCzmpdd5FvrY4kmC8T644WGdEhCH9fAdrxWuR2Mtzgywq8K7qetu5MGyb"),
nil,
false,
)
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "bonk" {
t.Fatalf("expected bonk signal, got %s", signal.Label)
}
if signal.Event != "sell" {
t.Fatalf("expected sell event, got %s", signal.Event)
}
if signal.Maker != "2xTT7XXCEYSCrRb3G4Egc4ZwpCe78qq6r7w6ChZhbTXc" {
t.Fatalf("expected maker 2xTT7XXCEYSCrRb3G4Egc4ZwpCe78qq6r7w6ChZhbTXc, got %s", signal.Maker)
}
if signal.Token0Address != "8pgpJDYuojYXvb8KE4Hv7DCty12FrkqpKChgfHzspump" {
t.Fatalf("expected token0 address 8pgpJDYuojYXvb8KE4Hv7DCty12FrkqpKChgfHzspump, got %s", signal.Token0Address)
}
if signal.Token0AmountUint64 != 6235736929390 {
t.Fatalf("expected token0 amount 6235736929390, got %d", signal.Token0AmountUint64)
}
if signal.Token1AmountUint64 != 1379707703 {
t.Fatalf("expected token1 amount 1379707703, got %d", signal.Token1AmountUint64)
}
}
func TestParsePhotonBuy(t *testing.T) {
rpcUrl := os.Getenv("SOL_RPC_URL")
if rpcUrl == "" {
t.Fatalf("SOL_RPC_URL is not set")
}
client := rpc.New(rpcUrl)
signals := ParseTransaction(
getTransaction(t, client, "4DCEcXAWBxagXoUNGhWsJ7qfxq5SuE5BG2cBDBqAY7sCHkBopaMJu33ZnXnFHqzPMmWxVxq6666KRF4hMHVB33Ux"),
nil,
false,
)
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "photon" {
t.Fatalf("expected terminal signal, got %s", signal.Label)
}
if signal.Event != "buy" {
t.Fatalf("expected buy event, got %s", signal.Event)
}
if signal.Maker != "8sUm7sLf3Steu6oVyVQqoA9GpFcMRz6YhrAidd4x7g7a" {
t.Fatalf("expected maker 8sUm7sLf3Steu6oVyVQqoA9GpFcMRz6YhrAidd4x7g7a, got %s", signal.Maker)
}
if signal.Token0Address != "jx4PF2MwC7AK9S8dTeYm29hM3vAN8Rtfs2VX4Vz5UVj" {
t.Fatalf("expected token0 address jx4PF2MwC7AK9S8dTeYm29hM3vAN8Rtfs2VX4Vz5UVj, got %s", signal.Token0Address)
}
if signal.Token0AmountUint64 != 1796593710706 {
t.Fatalf("expected token0 amount 1796593710706, got %d", signal.Token0AmountUint64)
}
if signal.Token1AmountUint64 != 1955555553 {
t.Fatalf("expected token1 amount 1955555553, got %d", signal.Token1AmountUint64)
}
}

View File

@@ -0,0 +1,205 @@
package clients
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"math/big"
"sync"
"time"
"github.com/gagliardetto/solana-go"
"github.com/mr-tron/base58"
"github.com/quic-go/quic-go"
)
const (
alpnTPUProtocolID = "solana-tpu"
defaultServerName = "soyas-landing"
defaultKeepAlive = 25 * time.Second
defaultIdleTimeout = 5 * time.Minute
)
type SoyasClient struct {
endpointAddr string
tlsConfig *tls.Config
quicConfig *quic.Config
connMu sync.RWMutex
conn *quic.Conn
reconnectMu sync.Mutex
}
// Connect creates a client using the whitelisted Solana keypair (base58-encoded secret key) as the mutual-TLS client identity.
func NewSoyasClient(ctx context.Context, url string) *SoyasClient {
cert, err := x509CertificateFromSolanaBase58Key("2ketcrBU1kBvr68sPVYdBdn5ztgg3VBKZP1xa1o5B8w47wemBXH73ZALdmj3ukcGzkxh6DhzLq3myu45XUwW1eNC")
if err != nil {
panic(err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: defaultServerName,
InsecureSkipVerify: true,
NextProtos: []string{alpnTPUProtocolID},
MinVersion: tls.VersionTLS13,
}
quicConfig := &quic.Config{
KeepAlivePeriod: defaultKeepAlive,
MaxIdleTimeout: defaultIdleTimeout,
}
client := &SoyasClient{
endpointAddr: url,
tlsConfig: tlsConfig,
quicConfig: quicConfig,
}
if err = client.reconnect(ctx); err != nil {
panic(err)
}
return client
}
// Close closes the underlying QUIC connection (if any). Safe to call multiple times.
func (c *SoyasClient) Close() error {
c.reconnectMu.Lock()
defer c.reconnectMu.Unlock()
c.connMu.Lock()
conn := c.conn
c.conn = nil
c.connMu.Unlock()
if conn == nil {
return nil
}
return conn.CloseWithError(0, "")
}
// SendTransaction sends a signed Solana transaction payload to Soyas.
// The payload should be the raw wire bytes (for example, from solana-go's tx.MarshalBinary()).
// If sending fails, it reconnects once and retries.
func (c *SoyasClient) SendTransaction(ctx context.Context, tx *solana.Transaction) error {
if c.endpointAddr == "" {
return fmt.Errorf("send tx url is empty")
}
raw, err := tx.MarshalBinary()
if err != nil {
return err
}
conn := c.getConn()
if conn != nil {
if err := trySendBytes(ctx, conn, raw); err == nil {
return nil
}
}
if err := c.reconnect(ctx); err != nil {
return err
}
conn = c.getConn()
if conn == nil {
return errors.New("missing QUIC connection")
}
return trySendBytes(ctx, conn, raw)
}
func (c *SoyasClient) SendBundle(ctx context.Context, txs []*solana.Transaction) error {
return fmt.Errorf("soyas client not support send bundle")
}
func (c *SoyasClient) getConn() *quic.Conn {
c.connMu.RLock()
defer c.connMu.RUnlock()
return c.conn
}
func (c *SoyasClient) reconnect(ctx context.Context) error {
c.reconnectMu.Lock()
defer c.reconnectMu.Unlock()
if existing := c.getConn(); existing != nil && existing.Context().Err() == nil {
return nil
}
conn, err := quic.DialAddr(ctx, c.endpointAddr, c.tlsConfig, c.quicConfig)
if err != nil {
return err
}
c.connMu.Lock()
old := c.conn
c.conn = conn
c.connMu.Unlock()
if old != nil {
_ = old.CloseWithError(0, "")
}
return nil
}
func trySendBytes(ctx context.Context, conn *quic.Conn, payload []byte) error {
stream, err := conn.OpenUniStreamSync(ctx)
if err != nil {
return err
}
if _, err := stream.Write(payload); err != nil {
_ = stream.Close()
return err
}
return stream.Close()
}
// x509CertificateFromSolanaBase58Key creates a short-lived self-signed X.509
// certificate whose public key is derived from the provided Solana Ed25519 key.
// The Soyas ingress extracts this public key to identify/allowlist the client.
func x509CertificateFromSolanaBase58Key(apiKeyBase58 string) (tls.Certificate, error) {
raw, err := base58.Decode(apiKeyBase58)
if err != nil {
return tls.Certificate{}, err
}
var seed []byte
switch len(raw) {
case ed25519.SeedSize:
seed = raw
case ed25519.PrivateKeySize:
seed = raw[:ed25519.SeedSize]
default:
return tls.Certificate{}, errors.New("api key must decode to 32 (seed) or 64 (secret) bytes")
}
priv := ed25519.NewKeyFromSeed(seed)
pub := priv.Public().(ed25519.PublicKey)
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return tls.Certificate{}, err
}
template := &x509.Certificate{
SerialNumber: serial,
NotBefore: time.Now().Add(-5 * time.Minute),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{
Certificate: [][]byte{der},
PrivateKey: priv,
}, nil
}

View File

@@ -24,6 +24,8 @@ func NewSWQoSClient(ctx context.Context, config *SWQoSClientConfig) (SWQoSClient
client = clients.NewAstralaneClient(config.SendTxUrl)
case enum.SWQoSAgentBlocxRoute:
client = clients.NewBloxrouteClient(config.SendTxUrl)
case enum.SWQoSAgentSoyas:
client = clients.NewSoyasClient(ctx, config.SendTxUrl)
case enum.SWQoSAgent0slot, enum.SWQoSAgentJito, enum.SWQoSAgentHelius, enum.SWQoSAgentNozomi, enum.SWQoSAgentStellium:
client = clients.NewHttpClient(config.SendTxUrl, config.SendBundleUrl)
default: