diff --git a/cmd/shreder/main.go b/cmd/shreder/main.go index 04623ed..c1a18cb 100644 --- a/cmd/shreder/main.go +++ b/cmd/shreder/main.go @@ -11,7 +11,7 @@ import ( "github.com/gagliardetto/solana-go/rpc" - "github.com/samlior/libsam/pkg/shreder" + "github.com/samlior/libsam/v2/pkg/shreder" ) func main() { @@ -76,9 +76,9 @@ func main() { cancel() }() // async read from shreder - txCh := make(chan shreder.TxSignalBatch, 1000) + txCh := make(chan shreder.TxSignal, 1000) go func() { - err := shrederClient.ReadSync(ctx, txCh) + err := shrederClient.ReadEntriesSync(ctx, txCh) if err != nil { if !errors.Is(err, context.Canceled) { panic(err) @@ -90,14 +90,10 @@ func main() { select { case <-ctx.Done(): return - case txBatch := <-txCh: - //jsonData, _ := json.MarshalIndent(txBatch, "", " ") - for _, tx := range txBatch { - if tx.Label == "okxdexroutev2" || tx.Label == "jupiterv6" || tx.Label == "dflow" { - fmt.Println("===============", tx.TxHash, tx.Label, tx.Event, tx.Token0Address, "token:", tx.Token0Amount, "parse time:", tx.ParseEnd.Sub(tx.ParseStart)) - } + case tx := <-txCh: + if tx.Label == "okxdexroutev2" || tx.Label == "jupiterv6" || tx.Label == "dflow" { + fmt.Println("===============", tx.TxHash, tx.Label, tx.Event, tx.Token0Address, "token:", tx.Token0Amount, "parse time:", tx.ParseEnd.Sub(tx.ParseStart)) } - //fmt.Println(txBatch[0].TxHash) } } } diff --git a/go.mod b/go.mod index 50621df..b5d9ecf 100644 --- a/go.mod +++ b/go.mod @@ -1,50 +1,50 @@ -module github.com/samlior/libsam +module github.com/samlior/libsam/v2 go 1.25.1 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/binary v0.8.0 - github.com/gagliardetto/solana-go v1.12.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/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454 github.com/panjf2000/ants/v2 v2.11.4 - github.com/quic-go/quic-go v0.58.0 + github.com/quic-go/quic-go v0.59.0 github.com/shopspring/decimal v1.4.0 - google.golang.org/grpc v1.75.0 - google.golang.org/protobuf v1.36.10 + google.golang.org/grpc v1.78.0 + google.golang.org/protobuf v1.36.11 ) 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/blendle/zapdriver v1.3.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/treeout v0.1.4 // indirect github.com/google/uuid v1.6.0 // 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/mattn/go-colorable v0.1.4 // indirect - github.com/mattn/go-isatty v0.0.11 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // 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/reflect2 v1.0.2 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect - github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect - go.mongodb.org/mongo-driver v1.12.2 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + github.com/streamingfast/logging v0.0.0-20260108192805-38f96de0a641 // indirect + go.mongodb.org/mongo-driver v1.17.7 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect - go.uber.org/zap v1.21.0 // indirect - golang.org/x/crypto v0.44.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + go.uber.org/zap v1.27.1 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // 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 ) diff --git a/go.sum b/go.sum index eb5b883..6d74770 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ 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.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/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/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/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= 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/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.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/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/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= 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.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/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= 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.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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/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.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.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.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/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -79,11 +93,15 @@ 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/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.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.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 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/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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -100,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= 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.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/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= @@ -115,6 +135,8 @@ 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.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 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/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= @@ -122,11 +144,15 @@ 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.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 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/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= 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/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -134,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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= 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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -146,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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -162,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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.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-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/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -176,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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -192,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= 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-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/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/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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/shreder/addresstables.go b/pkg/shreder/addresstables.go index 3c42b77..cb09a77 100644 --- a/pkg/shreder/addresstables.go +++ b/pkg/shreder/addresstables.go @@ -17,6 +17,8 @@ type TableInfo struct { addresses []solana.PublicKey } +const MaxOverErrCount = 10 + type AddressTables struct { showTableLoaded bool @@ -96,7 +98,7 @@ func (at *AddressTables) load(tablePubkey solana.PublicKey) { }) } -func (at *AddressTables) FillToTx(tx *versionedTransaction, tablePubkey solana.PublicKey, idx []uint8) bool { +func (at *AddressTables) FillToTx(tx *VersionedTransaction, tablePubkey solana.PublicKey, idx []uint8) bool { addresses, ok := at.tables.Get(tablePubkey) if !ok { at.load(tablePubkey) @@ -112,7 +114,7 @@ func (at *AddressTables) FillToTx(tx *versionedTransaction, tablePubkey solana.P } return false } - tx.Message.StaticAccountKeys = append(tx.Message.StaticAccountKeys, addresses.addresses[i]) + tx.StaticAccountKeys = append(tx.StaticAccountKeys, addresses.addresses[i]) } return true } @@ -129,7 +131,7 @@ func (at *AddressTables) GetAddressTable(tablePubkey solana.PublicKey, idx []uin if int(i) >= len(addresses.addresses) { logger.Error("over loadAddressTable failed", "idx", i, "table", tablePubkey) addresses.overErrCount++ - if addresses.overErrCount > 10 { + if addresses.overErrCount > MaxOverErrCount { at.load(tablePubkey) } break diff --git a/pkg/shreder/client.go b/pkg/shreder/client.go index ee23a35..e25fd1f 100644 --- a/pkg/shreder/client.go +++ b/pkg/shreder/client.go @@ -1,7 +1,9 @@ package shreder import ( + "bytes" "context" + "errors" "fmt" "runtime" "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 { return func(opts *ClientOpts) { opts.logParseStats = enable @@ -113,7 +117,40 @@ func (c *Client) Wait() { logger.Debug("shreder client stopped") } -func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error { +func (c *Client) ReadEntriesSync(ctx context.Context, txCh chan<- TxSignal) error { + stream, err := c.client.SubscribeEntries(ctx, &SubscribeEntriesRequest{}) + if err != nil { + return err + } + + logger.Debug("reading entries from shreder client") + for { + response, err := stream.Recv() + if err != nil { + return err + } + slot := response.Slot + if c.enableBlockStats { + now := time.Now() + if c.lastSlotTime.IsZero() || slot > c.lastSlot { + if !c.lastSlotTime.IsZero() { + logger.Info("block processed", "running", c.pool.Running(), "slot", slot, "prev_slot", c.lastSlot, "delta_ms", now.Sub(c.lastSlotTime).Milliseconds()) + } + c.lastSlot = slot + c.lastSlotTime = now + } + } + + err = c.pool.Submit(func() { + ParseTransactionForEntries(ctx, slot, bytes.NewReader(response.Entries), c.tableLoader, txCh) + }) + if err != nil && errors.Is(err, ants.ErrPoolOverload) { + logger.Warn("task pool is full") + } + } +} + +func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignal) error { stream, err := c.client.SubscribeTransactions(ctx) if err != nil { return err @@ -152,24 +189,11 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error txData := response.Transaction - err = c.pool.Submit(func() { - txBatch := ParseTransaction(txData, c.tableLoader, c.enableParseStats) - if len(txBatch) == 0 { - return - } - - for _, tx := range txBatch { - tx.Source = "shreder" - } - - select { - case <-ctx.Done(): - return - case txCh <- txBatch: - } + err := c.pool.Submit(func() { + ParseTransactionForSubscribe(ctx, txData, c.tableLoader, txCh, nil) }) - if err != nil { - break + if err != nil && errors.Is(err, ants.ErrPoolOverload) { + logger.Warn("task pool is full") } } diff --git a/pkg/shreder/entry.go b/pkg/shreder/entry.go new file mode 100644 index 0000000..bdc8295 --- /dev/null +++ b/pkg/shreder/entry.go @@ -0,0 +1,272 @@ +package shreder + +import ( + "encoding/binary" + "fmt" + "io" + "time" +) + +type wrapperReader struct { + io.Reader +} + +func (wr *wrapperReader) ReadU64() (uint64, error) { + var buf [8]byte + _, err := io.ReadFull(wr, buf[:]) + if err != nil { + 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 (wr *wrapperReader) Skip(n int) error { + _, err := io.CopyN(io.Discard, wr, int64(n)) + return err +} + +func (wr *wrapperReader) ReadCompactU16() (uint16, error) { + ln := 0 + size := 0 + var buf [1]byte + for i := 0; i < 3; i++ { + _, err := io.ReadFull(wr, buf[:]) + if err != nil { + return 0, fmt.Errorf("unable to decode compact u16 at %d: %w", i, err) + } + elem := int(buf[0]) + if elem == 0 && i != 0 { + return 0, fmt.Errorf("alias") + } + if i == 2 && (elem&0x80) != 0 { + return 0, fmt.Errorf("byte three continues") + } + ln |= (elem & 0x7f) << (size * 7) + size++ + if (elem & 0x80) == 0 { + break + } + } + return uint16(ln), nil +} + +func (wr *wrapperReader) ReadByte() (uint8, error) { + var buf [1]byte + _, err := io.ReadFull(wr, buf[:]) + if err != nil { + return 0, err + } + return buf[0], 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. +func entriesToVersionedTransaction(slot uint64, data io.Reader, callback func(tx VersionedTransaction)) error { + b := &wrapperReader{data} + var entriesNumBuf [8]byte + n, err := io.ReadFull(b, entriesNumBuf[:]) + if err != nil { + if err == io.EOF && n == 0 { + return nil + } + return fmt.Errorf("unable to read entries num: %w", err) + } + entriesNum := binary.LittleEndian.Uint64(entriesNumBuf[:]) + //if entriesNum == 0 { + // return nil, nil + //} + if entriesNum > 2048 { + return fmt.Errorf("entries num is too large: %d > %d", entriesNum, 2048) + } + + for i := uint64(0); i < entriesNum; i++ { + err = b.Skip(40) + if err != nil { + return fmt.Errorf("failed to skip num_hashes + hash of entry %d: %w", i, err) + } + numTx, err := b.ReadU64() + if err != nil { + return fmt.Errorf("failed to read num_transactions of entry %d: %w", i, err) + } + for j := 0; j < int(numTx); j++ { + numSignatures, err := b.ReadCompactU16() + if err != nil { + return fmt.Errorf("failed to read numSignatures in entry %d, txn %d: %w", i, j, err) + } + + // enforce a maximum number of signatures to prevent OOM + if numSignatures > 32 { + return fmt.Errorf("numSignatures %d exceeds maximum in entry %d, txn %d", numSignatures, i, j) + } + if numSignatures == 0 { + return fmt.Errorf("numSignatures is zero in entry %d, txn %d", i, j) + } + + versioned := VersionedTransaction{} + versioned.Block = slot + versioned.Time = time.Now() + versioned.Signatures = ResizeSlice(versioned.Signatures, int(numSignatures)) + for k := 0; k < int(numSignatures); k++ { + _, err = io.ReadFull(b, versioned.Signatures[k][:]) + if err != nil { + return fmt.Errorf("unable to read signature in entry %d, txn %d, sig: %d, %w", i, j, k, err) + } + } + + msgVersion, err := b.ReadByte() + if err != nil { + 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 + legacy := msgVersion == 0 + headerSkip := 2 + if !legacy { + headerSkip = 3 + } + // skip msg version, mx.Header+3 + + err = b.Skip(headerSkip) + if err != nil { + return fmt.Errorf("unable to skip message header in entry %d, txn %d: %w", i, j, err) + } + + // read mx.AccountKeys + // _, err = r.Read(u16[:]) + + numAccountKeys, err := b.ReadCompactU16() + // logger.Info("tx", "hash", versioned.Signatures[0].String(), "version", msgVersion) + if err != nil { + return fmt.Errorf("unable to decode numAccountKeys in entry %d, txn %d: %w", i, j, err) + } + // enforce a maximum number of account keys to prevent OOM + if numAccountKeys > 255 { + return fmt.Errorf("numAccountKeys %d exceeds maximum in entry %d, txn %d", numAccountKeys, i, j) + } + + versioned.StaticAccountKeys = ResizeSlice(versioned.StaticAccountKeys, int(numAccountKeys)) + + for k := 0; k < int(numAccountKeys); k++ { + _, err = io.ReadFull(b, versioned.StaticAccountKeys[k][:]) + if err != nil { + return fmt.Errorf("unable to read accountKey[%d] in entry %d, txn %d: %w", k, i, j, err) + } + } + + //skip solana hash + err = b.Skip(32) + if err != nil { + return fmt.Errorf("unable to skip recentBlockhash in entry %d, txn %d: %w", i, j, err) + } + + // read mx.Instructions + numInstructions, err := b.ReadCompactU16() + if err != nil { + return fmt.Errorf("unable to decode numInstructions in entry %d, txn %d: %w", i, j, err) + } + + // enforce a maximum number of instructions to prevent OOM + if numInstructions >= 256 { + return fmt.Errorf("numInstructions %d exceeds maximum in entry %d, txn %d, txHash: %s", numInstructions, i, j, versioned.GetSignature()) + } + versioned.Instructions = ResizeSlice(versioned.Instructions, int(numInstructions)) + for k := 0; k < int(numInstructions); k++ { + versioned.Instructions[k].ProgramIDIndex, err = b.ReadByte() + if err != nil { + return fmt.Errorf("unable to read mx.Instructions[%d].ProgramIDIndex in entry %d, txn %d: %w", k, i, j, err) + } + numAccounts, err := b.ReadCompactU16() + if err != nil { + return fmt.Errorf("unable to decode numAccounts for ix[%d] in entry %d, txn %d: %w", k, i, j, err) + } + + // enforce a maximum number of accounts to prevent OOM + if numAccounts >= 256 { + return fmt.Errorf("numAccounts %d exceeds maximum for ix[%d] in entry %d, txn %d", numAccounts, k, i, j) + } + versioned.Instructions[k].Accounts = ResizeSlice(versioned.Instructions[k].Accounts, int(numAccounts)) + + //.AccountsLen = int(numAccounts) + if numAccounts != 0 { + _, err = io.ReadFull(b, versioned.Instructions[k].Accounts) + if err != nil { + return fmt.Errorf("unable to read mx.Instructions[%d].Accounts in entry %d, txn %d: %w", k, i, j, err) + } + } + dataLen, err := b.ReadCompactU16() + if err != nil { + return fmt.Errorf("unable to decode mx.Instructions[%d].Data length in entry %d, txn %d: %w", k, i, j, err) + } + // enforce a maximum data length to prevent OOM + if dataLen > 2048 { + 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.Instructions[k].Data = ResizeSlice(versioned.Instructions[k].Data, int(dataLen)) + if dataLen > 0 { + _, err = io.ReadFull(b, versioned.Instructions[k].Data) + if err != nil { + return fmt.Errorf("unable to read mx.Instructions[%d].Data in entry %d, txn %d: %w", k, i, j, err) + } + } + } + + if !legacy { + // read mx.AddressTableLookups + numLookups, err := b.ReadByte() + if err != nil { + return fmt.Errorf("unable to read numAddressTableLookups in entry %d, txn %d: %w", i, j, err) + } + if numLookups >= 32 { + return fmt.Errorf("numLookups %d exceeds maximum in entry %d, txn %d", numLookups, i, j) + } + versioned.AddressTableLookups = ResizeSlice(versioned.AddressTableLookups, int(numLookups)) + for k := uint8(0); k < numLookups; k++ { + _, err = io.ReadFull(b, versioned.AddressTableLookups[k].AccountKey[:]) + if err != nil { + 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() + if err != nil { + return fmt.Errorf("unable to decode numWritableIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) + } + // enforce a maximum number of writable indexes to prevent OOM + if numWritable >= 256 { + return fmt.Errorf("numWritableIndexes %d exceeds maximum for lookup[%d] in entry %d, txn %d", numWritable, k, i, j) + } + versioned.AddressTableLookups[k].WritableIndexes = ResizeSlice(versioned.AddressTableLookups[k].WritableIndexes, int(numWritable)) + if numWritable > 0 { + _, err = io.ReadFull(b, versioned.AddressTableLookups[k].WritableIndexes) + if err != nil { + return fmt.Errorf("unable to read writableIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) + } + } + + numReadonly, err := b.ReadCompactU16() + if err != nil { + return fmt.Errorf("unable to decode numReadonlyIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) + } + // enforce a maximum number of readonly indexes to prevent OOM + if numReadonly > 256 { + return fmt.Errorf("numReadonlyIndexes %d exceeds maximum for lookup[%d] in entry %d, txn %d", numReadonly, k, i, j) + } + versioned.AddressTableLookups[k].ReadonlyIndexes = ResizeSlice(versioned.AddressTableLookups[k].ReadonlyIndexes, int(numReadonly)) + if numReadonly > 0 { + _, err = io.ReadFull(b, versioned.AddressTableLookups[k].ReadonlyIndexes) + if err != nil { + return fmt.Errorf("unable to read readonlyIndexes for lookup[%d] in entry %d, txn %d: %w", k, i, j, err) + } + } + } + } + + callback(versioned) + } + } + return nil +} diff --git a/pkg/shreder/entry_test.go b/pkg/shreder/entry_test.go new file mode 100644 index 0000000..d7f50b7 --- /dev/null +++ b/pkg/shreder/entry_test.go @@ -0,0 +1,30 @@ +package shreder + +import ( + "bytes" + "encoding/base64" + "testing" +) + +func TestDecodeEntry(t *testing.T) { + tests := []struct { + name string + data string + }{ + { + name: "TestDecodeEntry1", + data: "BgAAAAAAAADFCQAAAAAAABa3RhozUFDf4JW2I2fuaRUnS2jsksCNpvGoOCvKgSOXAQAAAAAAAAABMhXYcIO0FsOv+exTIC11M9XGWTL0HEn8NGWa5bUiWYHh3eCIKm7gmHNm++o04EB7a1YQm0auEH5lKtoxPuvCDoABAAYJKqrFxl/dG3sl/ET53EdOUY7GkME7PDLzYjrnjXECoJxSx/dHprrpooHmYGfnxMLcjOfV6ynWLJpdaaX3veAU0HJby+7Nj0f6IJgK9M/rtAyoL5FBP7e2TBAbNB7nNuodAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAxKoBKOldPhYiqlAcWFqesHs3NUwQjqC3kbRW3H7qM2DLf6u1L3pki7WzF9mgGLkFfLAkd0+v4B5sTfmMw4WIFTtjzvYd5D6GsWgxoFbrya06c75FbKVGasJZ2fGjzorLrhu5lE80vnhcE09VCUTOaWQ+aAp+uNUW/dgm/9Ojhie4glXf1oNciuf82NedY9xhuipRtE8GnjHbli1VNH6M4DBAAFAsXcAAAEAAkDECcAAAAAAAAFBwAGBwgCAQOMAxwJXZZWmbxzVQAAAABbvRzmF3krR2xVmRwnzf2JeU+fEzVrq8nJJAX18AeWgwAAAAAALdh5AAAAAAAAEi7////4AAAAAGlkfEEAAAAAaWR8QQAAAAAALcKzAAAAAAAAF8wNAAAAWrXF+hEGOoH4/IgPBLRNlnwy7XIaFhKwlUiRfXcp9dbfGvFiHlVY3wRKN84r3tCsJ2zEilNS1s4w7PDFr80TIp70TgTOwdlFbPDqR3oE1rYrzwj84FtD4vTlzf5Lb8jVUxLU5GW7ZYRKTiFDnxymoVg/OoD5jyHgzNXVxYjbW+X0H0729i9wQ8sRM9p9657ocK0lBQwWLnBRkzNvJO+CE8CFL3TMWkPJw9WXLmkBNT5CTFwQyCSEAy5J71XXNJRbH7AwF3rqpcpAlHGXQzmIhU3WAzqfqXzCFoSpH2ZgYWgr/Q+Msprylig2/NajTl1TNn1PbPqLAdEngizvXgzUjPlSEw0AAABbvRzmF3krR2xVmRwnzf2JeU+fEzVrq8nJJAX18AeWgwChDwAAAAAAAHpIeKeU5QUvIhET9QXDNbU1sNu1UZGiclEVaXh+lRTqAQAAAAAAAAABFdDD4PHwP1VOtJCmJQQ/K+MD8X6PhOoM3Jt99HR6SJLSLfq6ZUsjil1yVsijWgNJiChwIRapmsYvxrucGDYbC4ABAAYJKqrFxl/dG3sl/ET53EdOUY7GkME7PDLzYjrnjXECoJxyW8vuzY9H+iCYCvTP67QMqC+RQT+3tkwQGzQe5zbqHfwfzxCF7RlfsUB3A3x42YehLopADLOuNtdbgtPOifOcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAxKoBKOldPhYiqlAcWFqesHs3NUwQjqC3kbRW3H7qM2DLf6u1L3pki7WzF9mgGLkFfLAkd0+v4B5sTfmMw4WIFTtjzvYd5D6GsWgxoFbrya06c75FbKVGasJZ2fGjzorLrhu5lE80vnhcE09VCUTOaWQ+aAp+uNUW/dgm/9Ojhie4glXf1oNciuf82NedY9xhuipRtE8GnjHbli1VNH6M4DBAAFAsjcAAAEAAkDECcAAAAAAAAFBwAGBwgBAgOMAxwJXZZWmbxzVQAAAAB2+oUVi/FO3ncIf+OuRy9mIT9uovW0Ecst5HJ5SZD6XAAAAAAG8vwiAAAAAAADL5P////4AAAAAGlkfEEAAAAAaWR8QQAAAAAG8vzJAAAAAAADL6kNAAAAJm8WUjlxEejZ93dsaGw1Nx6RghTnNEWFbWjsVDmckpRv2z1oEuAZhgxQXW1500c+YQsAAMX7hHPSL/frWNo4S/RkrT1R/ZS4+InOFIoCGM/iHJOdy+hlYMloqICCNG5Zxfut6ygCpT0n9TX44lQxLAzva/xI8ZshVuN7aqIhxLBaoX9tJYjqS4HjMsbeqiMk6JZefzw2rhD/y675OLNdfDtAOFse814kbQimRFADLMHhSjdeyCSEAy5J71XXNJRbH7AwF3rqpcpAlHGXQzmIhU3WAzqfqXzCFoSpH2ZgYWgr/Q+Msprylig2/NajTl1TNn1PbPqLAdEngizvXgzUjPlSEw0AAAB2+oUVi/FO3ncIf+OuRy9mIT9uovW0Ecst5HJ5SZD6XAD1AQAAAAAAANvS5ucXWXc9QVH9Cq4c4LAGP490izdpUgcUufMycFG4AQAAAAAAAAACNw8RJiLgw2ExpFsGCRLWd1SlZ3l5fxzpclbVJkl9LSA56bjKwtOwRqWImPV8EYaL54WddYglsO0mkGB7tyZwAhZ/+McWh3V+BCHwfM7B5aNHE0OVJnVmTVQJzhdmsum3qSGJqvfZ07T0BEHODnazNOgcmKIeNlxz+HMnJvsaqAiAAgAOG/JJS5mGOGsRH0ssnpy9nYXUMg5nfZUcTJlPhozDahk3jWnWUY1v/4lpQP1C4Co7PK7v51+bVQsqz8u1fDl6rl0Ws6msFbah4llAUolNWci7rt+zbrKeEFBwHdglI4/Su5F3taZcZApPgR+uhuHsmRO7kcuKHuoFQS7xa3736a1VKCm1MRMYJh+fdA+vOpORxn3Z8L1uE6vLOiYioufyk+u0OZFWOPF28JuwCKM7yOOnXk5/Y6X+LfW2k2Jnw7UsDd41iSQ5EUjJrbPiDdooUdVylKv6acxNFd0pZdiTUZDKsuokmOXc8ndSZ+omuY8Pnif0oQ//Mzd53/qaixz9agcj6HopLl/IIEmSMf+KTzYWClgIplUc68POTRddZdr9R0uJB7ADVhAVx8UzZhEj0+x6euFjgrTD5wy1ZGtMpvLJtvHpMHNMg0mXbwCZToudtr6xSVa1E51fnCppnaA8tb2j17sSflitwSymj4NDfsLhw/mCDek+WPkXiikY3ar3tH0kUMBynorkKwHnnOWeB/U24cbtrbZzarHj4brTyGlbAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAACMlyWPTiSJ8bs9ECkUjg2DC1oTmdr/EIQEjnvY2+n4WQabiFf+q4GE+2h/Y0YYwDXaxDncGus7VZig8AAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQrxw0MhiMo6Y1E1oToYlRrOvSnmrC2uZ//bBtdBbnMoDBTe/IJexnaUJQgYu2VAZfQpjTFW1XG01PgJDBjpqGOJC6ZE/h9VqhnxHNLS7BTTIztuCkvq7vcraYWOIeFw1kUJtvSdN61HR5CSPKiGJMYwRl5NH01w56WjlyyVs2Mi/4ODgYuo+ijDzTttXpP5+rjwl5vDchWsxbJGh3uow8nlSnCVKIOfYcC5uGB5iRwTkhbkenG2L7c77HIWlFh0Xm1lkEO6pSoYG1hEI56NVC0f3tzWgc5+cdB1KdrJbiatQSRuzH14/oHkF3OkaWVBmTeSOgdkR5ffbz61FEJgEMsMNf+pBVqOVo2o97wHVhUnTPHJLKQfQACcUWqkFMJ8cM0FVHWEPtifKtl6Hc4Vb+jAtwo68/cMMNi5Lp5fMAq2Cg0ABQKg9wMADQAJAxAnAAAAAAAADgYAAgAPEBEAEAMAAhIMAgAAAEZ4v8kBAAAAEQECARETFQMAFA8VAgQFBhYHEREQDhcTCBgZGhgz5oWkAX+DrUZ4v8kBAAAA9HFQJGIFAAARAwIAAAEJDgYBCQEPEBEAExcDARQPFQkKBQYWBxEREA4XEwgYCwwZGhlmBj0SAdrr6gfhM/4BAAAA7aKejigGAAAAEQMJAQEBCQCJEwAAAAAAAA9mmuSO/pn1vVCwatBduahHAtWqOYsQckX7LVHGUWxEAQAAAAAAAAAB/08+MAvbV+UW3ONET6GCATN/0j6lr+zFAgtoUs3O9iGKsATjGupdFAfOuIz22ZUo9EhUYulerniPErJf1GwvDoABAAYJKqrFxl/dG3sl/ET53EdOUY7GkME7PDLzYjrnjXECoJwAPKMtldCc34a+8OJeXzB6SLmvDZnGJ1l8UI6v0zK583Jby+7Nj0f6IJgK9M/rtAyoL5FBP7e2TBAbNB7nNuodAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAxKoBKOldPhYiqlAcWFqesHs3NUwQjqC3kbRW3H7qM2DLf6u1L3pki7WzF9mgGLkFfLAkd0+v4B5sTfmMw4WIFTtjzvYd5D6GsWgxoFbrya06c75FbKVGasJZ2fGjzorLrhu5lE80vnhcE09VCUTOaWQ+aAp+uNUW/dgm/9Ojhie4glXf1oNciuf82NedY9xhuipRtE8GnjHbli1VNH6M4DBAAFAtncAAAEAAkDECcAAAAAAAAFBwAGBwgCAQOMAxwJXZZWmbxzVQAAAAApmslIdCp5nSehZJx2A1smV3rQ62WFpa4qaR0x8u6QxAAAAAACJM7+AAAAAAAA7MX////4AAAAAGlkfEEAAAAAaWR8QQAAAAACIRozAAAAAAAA5KoNAAAAEvs0gfitwlofdBEVF7Lt1lfN70g1GT3CdkPWiSgqNbDQbHzLR5E42IQjGe03az8gQegzC6d37HZlRZH/kJ22CZ0PM0dacJgXOMnuEsFIK7SXMgVDanpmZUtCy9Um74f1qdtNvu5ibFkPaKBjL/s5aWnhuQGYaoic/GYMRcWCbqjzKjyADMqIu8pvFtCR+xAqy1nWR5QFDyhatHN2WV6zW4I2Fz1ig+BjCNI8dzE8/PFRksFWEAEL5qLbcIJvDFxsjFOfxytrVXBYzNRn8eCuCKRC0cXFZpG/RtDkDWZgYWgr/Q+Msprylig2/NajTl1TNn1PbPqLAdEngizvXgzUjPlSEw0AAAApmslIdCp5nSehZJx2A1smV3rQ62WFpa4qaR0x8u6QxADpAwAAAAAAAEhcAJ18XU/9Gt8vnjmOC6hTZ4ZKZvmM+NF5mrgM9ffWAQAAAAAAAAABTL5A15gYd/UMJbcGOV2Ct6S7FSBoc2n66Zz6KL0UjdXW7A+zKwz9NgFLbipLVLeGe/p0Cckl9o1dbZ2kvqWUA4ABAAILTwBflcdWwqKPHy3t7aWLaS7OOgzcBUjNyNGjoAmrjJG2bDJOP9v/YR1WdvZFYzgoQx7dSp0XSn+/xVTHMZPeDakBaM20dllxS9FbwoHcAl4+5ayVmW750JcePad73nI5J3BukDE6OqL6buYdDLNet1XTPDVz9z/H4eW0SJdrUaY4roH+3oSwXKMJgUdMj2FNEXn0H06SonFdsNHP5afvsxOaXm74YlA/OiRkxhuwQ9M6IZwPF6HMfUX9J60KWzvUGU8ganVs499OZpNFuQK66ByjevaCa5CLSOWrX6xvXZKuQZuW9cEpZoJ6C0vX00Ct0mcs+8qvF5mFqLoYAvuXK407KiAcJcCKo34WRtNyN6bwxCCPVBnpPuz8JujdjnUOAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAADb8e/+4cdOK0o6VEHrgrX2LVzaqsoFoWn3PrAFnklFslLjHbkCnl1jpNNz1kAX0wyy369qIOkA9xcwzz2YZmryAwkABQJg2AMACQAJAx8CAAAAAAAACjUACwwNDissLS4vDxAwMRESExUBFhcYGTMUMhobHB0CAx4EHyAhIjQUMiMkJSYFBicoKSoHCFUKFAEAAAADAdgJBgIRExoGAgIAAA4DAgAABgIgIikGAgIAAA4DAgAABgIgIi8GAgIAAA4DAgAAAAUGBAIHCQYDBQYABQIEBwEBBwYHBwIIBwUGCAMHAzz5ttVrusK5h7tt/2RTSLpqafgA6KGY3eCUIL45wcZFCgKSA7ClqAoJr6YIBAUIBwa1tgtenKHzNpGxKsf+ZGPnHLf6WVam69e0+R+9WtPI9xzJZgnX2drb3N7f4OEB3W6sDIXL5UyNDw/eNdnWLD6HO2QfptI9yWG18bDGQ8YqDVFTVFVWWFlaW5KTlJUBV6oJAAAAAAAAAiT+oNqZdb0ugGzuxs1/Ak4VhBDLYWiUBIZQ51J7H2kAAAAAAAAAAA==", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, _ := base64.StdEncoding.DecodeString(tt.data) + err := entriesToVersionedTransaction(0, bytes.NewReader(data), func(versioned VersionedTransaction) {}) + if err != nil { + t.Errorf("decodeEntry() error = %v", err) + return + } + }) + } +} diff --git a/pkg/shreder/program_azcz.go b/pkg/shreder/program_azcz.go new file mode 100644 index 0000000..a5d7435 --- /dev/null +++ b/pkg/shreder/program_azcz.go @@ -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 +} diff --git a/pkg/shreder/program_bloomrouter.go b/pkg/shreder/program_bloomrouter.go new file mode 100644 index 0000000..8580c15 --- /dev/null +++ b/pkg/shreder/program_bloomrouter.go @@ -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 +} diff --git a/pkg/shreder/program_bobo.go b/pkg/shreder/program_bobo.go new file mode 100644 index 0000000..f041a09 --- /dev/null +++ b/pkg/shreder/program_bobo.go @@ -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 +} diff --git a/pkg/shreder/program_bonk.go b/pkg/shreder/program_bonk.go new file mode 100644 index 0000000..5e38c72 --- /dev/null +++ b/pkg/shreder/program_bonk.go @@ -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 + } +} diff --git a/pkg/shreder/dflow.go b/pkg/shreder/program_dflow.go similarity index 92% rename from pkg/shreder/dflow.go rename to pkg/shreder/program_dflow.go index 834d8a1..962aef0 100644 --- a/pkg/shreder/dflow.go +++ b/pkg/shreder/program_dflow.go @@ -232,9 +232,9 @@ func decodeSwap2Params(data []byte) (*dflowSwapParams, error) { return out, nil } -func findDflowPumpAmmMints(staticKeys []solana.PublicKey, accounts []uint8) (solana.PublicKey, solana.PublicKey, bool, error) { +func findDflowPumpAmmMints(tx VersionedTransaction, accounts []uint8) (solana.PublicKey, solana.PublicKey, bool, error) { for i, acctIdx := range accounts { - key, err := getStaticKey(staticKeys, int(acctIdx)) + key, err := tx.GetAccount(int(acctIdx)) if err != nil { return solana.PublicKey{}, solana.PublicKey{}, false, err } @@ -246,11 +246,11 @@ func findDflowPumpAmmMints(staticKeys []solana.PublicKey, accounts []uint8) (sol if baseIdx >= len(accounts) || quoteIdx >= len(accounts) { return solana.PublicKey{}, solana.PublicKey{}, false, nil } - baseMint, err := getStaticKey(staticKeys, int(accounts[baseIdx])) + baseMint, err := tx.GetAccount(int(accounts[baseIdx])) if err != nil { return solana.PublicKey{}, solana.PublicKey{}, false, err } - quoteMint, err := getStaticKey(staticKeys, int(accounts[quoteIdx])) + quoteMint, err := tx.GetAccount(int(accounts[quoteIdx])) if err != nil { return solana.PublicKey{}, solana.PublicKey{}, false, err } @@ -259,12 +259,11 @@ func findDflowPumpAmmMints(staticKeys []solana.PublicKey, accounts []uint8) (sol return solana.PublicKey{}, solana.PublicKey{}, false, nil } -func parseDFlowInstruction(tx *versionedTransaction, instructionIndex int) (TxSignalBatch, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { +func parseDFlowInstruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) { + if instructionIndex >= len(tx.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } - ix := msg.Instructions[instructionIndex] + ix := tx.Instructions[instructionIndex] if len(ix.Data) < 8 { return nil, nil } @@ -321,7 +320,7 @@ func parseDFlowInstruction(tx *versionedTransaction, instructionIndex int) (TxSi isBuy = true amt = pumpAmmBuy } - baseMint, quoteMint, ok, err := findDflowPumpAmmMints(tx.Message.StaticAccountKeys, ix.Accounts) + baseMint, quoteMint, ok, err := findDflowPumpAmmMints(tx, ix.Accounts) if err != nil { return nil, err } @@ -343,7 +342,7 @@ func parseDFlowInstruction(tx *versionedTransaction, instructionIndex int) (TxSi } out = append(out, &TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Program: "PumpAMM", Event: event, Token0Address: baseMint.String(), @@ -366,7 +365,7 @@ func parseDFlowInstruction(tx *versionedTransaction, instructionIndex int) (TxSi isBuy = true amt = pumpBuy } - mint, ok, err := findPumpFunMint(tx.Message.StaticAccountKeys, ix.Accounts) + mint, ok, err := findPumpFunMint(tx, ix.Accounts) if err != nil { return nil, err } @@ -388,7 +387,7 @@ func parseDFlowInstruction(tx *versionedTransaction, instructionIndex int) (TxSi } out = append(out, &TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Program: "Pump", Event: event, Token0Address: mint.String(), diff --git a/pkg/shreder/program_dlmm.go b/pkg/shreder/program_dlmm.go new file mode 100644 index 0000000..7b1238b --- /dev/null +++ b/pkg/shreder/program_dlmm.go @@ -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 +} diff --git a/pkg/shreder/program_f5tf.go b/pkg/shreder/program_f5tf.go new file mode 100644 index 0000000..b128ee8 --- /dev/null +++ b/pkg/shreder/program_f5tf.go @@ -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 +} diff --git a/pkg/shreder/program_fjsz.go b/pkg/shreder/program_fjsz.go new file mode 100644 index 0000000..1f10d32 --- /dev/null +++ b/pkg/shreder/program_fjsz.go @@ -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 +} diff --git a/pkg/shreder/program_flas.go b/pkg/shreder/program_flas.go new file mode 100644 index 0000000..503481f --- /dev/null +++ b/pkg/shreder/program_flas.go @@ -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 +} diff --git a/pkg/shreder/program_gmgn.go b/pkg/shreder/program_gmgn.go new file mode 100644 index 0000000..c9d4c9b --- /dev/null +++ b/pkg/shreder/program_gmgn.go @@ -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 +} diff --git a/pkg/shreder/juptierv6.go b/pkg/shreder/program_juptierv6.go similarity index 92% rename from pkg/shreder/juptierv6.go rename to pkg/shreder/program_juptierv6.go index 72b283d..f31df78 100644 --- a/pkg/shreder/juptierv6.go +++ b/pkg/shreder/program_juptierv6.go @@ -11,6 +11,8 @@ import ( ) var ( + jupiterV6ProgramID = solana.MustPublicKeyFromBase58("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4") + jupiterRouteV2 = []byte{187, 100, 250, 204, 49, 196, 175, 20} jupiterExactOutRouteV2 = []byte{157, 138, 184, 82, 21, 244, 243, 36} @@ -1098,7 +1100,7 @@ func pumpRoutePlanStatsV2(in uint64, out uint64, plan []RoutePlanStepV2, include return inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC } -func parseJupiterPumpAmmRoute(tx *versionedTransaction, instruction compiledInstruction, in uint64, out uint64, plan []RoutePlanStep) (*TxSignal, bool, error) { +func parseJupiterPumpAmmRoute(tx VersionedTransaction, instruction Instructions, in uint64, out uint64, plan []RoutePlanStep) (*TxSignal, bool, error) { var ( isBuy bool isSell bool @@ -1126,7 +1128,7 @@ func parseJupiterPumpAmmRoute(tx *versionedTransaction, instruction compiledInst if len(instruction.Accounts) < 14 { return nil, true, nil } - token0Key, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[13])) + token0Key, err := tx.GetAccount(int(instruction.Accounts[13])) if err != nil { return nil, true, err } @@ -1137,7 +1139,7 @@ func parseJupiterPumpAmmRoute(tx *versionedTransaction, instruction compiledInst } return &TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Token0Address: token0Key.String(), Token1Address: wsolMint, Token0Amount: token0Amount, @@ -1155,7 +1157,7 @@ func parseJupiterPumpAmmRoute(tx *versionedTransaction, instruction compiledInst if len(instruction.Accounts) < 15 { return nil, true, nil } - wsolKey, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[14])) + wsolKey, err := tx.GetAccount(int(instruction.Accounts[14])) if err != nil { return nil, true, err } @@ -1172,7 +1174,7 @@ func parseJupiterPumpAmmRoute(tx *versionedTransaction, instruction compiledInst } return &TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Token0Address: token0Key.String(), Token1Address: wsolMint, Token0Amount: token0Amount, @@ -1188,9 +1190,9 @@ func parseJupiterPumpAmmRoute(tx *versionedTransaction, instruction compiledInst }, true, nil } -func findPumpFunMint(staticKeys []solana.PublicKey, accounts []uint8) (solana.PublicKey, bool, error) { +func findPumpFunMint(tx VersionedTransaction, accounts []uint8) (solana.PublicKey, bool, error) { for i, acctIdx := range accounts { - key, err := getStaticKey(staticKeys, int(acctIdx)) + key, err := tx.GetAccount(int(acctIdx)) if err != nil { return solana.PublicKey{}, false, err } @@ -1200,7 +1202,7 @@ func findPumpFunMint(staticKeys []solana.PublicKey, accounts []uint8) (solana.Pu if i+3 >= len(accounts) { return solana.PublicKey{}, false, nil } - mint, err := getStaticKey(staticKeys, int(accounts[i+3])) + mint, err := tx.GetAccount(int(accounts[i+3])) if err != nil { return solana.PublicKey{}, false, err } @@ -1209,7 +1211,7 @@ func findPumpFunMint(staticKeys []solana.PublicKey, accounts []uint8) (solana.Pu return solana.PublicKey{}, false, nil } -func jupiterV6SourceDestMints(msg versionedMessage, instruction compiledInstruction, disc []byte) (solana.PublicKey, solana.PublicKey, bool, error) { +func jupiterV6SourceDestMints(msg VersionedTransaction, instruction Instructions, disc []byte) (solana.PublicKey, solana.PublicKey, bool, error) { switch { case bytes.Equal(disc, jupiterRouteV2), bytes.Equal(disc, jupiterSharedAccountsRouteV2), @@ -1218,11 +1220,11 @@ func jupiterV6SourceDestMints(msg versionedMessage, instruction compiledInstruct if len(instruction.Accounts) < 5 { return solana.PublicKey{}, solana.PublicKey{}, false, fmt.Errorf("not enough accounts for jupiter v6 v2 instruction") } - src, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[3])) + src, err := msg.GetAccount(int(instruction.Accounts[3])) if err != nil { return solana.PublicKey{}, solana.PublicKey{}, false, err } - dst, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[4])) + dst, err := msg.GetAccount(int(instruction.Accounts[4])) if err != nil { return solana.PublicKey{}, solana.PublicKey{}, false, err } @@ -1232,11 +1234,11 @@ func jupiterV6SourceDestMints(msg versionedMessage, instruction compiledInstruct if len(instruction.Accounts) < 9 { return solana.PublicKey{}, solana.PublicKey{}, false, fmt.Errorf("not enough accounts for jupiter v6 shared accounts instruction") } - src, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[7])) + src, err := msg.GetAccount(int(instruction.Accounts[7])) if err != nil { return solana.PublicKey{}, solana.PublicKey{}, false, err } - dst, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[8])) + dst, err := msg.GetAccount(int(instruction.Accounts[8])) if err != nil { return solana.PublicKey{}, solana.PublicKey{}, false, err } @@ -1247,13 +1249,12 @@ func jupiterV6SourceDestMints(msg versionedMessage, instruction compiledInstruct } // only decodes inputIdx = 0 container pumpSwap instructions for now -func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { +func parseJupiterV6Instruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) { + if instructionIndex >= len(tx.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } - instruction := msg.Instructions[instructionIndex] + instruction := tx.Instructions[instructionIndex] if len(instruction.Data) == 0 { return nil, fmt.Errorf("data is empty") } @@ -1326,7 +1327,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, err } if handled { - return sig, nil + return TxSignalBatch{sig}, nil } inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStats(args.In, args.QuotedOut, args.Plan, true) routeIn = args.In @@ -1356,19 +1357,19 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if len(instruction.Accounts) < 13 { return nil, nil } - destMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[5])) + destMint, err := tx.GetAccount(int(instruction.Accounts[5])) if err != nil { return nil, err } if isToken1Mint(destMint) { - pumpKey, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[9])) + pumpKey, err := tx.GetAccount(int(instruction.Accounts[9])) if err != nil { return nil, err } if !pumpKey.Equals(pumpProgramID) { return nil, nil } - token0Mint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[12])) + token0Mint, err := tx.GetAccount(int(instruction.Accounts[12])) if err != nil { return nil, err } @@ -1376,9 +1377,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if routeIn > 0 { token0Amount = formatTokenAmount(routeIn) } - return &TxSignal{ + return TxSignalBatch{&TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Token0Address: token0Mint.String(), Token1Address: destMint.String(), Token0Amount: token0Amount, @@ -1391,15 +1392,15 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( Block: tx.Block, Token0AmountUint64: routeIn, Token1AmountUint64: 0, - }, nil + }}, nil } token0Amount := decimal.Zero if routeOut > 0 { token0Amount = formatTokenAmount(routeOut) } - return &TxSignal{ + return TxSignalBatch{&TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Token0Address: destMint.String(), Token1Address: wsolMint, Token0Amount: token0Amount, @@ -1412,13 +1413,13 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( Block: tx.Block, Token0AmountUint64: routeOut, Token1AmountUint64: 0, - }, nil + }}, nil } if wrappedCnt > 1 { logger.Warn("pumpWrapped at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", wrappedCnt) } if wrapped.InAmount > 0 { - mint, ok, err := findPumpFunMint(tx.Message.StaticAccountKeys, instruction.Accounts) + mint, ok, err := findPumpFunMint(tx, instruction.Accounts) if err != nil { return nil, err } @@ -1427,7 +1428,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } token1Mint := solana.WrappedSol token1IsStable := false - srcMint, dstMint, ok, err := jupiterV6SourceDestMints(tx.Message, instruction, disc) + srcMint, dstMint, ok, err := jupiterV6SourceDestMints(tx, instruction, disc) if err != nil { return nil, err } @@ -1482,9 +1483,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if token1IsStable { token1Address = token1Mint.String() } - return &TxSignal{ + return TxSignalBatch{&TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Token0Address: mint.String(), Token1Address: token1Address, Token0Amount: token0Amount, @@ -1497,13 +1498,13 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( Block: tx.Block, Token0AmountUint64: token0AmountUint64, Token1AmountUint64: token1AmountUint64, - }, nil + }}, nil } if wrappedAnyC > 1 { logger.Warn("pumpWrapped at inputIdx!=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", wrappedAnyC) } if wrappedAnyC == 1 && routeIn > 0 && routeOut > 0 { - mint, ok, err := findPumpFunMint(tx.Message.StaticAccountKeys, instruction.Accounts) + mint, ok, err := findPumpFunMint(tx, instruction.Accounts) if err != nil { return nil, err } @@ -1512,7 +1513,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } token1Mint := solana.WrappedSol token1IsStable := false - srcMint, dstMint, ok, err := jupiterV6SourceDestMints(tx.Message, instruction, disc) + srcMint, dstMint, ok, err := jupiterV6SourceDestMints(tx, instruction, disc) if err != nil { return nil, err } @@ -1567,9 +1568,9 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if token1IsStable { token1Address = token1Mint.String() } - return &TxSignal{ + return TxSignalBatch{&TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Token0Address: mint.String(), Token1Address: token1Address, Token0Amount: token0Amount, @@ -1582,7 +1583,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( Block: tx.Block, Token0AmountUint64: token0AmountUint64, Token1AmountUint64: token1AmountUint64, - }, nil + }}, nil } if planCount > 1 { // multiple pumpSwapSell at inputIdx=0? should not happen @@ -1618,11 +1619,11 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if len(instruction.Accounts) < 6 { return nil, fmt.Errorf("not enough accounts for jupiter v6 v2 instruction") } - sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[3])) + sourceMint, err = tx.GetAccount(int(instruction.Accounts[3])) if err != nil { return nil, err } - destMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[4])) + destMint, err = tx.GetAccount(int(instruction.Accounts[4])) if err != nil { return nil, err } @@ -1637,7 +1638,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } accounts := instruction.Accounts[8:] for i, acctIdx := range accounts { - key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx)) + key, err := tx.GetAccount(int(acctIdx)) if err != nil { return nil, err } @@ -1650,11 +1651,11 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, nil } - baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) + baseMint, err = tx.GetAccount(int(accounts[srcIdx])) if err != nil { return nil, err } - quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) + quoteMint, err = tx.GetAccount(int(accounts[srcIdx+1])) if err != nil { return nil, err } @@ -1666,11 +1667,11 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if len(instruction.Accounts) < 12 { return nil, fmt.Errorf("not enough accounts for jupiter v6 jupiterSharedAccountsRoute instruction") } - sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[7])) + sourceMint, err = tx.GetAccount(int(instruction.Accounts[7])) if err != nil { return nil, err } - destMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[8])) + destMint, err = tx.GetAccount(int(instruction.Accounts[8])) if err != nil { return nil, err } @@ -1684,7 +1685,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( } accounts := instruction.Accounts[11:] for i, acctIdx := range accounts { - key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx)) + key, err := tx.GetAccount(int(acctIdx)) if err != nil { return nil, err } @@ -1697,12 +1698,12 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( return nil, nil } - baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) + baseMint, err = tx.GetAccount(int(accounts[srcIdx])) if err != nil { return nil, err } - quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) + quoteMint, err = tx.GetAccount(int(accounts[srcIdx+1])) if err != nil { return nil, err } @@ -1719,7 +1720,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( accounts := instruction.Accounts[9:] for i, acctIdx := range accounts { - key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx)) + key, err := tx.GetAccount(int(acctIdx)) if err != nil { return nil, err } @@ -1731,12 +1732,12 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) { return nil, nil } - baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) + baseMint, err = tx.GetAccount(int(accounts[srcIdx])) if err != nil { return nil, err } - quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) + quoteMint, err = tx.GetAccount(int(accounts[srcIdx+1])) if err != nil { return nil, err } @@ -1784,7 +1785,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( signal := &TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Token0Address: baseMint.String(), Token1Address: wsolMint, Token0Amount: token0Amount, @@ -1799,7 +1800,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) ( Token1AmountUint64: token1AmountUint64, } - return signal, nil + return TxSignalBatch{signal}, nil } // keep lints happy if solana-go isn't referenced elsewhere in this file for build tags diff --git a/pkg/shreder/juptierv6_test.go b/pkg/shreder/program_juptierv6_test.go similarity index 100% rename from pkg/shreder/juptierv6_test.go rename to pkg/shreder/program_juptierv6_test.go diff --git a/pkg/shreder/okxonchainlab.go b/pkg/shreder/program_okxonchainlab.go similarity index 93% rename from pkg/shreder/okxonchainlab.go rename to pkg/shreder/program_okxonchainlab.go index b1c546b..2f5ddc7 100644 --- a/pkg/shreder/okxonchainlab.go +++ b/pkg/shreder/program_okxonchainlab.go @@ -245,12 +245,11 @@ type OkxV2SwapScorch struct { Id [16]byte } -func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { +func parseOkxDexRouteV2Instruction(tx VersionedTransaction, instructionIndex int) (TxSignalBatch, error) { + if instructionIndex >= len(tx.Instructions) { return nil, fmt.Errorf("instruction index out of bounds") } - ix := msg.Instructions[instructionIndex] + ix := tx.Instructions[instructionIndex] if len(ix.Data) < 8 { return nil, nil } @@ -310,7 +309,7 @@ func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex in return nil, nil } - srcMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(ix.Accounts[3])) + srcMint, err := tx.GetAccount(int(ix.Accounts[3])) var ( srcIdx uint8 @@ -320,7 +319,7 @@ func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex in } accounts := ix.Accounts[14:] for i, acctIdx := range accounts { - key, err := getStaticKey(tx.Message.StaticAccountKeys, int(acctIdx)) + key, err := tx.GetAccount(int(acctIdx)) if err != nil { return nil, err } @@ -333,7 +332,7 @@ func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex in return nil, nil } - baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) + baseMint, err := tx.GetAccount(int(accounts[srcIdx])) if err != nil { return nil, err } @@ -341,7 +340,7 @@ func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex in return nil, nil } - quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) + quoteMint, err := tx.GetAccount(int(accounts[srcIdx+1])) if err != nil { return nil, err } @@ -349,9 +348,9 @@ func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex in return nil, nil } - return &TxSignal{ + return TxSignalBatch{&TxSignal{ TxHash: tx.Signatures[0].String(), - Maker: tx.Message.StaticAccountKeys[0].String(), + Maker: tx.StaticAccountKeys[0].String(), Token0Address: baseMint.String(), Token1Address: wsolMint, Token0Amount: formatTokenAmount(inputAmount), @@ -364,5 +363,5 @@ func parseOkxDexRouteV2Instruction(tx *versionedTransaction, instructionIndex in ExactSOL: false, Token0AmountUint64: inputAmount, Token1AmountUint64: 0, - }, nil + }}, nil } diff --git a/pkg/shreder/program_photon.go b/pkg/shreder/program_photon.go new file mode 100644 index 0000000..9b32eb1 --- /dev/null +++ b/pkg/shreder/program_photon.go @@ -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 +} diff --git a/pkg/shreder/program_pump.go b/pkg/shreder/program_pump.go new file mode 100644 index 0000000..580b256 --- /dev/null +++ b/pkg/shreder/program_pump.go @@ -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 +} diff --git a/pkg/shreder/program_pumpamm.go b/pkg/shreder/program_pumpamm.go new file mode 100644 index 0000000..99fa7e1 --- /dev/null +++ b/pkg/shreder/program_pumpamm.go @@ -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 +} diff --git a/pkg/shreder/program_qtkv.go b/pkg/shreder/program_qtkv.go new file mode 100644 index 0000000..3598e5b --- /dev/null +++ b/pkg/shreder/program_qtkv.go @@ -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 +} diff --git a/pkg/shreder/program_term.go b/pkg/shreder/program_term.go new file mode 100644 index 0000000..180120f --- /dev/null +++ b/pkg/shreder/program_term.go @@ -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 +} diff --git a/pkg/shreder/tx.go b/pkg/shreder/tx.go index f43340a..571eb58 100644 --- a/pkg/shreder/tx.go +++ b/pkg/shreder/tx.go @@ -48,7 +48,6 @@ type TxSignal struct { 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"` @@ -63,10 +62,4 @@ type TxSignal struct { 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 diff --git a/pkg/shreder/txparser.go b/pkg/shreder/txparser.go index c7b3f9c..726a800 100644 --- a/pkg/shreder/txparser.go +++ b/pkg/shreder/txparser.go @@ -2,16 +2,14 @@ package shreder import ( "bytes" - "encoding/binary" + "context" "fmt" + "io" "math/big" + "slices" "strings" - "sync" - "time" "github.com/gagliardetto/solana-go" - "github.com/mr-tron/base58" - "github.com/near/borsh-go" "github.com/shopspring/decimal" ) @@ -20,446 +18,202 @@ const ( tokenProgram = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ) -// program ids -var ( - pumpProgramID = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") - // has no sell function with pump and pump.amm program - azczProgramID = solana.MustPublicKeyFromBase58("AzcZqCRUQgKEg5FTAgY7JacATABEYCEfMbjXEzspLYFB") - - // only buy function with pump program - f5tfProgramID = solana.MustPublicKeyFromBase58("F5tfvbLog9VdGUPqBDTT8rgXvTTcq7e5UiGnupL1zvBq") - // only pump.fun function - photonProgramID = solana.MustPublicKeyFromBase58("BSfD6SHZigAfDWSjzD5Q41jw8LmKwtmjskPH9XW1mrRW") - - pumpAmmProgramID = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") - - boboProgramID = solana.MustPublicKeyFromBase58("BobogA5N2KN2GG4XN3E3rNNRw3L8H1QPXp7QLxGrNHGM") - - qtkvProgramID = solana.MustPublicKeyFromBase58("qtkvapJEvRWWrB7i5K6RaA1kvq5x3qmMKZ98ad71XQ7") - - // only buy function with pump program - fjszProgramID = solana.MustPublicKeyFromBase58("FJsZbftBqRLfF7uqUKpm4s2goDr6xsQ5Q3mN7AFJB6hK") - - flasProgramID = solana.MustPublicKeyFromBase58("FLASHX8DrLbgeR8FcfNV1F5krxYcYMUdBkrP1EPBtxB9") - - terminalProgramID = solana.MustPublicKeyFromBase58("term9YPb9mzAsABaqN71A4xdbxHmpBNZavpBiQKZzN3") - - jupiterV6ProgramID = solana.MustPublicKeyFromBase58("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4") - - gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb") - - bonkProgramID = solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD") - - bloomRouterProgramID = solana.MustPublicKeyFromBase58("b1oomGGqPKGD6errbyfbVMBuzSC8WtAAYo8MwNafWW1") - - // For Metaora dlmm - dlmmProgramID = solana.MustPublicKeyFromBase58("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo") -) - -type AccountNotFoundError struct { - Index int - Len int +type Handler struct { + Func func(tx VersionedTransaction, idx int) (TxSignalBatch, error) + Label string } -func NewAccountNotFoundError(i, l int) error { - return &AccountNotFoundError{i, l} +type FillAccount interface { + FillAccount(account solana.PublicKey) } -func (e AccountNotFoundError) Error() string { - return fmt.Sprintf("account index %d out of range, len=%d", e.Index, e.Len) -} - -// instruction discriminators -var ( - pumpCreateCoinIX = []byte{24, 30, 200, 40, 5, 28, 7, 119} - 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} - - azczBuyTokensIX = []byte{11} - azczAmmBuyTokensIX = []byte{0xf} - - f5tfBuyTokensIX = []byte{0} - - flasBuyTokensIX = []byte{0x00, 0x1, 0x4} - flasSellTokensIX = []byte{0x01, 0x1, 0x3} - flasAmmBuyTokensIX = []byte{0x00, 0x2, 0x2} - flasAmmSellTokensIX = []byte{0x01, 0x2, 0x2} - - 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} - - qtkvBuyTokensIX = []byte{0x02} - qtkvSellTokensIX = []byte{0x03} - qtkvAmmSellTokensIX = []byte{0x05} - - boboBuyPumpTokensIX = []byte{0xff, 0xe7, 0x11, 0x53, 0x15, 0xc5, 0xc3, 0xdf} - fjszBuyTokensIX = []byte{0xe7, 0x3f, 0x99, 0x83, 0xf3, 0xed, 0xe3, 0x3c} - photonBuyPumpTokensIX = []byte{0x52, 0xe1, 0x77, 0xe7, 0x4e, 0x1d, 0x2d, 0x46} - photonSwapPumpAmmIX = []byte{0x2c, 0x77, 0xaf, 0xda, 0xc7, 0x4d, 0xc4, 0xeb} - - terminalBuyTokensIX = []byte{0xa6, 0x54, 0x14, 0x96, 0x9f, 0x77, 0x59, 0xca} - terminalSellTokensIX = []byte{0xbe, 0x84, 0xa2, 0x96, 0x93, 0x7c, 0xf8, 0x6b} - terminalAmmSellTokensIX = []byte{0x40, 0x64, 0x97, 0xb9, 0x16, 0xfa, 0xec, 0xb1} - - gmgnBuyTokensIX = []byte{0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea} - - bonkBuyAndSellTokensIX = []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a} - - 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 compiledInstruction struct { - ProgramIDIndex uint8 - Accounts []uint8 - Data []byte -} - -type addressTableLookup struct { - AccountKey solana.PublicKey - WritableIndexes []uint8 - ReadonlyIndexes []uint8 -} - -type versionedMessage struct { - StaticAccountKeys []solana.PublicKey - Instructions []compiledInstruction - AddressTableLookups []addressTableLookup -} - -type versionedTransaction struct { - Signatures []solana.Signature - Message versionedMessage - Block uint64 - Time time.Time -} - -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 -} - -type azczBuyArgs struct { - SolAmount uint64 - TokenAmount uint64 -} - -type f5tfBuyArgs struct { - SolAmount uint64 - TokenAmount uint64 -} - -type flasArgs struct { - Amount1 uint64 - Amount2 uint64 - Placeholder [3]uint8 -} - -type photonBuyPumpArgs struct { - Timestamp uint64 - SolAmount uint64 - TokenAmount uint64 - Fee uint64 -} - -type photonSwapPumpAmmArgs struct { - FromAmount uint64 - ToAmount uint64 -} - -type bloomRouterArgs struct { - Side uint16 - SolAmount uint64 - TokenAmount uint64 -} - -type pumpAmmBuyArgs struct { - Amount uint64 - MaxSolCost uint64 -} - -type boboBuyArgs struct { - Placeholder1 uint64 - Placeholder2 uint64 - SolAmount uint64 - Placeholder3 uint64 - Placeholder4 uint64 - Placeholder5 uint64 - Placeholder6 uint64 -} - -type qtkvBuyArgs struct { - Placeholder uint64 - TokenNumber uint64 - SolAmount uint64 -} - -type fjszBuyArgs struct { - SolAmount uint64 - TokenAmount uint64 -} - -var ( - versionedPool = sync.Pool{} - - accIdxPool = sync.Pool{} -) - -func requireAccIdxSlice() []uint8 { - v := accIdxPool.Get() - if v == nil { - return make([]uint8, 0, 16) +func init() { + for account := range parsedMap { + parseProgram = append(parseProgram, account) } - return v.([]uint8) + //"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR", //Event Authority + //"5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx", // Fee Config + //"pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ", // pump fee program + parseProgram = append(parseProgram, + solana.MustPublicKeyFromBase58("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR"), + solana.MustPublicKeyFromBase58("5PHirr8joyTMp9JMm6nW7hNDVyEYdkzDqazxPD7RaTjx"), + solana.MustPublicKeyFromBase58("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"), + ) + slices.SortFunc(parseProgram, func(a, b solana.PublicKey) int { + return bytes.Compare(a[:], b[:]) + }) } -func releaseAccIdxSlice(s []uint8) { - if s == nil { +var ( + parseProgram []solana.PublicKey + + parsedMap = map[solana.PublicKey]Handler{ + pumpProgramID: {parsePumpInstruction, "pump"}, + azczProgramID: {parseAzczInstruction, "azcz"}, + f5tfProgramID: {parseF5tfInstruction, "f5tf"}, + flasProgramID: {parseFlasInstruction, "flas"}, + photonProgramID: {parsePhotonInstruction, "photon"}, + pumpAmmProgramID: {parsePumpAmmInstruction, "pumpamm"}, + boboProgramID: {parseBoboInstruction, "bobo"}, + qtkvProgramID: {parseQtkvInstruction, "qtkv"}, + fjszProgramID: {parseFjszInstruction, "fjsz"}, + terminalProgramID: {parseTermInstruction, "terminal"}, + jupiterV6ProgramID: {parseJupiterV6Instruction, "jupiterv6"}, + okxDexRouteV2ProgramID: {parseOkxDexRouteV2Instruction, "okxdexroutev2"}, + dflowProgramID: {parseDFlowInstruction, "dflow"}, + gmgnProgramID: {parseGMGNInstruction, "gmgn"}, + bonkProgramID: {parseBonkInstruction, "bonk"}, + bloomRouterProgramID: {parseBloomRouterInstruction, "bloomrouter"}, + dlmmProgramID: {parseDlmmInstruction, "dlmm"}, + } +) + +func ParseTransactionForSubscribe(ctx context.Context, update *SubscribeUpdateTransaction, loader *AddressTables, parsed chan<- TxSignal, done chan<- struct{}) { + versioned, err := toVersionedTransaction(update) + if err != nil { + logger.Debug("txparser: failed to convert to versioned transaction", "error", err) + close(done) return } - s = s[:0] - accIdxPool.Put(s) + ParseTransaction(ctx, versioned, loader, parsed) + close(done) } -func requireVersionedPool() *versionedTransaction { - v := versionedPool.Get() - if v == nil { - return &versionedTransaction{ - Signatures: make([]solana.Signature, 0, 10), - Message: versionedMessage{ - StaticAccountKeys: make([]solana.PublicKey, 0, 256), - Instructions: make([]compiledInstruction, 0, 16), - AddressTableLookups: make([]addressTableLookup, 0, 10), - }, +var VoteProgram = solana.MustPublicKeyFromBase58("Vote111111111111111111111111111111111111111") + +func FilterTransactionForEntries(versioned VersionedTransaction) bool { + if len(versioned.Instructions) >= 1 { + programKey, _ := versioned.GetAccount(int(versioned.Instructions[0].ProgramIDIndex)) + if programKey.Equals(VoteProgram) && len(versioned.AddressTableLookups) == 0 { + return true } } - return v.(*versionedTransaction) + + // accounts filter? + include := false + for _, key := range versioned.StaticAccountKeys { + _, include = slices.BinarySearchFunc(parseProgram, key, func(key solana.PublicKey, key2 solana.PublicKey) int { + return bytes.Compare(key[:], key2[:]) + }) + if include { + break + } + } + return !include } -func releaseVersionedPool(v *versionedTransaction) { - if v == nil { +func ParseTransactionForEntries(ctx context.Context, slot uint64, entriesReader io.Reader, loader *AddressTables, parsed chan<- TxSignal) { + err := entriesToVersionedTransaction(slot, entriesReader, func(versioned VersionedTransaction) { + // filter out vote transactions + if FilterTransactionForEntries(versioned) { + return + } + go ParseTransaction(ctx, versioned, loader, parsed) + }) + if err != nil { + logger.Debug("txparser: failed to parse entries", "error", err) return } - for i := range v.Message.Instructions { - releaseAccIdxSlice(v.Message.Instructions[i].Accounts) - } - for i := range v.Message.AddressTableLookups { - releaseAccIdxSlice(v.Message.AddressTableLookups[i].WritableIndexes) - releaseAccIdxSlice(v.Message.AddressTableLookups[i].ReadonlyIndexes) - } - versionedPool.Put(v) } -// ParseTransaction mirrors the Rust parse_transaction entry point. -func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables, stats bool) []*TxSignal { - var now time.Time - if stats { - now = time.Now() - } - versioned, err := toVersionedTransaction(update) - if err != nil || versioned == nil || len(versioned.Signatures) == 0 { - return nil - } - defer func() { - releaseVersionedPool(versioned) - }() - txHash := versioned.Signatures[0] +func ParseTransaction(ctx context.Context, versioned VersionedTransaction, loader *AddressTables, parsed chan<- TxSignal) { // staticKeys := versioned.Message.StaticAccountKeys - instructions := versioned.Message.Instructions - - if loader != nil && len(versioned.Message.AddressTableLookups) > 0 { + if loader != nil && len(versioned.AddressTableLookups) > 0 { lookupTableOk := true - for _, lookup := range versioned.Message.AddressTableLookups { - if len(lookup.WritableIndexes) == 0 { - continue - } - lookupTableOk = loader.FillToTx(versioned, lookup.AccountKey, lookup.WritableIndexes) + for _, lookups := range versioned.AddressTableLookups { + lookupTableOk = loader.FillToTx(&versioned, lookups.AccountKey, lookups.WritableIndexes) if !lookupTableOk { break } } if lookupTableOk { - for _, lookup := range versioned.Message.AddressTableLookups { - if len(lookup.ReadonlyIndexes) == 0 { - continue - } - lookupTableOk = loader.FillToTx(versioned, lookup.AccountKey, lookup.ReadonlyIndexes) + for _, lookups := range versioned.AddressTableLookups { + lookupTableOk = loader.FillToTx(&versioned, lookups.AccountKey, lookups.ReadonlyIndexes) if !lookupTableOk { break } } } - // versioned.Message.StaticAccountKeys = staticKeys } - var parsed []*TxSignal = make([]*TxSignal, 0, 3) - - for i := range instructions { - inst := instructions[i] - if int(inst.ProgramIDIndex) >= len(versioned.Message.StaticAccountKeys) { + for i, instruction := range versioned.Instructions { + //load from address table + program, err := versioned.GetAccount(int(instruction.ProgramIDIndex)) + if err != nil { continue } - - programID := versioned.Message.StaticAccountKeys[inst.ProgramIDIndex] - switch programID { - case pumpProgramID: - txRes, err := parsePumpInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "pump") - case azczProgramID: - txRes, err := parseAzczInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "azcz") - case f5tfProgramID: - txRes, err := parseF5tfInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "f5tf") - case flasProgramID: - txRes, err := parseFlasInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "flas") - case photonProgramID: - txRes, err := parsePhotonInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "photon") - case pumpAmmProgramID: - txRes, err := parsePumpAmmInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "pumpamm") - case boboProgramID: - txRes, err := parseBoboInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "bobo") - case qtkvProgramID: - txRes, err := parseQtkvInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "qtkv") - case fjszProgramID: - txRes, err := parseFjszInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "fjsz") - case terminalProgramID: - txRes, err := parseTermInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "terminal") - case jupiterV6ProgramID: - txRes, err := parseJupiterV6Instruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "jupiterv6") - case okxDexRouteV2ProgramID: - txRes, err := parseOkxDexRouteV2Instruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "okxdexroutev2") - case dflowProgramID: - txRes, err := parseDFlowInstruction(versioned, i) - parsed = appendParsedBatch(now, parsed, txRes, err, txHash, "dflow") - case gmgnProgramID: - txRes, err := parseGMGNInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "gmgn") - case bonkProgramID: - txRes, err := parseBonkInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "bonk") - case bloomRouterProgramID: - txRes, err := parseBloomRouterInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "bloomrouter") - case dlmmProgramID: - txRes, err := parseDlmmInstruction(versioned, i) - parsed = appendParsed(now, parsed, txRes, err, txHash, "dlmm") - } - } - - return parsed -} - -func appendParsed(start time.Time, list []*TxSignal, parsed *TxSignal, err error, txHash [64]byte, label string) []*TxSignal { - if err != nil { - if !strings.HasPrefix(err.Error(), "account index") { - logger.Debug("txparser: failed to parse", "label", label, "instruction", err, "tx_hash", base58.Encode(txHash[:])) - } - return list - } - if parsed != nil { - parsed.Label = label - if !start.IsZero() { - parsed.ParseEnd = time.Now() - parsed.ParseStart = start - } - list = append(list, parsed) - } - return list -} - -func appendParsedBatch(start time.Time, list []*TxSignal, parsed []*TxSignal, err error, txHash [64]byte, label string) []*TxSignal { - if err != nil { - if !strings.HasPrefix(err.Error(), "account index") { - logger.Debug("txparser: failed to parse", "label", label, "instruction", err, "tx_hash", base58.Encode(txHash[:])) - } - return list - } - if len(parsed) == 0 { - return list - } - var end time.Time - if !start.IsZero() { - end = time.Now() - } - for _, sig := range parsed { - if sig == nil { + handler, ok := parsedMap[program] + if !ok { continue } - sig.Label = label - if !start.IsZero() { - sig.ParseEnd = end - sig.ParseStart = start + txRes, err := handler.Func(versioned, i) + if err != nil { + if !strings.HasPrefix(err.Error(), "account index") { + logger.Debug("txparser: failed to parse", "label", handler.Label, "err", err, "tx_hash", versioned.Signatures[0].String()) + } + continue } - list = append(list, sig) + if txRes != nil { + for _, one := range txRes { + if one == nil { + continue + } + one.Label = handler.Label + one.Block = versioned.Block + select { + case <-ctx.Done(): + return + case parsed <- *one: + } + } + } + } - return list + + return } -func toVersionedTransaction(update *SubscribeUpdateTransaction) (*versionedTransaction, error) { +func toVersionedTransaction(update *SubscribeUpdateTransaction) (VersionedTransaction, error) { if update == nil || update.Transaction == nil || update.Transaction.Message == nil { - return nil, fmt.Errorf("transaction is nil") + return VersionedTransaction{}, fmt.Errorf("transaction is nil") } protoTx := update.Transaction msg := protoTx.Message - versioned := requireVersionedPool() - versioned.Signatures = versioned.Signatures[:0] + versioned := VersionedTransaction{ + Signatures: make([]solana.Signature, 0, 10), + StaticAccountKeys: make([]solana.PublicKey, 0, 256), + Instructions: make([]Instructions, 0, 16), + AddressTableLookups: make([]AddressTableLookup, 0, 10), + } + for _, rawSig := range protoTx.Signatures { versioned.Signatures = append(versioned.Signatures, solana.SignatureFromBytes(rawSig)) } - versioned.Message.StaticAccountKeys = versioned.Message.StaticAccountKeys[:0] + + versioned.StaticAccountKeys = versioned.StaticAccountKeys[:0] for _, key := range msg.AccountKeys { - versioned.Message.StaticAccountKeys = append(versioned.Message.StaticAccountKeys, solana.PublicKeyFromBytes(key)) - } - versioned.Message.Instructions = versioned.Message.Instructions[:0] - for _, instr := range msg.Instructions { - accounts := requireAccIdxSlice() - accounts = append(accounts, instr.Accounts...) - versioned.Message.Instructions = append(versioned.Message.Instructions, - compiledInstruction{ - ProgramIDIndex: uint8(instr.ProgramIdIndex), - Accounts: accounts, - Data: instr.Data, - }) + versioned.StaticAccountKeys = append(versioned.StaticAccountKeys, solana.PublicKeyFromBytes(key)) } - versioned.Message.AddressTableLookups = versioned.Message.AddressTableLookups[:0] + versioned.Instructions = versioned.Instructions[:0] + for _, instr := range msg.Instructions { + accounts := make([]uint8, 0, 16) + accounts = append(accounts, instr.Accounts...) + versioned.Instructions = append(versioned.Instructions, Instructions{ + ProgramIDIndex: uint8(instr.ProgramIdIndex), + Accounts: accounts, + Data: instr.Data, + }) + } + + versioned.AddressTableLookups = versioned.AddressTableLookups[:0] for _, lookup := range msg.AddressTableLookups { - writable := requireAccIdxSlice() + writable := make([]uint8, 0, 16) writable = append(writable, lookup.WritableIndexes...) - readonly := requireAccIdxSlice() + readonly := make([]uint8, 0, 16) readonly = append(readonly, lookup.ReadonlyIndexes...) - versioned.Message.AddressTableLookups = append(versioned.Message.AddressTableLookups, addressTableLookup{ + versioned.AddressTableLookups = append(versioned.AddressTableLookups, AddressTableLookup{ AccountKey: solana.PublicKeyFromBytes(lookup.AccountKey), WritableIndexes: writable, ReadonlyIndexes: readonly, @@ -480,933 +234,6 @@ func formatSolAmount(lamports uint64) decimal.Decimal { return val.Div(decimal.NewFromInt(1_000_000_000)) } -func getStaticKey(static []solana.PublicKey, index int) (solana.PublicKey, error) { - if index < 0 || index >= len(static) { - return solana.PublicKey{}, NewAccountNotFoundError(index, len(static)) - } - return static[index], nil -} - -func parsePumpInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) < 8 { - return nil, fmt.Errorf("data is empty") - } - - if matchMethod(instruction.Data[0:8], pumpBuyV2TokensIX) || matchMethod(instruction.Data[0:8], pumpBuyTokensIX) { - return parsePumpBuy(tx, &instruction) - } else if matchMethod(instruction.Data[0:8], pumpExtendedSellIX) { - return parsePumpSell(tx, &instruction) - } else if matchMethod(instruction.Data[0:8], pumpCreateCoinIX) { - return parsePumpCreate(tx, &instruction) - } else if matchMethod(instruction.Data[0:8], pumpCreateCoinV2IX) { - return parsePumpCreateV2(tx, &instruction) - } - return nil, nil - -} - -func parsePumpCreate(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) - if err != nil { - return nil, err - } - creator, err := getStaticKey(staticKeys, 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 *compiledInstruction) (*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)) - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) - if err != nil { - return nil, err - } - tokenProgramKey, err := getStaticKey(staticKeys, 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 *compiledInstruction) (*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") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) - if err != nil { - return nil, err - } - buyer, err := getStaticKey(staticKeys, 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 *compiledInstruction) (*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") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) - if err != nil { - return nil, err - } - seller, err := getStaticKey(staticKeys, 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 -} - -func parseAzczInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - - if matchMethod(instruction.Data, azczBuyTokensIX) { - return parseAzczBuy(tx, instructionIndex) - } else if matchMethod(instruction.Data, azczAmmBuyTokensIX) { - return parseAzczAmmBuy(tx, instructionIndex) - } - return nil, nil -} - -func parseAzczAmmBuy(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - instruction := tx.Message.Instructions[instructionIndex] - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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.Message.Instructions[instructionIndex] - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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 -} - -func parseF5tfInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - if !matchMethod(instruction.Data, f5tfBuyTokensIX) { - return nil, nil - } - - if len(instruction.Accounts) < 7 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := msg.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) - if err != nil { - return nil, err - } - - 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 &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 -} - -func parseFlasInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - if len(instruction.Data) == 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 - //} - if matchMethod(methodData, flasBuyTokensIX) { - return parseFlasBuy(tx, instructionIndex) - } else if matchMethod(methodData, flasSellTokensIX) { - return parseFlasSell(tx, instructionIndex) - } else if matchMethod(methodData, flasAmmBuyTokensIX) { - return parseFlasAmmBuy(tx, instructionIndex) - } else if matchMethod(methodData, flasAmmSellTokensIX) { - return parseFlasAmmSell(tx, instructionIndex) - } - return nil, nil -} - -func parseFlasAmmSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - instruction := tx.Message.Instructions[instructionIndex] - if len(instruction.Accounts) < 10 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[9])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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.Message.Instructions[instructionIndex] - if len(instruction.Accounts) < 10 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[9])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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.Message.Instructions[instructionIndex] - if len(instruction.Accounts) < 9 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[8])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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.Message.Instructions[instructionIndex] - if len(instruction.Accounts) < 9 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[8])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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 -} - -func parseGMGNInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - if len(instruction.Data) < 8 { - return nil, nil - } - - if matchMethod(instruction.Data, gmgnBuyTokensIX) { - return parseGMGNBuy(tx, &instruction) - } - return nil, nil -} - -func parseGMGNBuy(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - if len(instruction.Data) < 24 { - return nil, fmt.Errorf("data too short for gmgn buy args, len=%d", len(instruction.Data)) - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) - if err != nil { - return nil, err - } - - solAmount := binary.LittleEndian.Uint64(instruction.Data[8:16]) - tokenAmount := binary.LittleEndian.Uint64(instruction.Data[16:24]) - - return &TxSignal{ - TxHash: tx.Signatures[0].String(), - Label: "gmgn", - Maker: user.String(), - Token0Address: mint.String(), - Token1Address: wsolMint, - Token0Amount: formatTokenAmount(tokenAmount), - Token1Amount: formatSolAmount(solAmount), - Program: "Pump", - Event: "buy", - IsToken2022: false, - IsMayhemMode: false, - ExactSOL: true, - Block: tx.Block, - Token0AmountUint64: tokenAmount, - Token1AmountUint64: solAmount, - }, nil -} - -func parsePhotonInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - if len(instruction.Data) < 8 { - return nil, nil - } - - switch { - case bytes.Equal(instruction.Data[:8], photonBuyPumpTokensIX): - return parsePhotonBuy(tx, &instruction) - case bytes.Equal(instruction.Data[:8], photonSwapPumpAmmIX): - return parsePhotonSwap(tx, &instruction) - default: - return nil, nil - } -} - -func parsePhotonBuy(tx *versionedTransaction, instruction *compiledInstruction) (*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)) - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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 *compiledInstruction) (*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)) - } - - staticKeys := tx.Message.StaticAccountKeys - base, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) - if err != nil { - return nil, err - } - - quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) - if err != nil { - return nil, err - } - if !quote.Equals(solana.WrappedSol) { - return nil, nil - } - - buyer, err := getStaticKey(staticKeys, 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 -} - -func parsePumpAmmInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - - if matchMethod(instruction.Data, pumpAmmBuyTokensIX) || matchMethod(instruction.Data, pumpAmmBuyTokensV2IX) { - return parsePumpAmmBuy(tx, &instruction) - } else if matchMethod(instruction.Data, pumpAmmSellTokensIX) { - return parsePumpAmmSell(tx, &instruction) - } - - return nil, nil - -} - -func parseTermInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - if len(instruction.Data) < 24 { - return nil, nil - } - - switch { - case bytes.Equal(instruction.Data[:8], terminalBuyTokensIX): - return parseTermBuy(tx, &instruction) - case bytes.Equal(instruction.Data[:8], terminalSellTokensIX): - return parseTermSell(tx, &instruction) - case bytes.Equal(instruction.Data[:8], terminalAmmSellTokensIX): - return parseTermAmmSell(tx, &instruction) - default: - return nil, nil - } -} - -func parseTermAmmSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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 *compiledInstruction) (*TxSignal, error) { - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) - if err != nil { - return nil, err - } - - solAmount := binary.LittleEndian.Uint64(instruction.Data[8:16]) - tokenAmount := binary.LittleEndian.Uint64(instruction.Data[16:24]) - - return &TxSignal{ - TxHash: tx.Signatures[0].String(), - Label: "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 *compiledInstruction) (*TxSignal, error) { - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) - if err != nil { - return nil, err - } - - 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 -} - -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 findAssociatedTokenAddressWithTokenProgram(wallet, mint, tokenProgram solana.PublicKey) (solana.PublicKey, uint8, error) { return solana.FindProgramAddress([][]byte{ wallet[:], @@ -1415,757 +242,6 @@ func findAssociatedTokenAddressWithTokenProgram(wallet, mint, tokenProgram solan }, solana.SPLAssociatedTokenAccountProgramID) } -type dlmmParsedArgs struct { - AmountIn uint64 - AmountOut uint64 - ExactIn bool - ExactOut bool - ActiveBin int32 - MaxPriceImpactBps uint16 -} - -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) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) < 8 { - return nil, fmt.Errorf("data is empty") - } - if len(instruction.Accounts) < 13 { - return 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 - } - - staticKeys := tx.Message.StaticAccountKeys - userTokenIn, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) - if err != nil { - return nil, err - } - userTokenOut, err := getStaticKey(staticKeys, int(instruction.Accounts[5])) - if err != nil { - return nil, err - } - tokenX, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) - if err != nil { - return nil, err - } - tokenY, err := getStaticKey(staticKeys, int(instruction.Accounts[7])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, int(instruction.Accounts[10])) - if err != nil { - return nil, err - } - tokenXProgram, err := getStaticKey(staticKeys, int(instruction.Accounts[11])) - if err != nil { - return nil, err - } - tokenYProgram, err := getStaticKey(staticKeys, 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 &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 -} - -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 *compiledInstruction) (*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") - } - - staticKeys := tx.Message.StaticAccountKeys - base, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) - if err != nil { - return nil, err - } - quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) - if err != nil { - return nil, err - } - if !quote.Equals(solana.WrappedSol) { - return nil, nil - } - - buyer, err := getStaticKey(staticKeys, 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 *compiledInstruction) (*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") - } - - staticKeys := tx.Message.StaticAccountKeys - base, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) - if err != nil { - return nil, err - } - quote, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) - if err != nil { - return nil, err - } - if !quote.Equals(solana.WrappedSol) { - return nil, nil - } - - buyer, err := getStaticKey(staticKeys, 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 -} - -func parseBoboInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - if len(instruction.Data) < 8 || !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)) - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) - if err != nil { - return nil, err - } - - 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 &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 -} - -func parseQtkvInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - - if matchMethod(instruction.Data, qtkvBuyTokensIX) { - return parseQtkvBuy(tx, instructionIndex) - } else if matchMethod(instruction.Data, qtkvAmmSellTokensIX) { - return parseQtkvAmmSell(tx, instructionIndex) - } else if matchMethod(instruction.Data, qtkvSellTokensIX) { - return parseQtkvSell(tx, instructionIndex) - } - - return nil, nil -} - -func parseQtkvSell(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - instruction := tx.Message.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)) - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[10])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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.Message.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)) - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[10])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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.Message.Instructions[instructionIndex] - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[3])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, 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 -} - -func parseFjszInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - - if !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)) - } - - staticKeys := tx.Message.StaticAccountKeys - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[2])) - if err != nil { - return nil, err - } - user, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) - if err != nil { - return nil, err - } - - 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 &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 -} - -func parseBonkInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) == 0 { - return nil, fmt.Errorf("data is empty") - } - - if matchMethod(instruction.Data, bonkBuyAndSellTokensIX) { - return parseBonkBuyAndSell(tx, &instruction) - } - - return nil, nil -} - -func parseBonkBuyAndSell(tx *versionedTransaction, instruction *compiledInstruction) (*TxSignal, error) { - if len(instruction.Accounts) < 8 { - return nil, fmt.Errorf("accounts too short") - } - staticKeys := tx.Message.StaticAccountKeys - programId, err := getStaticKey(staticKeys, int(instruction.Accounts[7])) - if err != nil { - return nil, err - } - if programId != pumpProgramID { - return nil, nil - } - - user, err := getStaticKey(staticKeys, int(instruction.Accounts[0])) - if err != nil { - return nil, err - } - - flagAccount, err := getStaticKey(staticKeys, int(instruction.Accounts[4])) - if err != nil { - return nil, err - } - - amount1 := binary.LittleEndian.Uint64(instruction.Data[17:25]) - amount2 := binary.LittleEndian.Uint64(instruction.Data[25:33]) - - if user == flagAccount { - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[6])) - if err != nil { - return nil, err - } - - return &TxSignal{ - TxHash: tx.Signatures[0].String(), - Label: "bonk", - Maker: user.String(), - Token0Address: mint.String(), - Token1Address: wsolMint, - Token0Amount: formatTokenAmount(amount2), - Token1Amount: formatSolAmount(amount1), - Program: "Pump", - Event: "buy", - IsToken2022: false, - IsMayhemMode: false, - ExactSOL: true, - Block: tx.Block, - Token0AmountUint64: amount2, - Token1AmountUint64: amount1, - }, nil - } else { - mint, err := getStaticKey(staticKeys, int(instruction.Accounts[5])) - if err != nil { - return nil, err - } - - return &TxSignal{ - TxHash: tx.Signatures[0].String(), - Label: "bonk", - Maker: user.String(), - Token0Address: mint.String(), - Token1Address: wsolMint, - Token0Amount: formatTokenAmount(amount1), - Token1Amount: formatSolAmount(amount2), - Program: "Pump", - Event: "sell", - IsToken2022: false, - IsMayhemMode: false, - ExactSOL: false, - Block: tx.Block, - Token0AmountUint64: amount1, - Token1AmountUint64: amount2, - }, nil - } -} - -func parseBloomRouterInstruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { - msg := tx.Message - if instructionIndex >= len(msg.Instructions) { - return nil, fmt.Errorf("instruction index out of bounds") - } - - instruction := msg.Instructions[instructionIndex] - if len(instruction.Data) < 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 := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[0])) - if err != nil { - return nil, err - } - - var ( - mint solana.PublicKey - ok bool - ) - for _, acctIdx := range instruction.Accounts { - key, err := getStaticKey(msg.StaticAccountKeys, 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 &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 -} - func matchMethod(data []byte, methods []byte) bool { if len(data) < len(methods) { return false diff --git a/pkg/shreder/txparser_test.go b/pkg/shreder/txparser_test.go index 3954b7e..510c2f1 100644 --- a/pkg/shreder/txparser_test.go +++ b/pkg/shreder/txparser_test.go @@ -149,11 +149,21 @@ func TestParseTermBuy(t *testing.T) { } client := rpc.New(rpcUrl) - signals := ParseTransaction( - getTransaction(t, client, "5Gz1fa4Qhb35bkg9QCMXpxCX5uuNr7WcjcmrwajGZA7kXsvNS9pDnYe12ggWeSqf1nwZbVPob6DkX6fcwbE9ofBR"), - nil, - false, - ) + ch := make(chan TxSignal) + closed := make(chan struct{}) + go func() { + ParseTransactionForSubscribe( + context.Background(), + getTransaction(t, client, "5Gz1fa4Qhb35bkg9QCMXpxCX5uuNr7WcjcmrwajGZA7kXsvNS9pDnYe12ggWeSqf1nwZbVPob6DkX6fcwbE9ofBR"), + 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)) } @@ -186,11 +196,23 @@ func TestParseBonkBuy(t *testing.T) { } client := rpc.New(rpcUrl) - signals := ParseTransaction( - getTransaction(t, client, "3gHF3TA2aA8rpjdmoEs2vA89vrq9J9NnTTUSXHfE6uXcaYP9cJgLtEUjCmsK9EWAyHEg7cEiepehQf4GFv1272jW"), - nil, - false, - ) + + ch := make(chan TxSignal) + closed := make(chan struct{}) + go func() { + ParseTransactionForSubscribe( + context.Background(), + getTransaction(t, client, "3gHF3TA2aA8rpjdmoEs2vA89vrq9J9NnTTUSXHfE6uXcaYP9cJgLtEUjCmsK9EWAyHEg7cEiepehQf4GFv1272jW"), + 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)) } @@ -223,11 +245,22 @@ func TestParseBonkSell(t *testing.T) { } client := rpc.New(rpcUrl) - signals := ParseTransaction( - getTransaction(t, client, "3XNi6b3j69SSStqLLRQVH5BNGVfEoFxGCzmpdd5FvrY4kmC8T644WGdEhCH9fAdrxWuR2Mtzgywq8K7qetu5MGyb"), - nil, - false, - ) + ch := make(chan TxSignal) + closed := make(chan struct{}) + go func() { + ParseTransactionForSubscribe( + context.Background(), + getTransaction(t, client, "3XNi6b3j69SSStqLLRQVH5BNGVfEoFxGCzmpdd5FvrY4kmC8T644WGdEhCH9fAdrxWuR2Mtzgywq8K7qetu5MGyb"), + 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)) } @@ -260,11 +293,23 @@ func TestParsePhotonBuy(t *testing.T) { } client := rpc.New(rpcUrl) - signals := ParseTransaction( - getTransaction(t, client, "4DCEcXAWBxagXoUNGhWsJ7qfxq5SuE5BG2cBDBqAY7sCHkBopaMJu33ZnXnFHqzPMmWxVxq6666KRF4hMHVB33Ux"), - nil, - false, - ) + + ch := make(chan TxSignal) + closed := make(chan struct{}) + go func() { + ParseTransactionForSubscribe( + context.Background(), + getTransaction(t, client, "4DCEcXAWBxagXoUNGhWsJ7qfxq5SuE5BG2cBDBqAY7sCHkBopaMJu33ZnXnFHqzPMmWxVxq6666KRF4hMHVB33Ux"), + 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)) } @@ -297,11 +342,22 @@ func TestParseJupiterV6PumpFunBuy(t *testing.T) { } client := rpc.New(rpcUrl) - signals := ParseTransaction( - getTransaction(t, client, "4QF5whXwjx234fMXeH3HrJCy5knFJmKPtgbXys8xKGz1pZypqPvXBr4BoAqXfYn8jLL4HXPY1pcvxCCW1XREFNxd"), - nil, - false, - ) + 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)) } @@ -337,11 +393,21 @@ func TestParseJupiterV6PumpFunSell(t *testing.T) { } client := rpc.New(rpcUrl) - signals := ParseTransaction( - getTransaction(t, client, "yCnE7ZA8dqB5iAZtwpSN2ar5HXh3gBjgaG2xtnwXDPFyHAm5XFU8642uTZTH5A2iPQ6G9hrj5eEPAJiWrfe38gM"), - nil, - false, - ) + 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)) } diff --git a/pkg/shreder/versioned.go b/pkg/shreder/versioned.go new file mode 100644 index 0000000..d772bd4 --- /dev/null +++ b/pkg/shreder/versioned.go @@ -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)) +}