27 Commits
v1.3.2 ... 1.x

Author SHA1 Message Date
bijianing97
35c5c83f4b Add dflow pumpfun parse 2026-01-27 14:48:18 +08:00
bijianing97
5f97972194 Add jupiter pumpamm buy pase 2026-01-26 17:18:29 +08:00
bijianing97
741d333e1b Update juptier pumpfun usdc usdt usd1 filter 2026-01-23 15:07:03 +08:00
bijianing97
594c46a1d2 Add bloomrouter pumpfun parse 2026-01-22 17:50:26 +08:00
bijianing97
45107aa8c3 Add JupiterAggregatorV6 pumpfun parse 2026-01-22 17:10:13 +08:00
bijianing97
36db4729d4 Update metora dlmm program parse 2026-01-22 14:32:45 +08:00
23f37cff2c chore: add release pool 2026-01-19 09:35:47 +08:00
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
20 changed files with 13945 additions and 230 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
} }
``` ```

View File

@@ -50,8 +50,18 @@ func main() {
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", "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)
} }
@@ -65,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() {
@@ -84,8 +93,8 @@ func main() {
case txBatch := <-txCh: case txBatch := <-txCh:
//jsonData, _ := json.MarshalIndent(txBatch, "", " ") //jsonData, _ := json.MarshalIndent(txBatch, "", " ")
for _, tx := range txBatch { for _, tx := range txBatch {
if tx.Label == "jupiterV6" { if tx.Label == "okxdexroutev2" || tx.Label == "jupiterv6" || tx.Label == "dflow" {
fmt.Println("===============", tx.TxHash, tx.Token0Address, tx.Token0Amount) 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) //fmt.Println(txBatch[0].TxHash)

180
cmd/txparse/main.go Normal file
View File

@@ -0,0 +1,180 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/programs/address-lookup-table"
"github.com/gagliardetto/solana-go/rpc"
"github.com/samlior/libsam/pkg/shreder"
)
const (
rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
txSignature = "4YUQzsQcHxt5jA6qKPVBWCgw8VRuE6bZqAoXeiwptbdLwta3QnDbWHzjwP3mY8hJPPerSf1yGbpdL2SdyWZTJ9e1"
labelFilter = ""
enableStats = true
)
func main() {
if rpcURL == "" || rpcURL == "REPLACE_WITH_RPC_URL" {
log.Fatal("rpcURL is not set in cmd/dlmmparse/main.go")
}
if txSignature == "" || txSignature == "REPLACE_WITH_TX_SIGNATURE" {
log.Fatal("txSignature is not set in cmd/dlmmparse/main.go")
}
client := rpc.New(rpcURL)
sig, err := solana.SignatureFromBase58(txSignature)
if err != nil {
log.Fatalf("invalid txSignature: %v", err)
}
version := uint64(0)
tx, err := client.GetTransaction(
context.Background(),
sig,
&rpc.GetTransactionOpts{
Commitment: rpc.CommitmentFinalized,
MaxSupportedTransactionVersion: &version,
},
)
if err != nil {
log.Fatalf("getTransaction failed: %v", err)
}
if tx == nil || tx.Transaction == nil {
log.Fatal("transaction is empty")
}
rawTx, err := tx.Transaction.GetTransaction()
if err != nil {
log.Fatalf("decode transaction failed: %v", err)
}
if rawTx == nil {
log.Fatal("decoded transaction is nil")
}
if len(rawTx.Message.AddressTableLookups) > 0 {
tables := make(map[solana.PublicKey]solana.PublicKeySlice, len(rawTx.Message.AddressTableLookups))
for _, lookup := range rawTx.Message.AddressTableLookups {
state, err := addresslookuptable.GetAddressLookupTable(context.Background(), client, lookup.AccountKey)
if err != nil {
log.Fatalf("load address table %s failed: %v", lookup.AccountKey, err)
}
tables[lookup.AccountKey] = state.Addresses
}
if err := rawTx.Message.SetAddressTables(tables); err != nil {
log.Fatalf("set address tables failed: %v", err)
}
if err := rawTx.Message.ResolveLookups(); err != nil {
log.Fatalf("resolve address lookups failed: %v", err)
}
}
update := toSubscribeUpdate(tx.Slot, rawTx)
signals := shreder.ParseTransaction(update, nil, enableStats)
if len(signals) == 0 {
fmt.Println("no signals parsed")
return
}
printed := false
for _, signal := range signals {
if signal == nil {
continue
}
if labelFilter != "" && signal.Label != labelFilter {
continue
}
printed = true
output, err := json.MarshalIndent(signal, "", " ")
if err != nil {
log.Fatalf("marshal signal failed: %v", err)
}
fmt.Println(string(output))
}
if printed {
return
}
if labelFilter != "" {
fmt.Printf("no %s signal parsed, dump all signals:\n", labelFilter)
} else {
fmt.Println("no matching signal parsed, dump all signals:")
}
for _, signal := range signals {
if signal == nil {
continue
}
output, err := json.MarshalIndent(signal, "", " ")
if err != nil {
log.Fatalf("marshal signal failed: %v", err)
}
fmt.Println(string(output))
}
}
func toSubscribeUpdate(slot uint64, tx *solana.Transaction) *shreder.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([]*shreder.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] = &shreder.CompiledInstruction{
ProgramIdIndex: uint32(instr.ProgramIDIndex),
Accounts: accounts,
Data: instr.Data[:],
}
}
addressTableLookups := make([]*shreder.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] = &shreder.MessageAddressTableLookup{
AccountKey: lookup.AccountKey[:],
WritableIndexes: writable,
ReadonlyIndexes: readonly,
}
}
return &shreder.SubscribeUpdateTransaction{
Transaction: &shreder.Transaction{
Signatures: signatures,
Message: &shreder.Message{
Header: &shreder.MessageHeader{
NumRequiredSignatures: uint32(tx.Message.Header.NumRequiredSignatures),
NumReadonlySignedAccounts: uint32(tx.Message.Header.NumReadonlySignedAccounts),
NumReadonlyUnsignedAccounts: uint32(tx.Message.Header.NumReadonlyUnsignedAccounts),
},
AccountKeys: accountKeys,
RecentBlockhash: nil,
Instructions: instructions,
Versioned: false,
AddressTableLookups: addressTableLookups,
},
},
Slot: slot,
}
}

5
go.mod
View File

@@ -4,10 +4,13 @@ go 1.25.1
require ( require (
github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250722092120-44561cb37455 github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250722092120-44561cb37455
github.com/gagliardetto/binary v0.8.0
github.com/gagliardetto/solana-go v1.12.0 github.com/gagliardetto/solana-go v1.12.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/mr-tron/base58 v1.2.0 github.com/mr-tron/base58 v1.2.0
github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454 github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454
github.com/panjf2000/ants/v2 v2.11.4 github.com/panjf2000/ants/v2 v2.11.4
github.com/quic-go/quic-go v0.58.0
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
google.golang.org/grpc v1.75.0 google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.10 google.golang.org/protobuf v1.36.10
@@ -19,10 +22,8 @@ require (
github.com/blendle/zapdriver v1.3.1 // indirect github.com/blendle/zapdriver v1.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.9.0 // indirect github.com/fatih/color v1.9.0 // indirect
github.com/gagliardetto/binary v0.8.0 // indirect
github.com/gagliardetto/treeout v0.1.4 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect github.com/klauspost/compress v1.13.6 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect

8
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=
@@ -86,8 +88,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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=
@@ -115,6 +117,8 @@ go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=

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(5, 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) {
_ = at.pool.Submit(func() {
at.loadMux.Lock()
_, loading := at.loading[tablePubkey]
if loading {
at.loadMux.Unlock()
return
}
at.loading[tablePubkey] = struct{}{}
at.loadMux.Unlock()
func (at *AddressTables) GetAddressTable(tablePubkey solana.PublicKey, idx []uint8) []solana.PublicKey { table, err := at.loadAddressTable(tablePubkey)
at.mux.RLock() if err != nil {
logger.Error("loadAddressTable failed", "err", err, "table", tablePubkey)
at.loadMux.Lock()
delete(at.loading, tablePubkey)
at.loadMux.Unlock()
return
}
at.loadMux.Lock()
delete(at.loading, tablePubkey)
at.loadMux.Unlock()
at.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) addresses, ok := at.tables.Get(tablePubkey)
if !ok { if !ok {
at.mux.RUnlock() at.load(tablePubkey)
_ = at.pool.Submit(func() { return false
at.mux.RLock() }
_, loading := at.loading[tablePubkey]
if loading {
at.mux.RUnlock()
return
}
at.mux.RUnlock()
at.mux.Lock()
at.loading[tablePubkey] = struct{}{}
at.mux.Unlock()
table, err := at.loadAddressTable(tablePubkey) for _, i := range idx {
if err != nil { if int(i) >= len(addresses.addresses) {
logger.Error("loadAddressTable failed", "err", err, "table", tablePubkey) logger.Error("over loadAddressTable failed", "idx", i, "table", tablePubkey)
at.mux.Lock() addresses.overErrCount++
delete(at.loading, tablePubkey) if addresses.overErrCount > 10 {
at.mux.Unlock() at.load(tablePubkey)
return
} }
at.mux.Lock() return false
at.tables.Add(tablePubkey, table) }
total := at.tables.Len() tx.Message.StaticAccountKeys = append(tx.Message.StaticAccountKeys, addresses.addresses[i])
delete(at.loading, tablePubkey) }
at.mux.Unlock() return true
logger.Info("loadAddressTable", "table", tablePubkey.String(), "table count:", total) }
})
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)
}
break
} }
result = append(result, addresses[i]) 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,
}) })
@@ -66,26 +128,53 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error
return err return err
} }
// reboot the pool
c.pool.Reboot()
for { for {
response, err := stream.Recv() var response *SubscribeTransactionsResponse
response, err = stream.Recv()
if err != nil { if err != nil {
return err break
} }
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
}
} }
// set fixed source for tx signals txData := response.Transaction
for _, tx := range txBatch {
tx.Source = "shreder"
}
select { err = c.pool.Submit(func() {
case <-ctx.Done(): txBatch := ParseTransaction(txData, c.tableLoader, c.enableParseStats)
return ctx.Err() if len(txBatch) == 0 {
case txCh <- txBatch: return
}
for _, tx := range txBatch {
tx.Source = "shreder"
}
select {
case <-ctx.Done():
return
case txCh <- txBatch:
}
})
if err != nil {
break
} }
} }
// sync waiting for all tasks to complete
c.pool.Release()
return err
} }

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

