From da51b19b502d5b4d2d3e303d0eac83e1027d6e23 Mon Sep 17 00:00:00 2001 From: samlior Date: Thu, 8 Jan 2026 16:07:57 +0800 Subject: [PATCH] chore: add bonk parser --- pkg/shreder/txparser.go | 100 +++++++++++++++++++++++++++++++++++ pkg/shreder/txparser_test.go | 100 +++++++++++++++++++++++++++++++---- 2 files changed, 191 insertions(+), 9 deletions(-) diff --git a/pkg/shreder/txparser.go b/pkg/shreder/txparser.go index 3dab9de..8d39243 100644 --- a/pkg/shreder/txparser.go +++ b/pkg/shreder/txparser.go @@ -47,6 +47,8 @@ var ( jupiterV6ProgramID = solana.MustPublicKeyFromBase58("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4") gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb") + + bonkProgramID = solana.MustPublicKeyFromBase58("BBRouter1cVunVXvkcqeKkZQcBK7ruan37PPm3xzWaXD") ) type AccountNotFoundError struct { @@ -98,6 +100,8 @@ var ( terminalAmmSellTokensIX = []byte{0x40, 0x64, 0x97, 0xb9, 0x16, 0xfa, 0xec, 0xb1} gmgnBuyTokensIX = []byte{0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea} + + bonkBuyAndSellTokensIX = []byte{0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a} ) type compiledInstruction struct { @@ -342,6 +346,9 @@ func ParseTransaction(update *SubscribeUpdateTransaction, loader *AddressTables, 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") } } @@ -1702,6 +1709,99 @@ func parseFjszInstruction(tx *versionedTransaction, instructionIndex int) (*TxSi }, 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 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 68b7b52..651529b 100644 --- a/pkg/shreder/txparser_test.go +++ b/pkg/shreder/txparser_test.go @@ -120,17 +120,11 @@ func toUpdata(slot uint64, tx *solana.Transaction) *SubscribeUpdateTransaction { } } -func TestParseTerm(t *testing.T) { - rpcUrl := os.Getenv("SOL_RPC_URL") - if rpcUrl == "" { - t.Fatalf("SOL_RPC_URL is not set") - } - - client := rpc.New(rpcUrl) +func getTransaction(t *testing.T, client *rpc.Client, signature string) *SubscribeUpdateTransaction { version := uint64(0) tx, err := client.GetTransaction( context.Background(), - solana.MustSignatureFromBase58("5Gz1fa4Qhb35bkg9QCMXpxCX5uuNr7WcjcmrwajGZA7kXsvNS9pDnYe12ggWeSqf1nwZbVPob6DkX6fcwbE9ofBR"), + solana.MustSignatureFromBase58(signature), &rpc.GetTransactionOpts{ Commitment: rpc.CommitmentFinalized, MaxSupportedTransactionVersion: &version, @@ -145,7 +139,21 @@ func TestParseTerm(t *testing.T) { t.Fatalf("failed to get transaction: %v", err) } - signals := ParseTransaction(toUpdata(tx.Slot, _tx), nil, false) + return toUpdata(tx.Slot, _tx) +} + +func TestParseTermBuy(t *testing.T) { + rpcUrl := os.Getenv("SOL_RPC_URL") + if rpcUrl == "" { + t.Fatalf("SOL_RPC_URL is not set") + } + + client := rpc.New(rpcUrl) + signals := ParseTransaction( + getTransaction(t, client, "5Gz1fa4Qhb35bkg9QCMXpxCX5uuNr7WcjcmrwajGZA7kXsvNS9pDnYe12ggWeSqf1nwZbVPob6DkX6fcwbE9ofBR"), + nil, + false, + ) if len(signals) != 1 { t.Fatalf("expected 1 signal, got %d", len(signals)) } @@ -164,3 +172,77 @@ func TestParseTerm(t *testing.T) { t.Fatalf("expected token0 address 5Wgv54peXRKDHYHapAELzgNKEPEh9E5Bf3hUR3sTpump, got %s", signal.Token0Address) } } + +func TestParseBonkBuy(t *testing.T) { + rpcUrl := os.Getenv("SOL_RPC_URL") + if rpcUrl == "" { + t.Fatalf("SOL_RPC_URL is not set") + } + + client := rpc.New(rpcUrl) + signals := ParseTransaction( + getTransaction(t, client, "3gHF3TA2aA8rpjdmoEs2vA89vrq9J9NnTTUSXHfE6uXcaYP9cJgLtEUjCmsK9EWAyHEg7cEiepehQf4GFv1272jW"), + nil, + false, + ) + if len(signals) != 1 { + t.Fatalf("expected 1 signal, got %d", len(signals)) + } + + signal := signals[0] + if signal.Label != "bonk" { + t.Fatalf("expected bonk signal, got %s", signal.Label) + } + if signal.Event != "buy" { + t.Fatalf("expected buy event, got %s", signal.Event) + } + if signal.Maker != "BFobdhAbdBteBuDvHUdBthsQqJyMuWnG9SGUheW1Ni2C" { + t.Fatalf("expected maker BFobdhAbdBteBuDvHUdBthsQqJyMuWnG9SGUheW1Ni2C, got %s", signal.Maker) + } + if signal.Token0Address != "Awupo9Jxe1fsc7eEtCEcN9D3PoyReQhc9WEuEAHXpump" { + t.Fatalf("expected token0 address Awupo9Jxe1fsc7eEtCEcN9D3PoyReQhc9WEuEAHXpump, got %s", signal.Token0Address) + } + if signal.Token0AmountUint64 != 8616799656436 { + t.Fatalf("expected token0 amount 8616799656436, got %d", signal.Token0AmountUint64) + } + if signal.Token1AmountUint64 != 495000000 { + t.Fatalf("expected token1 amount 495000000, got %d", signal.Token1AmountUint64) + } +} + +func TestParseBonkSell(t *testing.T) { + rpcUrl := os.Getenv("SOL_RPC_URL") + if rpcUrl == "" { + t.Fatalf("SOL_RPC_URL is not set") + } + + client := rpc.New(rpcUrl) + signals := ParseTransaction( + getTransaction(t, client, "3XNi6b3j69SSStqLLRQVH5BNGVfEoFxGCzmpdd5FvrY4kmC8T644WGdEhCH9fAdrxWuR2Mtzgywq8K7qetu5MGyb"), + nil, + false, + ) + if len(signals) != 1 { + t.Fatalf("expected 1 signal, got %d", len(signals)) + } + + signal := signals[0] + if signal.Label != "bonk" { + t.Fatalf("expected bonk signal, got %s", signal.Label) + } + if signal.Event != "sell" { + t.Fatalf("expected sell event, got %s", signal.Event) + } + if signal.Maker != "2xTT7XXCEYSCrRb3G4Egc4ZwpCe78qq6r7w6ChZhbTXc" { + t.Fatalf("expected maker 2xTT7XXCEYSCrRb3G4Egc4ZwpCe78qq6r7w6ChZhbTXc, got %s", signal.Maker) + } + if signal.Token0Address != "8pgpJDYuojYXvb8KE4Hv7DCty12FrkqpKChgfHzspump" { + t.Fatalf("expected token0 address 8pgpJDYuojYXvb8KE4Hv7DCty12FrkqpKChgfHzspump, got %s", signal.Token0Address) + } + if signal.Token0AmountUint64 != 6235736929390 { + t.Fatalf("expected token0 amount 6235736929390, got %d", signal.Token0AmountUint64) + } + if signal.Token1AmountUint64 != 1379707703 { + t.Fatalf("expected token1 amount 1379707703, got %d", signal.Token1AmountUint64) + } +}