21 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
thloyi
c4d35bd3d4 merge 2026-01-07 13:19:48 +08:00
thloyi
214d9e984e fix axios parser 2026-01-07 13:16:22 +08:00
c30d64fe88 Merge branch 'master' of https://github.com/samlior/libsam 2026-01-07 12:19:12 +08:00
27dde60e93 chore: add entry contract and improve axiom parse 2026-01-07 12:18:24 +08:00
thloyi
122d474524 juptierv6 fix 2026-01-07 11:57:31 +08:00
thloyi
2d3f46ebbf juptierv6 2026-01-07 11:18:02 +08:00
thloyi
c732bb2b46 fix looptable index 2026-01-06 16:42:07 +08:00
21 changed files with 5739 additions and 173 deletions

187
README.md
View File

@@ -15,6 +15,8 @@ go get github.com/samlior/libsam
| fra | fra1.shreder.xyz:9991 | | fra | fra1.shreder.xyz:9991 |
| ams | ams1.shreder.xyz:9991 | | ams | ams1.shreder.xyz:9991 |
| ewr | ny1.shreder.xyz:9991 | | ewr | ny1.shreder.xyz:9991 |
| uk | lon.shreder.xyz:9991 |
| jp | tyo.shreder.xyz:9991 |
### Usage ### Usage
@@ -105,6 +107,13 @@ See [example](./cmd/shreder/main.go).
"keepAliveUrl": "http://germany.solana.dex.blxrbdn.com/ping", "keepAliveUrl": "http://germany.solana.dex.blxrbdn.com/ping",
"tips": "0.001", "tips": "0.001",
"rateLimit": 0 "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", "keepAliveUrl": "http://amsterdam.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001", "tips": "0.001",
"rateLimit": 0 "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", "keepAliveUrl": "http://ny.solana.dex.blxrbdn.com/api/v2/ping",
"tips": "0.001", "tips": "0.001",
"rateLimit": 0 "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
} }
``` ```

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

@@ -37,6 +37,7 @@ func main() {
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR", //Event Authority "GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR", //Event Authority
"5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx", // Fee Config "5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx", // Fee Config
"pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ", // pump fee program
}, },
}, },
"photon": { "photon": {
@@ -44,8 +45,23 @@ func main() {
"BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW", "BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW",
}, },
}, },
"jupiterV6": {
AccountRequired: []string{
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
},
},
"okxdexroutev2": {
AccountRequired: []string{
"proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u",
},
},
"dflow": {
AccountRequired: []string{
"DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH",
},
},
// TODO: axiom, gmgn, etc. // TODO: axiom, gmgn, etc.
}) }, shreder.BlocksStats(false), shreder.LogParsedStats(true), shreder.ShowTableLoaded(false))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -59,7 +75,6 @@ func main() {
<-exitSignal <-exitSignal
cancel() cancel()
}() }()
// async read from shreder // async read from shreder
txCh := make(chan shreder.TxSignalBatch, 1000) txCh := make(chan shreder.TxSignalBatch, 1000)
go func() { go func() {
@@ -77,7 +92,12 @@ func main() {
return return
case txBatch := <-txCh: case txBatch := <-txCh:
//jsonData, _ := json.MarshalIndent(txBatch, "", " ") //jsonData, _ := json.MarshalIndent(txBatch, "", " ")
fmt.Println(txBatch[0].TxHash) for _, tx := range txBatch {
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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // 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 github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect
go.mongodb.org/mongo-driver v1.12.2 // indirect go.mongodb.org/mongo-driver v1.12.2 // indirect
go.uber.org/atomic v1.7.0 // 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 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.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= 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= 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, "ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb": enum.SWQoSAgentStellium,
"ste11p7e2KLYou5bwtt35H7BM6uMdo4pvioGjJXKFcN": enum.SWQoSAgentStellium, "ste11p7e2KLYou5bwtt35H7BM6uMdo4pvioGjJXKFcN": enum.SWQoSAgentStellium,
"ste11TMV68LMi1BguM4RQujtbNCZvf1sjsASpqgAvSX": enum.SWQoSAgentStellium, "ste11TMV68LMi1BguM4RQujtbNCZvf1sjsASpqgAvSX": enum.SWQoSAgentStellium,
"soyas4s6L8KWZ8rsSk1mF3d1mQScoTGGAgjk98bF8nP": enum.SWQoSAgentSoyas,
} }

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -11,23 +11,33 @@ import (
"github.com/panjf2000/ants/v2" "github.com/panjf2000/ants/v2"
) )
type TableInfo struct {
overErrCount int
addresses []solana.PublicKey
}
type AddressTables struct { type AddressTables struct {
showTableLoaded bool
rpcClient *rpc.Client 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{} loading map[solana.PublicKey]struct{}
pool *ants.Pool pool *ants.Pool
} }
func NewAddressTables(rpcClient *rpc.Client) *AddressTables { func NewAddressTables(rpcClient *rpc.Client, showTableLoaded bool) *AddressTables {
pool, _ := ants.NewPool(10, ants.WithPreAlloc(true), ants.WithNonblocking(true)) 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{ return &AddressTables{
rpcClient: rpcClient, rpcClient: rpcClient,
tables: cache, tables: cache,
loading: make(map[solana.PublicKey]struct{}), loading: make(map[solana.PublicKey]struct{}),
pool: pool, pool: pool,
showTableLoaded: showTableLoaded,
} }
} }
@@ -53,49 +63,78 @@ func (at *AddressTables) loadAddressTable(tablePubkey solana.PublicKey) ([]solan
return addresses, nil return addresses, nil
} }
func (at *AddressTables) load(tablePubkey solana.PublicKey) {
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.pool.Submit(func() {
at.mux.RLock() at.loadMux.Lock()
_, loading := at.loading[tablePubkey] _, loading := at.loading[tablePubkey]
if loading { if loading {
at.mux.RUnlock() at.loadMux.Unlock()
return return
} }
at.mux.RUnlock()
at.mux.Lock()
at.loading[tablePubkey] = struct{}{} at.loading[tablePubkey] = struct{}{}
at.mux.Unlock() at.loadMux.Unlock()
table, err := at.loadAddressTable(tablePubkey) table, err := at.loadAddressTable(tablePubkey)
if err != nil { if err != nil {
logger.Error("loadAddressTable failed", "err", err, "table", tablePubkey) logger.Error("loadAddressTable failed", "err", err, "table", tablePubkey)
at.mux.Lock() at.loadMux.Lock()
delete(at.loading, tablePubkey) delete(at.loading, tablePubkey)
at.mux.Unlock() at.loadMux.Unlock()
return return
} }
at.mux.Lock() at.loadMux.Lock()
at.tables.Add(tablePubkey, table)
total := at.tables.Len()
delete(at.loading, tablePubkey) delete(at.loading, tablePubkey)
at.mux.Unlock() at.loadMux.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 return nil
} }
at.mux.RUnlock()
var result solana.PublicKeySlice = make([]solana.PublicKey, 0, len(idx)) var result solana.PublicKeySlice = make([]solana.PublicKey, 0, len(idx))
for _, i := range idx { for _, i := range idx {
if int(i) >= len(addresses) { if int(i) >= len(addresses.addresses) {
continue logger.Error("over loadAddressTable failed", "idx", i, "table", tablePubkey)
addresses.overErrCount++
if addresses.overErrCount > 10 {
at.load(tablePubkey)
} }
result = append(result, addresses[i]) break
}
result = append(result, addresses.addresses[i])
} }
return result return result
} }

View File