@@ -0,0 +1,409 @@
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
)
// PumpFun*Options { amount: u64, orchestrator_flags: OrchestratorFlags{flags u8} }
type pumpFunAction struct {
Amount uint64
Flags uint8
}
type dflowAction struct {
Tag uint8
Pump *pumpFunAction
}
type dflowSwapParams struct {
Actions []dflowAction
}
// bytes to skip for Action variants; only PumpFun* actions are decoded.
func skipDflowAction(dec *bin.Decoder, tag uint8) (*pumpFunAction, error) {
switch tag {
case ActWhirlpoolsSwap, ActClearpoolsSwap, ActWhirlpoolsSwapV2:
// amount u64 + bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1)
case ActRaydiumAmmSwap, ActLifinityV2Swap, 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, ActPumpFunBuy, ActPumpFunSell:
amt, err := dec.ReadUint64(binary.LittleEndian)
if err != nil {
return nil, err
}
flg, err := dec.ReadUint8()
if err != nil {
return nil, err
}
return &pumpFunAction{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 findDflowPumpAmmMints(staticKeys []solana.PublicKey, accounts []uint8) (solana.PublicKey, solana.PublicKey, bool, error) {
for i, acctIdx := range accounts {
key, err := getStaticKey(staticKeys, int(acctIdx))
if err != nil {
return solana.PublicKey{}, solana.PublicKey{}, false, err
}
if !key.Equals(pumpAmmProgramID) {
continue
}
baseIdx := i + 4
quoteIdx := i + 5
if baseIdx >= len(accounts) || quoteIdx >= len(accounts) {
return solana.PublicKey{}, solana.PublicKey{}, false, nil
}
baseMint, err := getStaticKey(staticKeys, int(accounts[baseIdx]))
if err != nil {
return solana.PublicKey{}, solana.PublicKey{}, false, err
}
quoteMint, err := getStaticKey(staticKeys, int(accounts[quoteIdx]))
if err != nil {
return solana.PublicKey{}, solana.PublicKey{}, false, err
}
return baseMint, quoteMint, true, nil
}
return solana.PublicKey{}, solana.PublicKey{}, false, nil
}
func parseDFlowInstruction(tx *versionedTransaction, instructionIndex int) (TxSignalBatch, 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 (
pumpAmmBuy *pumpFunAction
pumpAmmSell *pumpFunAction
pumpBuy *pumpFunAction
pumpSell *pumpFunAction
)
for _, act := range params.Actions {
if act.Pump == nil {
continue
}
switch act.Tag {
case ActPumpFunAmmSell:
pumpAmmSell = act.Pump
case ActPumpFunAmmBuy:
pumpAmmBuy = act.Pump
case ActPumpFunBuy:
pumpBuy = act.Pump
case ActPumpFunSell:
pumpSell = act.Pump
}
}
out := make(TxSignalBatch, 0, 2)
if pumpAmmSell != nil || pumpAmmBuy != nil {
event := "sell"
amt := pumpAmmSell
isBuy := false
if amt == nil {
event = "buy"
isBuy = true
amt = pumpAmmBuy
}
baseMint, quoteMint, ok, err := findDflowPumpAmmMints(tx.Message.StaticAccountKeys, ix.Accounts)
if err != nil {
return nil, err
}
if ok && quoteMint.Equals(solana.WrappedSol) {
var (
token0Amount decimal.Decimal
token1Amount decimal.Decimal
token0AmountUint64 uint64
token1AmountUint64 uint64
exactSol bool
)
if isBuy {
exactSol = true
token1Amount = formatSolAmount(amt.Amount)
token1AmountUint64 = amt.Amount
} else {
token0Amount = formatTokenAmount(amt.Amount)
token0AmountUint64 = amt.Amount
}
out = append(out, &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Program: "PumpAMM",
Event: event,
Token0Address: baseMint.String(),
Token1Address: wsolMint,
Token0Amount: token0Amount,
Token1Amount: token1Amount,
ExactSOL: exactSol,
Token0AmountUint64: token0AmountUint64,
Token1AmountUint64: token1AmountUint64,
})
}
}
if pumpSell != nil || pumpBuy != nil {
event := "sell"
amt := pumpSell
isBuy := false
if amt == nil {
event = "buy"
isBuy = true
amt = pumpBuy
}
mint, ok, err := findPumpFunMint(tx.Message.StaticAccountKeys, ix.Accounts)
if err != nil {
return nil, err
}
if ok {
var (
token0Amount decimal.Decimal
token1Amount decimal.Decimal
token0AmountUint64 uint64
token1AmountUint64 uint64
exactSol bool
)
if isBuy {
exactSol = true
token1Amount = formatSolAmount(amt.Amount)
token1AmountUint64 = amt.Amount
} else {
token0Amount = formatTokenAmount(amt.Amount)
token0AmountUint64 = amt.Amount
}
out = append(out, &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Program: "Pump",
Event: event,
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: token0Amount,
Token1Amount: token1Amount,
ExactSOL: exactSol,
Token0AmountUint64: token0AmountUint64,
Token1AmountUint64: token1AmountUint64,
})
}
}
if len(out) == 0 {
return nil, nil
}
return out, nil
}

1989
pkg/shreder/dflow_idl.json Normal file

File diff suppressed because it is too large Load Diff

8471
pkg/shreder/dlmm_idl.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

@@ -48,9 +48,19 @@ type TxSignal struct {
ExactSOL bool `json:"exact_in"` ExactSOL bool `json:"exact_in"`
//Just for metaora DLMM
// ActiveBin is the active bin id provided by swap_with_price_impact(2).
ActiveBin int32 `json:"active_bin"`
// MaxPriceImpactBps is the price impact guard for swap_with_price_impact(2).
MaxPriceImpactBps uint16 `json:"max_price_impact_bps"`
// 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 {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,371 @@
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)
}
}
func TestParseJupiterV6PumpFunBuy(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, "4QF5whXwjx234fMXeH3HrJCy5knFJmKPtgbXys8xKGz1pZypqPvXBr4BoAqXfYn8jLL4HXPY1pcvxCCW1XREFNxd"),
nil,
false,
)
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "jupiterv6" {
t.Fatalf("expected jupiterv6 signal, got %s", signal.Label)
}
if signal.Event != "buy" {
t.Fatalf("expected buy event, got %s", signal.Event)
}
if signal.Maker != "92ySgsZs3rsrUAq2aeEqYacXQQGmz6e4xHPrRGxLDJXb" {
t.Fatalf("expected maker 92ySgsZs3rsrUAq2aeEqYacXQQGmz6e4xHPrRGxLDJXb, got %s", signal.Maker)
}
if signal.Token0Address != "5kSWidFwDKPZiNf52TfincpVn8ufvkAfEzZ9pk8Dpump" {
t.Fatalf("expected token0 address 5kSWidFwDKPZiNf52TfincpVn8ufvkAfEzZ9pk8Dpump, got %s", signal.Token0Address)
}
if signal.Token0AmountUint64 != 2410530637576 {
t.Fatalf("expected token0 amount 2410530637576, got %d", signal.Token0AmountUint64)
}
if signal.Token1AmountUint64 != 380000000 {
t.Fatalf("expected token1 amount 380000000, got %d", signal.Token1AmountUint64)
}
if !signal.ExactSOL {
t.Fatalf("expected ExactSOL true, got false")
}
}
func TestParseJupiterV6PumpFunSell(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, "yCnE7ZA8dqB5iAZtwpSN2ar5HXh3gBjgaG2xtnwXDPFyHAm5XFU8642uTZTH5A2iPQ6G9hrj5eEPAJiWrfe38gM"),
nil,
false,
)
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "jupiterv6" {
t.Fatalf("expected jupiterv6 signal, got %s", signal.Label)
}
if signal.Event != "sell" {
t.Fatalf("expected sell event, got %s", signal.Event)
}
if signal.Maker != "CGfWcKKcVQNBCL1vpxXdg6rvfYpQmnS3WkyA22Lk5XnZ" {
t.Fatalf("expected maker CGfWcKKcVQNBCL1vpxXdg6rvfYpQmnS3WkyA22Lk5XnZ, got %s", signal.Maker)
}
if signal.Token0Address != "wp8Mwxy7btAD9hNWsfJyoPNJnjXS9fuNG4mnhQZpump" {
t.Fatalf("expected token0 address wp8Mwxy7btAD9hNWsfJyoPNJnjXS9fuNG4mnhQZpump, got %s", signal.Token0Address)
}
if signal.Token0AmountUint64 != 127531720509990 {
t.Fatalf("expected token0 amount 127531720509990, got %d", signal.Token0AmountUint64)
}
if signal.Token1AmountUint64 != 5296451290 {
t.Fatalf("expected token1 amount 5296451290, got %d", signal.Token1AmountUint64)
}
if signal.ExactSOL {
t.Fatalf("expected ExactSOL false, got true")
}
}

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: