7 Commits
v1.3.14 ... 1.x

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

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

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

6
go.mod
View File

@@ -4,10 +4,13 @@ go 1.25.1
require ( require (
github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250722092120-44561cb37455 github.com/BlockRazorinc/solana-trader-client-go v0.0.0-20250722092120-44561cb37455
github.com/gagliardetto/binary v0.8.0
github.com/gagliardetto/solana-go v1.12.0 github.com/gagliardetto/solana-go v1.12.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/mr-tron/base58 v1.2.0 github.com/mr-tron/base58 v1.2.0
github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454 github.com/near/borsh-go v0.3.2-0.20220516180422-1ff87d108454
github.com/panjf2000/ants/v2 v2.11.4 github.com/panjf2000/ants/v2 v2.11.4
github.com/quic-go/quic-go v0.58.0
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
google.golang.org/grpc v1.75.0 google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.10 google.golang.org/protobuf v1.36.10
@@ -19,10 +22,8 @@ require (
github.com/blendle/zapdriver v1.3.1 // indirect github.com/blendle/zapdriver v1.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.9.0 // indirect github.com/fatih/color v1.9.0 // indirect
github.com/gagliardetto/binary v0.8.0 // indirect
github.com/gagliardetto/treeout v0.1.4 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect github.com/klauspost/compress v1.13.6 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
@@ -32,7 +33,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
github.com/quic-go/quic-go v0.58.0 // indirect
github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect
go.mongodb.org/mongo-driver v1.12.2 // indirect go.mongodb.org/mongo-driver v1.12.2 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect

5
go.sum
View File

@@ -88,9 +88,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -118,6 +117,8 @@ go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=

View File

@@ -128,10 +128,14 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error
return err return err
} }
// reboot the pool
c.pool.Reboot()
for { for {
response, err := stream.Recv() var response *SubscribeTransactionsResponse
response, err = stream.Recv()
if err != nil { if err != nil {
return err break
} }
if c.enableBlockStats { if c.enableBlockStats {
@@ -165,7 +169,12 @@ func (c *Client) ReadSync(ctx context.Context, txCh chan<- TxSignalBatch) error
} }
}) })
if err != nil { if err != nil {
break
}
}
// sync waiting for all tasks to complete
c.pool.Release()
return err return err
}
}
} }

View File

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

File diff suppressed because one or more lines are too long

8471
pkg/shreder/dlmm_idl.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,10 @@ var (
jupiterSharedAccountsExactOutRouteV2 = []byte{53, 96, 229, 202, 216, 187, 250, 24} jupiterSharedAccountsExactOutRouteV2 = []byte{53, 96, 229, 202, 216, 187, 250, 24}
jupiterSharedAccountsRouteV2 = []byte{209, 152, 83, 147, 124, 254, 216, 233} jupiterSharedAccountsRouteV2 = []byte{209, 152, 83, 147, 124, 254, 216, 233}
usdcMint = solana.MustPublicKeyFromBase58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
usd1Mint = solana.MustPublicKeyFromBase58("USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB")
usdtMint = solana.MustPublicKeyFromBase58("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB")
) )
type Side uint8 type Side uint8
@@ -819,14 +823,37 @@ func decodeJupiterV6SharedAccountsRouteV2Arg(data []byte) (*JupiterV6SharedAccou
return &JupiterV6SharedAccountsRouteV2Arg{ID: id, In: inAmt, QuotedOut: quotedOut, Slippage: slippage, PlatFee: pf, PosSlip: pos, RoutePlan: plan}, nil return &JupiterV6SharedAccountsRouteV2Arg{ID: id, In: inAmt, QuotedOut: quotedOut, Slippage: slippage, PlatFee: pf, PosSlip: pos, RoutePlan: plan}, nil
} }
func isInputIdx0(idx uint8) bool {
return idx == 0
}
func isPumpSwapSellKind(kind SwapKind) bool {
switch kind {
case PumpSwapSell, PumpSwapSellV2, PumpSwapSellV3:
return true
default:
return false
}
}
func isPumpSwapBuyKind(kind SwapKind) bool {
switch kind {
case PumpSwapBuy, PumpSwapBuyV2, PumpSwapBuyV3:
return true
default:
return false
}
}
func pumpSwapSellAtIdx0(amount uint64, plan []RoutePlanStep) (uint64, int) { func pumpSwapSellAtIdx0(amount uint64, plan []RoutePlanStep) (uint64, int) {
var ( var (
ret uint64 ret uint64
i int i int
) )
for _, step := range plan { for _, step := range plan {
if step.InputIdx == 0 && if !isInputIdx0(step.InputIdx) || !isPumpSwapSellKind(step.Swap.Kind) {
(step.Swap.Kind == PumpSwapSell || step.Swap.Kind == PumpSwapSellV2 || step.Swap.Kind == PumpSwapSellV3) { continue
}
i++ i++
if ret > 0 { if ret > 0 {
// multiple pumpSwapSell at inputIdx=0? should not happen // multiple pumpSwapSell at inputIdx=0? should not happen
@@ -834,7 +861,6 @@ func pumpSwapSellAtIdx0(amount uint64, plan []RoutePlanStep) (uint64, int) {
} }
ret += amount * uint64(step.Percent) / 100 ret += amount * uint64(step.Percent) / 100
} }
}
return ret, i return ret, i
} }
@@ -844,20 +870,382 @@ func pumpSwapSellAtIdx0V2(amount uint64, plan []RoutePlanStepV2) (uint64, int) {
i int i int
) )
for _, step := range plan { for _, step := range plan {
if step.InputIdx == 0 && if !isInputIdx0(step.InputIdx) || !isPumpSwapSellKind(step.Swap.Kind) {
(step.Swap.Kind == PumpSwapSell || step.Swap.Kind == PumpSwapSellV2 || step.Swap.Kind == PumpSwapSellV3) { continue
}
i++ i++
if ret > 0 { if ret > 0 {
// multiple pumpSwapSell at inputIdx=0? should not happen // multiple pumpSwapSell at inputIdx=0? should not happen
return 0, i return 0, i
} }
ret += amount * uint64(step.Bps) / 10000 ret += amount * uint64(step.Bps) / 10000
} }
}
return ret, i return ret, i
} }
type pumpSwapBuyMatch struct {
InAmount uint64
OutAmount uint64
}
func pumpSwapBuyAtIdx0(in uint64, out uint64, plan []RoutePlanStep) (pumpSwapBuyMatch, int) {
var (
ret pumpSwapBuyMatch
count int
)
for _, step := range plan {
if !isInputIdx0(step.InputIdx) || !isPumpSwapBuyKind(step.Swap.Kind) {
continue
}
count++
if count > 1 {
return pumpSwapBuyMatch{}, count
}
ret.InAmount = in * uint64(step.Percent) / 100
if step.Percent == 100 {
ret.OutAmount = out
}
}
return ret, count
}
func pumpSwapBuyAtIdx0V2(in uint64, out uint64, plan []RoutePlanStepV2) (pumpSwapBuyMatch, int) {
var (
ret pumpSwapBuyMatch
count int
)
for _, step := range plan {
if !isInputIdx0(step.InputIdx) || !isPumpSwapBuyKind(step.Swap.Kind) {
continue
}
count++
if count > 1 {
return pumpSwapBuyMatch{}, count
}
ret.InAmount = in * uint64(step.Bps) / 10000
if step.Bps == 10000 {
ret.OutAmount = out
}
}
return ret, count
}
type pumpWrappedMatch struct {
IsBuy bool
InAmount uint64
OutAmount uint64
}
func isPumpWrappedBuy(kind SwapKind) bool {
switch kind {
case PumpWrappedBuy, PumpWrappedBuyV2, PumpWrappedBuyV3, PumpWrappedBuyV4:
return true
default:
return false
}
}
func isPumpWrappedSell(kind SwapKind) bool {
switch kind {
case PumpWrappedSell, PumpWrappedSellV2, PumpWrappedSellV3, PumpWrappedSellV4:
return true
default:
return false
}
}
func isPumpWrappedKind(kind SwapKind) bool {
return isPumpWrappedBuy(kind) || isPumpWrappedSell(kind)
}
func isStableMint(mint solana.PublicKey) bool {
if mint.Equals(usdcMint) {
return true
}
if mint.Equals(usd1Mint) {
return true
}
if mint.Equals(usdtMint) {
return true
}
return false
}
func isToken1Mint(mint solana.PublicKey) bool {
return mint.Equals(solana.WrappedSol) || mint.Equals(solana.SystemProgramID) || isStableMint(mint)
}
func isJupiterV6Token1RequiredDisc(disc []byte) bool {
return bytes.Equal(disc, jupiterRouteV2) ||
bytes.Equal(disc, jupiterSharedAccountsRouteV2) ||
bytes.Equal(disc, jupiterExactOutRouteV2) ||
bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2) ||
bytes.Equal(disc, jupiterSharedAccountsRoute) ||
bytes.Equal(disc, jupiterSharedAccountsExactOutRoute)
}
func pumpWrappedAtIdx0(in uint64, out uint64, plan []RoutePlanStep) (pumpWrappedMatch, int) {
var (
ret pumpWrappedMatch
count int
)
for _, step := range plan {
if !isInputIdx0(step.InputIdx) {
continue
}
if !isPumpWrappedKind(step.Swap.Kind) {
continue
}
count++
if count > 1 {
return pumpWrappedMatch{}, count
}
ret.IsBuy = isPumpWrappedBuy(step.Swap.Kind)
ret.InAmount = in * uint64(step.Percent) / 100
if step.Percent == 100 {
ret.OutAmount = out
}
}
return ret, count
}
func pumpWrappedAtIdx0V2(in uint64, out uint64, plan []RoutePlanStepV2) (pumpWrappedMatch, int) {
var (
ret pumpWrappedMatch
count int
)
for _, step := range plan {
if !isInputIdx0(step.InputIdx) {
continue
}
if !isPumpWrappedKind(step.Swap.Kind) {
continue
}
count++
if count > 1 {
return pumpWrappedMatch{}, count
}
ret.IsBuy = isPumpWrappedBuy(step.Swap.Kind)
ret.InAmount = in * uint64(step.Bps) / 10000
if step.Bps == 10000 {
ret.OutAmount = out
}
}
return ret, count
}
func pumpWrappedAny(plan []RoutePlanStep) (pumpWrappedMatch, int) {
var (
ret pumpWrappedMatch
count int
)
for _, step := range plan {
if !isPumpWrappedKind(step.Swap.Kind) {
continue
}
count++
if count > 1 {
return pumpWrappedMatch{}, count
}
ret.IsBuy = isPumpWrappedBuy(step.Swap.Kind)
}
return ret, count
}
func pumpWrappedAnyV2(plan []RoutePlanStepV2) (pumpWrappedMatch, int) {
var (
ret pumpWrappedMatch
count int
)
for _, step := range plan {
if !isPumpWrappedKind(step.Swap.Kind) {
continue
}
count++
if count > 1 {
return pumpWrappedMatch{}, count
}
ret.IsBuy = isPumpWrappedBuy(step.Swap.Kind)
}
return ret, count
}
func pumpRoutePlanStats(in uint64, out uint64, plan []RoutePlanStep, includeInput bool) (uint64, int, pumpSwapBuyMatch, int, pumpWrappedMatch, int, pumpWrappedMatch, int) {
var (
inputAmount uint64
planCount int
)
if includeInput {
inputAmount, planCount = pumpSwapSellAtIdx0(in, plan)
}
buySwap, buySwapCnt := pumpSwapBuyAtIdx0(in, out, plan)
wrapped, wrappedCnt := pumpWrappedAtIdx0(in, out, plan)
wrappedAny, wrappedAnyC := pumpWrappedAny(plan)
return inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC
}
func pumpRoutePlanStatsV2(in uint64, out uint64, plan []RoutePlanStepV2, includeInput bool) (uint64, int, pumpSwapBuyMatch, int, pumpWrappedMatch, int, pumpWrappedMatch, int) {
var (
inputAmount uint64
planCount int
)
if includeInput {
inputAmount, planCount = pumpSwapSellAtIdx0V2(in, plan)
}
buySwap, buySwapCnt := pumpSwapBuyAtIdx0V2(in, out, plan)
wrapped, wrappedCnt := pumpWrappedAtIdx0V2(in, out, plan)
wrappedAny, wrappedAnyC := pumpWrappedAnyV2(plan)
return inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC
}
func parseJupiterPumpAmmRoute(tx *versionedTransaction, instruction compiledInstruction, in uint64, out uint64, plan []RoutePlanStep) (*TxSignal, bool, error) {
var (
isBuy bool
isSell bool
count int
)
for _, step := range plan {
if !isInputIdx0(step.InputIdx) {
continue
}
if isPumpSwapSellKind(step.Swap.Kind) {
isSell = true
count++
} else if isPumpSwapBuyKind(step.Swap.Kind) {
isBuy = true
count++
}
}
if count == 0 {
return nil, false, nil
}
if count > 1 || (isBuy && isSell) {
logger.Warn("pumpamm route at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", count)
return nil, true, nil
}
if len(instruction.Accounts) < 14 {
return nil, true, nil
}
token0Key, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[13]))
if err != nil {
return nil, true, err
}
if isSell {
token0Amount := decimal.Zero
if in > 0 {
token0Amount = formatTokenAmount(in)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: token0Key.String(),
Token1Address: wsolMint,
Token0Amount: token0Amount,
Token1Amount: decimal.Zero,
Program: "PumpAMM",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: false,
Block: tx.Block,
Token0AmountUint64: in,
Token1AmountUint64: 0,
}, true, nil
}
if len(instruction.Accounts) < 15 {
return nil, true, nil
}
wsolKey, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[14]))
if err != nil {
return nil, true, err
}
if !wsolKey.Equals(solana.WrappedSol) {
return nil, true, nil
}
token0Amount := decimal.Zero
if out > 0 {
token0Amount = formatTokenAmount(out)
}
token1Amount := decimal.Zero
if in > 0 {
token1Amount = formatSolAmount(in)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: token0Key.String(),
Token1Address: wsolMint,
Token0Amount: token0Amount,
Token1Amount: token1Amount,
Program: "PumpAMM",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: true,
Block: tx.Block,
Token0AmountUint64: out,
Token1AmountUint64: in,
}, true, nil
}
func findPumpFunMint(staticKeys []solana.PublicKey, accounts []uint8) (solana.PublicKey, bool, error) {
for i, acctIdx := range accounts {
key, err := getStaticKey(staticKeys, int(acctIdx))
if err != nil {
return solana.PublicKey{}, false, err
}
if !key.Equals(pumpProgramID) {
continue
}
if i+3 >= len(accounts) {
return solana.PublicKey{}, false, nil
}
mint, err := getStaticKey(staticKeys, int(accounts[i+3]))
if err != nil {
return solana.PublicKey{}, false, err
}
return mint, true, nil
}
return solana.PublicKey{}, false, nil
}
func jupiterV6SourceDestMints(msg versionedMessage, instruction compiledInstruction, disc []byte) (solana.PublicKey, solana.PublicKey, bool, error) {
switch {
case bytes.Equal(disc, jupiterRouteV2),
bytes.Equal(disc, jupiterSharedAccountsRouteV2),
bytes.Equal(disc, jupiterExactOutRouteV2),
bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2):
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]))
if err != nil {
return solana.PublicKey{}, solana.PublicKey{}, false, err
}
dst, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[4]))
if err != nil {
return solana.PublicKey{}, solana.PublicKey{}, false, err
}
return src, dst, true, nil
case bytes.Equal(disc, jupiterSharedAccountsRoute),
bytes.Equal(disc, jupiterSharedAccountsExactOutRoute):
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]))
if err != nil {
return solana.PublicKey{}, solana.PublicKey{}, false, err
}
dst, err := getStaticKey(msg.StaticAccountKeys, int(instruction.Accounts[8]))
if err != nil {
return solana.PublicKey{}, solana.PublicKey{}, false, err
}
return src, dst, true, nil
default:
return solana.PublicKey{}, solana.PublicKey{}, false, nil
}
}
// only decodes inputIdx = 0 container pumpSwap instructions for now // only decodes inputIdx = 0 container pumpSwap instructions for now
func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) { func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (*TxSignal, error) {
msg := tx.Message msg := tx.Message
@@ -878,7 +1266,16 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
var ( var (
sourceMint solana.PublicKey sourceMint solana.PublicKey
inputAmount uint64 inputAmount uint64
routeIn uint64
routeOut uint64
planCount int planCount int
buySwap pumpSwapBuyMatch
buySwapCnt int
wrapped pumpWrappedMatch
wrappedCnt int
wrappedAny pumpWrappedMatch
wrappedAnyC int
exactOut bool
err error err error
) )
@@ -890,40 +1287,334 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
inputAmount, planCount = pumpSwapSellAtIdx0V2(args.In, args.Plan) inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStatsV2(args.In, args.Out, args.Plan, true)
routeIn = args.In
routeOut = args.Out
case bytes.Equal(disc, jupiterSharedAccountsRouteV2): case bytes.Equal(disc, jupiterSharedAccountsRouteV2):
args, err := decodeJupiterV6SharedAccountsRouteV2Arg(instruction.Data[8:]) args, err := decodeJupiterV6SharedAccountsRouteV2Arg(instruction.Data[8:])
if err != nil { if err != nil {
return nil, err return nil, err
} }
inputAmount, planCount = pumpSwapSellAtIdx0V2(args.In, args.RoutePlan) inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStatsV2(args.In, args.QuotedOut, args.RoutePlan, true)
routeIn = args.In
routeOut = args.QuotedOut
case bytes.Equal(disc, jupiterExactOutRouteV2):
args, err := decodeJupiterV6ExactOutRouteV2Arg(instruction.Data[8:])
if err != nil {
return nil, err
}
exactOut = true
inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStatsV2(args.QuotedIn, args.Out, args.RoutePlan, false)
routeIn = args.QuotedIn
routeOut = args.Out
case bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2):
args, err := decodeJupiterV6SharedAccountsExactOutRouteV2Arg(instruction.Data[8:])
if err != nil {
return nil, err
}
exactOut = true
inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStatsV2(args.QuotedIn, args.Out, args.RoutePlan, false)
routeIn = args.QuotedIn
routeOut = args.Out
case bytes.Equal(disc, jupiterRoute): case bytes.Equal(disc, jupiterRoute):
args, err := decodeJupiterV6RouteArg(instruction.Data[8:]) args, err := decodeJupiterV6RouteArg(instruction.Data[8:])
if err != nil { if err != nil {
return nil, err return nil, err
} }
_ = args sig, handled, err := parseJupiterPumpAmmRoute(tx, instruction, args.In, args.QuotedOut, args.Plan)
inputAmount, planCount = pumpSwapSellAtIdx0(args.In, args.Plan) if err != nil {
return nil, err
}
if handled {
return sig, nil
}
inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStats(args.In, args.QuotedOut, args.Plan, true)
routeIn = args.In
routeOut = args.QuotedOut
case bytes.Equal(disc, jupiterSharedAccountsExactOutRoute):
args, err := decodeJupiterV6SharedAccountsExactOutRouteArg(instruction.Data[8:])
if err != nil {
return nil, err
}
exactOut = true
inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStats(args.QuotedIn, args.Out, args.Plan, false)
routeIn = args.QuotedIn
routeOut = args.Out
case bytes.Equal(disc, jupiterSharedAccountsRoute): case bytes.Equal(disc, jupiterSharedAccountsRoute):
args, err := decodeJupiterV6SharedAccountsRouteArg(instruction.Data[8:]) args, err := decodeJupiterV6SharedAccountsRouteArg(instruction.Data[8:])
if err != nil { if err != nil {
return nil, err return nil, err
} }
_ = args _ = args
inputAmount, planCount = pumpSwapSellAtIdx0(args.In, args.Plan) inputAmount, planCount, buySwap, buySwapCnt, wrapped, wrappedCnt, wrappedAny, wrappedAnyC = pumpRoutePlanStats(args.In, args.QuotedOut, args.Plan, true)
routeIn = args.In
routeOut = args.QuotedOut
default: default:
return nil, nil return nil, nil
} }
if bytes.Equal(disc, jupiterRoute) {
if len(instruction.Accounts) < 13 {
return nil, nil
}
destMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[5]))
if err != nil {
return nil, err
}
if isToken1Mint(destMint) {
pumpKey, err := getStaticKey(tx.Message.StaticAccountKeys, 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]))
if err != nil {
return nil, err
}
token0Amount := decimal.Zero
if routeIn > 0 {
token0Amount = formatTokenAmount(routeIn)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: token0Mint.String(),
Token1Address: destMint.String(),
Token0Amount: token0Amount,
Token1Amount: decimal.Zero,
Program: "Pump",
Event: "sell",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: false,
Block: tx.Block,
Token0AmountUint64: routeIn,
Token1AmountUint64: 0,
}, nil
}
token0Amount := decimal.Zero
if routeOut > 0 {
token0Amount = formatTokenAmount(routeOut)
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: destMint.String(),
Token1Address: wsolMint,
Token0Amount: token0Amount,
Token1Amount: decimal.Zero,
Program: "Pump",
Event: "buy",
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: false,
Block: tx.Block,
Token0AmountUint64: routeOut,
Token1AmountUint64: 0,
}, 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)
if err != nil {
return nil, err
}
if !ok {
return nil, nil
}
token1Mint := solana.WrappedSol
token1IsStable := false
srcMint, dstMint, ok, err := jupiterV6SourceDestMints(tx.Message, instruction, disc)
if err != nil {
return nil, err
}
if isJupiterV6Token1RequiredDisc(disc) {
if !ok {
return nil, nil
}
if !isToken1Mint(srcMint) && !isToken1Mint(dstMint) {
return nil, nil
}
}
if ok {
if srcMint.Equals(solana.WrappedSol) || dstMint.Equals(solana.WrappedSol) {
token1Mint = solana.WrappedSol
} else if isStableMint(srcMint) {
token1Mint = srcMint
token1IsStable = true
} else if isStableMint(dstMint) {
token1Mint = dstMint
token1IsStable = true
}
}
event := "sell"
exactSol := false
var (
token0AmountUint64 uint64
token1AmountUint64 uint64
)
if wrapped.IsBuy {
event = "buy"
exactSol = !exactOut
token0AmountUint64 = wrapped.OutAmount
token1AmountUint64 = wrapped.InAmount
} else {
exactSol = exactOut && wrapped.OutAmount > 0
token0AmountUint64 = wrapped.InAmount
token1AmountUint64 = wrapped.OutAmount
}
token0Amount := decimal.Zero
if token0AmountUint64 > 0 {
token0Amount = formatTokenAmount(token0AmountUint64)
}
token1Amount := decimal.Zero
if token1AmountUint64 > 0 {
if token1IsStable {
token1Amount = formatTokenAmount(token1AmountUint64)
} else {
token1Amount = formatSolAmount(token1AmountUint64)
}
}
token1Address := wsolMint
if token1IsStable {
token1Address = token1Mint.String()
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: mint.String(),
Token1Address: token1Address,
Token0Amount: token0Amount,
Token1Amount: token1Amount,
Program: "Pump",
Event: event,
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: exactSol,
Block: tx.Block,
Token0AmountUint64: token0AmountUint64,
Token1AmountUint64: token1AmountUint64,
}, 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)
if err != nil {
return nil, err
}
if !ok {
return nil, nil
}
token1Mint := solana.WrappedSol
token1IsStable := false
srcMint, dstMint, ok, err := jupiterV6SourceDestMints(tx.Message, instruction, disc)
if err != nil {
return nil, err
}
if isJupiterV6Token1RequiredDisc(disc) {
if !ok {
return nil, nil
}
if !isToken1Mint(srcMint) && !isToken1Mint(dstMint) {
return nil, nil
}
}
if ok {
if srcMint.Equals(solana.WrappedSol) || dstMint.Equals(solana.WrappedSol) {
token1Mint = solana.WrappedSol
} else if isStableMint(srcMint) {
token1Mint = srcMint
token1IsStable = true
} else if isStableMint(dstMint) {
token1Mint = dstMint
token1IsStable = true
}
}
event := "sell"
exactSol := false
var (
token0AmountUint64 uint64
token1AmountUint64 uint64
)
if wrappedAny.IsBuy {
event = "buy"
exactSol = !exactOut
token0AmountUint64 = routeOut
token1AmountUint64 = routeIn
} else {
exactSol = exactOut && routeOut > 0
token0AmountUint64 = routeIn
token1AmountUint64 = routeOut
}
token0Amount := decimal.Zero
if token0AmountUint64 > 0 {
token0Amount = formatTokenAmount(token0AmountUint64)
}
token1Amount := decimal.Zero
if token1AmountUint64 > 0 {
if token1IsStable {
token1Amount = formatTokenAmount(token1AmountUint64)
} else {
token1Amount = formatSolAmount(token1AmountUint64)
}
}
token1Address := wsolMint
if token1IsStable {
token1Address = token1Mint.String()
}
return &TxSignal{
TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: mint.String(),
Token1Address: token1Address,
Token0Amount: token0Amount,
Token1Amount: token1Amount,
Program: "Pump",
Event: event,
IsToken2022: false,
IsMayhemMode: false,
ExactSOL: exactSol,
Block: tx.Block,
Token0AmountUint64: token0AmountUint64,
Token1AmountUint64: token1AmountUint64,
}, nil
}
if planCount > 1 { if planCount > 1 {
// multiple pumpSwapSell at inputIdx=0? should not happen // multiple pumpSwapSell at inputIdx=0? should not happen
logger.Warn("pumpSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", planCount) logger.Warn("pumpSwapSell at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", planCount)
} }
if inputAmount == 0 { if buySwapCnt > 1 {
// multiple pumpSwapBuy at inputIdx=0? should not happen
logger.Warn("pumpSwapBuy at inputIdx=0: multiple instances found", "tx", tx.Signatures[0].String(), "planCount", buySwapCnt)
}
hasSell := inputAmount > 0
hasBuy := buySwap.InAmount > 0
if hasSell && hasBuy {
logger.Warn("pumpSwap buy/sell at inputIdx=0: both found", "tx", tx.Signatures[0].String(), "sellCount", planCount, "buyCount", buySwapCnt)
return nil, nil
}
if !hasSell && !hasBuy {
return nil, nil return nil, nil
} }
var (
baseMint solana.PublicKey
quoteMint solana.PublicKey
destMint solana.PublicKey
destMintOK bool
sourceMintOK bool
)
// existing mint extraction logic only valid for route_v2/ exact_out_route_v2. Keep it but guard. // existing mint extraction logic only valid for route_v2/ exact_out_route_v2. Keep it but guard.
if bytes.Equal(disc, jupiterRouteV2) || bytes.Equal(disc, jupiterSharedAccountsRouteV2) { if bytes.Equal(disc, jupiterRouteV2) ||
bytes.Equal(disc, jupiterSharedAccountsRouteV2) ||
bytes.Equal(disc, jupiterExactOutRouteV2) ||
bytes.Equal(disc, jupiterSharedAccountsExactOutRouteV2) {
if len(instruction.Accounts) < 6 { if len(instruction.Accounts) < 6 {
return nil, fmt.Errorf("not enough accounts for jupiter v6 v2 instruction") return nil, fmt.Errorf("not enough accounts for jupiter v6 v2 instruction")
} }
@@ -931,6 +1622,12 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
destMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[4]))
if err != nil {
return nil, err
}
destMintOK = true
sourceMintOK = true
var ( var (
srcIdx uint8 srcIdx uint8
@@ -953,14 +1650,11 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
return nil, nil return nil, nil
} }
baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !sourceMint.Equals(baseMint) { quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
return nil, nil
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -968,7 +1662,7 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
return nil, nil return nil, nil
} }
} else if bytes.Equal(disc, jupiterSharedAccountsRoute) { } else if bytes.Equal(disc, jupiterSharedAccountsRoute) || bytes.Equal(disc, jupiterSharedAccountsExactOutRoute) {
if len(instruction.Accounts) < 12 { if len(instruction.Accounts) < 12 {
return nil, fmt.Errorf("not enough accounts for jupiter v6 jupiterSharedAccountsRoute instruction") return nil, fmt.Errorf("not enough accounts for jupiter v6 jupiterSharedAccountsRoute instruction")
} }
@@ -976,6 +1670,12 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
destMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(instruction.Accounts[8]))
if err != nil {
return nil, err
}
destMintOK = true
sourceMintOK = true
var ( var (
srcIdx uint8 srcIdx uint8
) )
@@ -997,15 +1697,12 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
return nil, nil return nil, nil
} }
baseMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !sourceMint.Equals(baseMint) {
return nil, nil
}
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1034,35 +1731,72 @@ func parseJupiterV6Instruction(tx *versionedTransaction, instructionIndex int) (
if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) { if srcIdx == 0 || srcIdx+1 >= uint8(len(accounts)) {
return nil, nil return nil, nil
} }
sourceMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx])) baseMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
quoteMint, err := getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1])) quoteMint, err = getStaticKey(tx.Message.StaticAccountKeys, int(accounts[srcIdx+1]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !quoteMint.Equals(solana.WrappedSol) { if !quoteMint.Equals(solana.WrappedSol) {
return nil, nil return nil, nil
} }
sourceMint = baseMint
}
if hasSell {
if sourceMintOK && !sourceMint.Equals(baseMint) {
return nil, nil
}
} else {
if !sourceMintOK {
return nil, nil
}
if !sourceMint.Equals(solana.WrappedSol) && !sourceMint.Equals(solana.SystemProgramID) {
return nil, nil
}
if destMintOK && !destMint.Equals(baseMint) {
return nil, nil
}
}
event := "sell"
exactSol := false
token0AmountUint64 := inputAmount
token1AmountUint64 := uint64(0)
if hasBuy {
event = "buy"
exactSol = !exactOut
token0AmountUint64 = buySwap.OutAmount
token1AmountUint64 = buySwap.InAmount
}
token0Amount := decimal.Zero
if token0AmountUint64 > 0 {
token0Amount = formatTokenAmount(token0AmountUint64)
}
token1Amount := decimal.Zero
if token1AmountUint64 > 0 {
token1Amount = formatSolAmount(token1AmountUint64)
} }
signal := &TxSignal{ signal := &TxSignal{
TxHash: tx.Signatures[0].String(), TxHash: tx.Signatures[0].String(),
Maker: tx.Message.StaticAccountKeys[0].String(), Maker: tx.Message.StaticAccountKeys[0].String(),
Token0Address: sourceMint.String(), Token0Address: baseMint.String(),
Token1Address: wsolMint, Token1Address: wsolMint,
Token0Amount: formatTokenAmount(inputAmount), Token0Amount: token0Amount,
Token1Amount: decimal.Zero, Token1Amount: token1Amount,
Program: "PumpAMM", Program: "PumpAMM",
Event: "sell", Event: event,
IsToken2022: false, IsToken2022: false,
IsMayhemMode: false, IsMayhemMode: false,
ExactSOL: false, ExactSOL: exactSol,
Block: tx.Block, Block: tx.Block,
Token0AmountUint64: inputAmount, Token0AmountUint64: token0AmountUint64,
Token1AmountUint64: 0, Token1AmountUint64: token1AmountUint64,
} }
return signal, nil return signal, nil