@@ -3,23 +3,61 @@ package shreder
import ( import (
"context" "context"
"fmt" "fmt"
"runtime"
"time"
"github.com/gagliardetto/solana-go/rpc" "github.com/gagliardetto/solana-go/rpc"
"github.com/panjf2000/ants/v2"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
) )
type Client struct { type Client struct {
enableBlockStats bool
enableParseStats bool
conn *grpc.ClientConn conn *grpc.ClientConn
client ShrederServiceClient client ShrederServiceClient
tableLoader *AddressTables tableLoader *AddressTables
subscription map[string]*SubscribeRequestFilterTransactions 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( func NewShrederClient(
url string, url string,
rpcClient *rpc.Client, rpcClient *rpc.Client,
subscription map[string]*SubscribeRequestFilterTransactions, subscription map[string]*SubscribeRequestFilterTransactions,
options ...ClientOption,
) (*Client, func(), error) { ) (*Client, func(), error) {
if rpcClient == nil { if rpcClient == nil {
return nil, func() {}, fmt.Errorf("rpc client is nil") return nil, func() {}, fmt.Errorf("rpc client is nil")
@@ -30,11 +68,29 @@ func NewShrederClient(
return nil, func() {}, err 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{ s := &Client{
conn: conn, conn: conn,
client: NewShrederServiceClient(conn), client: NewShrederServiceClient(conn),
subscription: subscription, subscription: subscription,
tableLoader: NewAddressTables(rpcClient), tableLoader: NewAddressTables(rpcClient, o.showTableLoaded),
pool: pool,
enableBlockStats: o.blockStats,
enableParseStats: o.logParseStats,
} }
return s, func() { return s, func() {
@@ -45,6 +101,10 @@ func NewShrederClient(
func (c *Client) Wait() { func (c *Client) Wait() {
logger.Debug("waiting for shreder client to stop") logger.Debug("waiting for shreder client to stop")
if c.pool != nil {
c.pool.Release()
}
err := c.conn.Close() err := c.conn.Close()
if err != nil { if err != nil {
logger.Error("failed to close connection: ", "err", err) 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 return err
} }
logger.Debug("subscribing to transactions")
err = stream.Send(&SubscribeTransactionsRequest{ err = stream.Send(&SubscribeTransactionsRequest{
Transactions: c.subscription, Transactions: c.subscription,
}) })
@@ -72,20 +134,38 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error
return err return err
} }
txBatch := ParseTransaction(response.Transaction, c.tableLoader) if c.enableBlockStats {
if len(txBatch) == 0 { slot := response.Transaction.Slot
continue 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
}
}
txData := response.Transaction
err = c.pool.Submit(func() {
txBatch := ParseTransaction(txData, c.tableLoader, c.enableParseStats)
if len(txBatch) == 0 {
return
} }
// set fixed source for tx signals
for _, tx := range txBatch { for _, tx := range txBatch {
tx.Source = "shreder" tx.Source = "shreder"
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return
case txCh <- txBatch: 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

File diff suppressed because it is too large Load Diff

1072
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,368 @@
package shreder
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

@@ -29,6 +29,7 @@ func SetLogLevel(level slog.Level) {
type TxSignal struct { type TxSignal struct {
Source string `json:"source"` Source string `json:"source"`
Label string `json:"label"`
TxHash string `json:"tx_hash"` TxHash string `json:"tx_hash"`
Maker string `json:"maker"` Maker string `json:"maker"`
Token0Address string `json:"token0_address"` Token0Address string `json:"token0_address"`
@@ -50,6 +51,9 @@ type TxSignal struct {
// parsed values // parsed values
Token0AmountUint64 uint64 `json:"-"` Token0AmountUint64 uint64 `json:"-"`
Token1AmountUint64 uint64 `json:"-"` Token1AmountUint64 uint64 `json:"-"`
ParseStart time.Time `json:"parse_start"`
ParseEnd time.Time `json:"parse_end"`
} }
func (t *TxSignal) Parse() *TxSignal { func (t *TxSignal) Parse() *TxSignal {

View File

@@ -5,6 +5,9 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"math/big" "math/big"
"strings"
"sync"
"time"
"github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go"
"github.com/mr-tron/base58" "github.com/mr-tron/base58"
@@ -20,13 +23,11 @@ const (
// program ids // program ids
var ( var (
pumpProgramID = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") pumpProgramID = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
// has no sell function with pump and pump.amm program // has no sell function with pump and pump.amm program
azczProgramID = solana.MustPublicKeyFromBase58("AzcZqCRUQgKEg5FTAgY7JacATABEYCEfMbjXEzspLYFB") azczProgramID = solana.MustPublicKeyFromBase58("AzcZqCRUQgKEg5FTAgY7JacATABEYCEfMbjXEzspLYFB")
// only buy function with pump program // only buy function with pump program
f5tfProgramID = solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq") f5tfProgramID = solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq")
// only pump.fun function // only pump.fun function
photonProgramID = solana.MustPublicKeyFromBase58("BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW") photonProgramID = solana.MustPublicKeyFromBase58("BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW")
@@ -42,8 +43,27 @@ var (
flasProgramID = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9") flasProgramID = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9")
terminalProgramID = solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3") terminalProgramID = solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3")
jupiterV6ProgramID = solana.MustPublicKeyFromBase58("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4")
gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb")
bonkProgramID = solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD")
) )
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 // instruction discriminators
var ( var (
pumpCreateCoinIX = []byte{24, 30, 200, 40, 5, 28, 7, 119} pumpCreateCoinIX = []byte{24, 30, 200, 40, 5, 28, 7, 119}
@@ -78,6 +98,10 @@ var (
terminalBuyTokensIX = []byte{0xa6, 0x54, 0x14, 0x96, 0x9f, 0x77, 0x59, 0xca} terminalBuyTokensIX = []byte{0xa6, 0x54, 0x14, 0x96, 0x9f, 0x77, 0x59, 0xca}
terminalSellTokensIX = []byte{0xbe, 0x84, 0xa2, 0x96, 0x93, 0x7c, 0xf8, 0x6b} terminalSellTokensIX = []byte{0xbe, 0x84, 0xa2, 0x96, 0x93, 0x7c, 0xf8, 0x6b}
terminalAmmSellTokensIX = []byte{0x40, 0x64, 0x97, 0xb9, 0x16, 0xfa, 0xec, 0xb1} 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 { type compiledInstruction struct {
@@ -102,6 +126,7 @@ type versionedTransaction struct {
Signatures []solana.Signature Signatures []solana.Signature
Message versionedMessage Message versionedMessage
Block uint64 Block uint64
Time time.Time
} }
type pumpExtendedSellArgs struct { type pumpExtendedSellArgs struct {
@@ -132,9 +157,9 @@ type f5tfBuyArgs struct {
TokenAmount uint64 TokenAmount uint64
} }
type flasBuyArgs struct { type flasArgs struct {
SolAmount uint64 Amount1 uint64
TokenAmount uint64 Amount2 uint64
Placeholder [3]uint8 Placeholder [3]uint8
} }
@@ -155,16 +180,6 @@ type pumpAmmBuyArgs struct {
MaxSolCost uint64 MaxSolCost uint64
} }
type _6hb1BuyArgs struct {
SolAmount uint64
TokenNumber uint64
}
type _8rsrBuyArgs struct {
SolIn uint64
TokenOut uint64
}
type boboBuyArgs struct { type boboBuyArgs struct {
Placeholder1 uint64 Placeholder1 uint64
Placeholder2 uint64 Placeholder2 uint64
@@ -186,85 +201,173 @@ type fjszBuyArgs struct {
TokenAmount uint64 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. // 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) versioned, err := toVersionedTransaction(update)
if err != nil || versioned == nil || len(versioned.Signatures) == 0 { if err != nil || versioned == nil || len(versioned.Signatures) == 0 {
return nil return nil
} }
defer func() {
releaseVersionedPool(versioned)
}()
txHash := versioned.Signatures[0] txHash := versioned.Signatures[0]
staticKeys := versioned.Message.StaticAccountKeys // staticKeys := versioned.Message.StaticAccountKeys
instructions := versioned.Message.Instructions instructions := versioned.Message.Instructions
if loader != nil && len(versioned.Message.AddressTableLookups) > 0 { if loader != nil && len(versioned.Message.AddressTableLookups) > 0 {
// currently we only care about photon table lookup lookupTableOk := true
for _, lookup := range versioned.Message.AddressTableLookups { for _, lookup := range versioned.Message.AddressTableLookups {
accounts := loader.GetAddressTable(lookup.AccountKey, lookup.WritableIndexes) if len(lookup.WritableIndexes) == 0 {
if len(accounts) != len(lookup.WritableIndexes) { continue
}
lookupTableOk = loader.FillToTx(versioned, lookup.AccountKey, lookup.WritableIndexes)
if !lookupTableOk {
break break
} }
staticKeys = append(staticKeys, accounts...) }
accounts2 := loader.GetAddressTable(lookup.AccountKey, lookup.ReadonlyIndexes) if lookupTableOk {
if len(accounts2) != len(lookup.ReadonlyIndexes) { for _, lookup := range versioned.Message.AddressTableLookups {
if len(lookup.ReadonlyIndexes) == 0 {
continue
}
lookupTableOk = loader.FillToTx(versioned, lookup.AccountKey, lookup.ReadonlyIndexes)
if !lookupTableOk {
break break
} }
staticKeys = append(staticKeys, accounts2...)
} }
} }
// versioned.Message.StaticAccountKeys = staticKeys
}
var parsed []*TxSignal var parsed []*TxSignal = make([]*TxSignal, 0, 3)
for i := range instructions { for i := range instructions {
inst := instructions[i] inst := instructions[i]
if int(inst.ProgramIDIndex) >= len(staticKeys) { if int(inst.ProgramIDIndex) >= len(versioned.Message.StaticAccountKeys) {
continue continue
} }
programID := staticKeys[inst.ProgramIDIndex] programID := versioned.Message.StaticAccountKeys[inst.ProgramIDIndex]
switch programID { switch programID {
case pumpProgramID: case pumpProgramID:
txRes, err := parsePumpInstruction(versioned, i) txRes, err := parsePumpInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "pump") parsed = appendParsed(now, parsed, txRes, err, txHash, "pump")
case azczProgramID: case azczProgramID:
txRes, err := parseAzczInstruction(versioned, i) txRes, err := parseAzczInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "azcz") parsed = appendParsed(now, parsed, txRes, err, txHash, "azcz")
case f5tfProgramID: case f5tfProgramID:
txRes, err := parseF5tfInstruction(versioned, i) txRes, err := parseF5tfInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "f5tf") parsed = appendParsed(now, parsed, txRes, err, txHash, "f5tf")
case flasProgramID: case flasProgramID:
txRes, err := parseFlasInstruction(versioned, i) txRes, err := parseFlasInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "flas") parsed = appendParsed(now, parsed, txRes, err, txHash, "flas")
case photonProgramID: case photonProgramID:
txRes, err := parsePhotonInstruction(versioned, i) txRes, err := parsePhotonInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "photon") parsed = appendParsed(now, parsed, txRes, err, txHash, "photon")
case pumpAmmProgramID: case pumpAmmProgramID:
txRes, err := parsePumpAmmInstruction(versioned, i) txRes, err := parsePumpAmmInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "pumpamm") parsed = appendParsed(now, parsed, txRes, err, txHash, "pumpamm")
case boboProgramID: case boboProgramID:
txRes, err := parseBoboInstruction(versioned, i) txRes, err := parseBoboInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "bobo") parsed = appendParsed(now, parsed, txRes, err, txHash, "bobo")
case qtkvProgramID: case qtkvProgramID:
txRes, err := parseQtkvInstruction(versioned, i) txRes, err := parseQtkvInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "qtkv") parsed = appendParsed(now, parsed, txRes, err, txHash, "qtkv")
case fjszProgramID: case fjszProgramID:
txRes, err := parseFjszInstruction(versioned, i) txRes, err := parseFjszInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "fjsz") parsed = appendParsed(now, parsed, txRes, err, txHash, "fjsz")
case terminalProgramID: case terminalProgramID:
txRes, err := parseTermInstruction(versioned, i) txRes, err := parseTermInstruction(versioned, i)
parsed = appendParsed(parsed, txRes, err, txHash, "terminal") parsed = appendParsed(now, parsed, txRes, err, txHash, "terminal")
case jupiterV6ProgramID:
txRes, err := parseJupiterV6Instruction(versioned, i)
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 return parsed
} }
func appendParsed(list []*TxSignal, parsed *TxSignal, err error, txHash [64]byte, label string) []*TxSignal { func appendParsed(start time.Time, list []*TxSignal, parsed *TxSignal, err error, txHash [64]byte, label string) []*TxSignal {
if err != nil { 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[:])) logger.Debug("txparser: failed to parse", "label", label, "instruction", err, "tx_hash", base58.Encode(txHash[:]))
}
return list return list
} }
if parsed != nil { if parsed != nil {
parsed.Label = label
if !start.IsZero() {
parsed.ParseEnd = time.Now()
parsed.ParseStart = start
}
list = append(list, parsed) list = append(list, parsed)
} }
return list return list
@@ -277,47 +380,42 @@ func toVersionedTransaction(update *SubscribeUpdateTransaction) (*versionedTrans
protoTx := update.Transaction protoTx := update.Transaction
msg := protoTx.Message msg := protoTx.Message
versioned := requireVersionedPool()
signatures := make([]solana.Signature, len(protoTx.Signatures)) versioned.Signatures = versioned.Signatures[:0]
for i, rawSig := range protoTx.Signatures { for _, rawSig := range protoTx.Signatures {
signatures[i] = solana.SignatureFromBytes(rawSig) versioned.Signatures = append(versioned.Signatures, solana.SignatureFromBytes(rawSig))
} }
versioned.Message.StaticAccountKeys = versioned.Message.StaticAccountKeys[:0]
staticKeys := make([]solana.PublicKey, len(msg.AccountKeys)) for _, key := range msg.AccountKeys {
for i, key := range msg.AccountKeys { versioned.Message.StaticAccountKeys = append(versioned.Message.StaticAccountKeys, solana.PublicKeyFromBytes(key))
staticKeys[i] = solana.PublicKeyFromBytes(key)
} }
versioned.Message.Instructions = versioned.Message.Instructions[:0]
instructions := make([]compiledInstruction, len(msg.Instructions)) for _, instr := range msg.Instructions {
for i, instr := range msg.Instructions { accounts := requireAccIdxSlice()
accounts := append([]uint8(nil), instr.Accounts...) accounts = append(accounts, instr.Accounts...)
instructions[i] = compiledInstruction{ versioned.Message.Instructions = append(versioned.Message.Instructions,
compiledInstruction{
ProgramIDIndex: uint8(instr.ProgramIdIndex), ProgramIDIndex: uint8(instr.ProgramIdIndex),
Accounts: accounts, Accounts: accounts,
Data: instr.Data, Data: instr.Data,
} })
} }
lookups := make([]addressTableLookup, len(msg.AddressTableLookups)) versioned.Message.AddressTableLookups = versioned.Message.AddressTableLookups[:0]
for i, lookup := range msg.AddressTableLookups { for _, lookup := range msg.AddressTableLookups {
writable := append([]uint8(nil), lookup.WritableIndexes...) writable := requireAccIdxSlice()
readonly := append([]uint8(nil), lookup.ReadonlyIndexes...) writable = append(writable, lookup.WritableIndexes...)
lookups[i] = addressTableLookup{ readonly := requireAccIdxSlice()
readonly = append(readonly, lookup.ReadonlyIndexes...)
versioned.Message.AddressTableLookups = append(versioned.Message.AddressTableLookups, addressTableLookup{
AccountKey: solana.PublicKeyFromBytes(lookup.AccountKey), AccountKey: solana.PublicKeyFromBytes(lookup.AccountKey),
WritableIndexes: writable, WritableIndexes: writable,
ReadonlyIndexes: readonly, ReadonlyIndexes: readonly,
} })
} }
return &versionedTransaction{ versioned.Block = update.GetSlot()
Signatures: signatures, return versioned, nil
Message: versionedMessage{
StaticAccountKeys: staticKeys,
Instructions: instructions,
AddressTableLookups: lookups,
},
Block: update.GetSlot(),
}, nil
} }
func formatTokenAmount(amount uint64) decimal.Decimal { func formatTokenAmount(amount uint64) decimal.Decimal {
@@ -331,11 +429,8 @@ func formatSolAmount(lamports uint64) decimal.Decimal {
} }
func getStaticKey(static []solana.PublicKey, index int) (solana.PublicKey, error) { func getStaticKey(static []solana.PublicKey, index int) (solana.PublicKey, error) {
if index < 0 { if index < 0 || index >= len(static) {
return solana.PublicKey{}, fmt.Errorf("account index %d less then 0", index) return solana.PublicKey{}, NewAccountNotFoundError(index, len(static))
}
if index >= len(static) {
return solana.PublicKey{}, fmt.Errorf("account index %d out of range", index)
} }
return static[index], nil return static[index], nil
} }
@@ -381,6 +476,7 @@ func parsePumpCreate(tx *versionedTransaction, instruction *compiledInstruction)
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: creator.String(), Maker: creator.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -421,6 +517,7 @@ func parsePumpCreateV2(tx *versionedTransaction, instruction *compiledInstructio
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: args.Creator.String(), Maker: args.Creator.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -484,6 +581,7 @@ func parsePumpBuy(tx *versionedTransaction, instruction *compiledInstruction) (*
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: buyer.String(), Maker: buyer.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -541,6 +639,7 @@ func parsePumpSell(tx *versionedTransaction, instruction *compiledInstruction) (
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: seller.String(), Maker: seller.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -599,6 +698,7 @@ func parseAzczAmmBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal,
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "azcz",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -642,6 +742,7 @@ func parseAzczBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, er
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "azcz",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -696,6 +797,7 @@ func parseF5tfInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "f5tf",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -728,10 +830,10 @@ func parseFlasInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return nil, fmt.Errorf("data too short for args flas instruction, len: %d", len(instruction.Data)) return nil, fmt.Errorf("data too short for args flas instruction, len: %d", len(instruction.Data))
} }
methodData := instruction.Data[17:20] methodData := instruction.Data[17:20]
if !matchMethod(methodData, flasBuyTokensIX) { //if !matchMethod(methodData, flasBuyTokensIX) {
return nil, nil // return nil, nil
} //}
if matchMethod(methodData, f5tfBuyTokensIX) { if matchMethod(methodData, flasBuyTokensIX) {
return parseFlasBuy(tx, instructionIndex) return parseFlasBuy(tx, instructionIndex)
} else if matchMethod(methodData, flasSellTokensIX) { } else if matchMethod(methodData, flasSellTokensIX) {
return parseFlasSell(tx, instructionIndex) return parseFlasSell(tx, instructionIndex)
@@ -759,26 +861,27 @@ func parseFlasAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal
return nil, err return nil, err
} }
var args flasBuyArgs var args flasArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
} }
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
Token0Amount: decimal.Zero, Token0Amount: formatTokenAmount(args.Amount1),
Token1Amount: formatSolAmount(args.TokenAmount), Token1Amount: formatSolAmount(args.Amount2),
Program: "Pump", Program: "PumpAMM",
Event: "sell", Event: "sell",
IsToken2022: false, IsToken2022: false,
IsMayhemMode: false, IsMayhemMode: false,
ExactSOL: true, ExactSOL: false,
Block: tx.Block, Block: tx.Block,
Token0AmountUint64: 0, Token0AmountUint64: args.Amount1,
Token1AmountUint64: args.TokenAmount, Token1AmountUint64: args.Amount2,
}, nil }, nil
} }
@@ -798,26 +901,27 @@ func parseFlasAmmBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal,
return nil, err return nil, err
} }
var args flasBuyArgs var args flasArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
} }
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
Token0Amount: decimal.Zero, Token0Amount: decimal.Zero,
Token1Amount: formatSolAmount(args.TokenAmount), Token1Amount: formatSolAmount(args.Amount1),
Program: "Pump", Program: "PumpAMM",
Event: "sell", Event: "buy",
IsToken2022: false, IsToken2022: false,
IsMayhemMode: false, IsMayhemMode: false,
ExactSOL: true, ExactSOL: true,
Block: tx.Block, Block: tx.Block,
Token0AmountUint64: 0, Token0AmountUint64: 0,
Token1AmountUint64: args.TokenAmount, Token1AmountUint64: args.Amount1,
}, nil }, nil
} }
@@ -837,25 +941,26 @@ func parseFlasSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, e
return nil, err return nil, err
} }
var args flasBuyArgs var args flasArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
} }
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.TokenAmount), Token0Amount: formatTokenAmount(args.Amount1),
Token1Amount: formatSolAmount(args.SolAmount), Token1Amount: formatSolAmount(args.Amount2),
Program: "Pump", Program: "Pump",
Event: "sell", Event: "sell",
IsToken2022: false, IsToken2022: false,
IsMayhemMode: false, IsMayhemMode: false,
Block: tx.Block, Block: tx.Block,
Token0AmountUint64: args.TokenAmount, Token0AmountUint64: args.Amount1,
Token1AmountUint64: args.SolAmount, Token1AmountUint64: args.Amount2,
}, nil }, nil
} }
@@ -874,27 +979,90 @@ func parseFlasBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(instruction.Data) > 20 {
var args flasBuyArgs instruction.Data = instruction.Data[:20]
}
var args flasArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil { if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err) return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
} }
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.TokenAmount), Token0Amount: formatTokenAmount(args.Amount2),
Token1Amount: formatSolAmount(args.SolAmount), Token1Amount: formatSolAmount(args.Amount1),
Program: "Pump", Program: "Pump",
Event: "buy", Event: "buy",
IsToken2022: false, IsToken2022: false,
IsMayhemMode: false, IsMayhemMode: false,
ExactSOL: true, ExactSOL: true,
Block: tx.Block, Block: tx.Block,
Token0AmountUint64: args.TokenAmount, Token0AmountUint64: args.Amount2,
Token1AmountUint64: args.SolAmount, Token1AmountUint64: args.Amount1,
}, 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 }, nil
} }
@@ -948,6 +1116,7 @@ func parsePhotonBuy(tx *versionedTransaction, instruction *compiledInstruction)
solAmount := args.SolAmount * (100000000 - 1234568) / 100000000 solAmount := args.SolAmount * (100000000 - 1234568) / 100000000
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "photon",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -957,6 +1126,7 @@ func parsePhotonBuy(tx *versionedTransaction, instruction *compiledInstruction)
Event: "buy", Event: "buy",
IsToken2022: false, IsToken2022: false,
IsMayhemMode: false, IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block, Block: tx.Block,
Token0AmountUint64: args.TokenAmount, Token0AmountUint64: args.TokenAmount,
Token1AmountUint64: solAmount, Token1AmountUint64: solAmount,
@@ -1003,6 +1173,7 @@ func parsePhotonSwap(tx *versionedTransaction, instruction *compiledInstruction)
solAmount := args.FromAmount * (100000000 - 1234568) / 100000000 solAmount := args.FromAmount * (100000000 - 1234568) / 100000000
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "photon",
Maker: buyer.String(), Maker: buyer.String(),
Token0Address: base.String(), Token0Address: base.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1084,6 +1255,7 @@ func parseTermAmmSell(tx *versionedTransaction, instruction *compiledInstruction
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "term",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1119,6 +1291,7 @@ func parseTermBuy(tx *versionedTransaction, instruction *compiledInstruction) (*
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "term",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1128,6 +1301,7 @@ func parseTermBuy(tx *versionedTransaction, instruction *compiledInstruction) (*
Event: "buy", Event: "buy",
IsToken2022: false, IsToken2022: false,
IsMayhemMode: false, IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block, Block: tx.Block,
Token0AmountUint64: tokenAmount, Token0AmountUint64: tokenAmount,
Token1AmountUint64: solAmount, Token1AmountUint64: solAmount,
@@ -1153,6 +1327,7 @@ func parseTermSell(tx *versionedTransaction, instruction *compiledInstruction) (
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "term",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1162,6 +1337,7 @@ func parseTermSell(tx *versionedTransaction, instruction *compiledInstruction) (
Event: "buy", Event: "buy",
IsToken2022: false, IsToken2022: false,
IsMayhemMode: false, IsMayhemMode: false,
ExactSOL: false,
Block: tx.Block, Block: tx.Block,
Token0AmountUint64: tokenAmount, Token0AmountUint64: tokenAmount,
Token1AmountUint64: solAmount, Token1AmountUint64: solAmount,
@@ -1224,6 +1400,7 @@ func parsePumpAmmBuy(tx *versionedTransaction, instruction *compiledInstruction)
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "pumpamm",
Maker: buyer.String(), Maker: buyer.String(),
Token0Address: base.String(), Token0Address: base.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1270,6 +1447,7 @@ func parsePumpAmmSell(tx *versionedTransaction, instruction *compiledInstruction
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "pumpamm",
Maker: buyer.String(), Maker: buyer.String(),
Token0Address: base.String(), Token0Address: base.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1323,6 +1501,7 @@ func parseBoboInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "bobo",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1383,6 +1562,7 @@ func parseQtkvSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, e
tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25]) tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25])
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1421,6 +1601,7 @@ func parseQtkvAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal
tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25]) tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25])
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1459,6 +1640,7 @@ func parseQtkvBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, er
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1512,6 +1694,7 @@ func parseFjszInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
return &TxSignal{ return &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Label: "fjsz",
Maker: user.String(), Maker: user.String(),
Token0Address: mint.String(), Token0Address: mint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
@@ -1527,7 +1710,7 @@ func parseFjszInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi
}, nil }, nil
} }
func parseTerminalInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { func parseBonkInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
msg := tx.Message msg := tx.Message
if instructionIndex >= len(msg.Instructions) { if instructionIndex >= len(msg.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds") return nil, fmt.Errorf("instruction index out of bounds")
@@ -1537,24 +1720,87 @@ func parseTerminalInstruction(tx *versionedTransaction, instructionIndex int) (*
if len(instruction.Data) == 0 { if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty") return nil, fmt.Errorf("data is empty")
} }
if matchMethod(instruction.Data, terminalBuyTokensIX) {
return parseTermBuy(tx, &instruction) if matchMethod(instruction.Data, bonkBuyAndSellTokensIX) {
} else if matchMethod(instruction.Data, terminalSellTokensIX) { return parseBonkBuyAndSell(tx, &instruction)
return parseTermSell(tx, &instruction)
} else if matchMethod(instruction.Data, terminalAmmSellTokensIX) {
return parseTermAmmSell(tx, &instruction)
} }
return nil, nil return nil, nil
} }
func indexOf(haystack []uint8, needle uint8) int { func parseBonkBuyAndSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) {
for i, v := range haystack { if len(instruction.Accounts) < 8 {
if v == needle { return nil, fmt.Errorf("accounts too short")
return i
} }
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 { func matchMethod(data []byte, methods []byte) bool {

View File

@@ -0,0 +1,291 @@
package shreder
import (
"context"
"encoding/hex"
"os"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/near/borsh-go"
)
func TestDecodeAxiomArgs(t *testing.T) {
tests := []struct {
name string
hexData string
}{
{
name: "pump amm sell Test 0",
hexData: "00686f08bb1b0000007eb4ac020000000001020200183c",
},
{
name: "pump amm buy Test 1",
hexData: "00c09ee6050000000001c94d882600000000020200323c",
},
{
name: "pump buy Test 2",
hexData: "00d8d3bc0000000000bb7c53f009000000000104185a",
},
{
name: "pump sell Test 3",
hexData: "009bbf69ec08080000830bc61200000000010103a000",
},
{
name: "pump swap sell Test 4",
hexData: "00c98ea7588b0000009adf3b010000000001020200283c",
},
{
name: "pump swap sell Test 5",
hexData: "00d3727f9301000000f9a50b0100000000010202001e00",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := hex.DecodeString(tt.hexData)
if err != nil {
t.Fatalf("failed to decode hex string: %v", err)
return
}
var args flasArgs
if err := borsh.Deserialize(&args, data[1:]); err != nil {
t.Fatalf("failed to decode Axiom args: %v", err)
return
}
t.Logf("Decoded Axiom Args: %+v", args)
})
}
}
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) client = clients.NewAstralaneClient(config.SendTxUrl)
case enum.SWQoSAgentBlocxRoute: case enum.SWQoSAgentBlocxRoute:
client = clients.NewBloxrouteClient(config.SendTxUrl) 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: case enum.SWQoSAgent0slot, enum.SWQoSAgentJito, enum.SWQoSAgentHelius, enum.SWQoSAgentNozomi, enum.SWQoSAgentStellium:
client = clients.NewHttpClient(config.SendTxUrl, config.SendBundleUrl) client = clients.NewHttpClient(config.SendTxUrl, config.SendBundleUrl)
default: default: