8 Commits
test ... v2.0.0

Author SHA1 Message Date
thloyi
be86c888eb split program source file 2026-01-28 15:02:54 +08:00
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
33 changed files with 14313 additions and 2263 deletions

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

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

View File

@@ -8,12 +8,10 @@ import (
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
"github.com/gagliardetto/solana-go/rpc" "github.com/gagliardetto/solana-go/rpc"
"github.com/shopspring/decimal"
"github.com/samlior/libsam/pkg/shreder" "github.com/samlior/libsam/v2/pkg/shreder"
) )
func main() { func main() {
@@ -93,20 +91,9 @@ func main() {
case <-ctx.Done(): case <-ctx.Done():
return return
case tx := <-txCh: case tx := <-txCh:
//jsonData, _ := json.MarshalIndent(txBatch, "", " ") if tx.Label == "okxdexroutev2" || tx.Label == "jupiterv6" || tx.Label == "dflow" {
if tx.Token0Amount.GreaterThan(decimal.NewFromInt(100)) && (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(time.Now(), "===============", tx.TxHash,
"parse time:", tx.Stats.Done.Sub(tx.Stats.Filter),
"decode time:", tx.Stats.Decoded.Sub(tx.Stats.FEC),
"filter time:", tx.Stats.Filter.Sub(tx.Stats.Decoded),
"dataLen", tx.Stats.DataLen, "txCount", tx.Stats.TxCount, "txOffset", tx.Stats.TxOffset, tx.Label, tx.Event, "token:", tx.Token0Amount)
} }
//if tx.Token0Amount.GreaterThan(decimal.NewFromInt(100)) && (tx.Label == "okxdexroutev2" || tx.Label == "jupiterv6" || tx.Label == "dflow") {
// fmt.Println(time.Now(), "===============", tx.TxHash,
// tx.Label, tx.Event, "token:", tx.Token0Amount)
//}
//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,
}
}

50
go.mod
View File

@@ -1,50 +1,50 @@
module github.com/samlior/libsam module github.com/samlior/libsam/v2
go 1.25.1 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-20250908052524-06493dcc1bb4
github.com/gagliardetto/solana-go v1.12.0 github.com/gagliardetto/binary v0.8.0
github.com/gagliardetto/solana-go v1.14.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.59.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.78.0
google.golang.org/protobuf v1.36.10 google.golang.org/protobuf v1.36.11
) )
require ( require (
filippo.io/edwards25519 v1.0.0-rc.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
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.18.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.18.3 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.11 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
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-20260108192805-38f96de0a641 // indirect
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect go.mongodb.org/mongo-driver v1.17.7 // indirect
go.mongodb.org/mongo-driver v1.12.2 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect
go.uber.org/zap v1.21.0 // indirect go.uber.org/zap v1.27.1 // indirect
golang.org/x/crypto v0.44.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.37.0 // indirect golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
) )

50
go.sum
View File

@@ -1,9 +1,13 @@
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI=
github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE=
github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250722092120-44561cb37455 h1:MMc4/hemlMtG76VkDZt6zNL0u+OwNYBCNp2KX5jqolQ= github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250722092120-44561cb37455 h1:MMc4/hemlMtG76VkDZt6zNL0u+OwNYBCNp2KX5jqolQ=
github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250722092120-44561cb37455/go.mod h1:vKj1SKlrekR9fuZgWQNNAWt/PUZIfzpGjDpIcbf1kT0= github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250722092120-44561cb37455/go.mod h1:vKj1SKlrekR9fuZgWQNNAWt/PUZIfzpGjDpIcbf1kT0=
github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250908052524-06493dcc1bb4 h1:yvrhmN9vQIrquQP1fYul30khwfoE8oEL0VmwFZ37Mq8=
github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250908052524-06493dcc1bb4/go.mod h1:vKj1SKlrekR9fuZgWQNNAWt/PUZIfzpGjDpIcbf1kT0=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
@@ -15,12 +19,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg=
github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c=
github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw=
github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY=
github.com/gagliardetto/solana-go v1.12.0 h1:rzsbilDPj6p+/DOPXBMLhwMZeBgeRuXjm5zQFCoXgsg= github.com/gagliardetto/solana-go v1.12.0 h1:rzsbilDPj6p+/DOPXBMLhwMZeBgeRuXjm5zQFCoXgsg=
github.com/gagliardetto/solana-go v1.12.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= github.com/gagliardetto/solana-go v1.12.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k=
github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXsDNWJtOLw=
github.com/gagliardetto/solana-go v1.14.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k=
github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw=
github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@@ -43,6 +51,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -51,9 +61,13 @@ github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczG
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -79,18 +93,21 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.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=
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo=
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU=
github.com/streamingfast/logging v0.0.0-20260108192805-38f96de0a641 h1:dI+b2TyFS0rJw1xVGzxBBvn/RrSgBpqM/RjwDInxEfo=
github.com/streamingfast/logging v0.0.0-20260108192805-38f96de0a641/go.mod h1:fJ5nP7ZSMB4MQQ6RM7cF+LiSQ43b5cVletcSUNL8z2M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.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.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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=
@@ -101,6 +118,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws=
go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU=
go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
@@ -116,16 +135,24 @@ go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXe
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 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/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
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=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA=
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -133,6 +160,8 @@ golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -145,11 +174,15 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -161,12 +194,17 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -175,6 +213,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -191,10 +231,16 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -17,6 +17,8 @@ type TableInfo struct {
addresses []solana.PublicKey addresses []solana.PublicKey
} }
const MaxOverErrCount = 10
type AddressTables struct { type AddressTables struct {
showTableLoaded bool showTableLoaded bool
@@ -59,6 +61,7 @@ func (at *AddressTables) loadAddressTable(tablePubkey solana.PublicKey) ([]solan
addresses = append(addresses, solana.PublicKeyFromBytes(data[offset:offset+32])) addresses = append(addresses, solana.PublicKeyFromBytes(data[offset:offset+32]))
offset += 32 offset += 32
} }
// addresses = append(addresses, solana.PublicKeyFromBytes(data[start:start+32]))
return addresses, nil return addresses, nil
} }
@@ -92,11 +95,10 @@ func (at *AddressTables) load(tablePubkey solana.PublicKey) {
total := at.tables.Len() total := at.tables.Len()
logger.Info("loadAddressTable", "table", tablePubkey.String(), "table count:", total) logger.Info("loadAddressTable", "table", tablePubkey.String(), "table count:", total)
} }
}) })
} }
func (at *AddressTables) FillToTx(tx FillableTransaction, tablePubkey solana.PublicKey, idx []uint8) bool { 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.load(tablePubkey) at.load(tablePubkey)
@@ -112,7 +114,7 @@ func (at *AddressTables) FillToTx(tx FillableTransaction, tablePubkey solana.Pub
} }
return false return false
} }
tx.FillLookupTable(addresses.addresses[i]) tx.StaticAccountKeys = append(tx.StaticAccountKeys, addresses.addresses[i])
} }
return true return true
} }
@@ -129,7 +131,7 @@ func (at *AddressTables) GetAddressTable(tablePubkey solana.PublicKey, idx []uin
if int(i) >= len(addresses.addresses) { if int(i) >= len(addresses.addresses) {
logger.Error("over loadAddressTable failed", "idx", i, "table", tablePubkey) logger.Error("over loadAddressTable failed", "idx", i, "table", tablePubkey)
addresses.overErrCount++ addresses.overErrCount++
if addresses.overErrCount > 10 { if addresses.overErrCount > MaxOverErrCount {
at.load(tablePubkey) at.load(tablePubkey)
} }
break break

View File

@@ -1,7 +1,9 @@
package shreder package shreder
import ( import (
"bytes"
"context" "context"
"errors"
"fmt" "fmt"
"runtime" "runtime"
"time" "time"
@@ -47,6 +49,8 @@ func BlocksStats(enable bool) ClientOption {
} }
} }
// LogParsedStats enables logging of parsed transaction statistics.
// Deprecated: do not use.
func LogParsedStats(enable bool) ClientOption { func LogParsedStats(enable bool) ClientOption {
return func(opts *ClientOpts) { return func(opts *ClientOpts) {
opts.logParseStats = enable opts.logParseStats = enable
@@ -70,7 +74,7 @@ func NewShrederClient(
poolSize := runtime.NumCPU()*2 + 2 poolSize := runtime.NumCPU()*2 + 2
logger.Info("creating shreder client", "url", url, "pool_size", poolSize) logger.Info("creating shreder client", "url", url, "pool_size", poolSize)
pool, err := ants.NewPool(poolSize, ants.WithNonblocking(true)) pool, err := ants.NewPool(poolSize, ants.WithNonblocking(false))
if err != nil { if err != nil {
return nil, func() {}, err return nil, func() {}, err
} }
@@ -137,13 +141,11 @@ func (c *Client) ReadEntriesSync(ctx context.Context, txCh chan<- TxSignal) erro
} }
} }
entries := response.Entries
err = c.pool.Submit(func() { err = c.pool.Submit(func() {
ParseEntries(slot, entries, c.tableLoader, txCh, c.enableParseStats) ParseTransactionForEntries(ctx, slot, bytes.NewReader(response.Entries), c.tableLoader, txCh)
}) })
if err != nil { if err != nil && errors.Is(err, ants.ErrPoolOverload) {
return err logger.Warn("task pool is full")
} }
} }
} }
@@ -163,10 +165,14 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignal) 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
} }
if c.enableBlockStats { if c.enableBlockStats {
@@ -183,12 +189,16 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignal) error {
txData := response.Transaction txData := response.Transaction
err = c.pool.Submit(func() { err := c.pool.Submit(func() {
ParseTransaction(txData, c.tableLoader, txCh, c.enableParseStats) ParseTransactionForSubscribe(ctx, txData, c.tableLoader, txCh, nil)
}) })
if err != nil { if err != nil && errors.Is(err, ants.ErrPoolOverload) {
logger.Error("failed to submit transaction: ", "err", err) logger.Warn("task pool is full")
}
} }
} // sync waiting for all tasks to complete
c.pool.Release()
return err
} }

File diff suppressed because one or more lines are too long

8471
pkg/shreder/dlmm_idl.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,70 +4,38 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "io"
"time"
) )
type constArray struct { type wrapperReader struct {
data []byte io.Reader
size int
offset int
} }
func newConstArray(data []byte) constArray { func (wr *wrapperReader) ReadU64() (uint64, error) {
return constArray{ var buf [8]byte
data: data, _, err := io.ReadFull(wr, buf[:])
size: len(data), if err != nil {
offset: 0, return 0, err
} }
return uint64(buf[0]) | uint64(buf[1])<<8 | uint64(buf[2])<<16 | uint64(buf[3])<<24 |
uint64(buf[4])<<32 | uint64(buf[5])<<40 | uint64(buf[6])<<48 | uint64(buf[7])<<56, nil
} }
func (c *constArray) Len() int { func (wr *wrapperReader) Skip(n int) error {
return c.size _, err := io.CopyN(io.Discard, wr, int64(n))
return err
} }
func (c *constArray) Offset() int { func (wr *wrapperReader) ReadCompactU16() (uint16, error) {
return c.offset
}
func (c *constArray) Read(cap int) ([]byte, error) {
if c.offset+cap > c.size {
return nil, io.EOF
}
c.offset += cap
return c.data[c.offset-cap : c.offset], nil
}
func (c *constArray) ReadBytes() (byte, error) {
if c.offset >= c.size {
return 0, io.EOF
}
c.offset++
return c.data[c.offset-1], nil
}
func (c *constArray) PeekBytes() (byte, error) {
if c.offset >= c.size {
return 0, io.EOF
}
return c.data[c.offset], nil
}
func (c *constArray) ReadU64() (uint64, error) {
if c.offset+8 > c.size {
return 0, io.EOF
}
c.offset += 8
return binary.LittleEndian.Uint64(c.data[c.offset-8 : c.offset]), nil
}
func (c *constArray) ReadCompactU16() (uint16, error) {
ln := 0 ln := 0
size := 0 size := 0
var buf [1]byte
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
if len(c.data[c.offset:]) == 0 { _, err := io.ReadFull(wr, buf[:])
return 0, fmt.Errorf("unable to decode compact u16 at %d: zero byte", i) if err != nil {
return 0, fmt.Errorf("unable to decode compact u16 at %d: %w", i, err)
} }
elem := int(c.data[c.offset+i]) elem := int(buf[0])
if elem == 0 && i != 0 { if elem == 0 && i != 0 {
return 0, fmt.Errorf("alias") return 0, fmt.Errorf("alias")
} }
@@ -80,85 +48,93 @@ func (c *constArray) ReadCompactU16() (uint16, error) {
break break
} }
} }
c.offset += size
return uint16(ln), nil return uint16(ln), nil
} }
func (c *constArray) Skip(size int) error { func (wr *wrapperReader) ReadByte() (uint8, error) {
if c.offset+size > c.size { var buf [1]byte
return io.EOF _, err := io.ReadFull(wr, buf[:])
if err != nil {
return 0, err
} }
c.offset += size return buf[0], nil
return nil }
func ResizeSlice[T any](slice []T, newSize int) []T {
if cap(slice) < newSize {
slice = append(slice, make([]T, newSize-len(slice))...)
}
return slice[:newSize]
} }
// entriesToVersionedTransaction converts raw entry bytes to versioned transactions. // entriesToVersionedTransaction converts raw entry bytes to versioned transactions.
func entriesToVersionedTransaction(slot uint64, b constArray) ([]*versionedTransaction, error) { func entriesToVersionedTransaction(slot uint64, data io.Reader, callback func(tx VersionedTransaction)) error {
b.offset = 0 b := &wrapperReader{data}
var entriesNumBuf [8]byte
entriesNum, err := b.ReadU64() n, err := io.ReadFull(b, entriesNumBuf[:])
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read entries num: %w", err) if err == io.EOF && n == 0 {
return nil
} }
if entriesNum == 0 { return fmt.Errorf("unable to read entries num: %w", err)
return nil, nil
} }
//preCap := b.Len() / 256 entriesNum := binary.LittleEndian.Uint64(entriesNumBuf[:])
//if preCap < int(entriesNum) { //if entriesNum == 0 {
// preCap = int(entriesNum) // return nil, nil
//} //}
vs := make([]*versionedTransaction, 0, entriesNum) if entriesNum > 2048 {
return fmt.Errorf("entries num is too large: %d > %d", entriesNum, 2048)
}
// logger.Debug("parsing entries", "count", entriesNum, "data len", b.Len(), "data", base64.StdEncoding.EncodeToString(b.data))
for i := uint64(0); i < entriesNum; i++ { for i := uint64(0); i < entriesNum; i++ {
err = b.Skip(40) err = b.Skip(40)
if err != nil { if err != nil {
return vs, fmt.Errorf("failed to skip num_hashes + hash of entry %d: %w", i, err) return fmt.Errorf("failed to skip num_hashes + hash of entry %d: %w", i, err)
} }
numTx, err := b.ReadU64() numTx, err := b.ReadU64()
if err != nil { if err != nil {
return vs, fmt.Errorf("failed to read num_transactions of entry %d: %w", i, err) return fmt.Errorf("failed to read num_transactions of entry %d: %w", i, err)
} }
for j := 0; j < int(numTx); j++ { for j := 0; j < int(numTx); j++ {
numSignatures, err := b.ReadCompactU16() numSignatures, err := b.ReadCompactU16()
if err != nil { if err != nil {
return vs, fmt.Errorf("failed to read numSignatures in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("failed to read numSignatures in entry %d, txn %d: %w", i, j, err)
} }
// todo : enforce a maximum number of signatures to prevent OOM // enforce a maximum number of signatures to prevent OOM
if numSignatures > 16 { if numSignatures > 32 {
return vs, fmt.Errorf("numSignatures %d exceeds maximum in entry %d, txn %d", numSignatures, i, j) return fmt.Errorf("numSignatures %d exceeds maximum in entry %d, txn %d", numSignatures, i, j)
} }
if numSignatures == 0 { if numSignatures == 0 {
return vs, fmt.Errorf("numSignatures is zero in entry %d, txn %d", i, j) return fmt.Errorf("numSignatures is zero in entry %d, txn %d", i, j)
} }
versioned := requireVersionedPool() // get a versioned transaction from the pool versioned := VersionedTransaction{}
vs = append(vs, versioned) versioned.Block = slot
versioned.block = slot versioned.Time = time.Now()
versioned.bindArray = b.data versioned.Signatures = ResizeSlice(versioned.Signatures, int(numSignatures))
versioned.signatures = int(numSignatures) for k := 0; k < int(numSignatures); k++ {
versioned.signaturesOffset = b.Offset() _, err = io.ReadFull(b, versioned.Signatures[k][:])
err = b.Skip(64 * int(numSignatures))
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read signature in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("unable to read signature in entry %d, txn %d, sig: %d, %w", i, j, k, err)
}
} }
msgVersion, err := b.PeekBytes() msgVersion, err := b.ReadByte()
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read message version in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("unable to read message version in entry %d, txn %d: %w", i, j, err)
} }
msgVersion = msgVersion & 0x80 >> 7 // mask to get only the version bits msgVersion = (msgVersion & 0x80) >> 7 // mask to get only the version bits
legacy := msgVersion == 0 legacy := msgVersion == 0
headerSkip := 3 headerSkip := 2
if !legacy { if !legacy {
headerSkip = 4 headerSkip = 3
} }
// skip msg version, mx.Header+3 // skip msg version, mx.Header+3
err = b.Skip(headerSkip) err = b.Skip(headerSkip)
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to skip message header in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("unable to skip message header in entry %d, txn %d: %w", i, j, err)
} }
// read mx.AccountKeys // read mx.AccountKeys
@@ -167,168 +143,130 @@ func entriesToVersionedTransaction(slot uint64, b constArray) ([]*versionedTrans
numAccountKeys, err := b.ReadCompactU16() numAccountKeys, err := b.ReadCompactU16()
// logger.Info("tx", "hash", versioned.Signatures[0].String(), "version", msgVersion) // logger.Info("tx", "hash", versioned.Signatures[0].String(), "version", msgVersion)
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to decode numAccountKeys in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("unable to decode numAccountKeys in entry %d, txn %d: %w", i, j, err)
} }
// todo : enforce a maximum number of account keys to prevent OOM // enforce a maximum number of account keys to prevent OOM
if numAccountKeys > 255 { if numAccountKeys > 255 {
return vs, fmt.Errorf("numAccountKeys %d exceeds maximum in entry %d, txn %d", numAccountKeys, i, j) return fmt.Errorf("numAccountKeys %d exceeds maximum in entry %d, txn %d", numAccountKeys, i, j)
} }
versioned.staticAccountKeys = uint8(numAccountKeys)
versioned.staticAccountKeysOffset = b.Offset() versioned.StaticAccountKeys = ResizeSlice(versioned.StaticAccountKeys, int(numAccountKeys))
err = b.Skip(32 * int(numAccountKeys))
for k := 0; k < int(numAccountKeys); k++ {
_, err = io.ReadFull(b, versioned.StaticAccountKeys[k][:])
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read accountKey in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("unable to read accountKey[%d] in entry %d, txn %d: %w", k, i, j, err)
}
} }
//skip solana hash //skip solana hash
err = b.Skip(32) err = b.Skip(32)
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to skip recentBlockhash in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("unable to skip recentBlockhash in entry %d, txn %d: %w", i, j, err)
} }
// read mx.Instructions // read mx.Instructions
numInstructions, err := b.ReadCompactU16() numInstructions, err := b.ReadCompactU16()
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to decode numInstructions in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("unable to decode numInstructions in entry %d, txn %d: %w", i, j, err)
} }
// todo : enforce a maximum number of instructions to prevent OOM // enforce a maximum number of instructions to prevent OOM
if numInstructions >= 256 { if numInstructions >= 256 {
return vs, fmt.Errorf("numInstructions %d exceeds maximum in entry %d, txn %d, txHash: %s", numInstructions, i, j, versioned.Signatures()) return fmt.Errorf("numInstructions %d exceeds maximum in entry %d, txn %d, txHash: %s", numInstructions, i, j, versioned.GetSignature())
}
versioned.instructions = int(numInstructions)
if cap(versioned.Instrs) < int(numInstructions) {
versioned.Instrs = make([]compiledInstruction, numInstructions)
} else {
versioned.Instrs = versioned.Instrs[:numInstructions]
} }
versioned.Instructions = ResizeSlice(versioned.Instructions, int(numInstructions))
for k := 0; k < int(numInstructions); k++ { for k := 0; k < int(numInstructions); k++ {
versioned.Instrs[k].ProgramIDIndex, err = b.ReadBytes() versioned.Instructions[k].ProgramIDIndex, err = b.ReadByte()
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read mx.Instructions[%d].ProgramIDIndex in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to read mx.Instructions[%d].ProgramIDIndex in entry %d, txn %d: %w", k, i, j, err)
} }
numAccounts, err := b.ReadCompactU16() numAccounts, err := b.ReadCompactU16()
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to decode numAccounts for ix[%d] in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to decode numAccounts for ix[%d] in entry %d, txn %d: %w", k, i, j, err)
} }
// todo : enforce a maximum number of accounts to prevent OOM // enforce a maximum number of accounts to prevent OOM
if numAccounts >= 256 { if numAccounts >= 256 {
return vs, fmt.Errorf("numAccounts %d exceeds maximum for ix[%d] in entry %d, txn %d", numAccounts, k, i, j) return fmt.Errorf("numAccounts %d exceeds maximum for ix[%d] in entry %d, txn %d", numAccounts, k, i, j)
} }
versioned.Instrs[k].AccountsLen = int(numAccounts) versioned.Instructions[k].Accounts = ResizeSlice(versioned.Instructions[k].Accounts, int(numAccounts))
//.AccountsLen = int(numAccounts)
if numAccounts != 0 { if numAccounts != 0 {
versioned.Instrs[k].AccountsOffset = b.Offset() _, err = io.ReadFull(b, versioned.Instructions[k].Accounts)
err = b.Skip(int(numAccounts))
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read mx.Instructions[%d].Accounts in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to read mx.Instructions[%d].Accounts in entry %d, txn %d: %w", k, i, j, err)
} }
} }
// _, err = r.Read(u16[:])
dataLen, err := b.ReadCompactU16() dataLen, err := b.ReadCompactU16()
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to decode mx.Instructions[%d].Data length in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to decode mx.Instructions[%d].Data length in entry %d, txn %d: %w", k, i, j, err)
} }
//todo : enforce a maximum data length to prevent OOM // enforce a maximum data length to prevent OOM
if dataLen > 2048 { if dataLen > 2048 {
return vs, fmt.Errorf("mx.Instructions[%d].Data length %d exceeds maximum in entry %d, txn %d, txHash: %s", k, dataLen, i, j, versioned.Signatures()) return fmt.Errorf("mx.Instructions[%d].Data length %d exceeds maximum in entry %d, txn %d, txHash: %s", k, dataLen, i, j, versioned.GetSignature())
} }
versioned.Instrs[k].DataLen = int(dataLen) versioned.Instructions[k].Data = ResizeSlice(versioned.Instructions[k].Data, int(dataLen))
if dataLen > 0 { if dataLen > 0 {
versioned.Instrs[k].DataOffset = b.Offset() _, err = io.ReadFull(b, versioned.Instructions[k].Data)
err = b.Skip(int(dataLen))
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read mx.Instructions[%d].Data in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to read mx.Instructions[%d].Data in entry %d, txn %d: %w", k, i, j, err)
} }
} }
} }
if !legacy { if !legacy {
// read mx.AddressTableLookups // read mx.AddressTableLookups
numLookups, err := b.ReadBytes() numLookups, err := b.ReadByte()
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read numAddressTableLookups in entry %d, txn %d: %w", i, j, err) return fmt.Errorf("unable to read numAddressTableLookups in entry %d, txn %d: %w", i, j, err)
} }
if numLookups >= 32 {
if cap(versioned.ATL) < int(numLookups) { return fmt.Errorf("numLookups %d exceeds maximum in entry %d, txn %d", numLookups, i, j)
versioned.ATL = make([]addressTableLookup, numLookups)
} else {
versioned.ATL = versioned.ATL[:numLookups]
} }
versioned.addressTableLookups = int(numLookups) versioned.AddressTableLookups = ResizeSlice(versioned.AddressTableLookups, int(numLookups))
for k := uint8(0); k < numLookups; k++ { for k := uint8(0); k < numLookups; k++ {
versioned.ATL[k].AccountKeyOffset = b.Offset() _, err = io.ReadFull(b, versioned.AddressTableLookups[k].AccountKey[:])
err = b.Skip(32)
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read address table account key for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to read address table account key for lookup[%d] in entry %d, txn %d: %w", k, i, j, err)
} }
numWritable, err := b.ReadCompactU16() numWritable, err := b.ReadCompactU16()
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to decode numWritableIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to decode numWritableIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err)
} }
// todo : enforce a maximum number of writable indexes to prevent OOM // enforce a maximum number of writable indexes to prevent OOM
if numWritable >= 256 { if numWritable >= 256 {
return vs, fmt.Errorf("numWritableIndexes %d exceeds maximum for lookup[%d] in entry %d, txn %d", numWritable, k, i, j) return fmt.Errorf("numWritableIndexes %d exceeds maximum for lookup[%d] in entry %d, txn %d", numWritable, k, i, j)
} }
versioned.ATL[k].WriteLen = int(numWritable) versioned.AddressTableLookups[k].WritableIndexes = ResizeSlice(versioned.AddressTableLookups[k].WritableIndexes, int(numWritable))
if numWritable > 0 { if numWritable > 0 {
versioned.ATL[k].WriteOffset = b.Offset() _, err = io.ReadFull(b, versioned.AddressTableLookups[k].WritableIndexes)
err = b.Skip(int(numWritable))
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read writableIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to read writableIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err)
} }
} }
// _, err = r.Read(u16[:])
numReadonly, err := b.ReadCompactU16() numReadonly, err := b.ReadCompactU16()
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to decode numReadonlyIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to decode numReadonlyIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err)
} }
// todo : enforce a maximum number of readonly indexes to prevent OOM // enforce a maximum number of readonly indexes to prevent OOM
if numReadonly > 256 { if numReadonly > 256 {
return vs, fmt.Errorf("numReadonlyIndexes %d exceeds maximum for lookup[%d] in entry %d, txn %d", numReadonly, k, i, j) return fmt.Errorf("numReadonlyIndexes %d exceeds maximum for lookup[%d] in entry %d, txn %d", numReadonly, k, i, j)
} }
versioned.ATL[k].ReadLen = int(numReadonly) versioned.AddressTableLookups[k].ReadonlyIndexes = ResizeSlice(versioned.AddressTableLookups[k].ReadonlyIndexes, int(numReadonly))
if numReadonly > 0 { if numReadonly > 0 {
versioned.ATL[k].ReadOffset = b.Offset() _, err = io.ReadFull(b, versioned.AddressTableLookups[k].ReadonlyIndexes)
err = b.Skip(int(numReadonly))
if err != nil { if err != nil {
return vs, fmt.Errorf("unable to read readonlyIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) return fmt.Errorf("unable to read readonlyIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err)
} }
} }
} }
} }
}
}
// logger.Debug("parsing num_transactions of entry", "slot", slot, "count", entriesNum, "data len", b.Len(), "num_tx", uint32(len(vs)))
//logger.Debug("parsing entries", "slot", slot, "count", entriesNum, "data len", b.Len(), "txns", len(vs)) callback(versioned)
return vs, nil }
} }
return nil
func decodeCompactU16(b []byte) (int, uint16, error) {
ln := 0
size := 0
for i := 0; i < 3; i++ {
if len(b) == 0 {
return 0, 0, fmt.Errorf("unable to decode compact u16 at %d: zero byte", i)
}
elem := int(b[0])
b = b[1:]
if elem == 0 && i != 0 {
return 0, 0, fmt.Errorf("alias")
}
if i == 2 && (elem&0x80) != 0 {
return 0, 0, fmt.Errorf("byte three continues")
}
ln |= (elem & 0x7f) << (size * 7)
size++
if (elem & 0x80) == 0 {
break
}
}
return size, uint16(ln), nil
} }

File diff suppressed because one or more lines are too long

129
pkg/shreder/program_azcz.go Normal file
View File

@@ -0,0 +1,129 @@
package shreder
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
"github.com/shopspring/decimal"
)
// has no sell function with pump and pump.amm program
var azczProgramID = solana.MustPublicKeyFromBase58("AzcZqCRUQgKEg5FTAgY7JacATABEYCEfMbjXEzspLYFB")
var (
azczBuyTokensIX = []byte{11}
azczAmmBuyTokensIX = []byte{0xf}
)
type azczBuyArgs struct {
SolAmount uint64
TokenAmount uint64
}
func parseAzczInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
var (
err error
txSignal *TxSignal
)
if matchMethod(instruction.Data, azczBuyTokensIX) {
txSignal, err = parseAzczBuy(tx, instructionIndex)
} else if matchMethod(instruction.Data, azczAmmBuyTokensIX) {
txSignal, err = parseAzczAmmBuy(tx, instructionIndex)
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func parseAzczAmmBuy(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[3]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[1]))
if err != nil {
return nil, err
}
if len(instruction.Data) < 17 {
return nil, fmt.Errorf("data too short for azcz amm buy args, len=%d", len(instruction.Data))
}
solAmount := binary.LittleEndian.Uint64(instruction.Data[1:9])
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "azcz",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: decimal.Zero,
Token1Amount: formatSolAmount(solAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: 0,
Token1AmountUint64: solAmount,
}, nil
}
func parseAzczBuy(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[3]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[7]))
if err != nil {
return nil, err
}
if len(instruction.Data) < 2 {
return nil, fmt.Errorf("data too short for azcz buy args len=%d", len(instruction.Data))
}
var args azczBuyArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "azcz",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.TokenAmount),
Token1Amount: formatSolAmount(args.SolAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: args.TokenAmount,
Token1AmountUint64: args.SolAmount,
}, nil
}

View File

@@ -0,0 +1,111 @@
package shreder
import (
"encoding/binary"
"fmt"
"strings"
"github.com/gagliardetto/solana-go"
)
var bloomRouterProgramID = solana.MustPublicKeyFromBase58("b1oomGGqPKGD6errbyfbVMBuzSC8WtAAYo8MwNafWW1")
type bloomRouterArgs struct {
Side uint16
SolAmount uint64
TokenAmount uint64
}
func parseBloomRouterInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) < 26 {
return nil, nil
}
var (
amount uint64
sol uint64
exactIn bool
event string
)
args, err := decodeBloomRouterArgs(instruction.Data)
if err != nil {
return nil, err
}
switch args.Side {
case 0:
event = "buy"
exactIn = true
case 1:
event = "sell"
default:
return nil, nil
}
if args.SolAmount > ^uint64(0)/100 {
return nil, fmt.Errorf("bloomrouter sol amount overflow")
}
// bloomrouter SOL amount has 2 fewer decimals than lamports.
sol = args.SolAmount * 100
amount = args.TokenAmount
if len(instruction.Accounts) == 0 {
return nil, fmt.Errorf("accounts too short")
}
maker, err := tx.GetAccount(int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
var (
mint solana.PublicKey
ok bool
)
for _, acctIdx := range instruction.Accounts {
key, err := tx.GetAccount(int(acctIdx))
if err != nil {
return nil, err
}
if strings.HasSuffix(key.String(), "pump") {
mint = key
ok = true
break
}
}
if !ok {
return nil, nil
}
return TxSignalBatch{&TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "bloomrouter",
Maker: maker.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(amount),
Token1Amount: formatSolAmount(sol),
Program: "Pump",
Event: event,
ExactSOL: exactIn,
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: amount,
Token1AmountUint64: sol,
}}, nil
}
func decodeBloomRouterArgs(data []byte) (bloomRouterArgs, error) {
if len(data) < 26 {
return bloomRouterArgs{}, fmt.Errorf("data too short for bloomrouter args, len=%d", len(data))
}
return bloomRouterArgs{
Side: binary.BigEndian.Uint16(data[8:10]),
SolAmount: binary.LittleEndian.Uint64(data[10:18]),
TokenAmount: binary.LittleEndian.Uint64(data[18:26]),
}, nil
}

View File

@@ -0,0 +1,77 @@
package shreder
import (
"bytes"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
"github.com/shopspring/decimal"
)
var boboProgramID = solana.MustPublicKeyFromBase58("BobogA5N2KN2GG4XN3E3rNNRw3L8H1QPXp7QLxGrNHGM")
var (
boboBuyPumpTokensIX = []byte{0xff, 0xe7, 0x11, 0x53, 0x15, 0xc5, 0xc3, 0xdf}
)
type boboBuyArgs struct {
Placeholder1 uint64
Placeholder2 uint64
SolAmount uint64
Placeholder3 uint64
Placeholder4 uint64
Placeholder5 uint64
Placeholder6 uint64
}
func parseBoboInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if len(instruction.Data) < 8 || !bytes.Equal(instruction.Data[:8], boboBuyPumpTokensIX) {
return nil, nil
}
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 16 {
return nil, fmt.Errorf("data too short for bobo buy args, len=%d", len(instruction.Data))
}
mint, err := tx.GetAccount(int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
var args boboBuyArgs
if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return TxSignalBatch{&TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "bobo",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: decimal.NewFromInt(1),
Token1Amount: formatSolAmount(args.SolAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: 1,
Token1AmountUint64: args.SolAmount,
}}, nil
}

111
pkg/shreder/program_bonk.go Normal file
View File