View File

@@ -48,6 +48,13 @@ type TxSignal struct {
ExactSOL bool `json:"exact_in"` ExactSOL bool `json:"exact_in"`
//Just for metaora DLMM
// ActiveBin is the active bin id provided by swap_with_price_impact(2).
ActiveBin int32 `json:"active_bin"`
// MaxPriceImpactBps is the price impact guard for swap_with_price_impact(2).
MaxPriceImpactBps uint16 `json:"max_price_impact_bps"`
// parsed values // parsed values
Token0AmountUint64 uint64 `json:"-"` Token0AmountUint64 uint64 `json:"-"`
Token1AmountUint64 uint64 `json:"-"` Token1AmountUint64 uint64 `json:"-"`

View File

@@ -49,6 +49,11 @@ var (
gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb") gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb")
bonkProgramID = solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD") bonkProgramID = solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD")
bloomRouterProgramID = solana.MustPublicKeyFromBase58("b1oomGGqPKGD6errbyfbVMBuzSC8WtAAYo8MwNafWW1")
// For Metaora dlmm
dlmmProgramID = solana.MustPublicKeyFromBase58("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo")
) )
type AccountNotFoundError struct { type AccountNotFoundError struct {
@@ -102,6 +107,13 @@ var (
gmgnBuyTokensIX = []byte{0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea} 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} 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 { type compiledInstruction struct {
@@ -175,6 +187,12 @@ type photonSwapPumpAmmArgs struct {
ToAmount uint64 ToAmount uint64
} }
type bloomRouterArgs struct {
Side uint16
SolAmount uint64
TokenAmount uint64
}
type pumpAmmBuyArgs struct { type pumpAmmBuyArgs struct {
Amount uint64 Amount uint64
MaxSolCost uint64 MaxSolCost uint64
@@ -342,13 +360,19 @@ func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables,
parsed = appendParsed(now, parsed, txRes, err, txHash, "okxdexroutev2") parsed = appendParsed(now, parsed, txRes, err, txHash, "okxdexroutev2")
case dflowProgramID: case dflowProgramID:
txRes, err := parseDFlowInstruction(versioned, i) txRes, err := parseDFlowInstruction(versioned, i)
parsed = appendParsed(now, parsed, txRes, err, txHash, "dflow") parsed = appendParsedBatch(now, parsed, txRes, err, txHash, "dflow")
case gmgnProgramID: case gmgnProgramID:
txRes, err := parseGMGNInstruction(versioned, i) txRes, err := parseGMGNInstruction(versioned, i)
parsed = appendParsed(now, parsed, txRes, err, txHash, "gmgn") parsed = appendParsed(now, parsed, txRes, err, txHash, "gmgn")
case bonkProgramID: case bonkProgramID:
txRes, err := parseBonkInstruction(versioned, i) txRes, err := parseBonkInstruction(versioned, i)
parsed = appendParsed(now, parsed, txRes, err, txHash, "bonk") 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")
} }
} }
@@ -373,6 +397,34 @@ func appendParsed(start time.Time, list []*TxSignal, parsed *TxSignal, err error
return list 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 {
continue
}
sig.Label = label
if !start.IsZero() {
sig.ParseEnd = end
sig.ParseStart = start
}
list = append(list, sig)
}
return list
}
func toVersionedTransaction(update *SubscribeUpdateTransaction) (*versionedTransaction, error) { func toVersionedTransaction(update *SubscribeUpdateTransaction) (*versionedTransaction, error) {
if update == nil || update.Transaction == nil || update.Transaction.Message == nil { if update == nil || update.Transaction == nil || update.Transaction.Message == nil {
return nil, fmt.Errorf("transaction is nil") return nil, fmt.Errorf("transaction is nil")
@@ -1344,6 +1396,222 @@ func parseTermSell(tx *versionedTransaction, instruction *compiledInstruction) (
}, nil }, 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[:],
tokenProgram[:],
mint[:],
}, 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) { func decodePumpAmmBuyArgs(data []byte) (uint64, uint64, error) {
if len(data) < 9 { if len(data) < 9 {
return 0, 0, fmt.Errorf("data too short for pump amm buy args, len=%d", len(data)) return 0, 0, fmt.Errorf("data too short for pump amm buy args, len=%d", len(data))
@@ -1803,6 +2071,101 @@ func parseBonkBuyAndSell(tx *versionedTransaction, instruction *compiledInstruct
} }
} }
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 { func matchMethod(data []byte, methods []byte) bool {
if len(data) < len(methods) { if len(data) < len(methods) {
return false return false

View File

@@ -289,3 +289,83 @@ func TestParsePhotonBuy(t *testing.T) {
t.Fatalf("expected token1 amount 1955555553, got %d", signal.Token1AmountUint64) t.Fatalf("expected token1 amount 1955555553, got %d", signal.Token1AmountUint64)
} }
} }
func TestParseJupiterV6PumpFunBuy(t *testing.T) {
rpcUrl := os.Getenv("SOL_RPC_URL")
if rpcUrl == "" {
t.Fatalf("SOL_RPC_URL is not set")
}
client := rpc.New(rpcUrl)
signals := ParseTransaction(
getTransaction(t, client, "4QF5whXwjx234fMXeH3HrJCy5knFJmKPtgbXys8xKGz1pZypqPvXBr4BoAqXfYn8jLL4HXPY1pcvxCCW1XREFNxd"),
nil,
false,
)
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "jupiterv6" {
t.Fatalf("expected jupiterv6 signal, got %s", signal.Label)
}
if signal.Event != "buy" {
t.Fatalf("expected buy event, got %s", signal.Event)
}
if signal.Maker != "92ySgsZs3rsrUAq2aeEqYacXQQGmz6e4xHPrRGxLDJXb" {
t.Fatalf("expected maker 92ySgsZs3rsrUAq2aeEqYacXQQGmz6e4xHPrRGxLDJXb, got %s", signal.Maker)
}
if signal.Token0Address != "5kSWidFwDKPZiNf52TfincpVn8ufvkAfEzZ9pk8Dpump" {
t.Fatalf("expected token0 address 5kSWidFwDKPZiNf52TfincpVn8ufvkAfEzZ9pk8Dpump, got %s", signal.Token0Address)
}
if signal.Token0AmountUint64 != 2410530637576 {
t.Fatalf("expected token0 amount 2410530637576, got %d", signal.Token0AmountUint64)
}
if signal.Token1AmountUint64 != 380000000 {
t.Fatalf("expected token1 amount 380000000, got %d", signal.Token1AmountUint64)
}
if !signal.ExactSOL {
t.Fatalf("expected ExactSOL true, got false")
}
}
func TestParseJupiterV6PumpFunSell(t *testing.T) {
rpcUrl := os.Getenv("SOL_RPC_URL")
if rpcUrl == "" {
t.Fatalf("SOL_RPC_URL is not set")
}
client := rpc.New(rpcUrl)
signals := ParseTransaction(
getTransaction(t, client, "yCnE7ZA8dqB5iAZtwpSN2ar5HXh3gBjgaG2xtnwXDPFyHAm5XFU8642uTZTH5A2iPQ6G9hrj5eEPAJiWrfe38gM"),
nil,
false,
)
if len(signals) != 1 {
t.Fatalf("expected 1 signal, got %d", len(signals))
}
signal := signals[0]
if signal.Label != "jupiterv6" {
t.Fatalf("expected jupiterv6 signal, got %s", signal.Label)
}
if signal.Event != "sell" {
t.Fatalf("expected sell event, got %s", signal.Event)
}
if signal.Maker != "CGfWcKKcVQNBCL1vpxXdg6rvfYpQmnS3WkyA22Lk5XnZ" {
t.Fatalf("expected maker CGfWcKKcVQNBCL1vpxXdg6rvfYpQmnS3WkyA22Lk5XnZ, got %s", signal.Maker)
}
if signal.Token0Address != "wp8Mwxy7btAD9hNWsfJyoPNJnjXS9fuNG4mnhQZpump" {
t.Fatalf("expected token0 address wp8Mwxy7btAD9hNWsfJyoPNJnjXS9fuNG4mnhQZpump, got %s", signal.Token0Address)
}
if signal.Token0AmountUint64 != 127531720509990 {
t.Fatalf("expected token0 amount 127531720509990, got %d", signal.Token0AmountUint64)
}
if signal.Token1AmountUint64 != 5296451290 {
t.Fatalf("expected token1 amount 5296451290, got %d", signal.Token1AmountUint64)
}
if signal.ExactSOL {
t.Fatalf("expected ExactSOL false, got true")
}
}