@@ -0,0 +1,111 @@
package shreder
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
)
var bonkProgramID = solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD")
var (
bonkBuyAndSellTokensIX = []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a}
)
func parseBonkInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
var (
err error
txSignal *TxSignal
)
if matchMethod(instruction.Data, bonkBuyAndSellTokensIX) {
txSignal, err = parseBonkBuyAndSell(tx, instruction)
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func parseBonkBuyAndSell(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
programId, err := tx.GetAccount(int(instruction.Accounts[7]))
if err != nil {
return nil, err
}
if programId != pumpProgramID {
return nil, nil
}
user, err := tx.GetAccount(int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
flagAccount, err := tx.GetAccount(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 := tx.GetAccount(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 := tx.GetAccount(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
}
}

View File

@@ -86,28 +86,28 @@ const (
drv1Nexus drv1Nexus
) )
// PumpFunAmmSellOptions { amount: u64, orchestrator_flags: OrchestratorFlags{flags u8} } // PumpFun*Options { amount: u64, orchestrator_flags: OrchestratorFlags{flags u8} }
type pumpFunAmm struct { type pumpFunAction struct {
Amount uint64 Amount uint64
Flags uint8 Flags uint8
} }
type dflowAction struct { type dflowAction struct {
Tag uint8 Tag uint8
Pump *pumpFunAmm Pump *pumpFunAction
} }
type dflowSwapParams struct { type dflowSwapParams struct {
Actions []dflowAction Actions []dflowAction
} }
// bytes to skip for Action variants before/after PumpFunAmmSell; only PumpFunAmmSell is decoded. // bytes to skip for Action variants; only PumpFun* actions are decoded.
func skipDflowAction(dec *bin.Decoder, tag uint8) (*pumpFunAmm, error) { func skipDflowAction(dec *bin.Decoder, tag uint8) (*pumpFunAction, error) {
switch tag { switch tag {
case ActWhirlpoolsSwap, ActClearpoolsSwap, ActWhirlpoolsSwapV2: case ActWhirlpoolsSwap, ActClearpoolsSwap, ActWhirlpoolsSwapV2:
// amount u64 + bool + orchestrator_flags u8 // amount u64 + bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1) return nil, dec.SkipBytes(8 + 1 + 1)
case ActRaydiumAmmSwap, ActLifinityV2Swap, ActPumpFunBuy, ActPumpFunSell, ActObricV2Swap, case ActRaydiumAmmSwap, ActLifinityV2Swap, ActObricV2Swap,
ActSolFiSwap, ActRubiconSwap, ActMeteoraDammV1Swap, ActRaydiumCpSwap, ActSolFiSwap, ActRubiconSwap, ActMeteoraDammV1Swap, ActRaydiumCpSwap,
ActStabbleStableSwap, ActTesseraVSwap, ActMeteoraDammV2Swap, ActRaydiumLaunchlabSwap, ActStabbleStableSwap, ActTesseraVSwap, ActMeteoraDammV2Swap, ActRaydiumLaunchlabSwap,
ActZeroFiSwap, ActAlphaQSwap, ActTokenSwap, ActSolFiV2Swap, ActMozartSwap, ActHeavenSwap, ActZeroFiSwap, ActAlphaQSwap, ActTokenSwap, ActSolFiV2Swap, ActMozartSwap, ActHeavenSwap,
@@ -123,7 +123,7 @@ func skipDflowAction(dec *bin.Decoder, tag uint8) (*pumpFunAmm, error) {
case ActGammaSwap: case ActGammaSwap:
// amount u64 + endorsed bool + orchestrator_flags u8 // amount u64 + endorsed bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1) return nil, dec.SkipBytes(8 + 1 + 1)
case ActPumpFunAmmSell, ActPumpFunAmmBuy: case ActPumpFunAmmSell, ActPumpFunAmmBuy, ActPumpFunBuy, ActPumpFunSell:
amt, err := dec.ReadUint64(binary.LittleEndian) amt, err := dec.ReadUint64(binary.LittleEndian)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -132,7 +132,7 @@ func skipDflowAction(dec *bin.Decoder, tag uint8) (*pumpFunAmm, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &pumpFunAmm{Amount: amt, Flags: flg}, nil return &pumpFunAction{Amount: amt, Flags: flg}, nil
case ActMeteoraDbcSwap: case ActMeteoraDbcSwap:
// amount u64 + is_rate_limiter_applied bool + orchestrator_flags u8 // amount u64 + is_rate_limiter_applied bool + orchestrator_flags u8
return nil, dec.SkipBytes(8 + 1 + 1) return nil, dec.SkipBytes(8 + 1 + 1)
@@ -232,14 +232,45 @@ func decodeSwap2Params(data []byte) (*dflowSwapParams, error) {
return out, nil return out, nil
} }
func parseDFlowInstruction(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { func findDflowPumpAmmMints(tx VersionedTransaction, accounts []uint8) (solana.PublicKey, solana.PublicKey, bool, error) {
if len(data) < 8 { for i, acctIdx := range accounts {
key, err := tx.GetAccount(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 := tx.GetAccount(int(accounts[baseIdx]))
if err != nil {
return solana.PublicKey{}, solana.PublicKey{}, false, err
}
quoteMint, err := tx.GetAccount(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) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
ix := tx.Instructions[instructionIndex]
if len(ix.Data) < 8 {
return nil, nil return nil, nil
} }
var err error var err error
disc := data[:8] disc := ix.Data[:8]
payload := data[8:] payload := ix.Data[8:]
var params *dflowSwapParams var params *dflowSwapParams
switch { switch {
@@ -257,65 +288,121 @@ func parseDFlowInstruction(tx TransactionGetter, accounts []uint8, data []byte)
return nil, 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 ( var (
srcIdx uint8 pumpAmmBuy *pumpFunAction
pumpAmmSell *pumpFunAction
pumpBuy *pumpFunAction
pumpSell *pumpFunAction
) )
if len(accounts) <= 6 { for _, act := range params.Actions {
return nil, nil if act.Pump == nil {
continue
} }
accounts = accounts[5:] switch act.Tag {
for i, acctIdx := range accounts { case ActPumpFunAmmSell:
key, err := tx.GetAccount(acctIdx) //getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx)) pumpAmmSell = act.Pump
if err != nil { case ActPumpFunAmmBuy:
return nil, err pumpAmmBuy = act.Pump
case ActPumpFunBuy:
pumpBuy = act.Pump
case ActPumpFunSell:
pumpSell = act.Pump
} }
if key.Equals(pumpAmmProgramID) {
srcIdx = uint8(i + 4)
break
}
}
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
return nil, nil
} }
baseMint, err := tx.GetAccount(accounts[srcIdx]) // getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) 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, ix.Accounts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
quoteMint, err := tx.GetAccount(accounts[srcIdx+1]) // getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) if ok && quoteMint.Equals(solana.WrappedSol) {
if err != nil { var (
return nil, err 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
} }
if !quoteMint.Equals(solana.WrappedSol) { out = append(out, &TxSignal{
return nil, nil TxHash: tx.Signatures[0].String(),
} Maker: tx.StaticAccountKeys[0].String(),
maker, _ := tx.GetAccount(0)
// Build TxSignal
sig := &TxSignal{
TxHash: tx.Signatures(),
Maker: maker.String(),
Program: "PumpAMM", Program: "PumpAMM",
Event: "sell", Event: event,
Token0Address: baseMint.String(), Token0Address: baseMint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
Token0Amount: formatTokenAmount(pump.Amount), Token0Amount: token0Amount,
Token1Amount: decimal.Zero, Token1Amount: token1Amount,
Token0AmountUint64: pump.Amount, ExactSOL: exactSol,
Block: tx.Block(), Token0AmountUint64: token0AmountUint64,
Token1AmountUint64: 0, Token1AmountUint64: token1AmountUint64,
})
} }
return sig, nil }
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, 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.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
} }

227
pkg/shreder/program_dlmm.go Normal file
View File

@@ -0,0 +1,227 @@
package shreder
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
// For Metaora dlmm
var dlmmProgramID = solana.MustPublicKeyFromBase58("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo")
var (
dlmmSwapIX = []byte{248, 198, 158, 145, 225, 117, 135, 200}
dlmmSwap2IX = []byte{65, 75, 63, 76, 235, 91, 91, 136}
dlmmSwapExactOutIX = []byte{250, 73, 101, 33, 38, 207, 75, 184}
dlmmSwapExactOut2IX = []byte{43, 215, 247, 132, 137, 60, 243, 81}
dlmmSwapPriceImpactIX = []byte{56, 173, 230, 208, 173, 228, 156, 205}
dlmmSwapPriceImpact2IX = []byte{74, 98, 192, 214, 177, 51, 75, 51}
)
type dlmmParsedArgs struct {
AmountIn uint64
AmountOut uint64
ExactIn bool
ExactOut bool
ActiveBin int32
MaxPriceImpactBps uint16
}
func dlmmTokenOrder(tokenX, tokenY solana.PublicKey) (solana.PublicKey, solana.PublicKey) {
switch {
case tokenX.Equals(solana.WrappedSol):
return tokenY, tokenX
case tokenY.Equals(solana.WrappedSol):
return tokenX, tokenY
default:
return tokenX, tokenY
}
}
func parseDlmmSwapArgs(disc []byte, payload []byte) (*dlmmParsedArgs, error) {
switch {
case bytes.Equal(disc, dlmmSwapIX), bytes.Equal(disc, dlmmSwap2IX):
if len(payload) < 16 {
return nil, fmt.Errorf("data too short for dlmm swap args, len=%d", len(payload))
}
return &dlmmParsedArgs{
AmountIn: binary.LittleEndian.Uint64(payload[0:8]),
AmountOut: binary.LittleEndian.Uint64(payload[8:16]),
ExactIn: true,
}, nil
case bytes.Equal(disc, dlmmSwapExactOutIX), bytes.Equal(disc, dlmmSwapExactOut2IX):
if len(payload) < 16 {
return nil, fmt.Errorf("data too short for dlmm swap exact out args, len=%d", len(payload))
}
return &dlmmParsedArgs{
AmountIn: binary.LittleEndian.Uint64(payload[0:8]),
AmountOut: binary.LittleEndian.Uint64(payload[8:16]),
ExactOut: true,
}, nil
case bytes.Equal(disc, dlmmSwapPriceImpactIX), bytes.Equal(disc, dlmmSwapPriceImpact2IX):
if len(payload) < 11 {
return nil, fmt.Errorf("data too short for dlmm swap with price impact args, len=%d", len(payload))
}
amountIn := binary.LittleEndian.Uint64(payload[0:8])
idx := 8
if len(payload) < idx+1 {
return nil, fmt.Errorf("data too short for dlmm swap with price impact args, len=%d", len(payload))
}
activeBinTag := payload[idx]
idx++
var activeBin int32
if activeBinTag == 1 {
if len(payload) < idx+4 {
return nil, fmt.Errorf("data too short for dlmm swap with price impact args, len=%d", len(payload))
}
activeBin = int32(binary.LittleEndian.Uint32(payload[idx : idx+4]))
idx += 4
} else if activeBinTag != 0 {
return nil, fmt.Errorf("invalid active_id tag %d", activeBinTag)
}
if len(payload) < idx+2 {
return nil, fmt.Errorf("data too short for dlmm swap with price impact args, len=%d", len(payload))
}
return &dlmmParsedArgs{
AmountIn: amountIn,
ExactIn: true,
ActiveBin: activeBin,
MaxPriceImpactBps: binary.LittleEndian.Uint16(payload[idx : idx+2]),
}, nil
default:
return nil, nil
}
}
func parseDlmmInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) < 8 {
return nil, fmt.Errorf("data is empty")
}
if len(instruction.Accounts) < 13 {
return nil, nil // fmt.Errorf("accounts too short")
}
disc := instruction.Data[:8]
payload := instruction.Data[8:]
args, err := parseDlmmSwapArgs(disc, payload)
if err != nil {
return nil, err
}
if args == nil {
return nil, nil
}
userTokenIn, err := tx.GetAccount(int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
userTokenOut, err := tx.GetAccount(int(instruction.Accounts[5]))
if err != nil {
return nil, err
}
tokenX, err := tx.GetAccount(int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
tokenY, err := tx.GetAccount(int(instruction.Accounts[7]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[10]))
if err != nil {
return nil, err
}
tokenXProgram, err := tx.GetAccount(int(instruction.Accounts[11]))
if err != nil {
return nil, err
}
tokenYProgram, err := tx.GetAccount(int(instruction.Accounts[12]))
if err != nil {
return nil, err
}
token0Mint, token1Mint := dlmmTokenOrder(tokenX, tokenY)
var (
token0AmountUint64 uint64
token1AmountUint64 uint64
)
if !tokenX.Equals(solana.WrappedSol) && !tokenY.Equals(solana.WrappedSol) {
return nil, nil
}
wsolProgram := tokenXProgram
if tokenY.Equals(solana.WrappedSol) {
wsolProgram = tokenYProgram
}
wsolAta, _, err := findAssociatedTokenAddressWithTokenProgram(user, solana.WrappedSol, wsolProgram)
if err != nil {
return nil, nil
}
wsolIn := userTokenIn.Equals(wsolAta)
wsolOut := userTokenOut.Equals(wsolAta)
if !wsolIn && !wsolOut {
return nil, nil
}
event := "sell"
if wsolIn {
event = "buy"
}
exactSol := (args.ExactIn && wsolIn) || (args.ExactOut && wsolOut)
if wsolIn {
if args.ExactIn {
token1AmountUint64 = args.AmountIn
}
if args.ExactOut {
token0AmountUint64 = args.AmountOut
}
} else {
if args.ExactOut {
token1AmountUint64 = args.AmountOut
}
if args.ExactIn {
token0AmountUint64 = args.AmountIn
}
}
token0Amount := formatTokenAmount(token0AmountUint64)
if token0Mint.Equals(solana.WrappedSol) {
token0Amount = formatSolAmount(token0AmountUint64)
}
token1Amount := decimal.Zero
if token1AmountUint64 > 0 {
if token1Mint.Equals(solana.WrappedSol) {
token1Amount = formatSolAmount(token1AmountUint64)
} else {
token1Amount = formatTokenAmount(token1AmountUint64)
}
}
return TxSignalBatch{&TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "dlmm",
Maker: user.String(),
Token0Address: token0Mint.String(),
Token1Address: token1Mint.String(),
Token0Amount: token0Amount,
Token1Amount: token1Amount,
Program: "MeteoraDLMM",
Event: event,
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: exactSol,
ActiveBin: args.ActiveBin,
MaxPriceImpactBps: args.MaxPriceImpactBps,
Block: tx.Block,
Token0AmountUint64: token0AmountUint64,
Token1AmountUint64: token1AmountUint64,
}}, nil
}

View File

@@ -0,0 +1,72 @@
package shreder
import (
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
)
// only buy function with pump program
var f5tfProgramID = solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq")
var (
f5tfBuyTokensIX = []byte{0}
)
type f5tfBuyArgs struct {
SolAmount uint64
TokenAmount uint64
}
func parseF5tfInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if !matchMethod(instruction.Data, f5tfBuyTokensIX) {
return nil, nil
}
if len(instruction.Accounts) < 7 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
if len(instruction.Data) < 2 {
return nil, fmt.Errorf("data too short for f5tf buy args len=%d", len(instruction.Data))
}
var args f5tfBuyArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return TxSignalBatch{&TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "f5tf",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.TokenAmount),
Token1Amount: formatSolAmount(args.SolAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: args.TokenAmount,
Token1AmountUint64: args.SolAmount,
}}, nil
}

View File

@@ -0,0 +1,71 @@
package shreder
import (
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
)
// only buy function with pump program
var fjszProgramID = solana.MustPublicKeyFromBase58("FJsZbftBqRLfF7uqUKpm4s2goDr6xsQ5Q3mN7AFJB6hK")
var (
fjszBuyTokensIX = []byte{0xe7, 0x3f, 0x99, 0x83, 0xf3, 0xed, 0xe3, 0x3c}
)
type fjszBuyArgs struct {
SolAmount uint64
TokenAmount uint64
}
func parseFjszInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if !matchMethod(instruction.Data, fjszBuyTokensIX) {
return nil, nil
}
if len(instruction.Accounts) < 7 {
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 16 {
return nil, fmt.Errorf("data too short for fjzs buy args, len=%d", len(instruction.Data))
}
mint, err := tx.GetAccount(int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
var args fjszBuyArgs
if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return TxSignalBatch{&TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "fjsz",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.TokenAmount),
Token1Amount: formatSolAmount(args.SolAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: args.TokenAmount,
Token1AmountUint64: args.SolAmount,
}}, nil
}

218
pkg/shreder/program_flas.go Normal file
View File

@@ -0,0 +1,218 @@
package shreder
import (
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
"github.com/shopspring/decimal"
)
var flasProgramID = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9")
var (
flasBuyTokensIX = []byte{0x00, 0x1, 0x4}
flasSellTokensIX = []byte{0x01, 0x1, 0x3}
flasAmmBuyTokensIX = []byte{0x00, 0x2, 0x2}
flasAmmSellTokensIX = []byte{0x01, 0x2, 0x2}
)
type flasArgs struct {
Amount1 uint64
Amount2 uint64
Placeholder [3]uint8
}
func parseFlasInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if len(instruction.Data) == 10 && instruction.Data[0] == 1 {
return nil, nil
}
if len(instruction.Data) < 20 {
return nil, fmt.Errorf("data too short for args flas instruction, len: %d", len(instruction.Data))
}
methodData := instruction.Data[17:20]
//if !matchMethod(methodData, flasBuyTokensIX) {
// return nil, nil
//}
var (
err error
txSignal *TxSignal
)
if matchMethod(methodData, flasBuyTokensIX) {
txSignal, err = parseFlasBuy(tx, instructionIndex)
} else if matchMethod(methodData, flasSellTokensIX) {
txSignal, err = parseFlasSell(tx, instructionIndex)
} else if matchMethod(methodData, flasAmmBuyTokensIX) {
txSignal, err = parseFlasAmmBuy(tx, instructionIndex)
} else if matchMethod(methodData, flasAmmSellTokensIX) {
txSignal, err = parseFlasAmmSell(tx, instructionIndex)
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func parseFlasAmmSell(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 10 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[9]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[1]))
if err != nil {
return nil, err
}
var args flasArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.Amount1),
Token1Amount: formatSolAmount(args.Amount2),
Program: "PumpAMM",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: false,
Block: tx.Block,
Token0AmountUint64: args.Amount1,
Token1AmountUint64: args.Amount2,
}, nil
}
func parseFlasAmmBuy(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 10 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[9]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[1]))
if err != nil {
return nil, err
}
var args flasArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: decimal.Zero,
Token1Amount: formatSolAmount(args.Amount1),
Program: "PumpAMM",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: 0,
Token1AmountUint64: args.Amount1,
}, nil
}
func parseFlasSell(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 9 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[8]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[1]))
if err != nil {
return nil, err
}
var args flasArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.Amount1),
Token1Amount: formatSolAmount(args.Amount2),
Program: "Pump",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: args.Amount1,
Token1AmountUint64: args.Amount2,
}, nil
}
func parseFlasBuy(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 9 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[8]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
if len(instruction.Data) > 20 {
instruction.Data = instruction.Data[:20]
}
var args flasArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "flas",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.Amount2),
Token1Amount: formatSolAmount(args.Amount1),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: args.Amount2,
Token1AmountUint64: args.Amount1,
}, nil
}

View File

@@ -0,0 +1,78 @@
package shreder
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
)
var gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb")
var (
gmgnBuyTokensIX = []byte{0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea}
)
func parseGMGNInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if len(instruction.Data) < 8 {
return nil, nil
}
var (
err error
txSignal *TxSignal
)
if matchMethod(instruction.Data, gmgnBuyTokensIX) {
txSignal, err = parseGMGNBuy(tx, instruction)
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func parseGMGNBuy(tx VersionedTransaction, instruction Instructions) (*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))
}
mint, err := tx.GetAccount(int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(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
}

File diff suppressed because it is too large Load Diff

View File

@@ -245,13 +245,16 @@ type OkxV2SwapScorch struct {
Id [16]byte Id [16]byte
} }
func parseOkxDexRouteV2Instruction(tx TransactionGetter, accounts []uint8, data []byte) (*TxSignal, error) { func parseOkxDexRouteV2Instruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
if len(data) < 8 { return nil, fmt.Errorf("instruction index out of bounds")
}
ix := tx.Instructions[instructionIndex]
if len(ix.Data) < 8 {
return nil, nil return nil, nil
} }
disc := data[:8] disc := ix.Data[:8]
data = data[8:] data := ix.Data[8:]
var ( var (
args *OkxV2SwapArgs args *OkxV2SwapArgs
@@ -284,8 +287,8 @@ func parseOkxDexRouteV2Instruction(tx TransactionGetter, accounts []uint8, data
default: default:
return nil, nil return nil, nil
} }
if len(accounts) < 15 { if len(ix.Accounts) < 15 {
return nil, fmt.Errorf("invalid account count: %d", len(accounts)) return nil, fmt.Errorf("invalid account count: %d", len(ix.Accounts))
} }
var ( var (
inputAmount uint64 inputAmount uint64
@@ -299,24 +302,24 @@ func parseOkxDexRouteV2Instruction(tx TransactionGetter, accounts []uint8, data
} }
} }
if routeCount > 1 { if routeCount > 1 {
logger.Warn("pumpSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures(), "routeCount", routeCount) logger.Warn("pumpSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "routeCount", routeCount)
return nil, nil return nil, nil
} }
if inputAmount == 0 { if inputAmount == 0 {
return nil, nil return nil, nil
} }
srcMint, err := tx.GetAccount(accounts[3]) //getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[3])) srcMint, err := tx.GetAccount(int(ix.Accounts[3]))
var ( var (
srcIdx uint8 srcIdx uint8
) )
if len(accounts) <= 15 { if len(ix.Accounts) <= 15 {
return nil, nil return nil, nil
} }
accounts = accounts[14:] accounts := ix.Accounts[14:]
for i, acctIdx := range accounts { for i, acctIdx := range accounts {
key, err := tx.GetAccount(acctIdx) // getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx)) key, err := tx.GetAccount(int(acctIdx))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -329,7 +332,7 @@ func parseOkxDexRouteV2Instruction(tx TransactionGetter, accounts []uint8, data
return nil, nil return nil, nil
} }
baseMint, err := tx.GetAccount(accounts[srcIdx]) // getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) baseMint, err := tx.GetAccount(int(accounts[srcIdx]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -337,18 +340,17 @@ func parseOkxDexRouteV2Instruction(tx TransactionGetter, accounts []uint8, data
return nil, nil return nil, nil
} }
quoteMint, err := tx.GetAccount(accounts[srcIdx+1]) // getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) quoteMint, err := tx.GetAccount(int(accounts[srcIdx+1]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !quoteMint.Equals(solana.WrappedSol) { if !quoteMint.Equals(solana.WrappedSol) {
return nil, nil return nil, nil
} }
maker, _ := tx.GetAccount(0)
return &TxSignal{ return TxSignalBatch{&TxSignal{
TxHash: tx.Signatures(), TxHash: tx.Signatures[0].String(),
Maker: maker.String(), Maker: tx.StaticAccountKeys[0].String(),
Token0Address: baseMint.String(), Token0Address: baseMint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
Token0Amount: formatTokenAmount(inputAmount), Token0Amount: formatTokenAmount(inputAmount),
@@ -360,7 +362,6 @@ func parseOkxDexRouteV2Instruction(tx TransactionGetter, accounts []uint8, data
IsMayhemMode: false, IsMayhemMode: false,
ExactSOL: false, ExactSOL: false,
Token0AmountUint64: inputAmount, Token0AmountUint64: inputAmount,
Block: tx.Block(),
Token1AmountUint64: 0, Token1AmountUint64: 0,
}, nil }}, nil
} }

View File

@@ -0,0 +1,156 @@
package shreder
import (
"bytes"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
)
// only pump.fun function
var photonProgramID = solana.MustPublicKeyFromBase58("BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW")
var (
photonBuyPumpTokensIX = []byte{0x52, 0xe1, 0x77, 0xe7, 0x4e, 0x1d, 0x2d, 0x46}
photonSwapPumpAmmIX = []byte{0x2c, 0x77, 0xaf, 0xda, 0xc7, 0x4d, 0xc4, 0xeb}
)
type photonBuyPumpArgs struct {
Timestamp uint64
SolAmount uint64
TokenAmount uint64
Fee uint64
}
type photonSwapPumpAmmArgs struct {
FromAmount uint64
ToAmount uint64
}
func parsePhotonInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if len(instruction.Data) < 8 {
return nil, nil
}
var (
err error
txSignal *TxSignal
)
switch {
case bytes.Equal(instruction.Data[:8], photonBuyPumpTokensIX):
txSignal, err = parsePhotonBuy(tx, instruction)
case bytes.Equal(instruction.Data[:8], photonSwapPumpAmmIX):
txSignal, err = parsePhotonSwap(tx, instruction)
default:
return nil, nil
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func parsePhotonBuy(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 16 {
return nil, fmt.Errorf("data too short for photon buy args, len=%d", len(instruction.Data))
}
mint, err := tx.GetAccount(int(instruction.Accounts[3]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[7]))
if err != nil {
return nil, err
}
var args photonBuyPumpArgs
if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
solAmount := args.SolAmount * (100000000 - 1234568) / 100000000
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "photon",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.TokenAmount),
Token1Amount: formatSolAmount(solAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: args.TokenAmount,
Token1AmountUint64: solAmount,
}, nil
}
func parsePhotonSwap(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 16 {
return nil, fmt.Errorf("data too short for swap args for photon. len=%d", len(instruction.Data))
}
base, err := tx.GetAccount(int(instruction.Accounts[3]))
if err != nil {
return nil, err
}
quote, err := tx.GetAccount(int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
if !quote.Equals(solana.WrappedSol) {
return nil, nil
}
buyer, err := tx.GetAccount(int(instruction.Accounts[1]))
if err != nil {
return nil, err
}
var args photonSwapPumpAmmArgs
if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil {
return nil, fmt.Errorf("failed to parse swap pump amm tokens args: %w", err)
}
if args.FromAmount > args.ToAmount {
// sell; ignore
return nil, nil
}
solAmount := args.FromAmount * (100000000 - 1234568) / 100000000
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "photon",
Maker: buyer.String(),
Token0Address: base.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.ToAmount),
Token1Amount: formatSolAmount(solAmount),
Program: "PumpAMM",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: args.ToAmount,
Token1AmountUint64: solAmount,
}, nil
}

256
pkg/shreder/program_pump.go Normal file
View File

@@ -0,0 +1,256 @@
package shreder
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
"github.com/shopspring/decimal"
)
var pumpProgramID = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
var (
pumpCreateCoinIX = []byte{24, 30, 200, 40, 5, 28, 7, 119}
pumpCreateCoinV2IX = []byte{214, 144, 76, 236, 95, 139, 49, 180}
pumpExtendedSellIX = []byte{51, 230, 133, 164, 1, 127, 131, 173}
pumpBuyTokensIX = []byte{102, 6, 61, 18, 1, 218, 235, 234}
pumpBuyV2TokensIX = []byte{56, 252, 116, 8, 158, 223, 205, 95}
)
type pumpExtendedSellArgs struct {
Amount uint64
MinSolOutput uint64
}
type pumpBuyArgs struct {
Amount uint64
MaxSolCost uint64
}
type pumpCreateCoinV2Args struct {
Name string
Symbol string
Uri string
Creator solana.PublicKey
IsMayhemMode bool
}
func parsePumpInstruction(msg VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
instruction := msg.Instructions[instructionIndex]
if len(instruction.Data) < 8 {
return nil, fmt.Errorf("data is empty")
}
var (
err error
txSignal *TxSignal
)
if matchMethod(instruction.Data[0:8], pumpBuyV2TokensIX) || matchMethod(instruction.Data[0:8], pumpBuyTokensIX) {
txSignal, err = parsePumpBuy(msg, instruction)
} else if matchMethod(instruction.Data[0:8], pumpExtendedSellIX) {
txSignal, err = parsePumpSell(msg, instruction)
} else if matchMethod(instruction.Data[0:8], pumpCreateCoinIX) {
txSignal, err = parsePumpCreate(msg, instruction)
} else if matchMethod(instruction.Data[0:8], pumpCreateCoinV2IX) {
txSignal, err = parsePumpCreateV2(msg, instruction)
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func parsePumpCreate(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
creator, err := tx.GetAccount(int(instruction.Accounts[7]))
if err != nil {
return nil, err
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: creator.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: decimal.Zero,
Token1Amount: decimal.Zero,
Program: "Pump",
Event: "create",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: 0,
Token1AmountUint64: 0,
}, nil
}
func parsePumpCreateV2(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 8 {
return nil, fmt.Errorf("data too short for pump create v2 args, len=%d", len(instruction.Data))
}
mint, err := tx.GetAccount(int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
tokenProgramKey, err := tx.GetAccount(int(instruction.Accounts[7]))
if err != nil {
return nil, err
}
var args pumpCreateCoinV2Args
if err := borsh.Deserialize(&args, instruction.Data[8:]); err != nil {
return nil, fmt.Errorf("failed to parse create coin v2 args: %w", err)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: args.Creator.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: decimal.Zero,
Token1Amount: decimal.Zero,
Program: "Pump",
Event: "create",
IsToken2022: tokenProgramKey.String() != tokenProgram,
IsMayhemMode: args.IsMayhemMode,
Block: tx.Block,
Token0AmountUint64: 0,
Token1AmountUint64: 0,
}, nil
}
func decodePumpBuyArgs(data []byte) (uint64, uint64, error) {
if len(data) < 9 {
return 0, 0, fmt.Errorf("data too short for pump buy buy args, len=%d", len(data))
}
var args pumpBuyArgs
if err := borsh.Deserialize(&args, data[8:]); err == nil {
return args.Amount, args.MaxSolCost, nil
}
if len(data) >= 24 {
amount := binary.LittleEndian.Uint64(data[8:16])
maxSol := binary.LittleEndian.Uint64(data[16:24])
return amount, maxSol, nil
}
return 0, 0, fmt.Errorf("failed to parse buy tokens args")
}
func parsePumpBuy(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
amount, sol, err := decodePumpBuyArgs(instruction.Data)
if err != nil {
return nil, err
}
exactIn := false
if matchMethod(instruction.Data, pumpBuyV2TokensIX) {
temp := amount
amount = sol
sol = temp
exactIn = true
}
if len(instruction.Accounts) < 7 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
buyer, err := tx.GetAccount(int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: buyer.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(amount),
Token1Amount: formatSolAmount(sol),
Program: "Pump",
Event: "buy",
ExactSOL: exactIn,
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: amount,
Token1AmountUint64: sol,
}, nil
}
func decodePumpSellArgs(data []byte) (uint64, uint64, error) {
if len(data) < 9 {
return 0, 0, fmt.Errorf("data too short for pump sell sell args, len=%d", len(data))
}
var args pumpExtendedSellArgs
if err := borsh.Deserialize(&args, data[8:]); err == nil {
return args.Amount, args.MinSolOutput, nil
}
if len(data) >= 24 {
amount := binary.LittleEndian.Uint64(data[8:16])
minSol := binary.LittleEndian.Uint64(data[16:24])
return amount, minSol, nil
}
return 0, 0, fmt.Errorf("failed to parse sell tokens args")
}
func parsePumpSell(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
amount, minSol, err := decodePumpSellArgs(instruction.Data)
if err != nil {
return nil, err
}
if len(instruction.Accounts) < 7 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
seller, err := tx.GetAccount(int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pump",
Maker: seller.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(amount),
Token1Amount: formatSolAmount(minSol),
Program: "Pump",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: amount,
Token1AmountUint64: minSol,
}, nil
}

View File

@@ -0,0 +1,166 @@
package shreder
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
)
var pumpAmmProgramID = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
var (
pumpAmmBuyTokensV2IX = []byte{198, 46, 21, 82, 180, 217, 232, 112}
pumpAmmBuyTokensIX = []byte{102, 6, 61, 18, 1, 218, 235, 234}
pumpAmmSellTokensIX = []byte{51, 230, 133, 164, 1, 127, 131, 173}
)
type pumpAmmBuyArgs struct {
Amount uint64
MaxSolCost uint64
}
func parsePumpAmmInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
var (
err error
txSignal *TxSignal
)
if matchMethod(instruction.Data, pumpAmmBuyTokensIX) || matchMethod(instruction.Data, pumpAmmBuyTokensV2IX) {
txSignal, err = parsePumpAmmBuy(tx, instruction)
} else if matchMethod(instruction.Data, pumpAmmSellTokensIX) {
txSignal, err = parsePumpAmmSell(tx, instruction)
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func decodePumpAmmBuyArgs(data []byte) (uint64, uint64, error) {
if len(data) < 9 {
return 0, 0, fmt.Errorf("data too short for pump amm buy args, len=%d", len(data))
}
var args pumpAmmBuyArgs
if err := borsh.Deserialize(&args, data[8:]); err == nil {
return args.Amount, args.MaxSolCost, nil
}
if len(data) >= 24 {
amount := binary.LittleEndian.Uint64(data[8:16])
maxSol := binary.LittleEndian.Uint64(data[16:24])
return amount, maxSol, nil
}
return 0, 0, fmt.Errorf("failed to parse buy tokens args")
}
func parsePumpAmmBuy(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
amount, maxSol, err := decodePumpAmmBuyArgs(instruction.Data)
if err != nil {
return nil, err
}
exactIn := false
if matchMethod(instruction.Data, pumpAmmBuyTokensV2IX) {
temp := amount
amount = maxSol
maxSol = temp
exactIn = true
}
if len(instruction.Accounts) < 7 {
return nil, fmt.Errorf("accounts too short")
}
base, err := tx.GetAccount(int(instruction.Accounts[3]))
if err != nil {
return nil, err
}
quote, err := tx.GetAccount(int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
if !quote.Equals(solana.WrappedSol) {
return nil, nil
}
buyer, err := tx.GetAccount(int(instruction.Accounts[1]))
if err != nil {
return nil, err
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pumpamm",
Maker: buyer.String(),
Token0Address: base.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(amount),
Token1Amount: formatSolAmount(maxSol),
Program: "PumpAMM",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: exactIn,
Block: tx.Block,
Token0AmountUint64: amount,
Token1AmountUint64: maxSol,
}, nil
}
func parsePumpAmmSell(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
amount, minSol, err := decodePumpAmmBuyArgs(instruction.Data)
if err != nil {
return nil, err
}
if len(instruction.Accounts) < 7 {
return nil, fmt.Errorf("accounts too short")
}
base, err := tx.GetAccount(int(instruction.Accounts[3]))
if err != nil {
return nil, err
}
quote, err := tx.GetAccount(int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
if !quote.Equals(solana.WrappedSol) {
return nil, nil
}
buyer, err := tx.GetAccount(int(instruction.Accounts[1]))
if err != nil {
return nil, err
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "pumpamm",
Maker: buyer.String(),
Token0Address: base.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(amount),
Token1Amount: formatSolAmount(minSol),
Program: "PumpAMM",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: amount,
Token1AmountUint64: minSol,
}, nil
}

166
pkg/shreder/program_qtkv.go Normal file
View File

@@ -0,0 +1,166 @@
package shreder
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/near/borsh-go"
"github.com/shopspring/decimal"
)
var qtkvProgramID = solana.MustPublicKeyFromBase58("qtkvapJEvRWWrB7i5K6RaA1kvq5x3qmMKZ98ad71XQ7")
var (
qtkvBuyTokensIX = []byte{0x02}
qtkvSellTokensIX = []byte{0x03}
qtkvAmmSellTokensIX = []byte{0x05}
)
type qtkvBuyArgs struct {
Placeholder uint64
TokenNumber uint64
SolAmount uint64
}
func parseQtkvInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
var (
err error
txSignal *TxSignal
)
if matchMethod(instruction.Data, qtkvBuyTokensIX) {
txSignal, err = parseQtkvBuy(tx, instructionIndex)
} else if matchMethod(instruction.Data, qtkvAmmSellTokensIX) {
txSignal, err = parseQtkvAmmSell(tx, instructionIndex)
} else if matchMethod(instruction.Data, qtkvSellTokensIX) {
txSignal, err = parseQtkvSell(tx, instructionIndex)
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func parseQtkvSell(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 11 {
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 24 {
return nil, fmt.Errorf("data too short for qtkv sell args, len=%d", len(instruction.Data))
}
mint, err := tx.GetAccount(int(instruction.Accounts[10]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
// in sell, sol amount is not directly provided, so we set it to 0
tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25])
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(tokenAmount),
Token1Amount: decimal.Zero,
Program: "Pump",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: tokenAmount,
Token1AmountUint64: 0,
}, nil
}
func parseQtkvAmmSell(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 11 {
return nil, fmt.Errorf("accounts too short")
}
if len(instruction.Data) < 24 {
return nil, fmt.Errorf("data too short for qtkv amm sell args, len=%d", len(instruction.Data))
}
mint, err := tx.GetAccount(int(instruction.Accounts[10]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
// in sell, sol amount is not directly provided, so we set it to 0
tokenAmount := binary.LittleEndian.Uint64(instruction.Data[19:25])
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(tokenAmount),
Token1Amount: decimal.Zero,
Program: "PumpAMM",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: tokenAmount,
Token1AmountUint64: 0,
}, nil
}
func parseQtkvBuy(tx VersionedTransaction, instructionIndex int) (*TxSignal, error) {
instruction := tx.Instructions[instructionIndex]
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[3]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[0]))
if err != nil {
return nil, err
}
var args qtkvBuyArgs
if err := borsh.Deserialize(&args, instruction.Data[1:]); err != nil {
return nil, fmt.Errorf("failed to parse buy tokens args: %w", err)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "qtkv",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(args.TokenNumber),
Token1Amount: formatSolAmount(args.SolAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
Block: tx.Block,
Token0AmountUint64: args.TokenNumber,
Token1AmountUint64: args.SolAmount,
}, nil
}

155
pkg/shreder/program_term.go Normal file
View File

@@ -0,0 +1,155 @@
package shreder
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
)
var terminalProgramID = solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3")
var (
terminalBuyTokensIX = []byte{0xa6, 0x54, 0x14, 0x96, 0x9f, 0x77, 0x59, 0xca}
terminalSellTokensIX = []byte{0xbe, 0x84, 0xa2, 0x96, 0x93, 0x7c, 0xf8, 0x6b}
terminalAmmSellTokensIX = []byte{0x40, 0x64, 0x97, 0xb9, 0x16, 0xfa, 0xec, 0xb1}
)
func parseTermInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) {
if instructionIndex >= len(tx.Instructions) {
return nil, fmt.Errorf("instruction index out of bounds")
}
instruction := tx.Instructions[instructionIndex]
if len(instruction.Data) == 0 {
return nil, fmt.Errorf("data is empty")
}
if len(instruction.Data) < 24 {
return nil, nil
}
var (
err error
txSignal *TxSignal
)
switch {
case bytes.Equal(instruction.Data[:8], terminalBuyTokensIX):
txSignal, err = parseTermBuy(tx, instruction)
case bytes.Equal(instruction.Data[:8], terminalSellTokensIX):
txSignal, err = parseTermSell(tx, instruction)
case bytes.Equal(instruction.Data[:8], terminalAmmSellTokensIX):
txSignal, err = parseTermAmmSell(tx, instruction)
default:
return nil, nil
}
if txSignal != nil {
return TxSignalBatch{txSignal}, err
}
return nil, err
}
func parseTermAmmSell(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[3]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[1]))
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: "term",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(tokenAmount),
Token1Amount: formatSolAmount(solAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: tokenAmount,
Token1AmountUint64: solAmount,
}, nil
}
func parseTermBuy(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(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: "term",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(tokenAmount),
Token1Amount: formatSolAmount(solAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: tokenAmount,
Token1AmountUint64: solAmount,
}, nil
}
func parseTermSell(tx VersionedTransaction, instruction Instructions) (*TxSignal, error) {
if len(instruction.Accounts) < 8 {
return nil, fmt.Errorf("accounts too short")
}
mint, err := tx.GetAccount(int(instruction.Accounts[2]))
if err != nil {
return nil, err
}
user, err := tx.GetAccount(int(instruction.Accounts[6]))
if err != nil {
return nil, err
}
tokenAmount := binary.LittleEndian.Uint64(instruction.Data[8:16])
solAmount := binary.LittleEndian.Uint64(instruction.Data[16:24])
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Label: "term",
Maker: user.String(),
Token0Address: mint.String(),
Token1Address: wsolMint,
Token0Amount: formatTokenAmount(tokenAmount),
Token1Amount: formatSolAmount(solAmount),
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: false,
Block: tx.Block,
Token0AmountUint64: tokenAmount,
Token1AmountUint64: solAmount,
}, nil
}

View File

@@ -48,17 +48,18 @@ 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:"-"`
Stats Stats `json:"-"` ParseStart time.Time `json:"parse_start"`
} ParseEnd time.Time `json:"parse_end"`
func (t *TxSignal) Parse() *TxSignal {
t.Token0AmountUint64 = t.Token0Amount.Mul(decimal.New(1, TokenDecimals)).BigInt().Uint64()
t.Token1AmountUint64 = t.Token1Amount.Mul(decimal.New(1, SolDecimals)).BigInt().Uint64()
return t
} }
type TxSignalBatch = []*TxSignal type TxSignalBatch = []*TxSignal

File diff suppressed because it is too large Load Diff

View File

@@ -149,16 +149,26 @@ func TestParseTermBuy(t *testing.T) {
} }
client := rpc.New(rpcUrl) client := rpc.New(rpcUrl)
txChannel := make(chan TxSignal, 1) ch := make(chan TxSignal)
closed := make(chan struct{})
go func() { go func() {
ParseTransaction( ParseTransactionForSubscribe(
context.Background(),
getTransaction(t, client, "5Gz1fa4Qhb35bkg9QCMXpxCX5uuNr7WcjcmrwajGZA7kXsvNS9pDnYe12ggWeSqf1nwZbVPob6DkX6fcwbE9ofBR"), getTransaction(t, client, "5Gz1fa4Qhb35bkg9QCMXpxCX5uuNr7WcjcmrwajGZA7kXsvNS9pDnYe12ggWeSqf1nwZbVPob6DkX6fcwbE9ofBR"),
nil, txChannel, nil,
false, ch,
closed,
) )
}() }()
signals := make([]TxSignal, 0)
for signal := range ch {
signals = append(signals, signal)
}
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := <-txChannel signal := signals[0]
if signal.Label != "terminal" { if signal.Label != "terminal" {
t.Fatalf("expected terminal signal, got %s", signal.Label) t.Fatalf("expected terminal signal, got %s", signal.Label)
} }
@@ -186,16 +196,28 @@ func TestParseBonkBuy(t *testing.T) {
} }
client := rpc.New(rpcUrl) client := rpc.New(rpcUrl)
txChannel := make(chan TxSignal, 1)
ch := make(chan TxSignal)
closed := make(chan struct{})
go func() { go func() {
ParseTransaction( ParseTransactionForSubscribe(
context.Background(),
getTransaction(t, client, "3gHF3TA2aA8rpjdmoEs2vA89vrq9J9NnTTUSXHfE6uXcaYP9cJgLtEUjCmsK9EWAyHEg7cEiepehQf4GFv1272jW"), getTransaction(t, client, "3gHF3TA2aA8rpjdmoEs2vA89vrq9J9NnTTUSXHfE6uXcaYP9cJgLtEUjCmsK9EWAyHEg7cEiepehQf4GFv1272jW"),
nil, txChannel, nil,
false, ch,
closed,
) )
}() }()
signals := make([]TxSignal, 0)
for signal := range ch {
signals = append(signals, signal)
}
signal := <-txChannel if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "bonk" { if signal.Label != "bonk" {
t.Fatalf("expected bonk signal, got %s", signal.Label) t.Fatalf("expected bonk signal, got %s", signal.Label)
} }
@@ -223,16 +245,27 @@ func TestParseBonkSell(t *testing.T) {
} }
client := rpc.New(rpcUrl) client := rpc.New(rpcUrl)
txChannel := make(chan TxSignal, 1) ch := make(chan TxSignal)
closed := make(chan struct{})
go func() { go func() {
ParseTransaction( ParseTransactionForSubscribe(
context.Background(),
getTransaction(t, client, "3XNi6b3j69SSStqLLRQVH5BNGVfEoFxGCzmpdd5FvrY4kmC8T644WGdEhCH9fAdrxWuR2Mtzgywq8K7qetu5MGyb"), getTransaction(t, client, "3XNi6b3j69SSStqLLRQVH5BNGVfEoFxGCzmpdd5FvrY4kmC8T644WGdEhCH9fAdrxWuR2Mtzgywq8K7qetu5MGyb"),
nil, txChannel, nil,
false, ch,
closed,
) )
}() }()
signals := make([]TxSignal, 0)
for signal := range ch {
signals = append(signals, signal)
}
signal := <-txChannel if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "bonk" { if signal.Label != "bonk" {
t.Fatalf("expected bonk signal, got %s", signal.Label) t.Fatalf("expected bonk signal, got %s", signal.Label)
} }
@@ -260,16 +293,28 @@ func TestParsePhotonBuy(t *testing.T) {
} }
client := rpc.New(rpcUrl) client := rpc.New(rpcUrl)
txChannel := make(chan TxSignal, 1)
ch := make(chan TxSignal)
closed := make(chan struct{})
go func() { go func() {
ParseTransaction( ParseTransactionForSubscribe(
context.Background(),
getTransaction(t, client, "4DCEcXAWBxagXoUNGhWsJ7qfxq5SuE5BG2cBDBqAY7sCHkBopaMJu33ZnXnFHqzPMmWxVxq6666KRF4hMHVB33Ux"), getTransaction(t, client, "4DCEcXAWBxagXoUNGhWsJ7qfxq5SuE5BG2cBDBqAY7sCHkBopaMJu33ZnXnFHqzPMmWxVxq6666KRF4hMHVB33Ux"),
nil, txChannel, nil,
false, ch,
closed,
) )
}() }()
signals := make([]TxSignal, 0)
for signal := range ch {
signals = append(signals, signal)
}
signal := <-txChannel if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "photon" { if signal.Label != "photon" {
t.Fatalf("expected terminal signal, got %s", signal.Label) t.Fatalf("expected terminal signal, got %s", signal.Label)
} }
@@ -289,3 +334,104 @@ func TestParsePhotonBuy(t *testing.T) {
t.Fatalf("expected token1 amount 1955555553, got %d", signal.Token1AmountUint64) 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)
ch := make(chan TxSignal)
closed := make(chan struct{})
go func() {
ParseTransactionForSubscribe(
context.Background(),
getTransaction(t, client, "4QF5whXwjx234fMXeH3HrJCy5knFJmKPtgbXys8xKGz1pZypqPvXBr4BoAqXfYn8jLL4HXPY1pcvxCCW1XREFNxd"),
nil,
ch,
closed,
)
}()
signals := make([]TxSignal, 0)
for signal := range ch {
signals = append(signals, signal)
}
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)
ch := make(chan TxSignal)
closed := make(chan struct{})
go func() {
ParseTransactionForSubscribe(
context.Background(),
getTransaction(t, client, "yCnE7ZA8dqB5iAZtwpSN2ar5HXh3gBjgaG2xtnwXDPFyHAm5XFU8642uTZTH5A2iPQ6G9hrj5eEPAJiWrfe38gM"),
nil,
ch,
closed,
)
}()
signals := make([]TxSignal, 0)
for signal := range ch {
signals = append(signals, signal)
}
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")
}
}

62
pkg/shreder/versioned.go Normal file
View File

@@ -0,0 +1,62 @@
package shreder
import (
"fmt"
"time"
"github.com/gagliardetto/solana-go"
)
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)
}
type Instructions struct {
ProgramIDIndex uint8
Accounts []uint8
Data []byte
}
type AddressTableLookup struct {
AccountKey solana.PublicKey
WritableIndexes []uint8
ReadonlyIndexes []uint8
}
type VersionedTransaction struct {
Signatures []solana.Signature
StaticAccountKeys []solana.PublicKey
Instructions []Instructions
AddressTableLookups []AddressTableLookup
Block uint64
Time time.Time
}
func (vt VersionedTransaction) GetSignature() string {
if len(vt.Signatures) == 0 {
return ""
}
return vt.Signatures[0].String()
}
func (vtp *VersionedTransaction) FillAccount(account solana.PublicKey) {
vtp.StaticAccountKeys = append(vtp.StaticAccountKeys, account)
}
func (vt VersionedTransaction) GetAccount(idx int) (solana.PublicKey, error) {
if idx < len(vt.StaticAccountKeys) {
return vt.StaticAccountKeys[idx], nil
}
return solana.PublicKey{}, NewAccountNotFoundError(idx, len(vt.StaticAccountKeys))
}