From 9b8f784c33cdec858876bd1ffe45daf67de62cfe Mon Sep 17 00:00:00 2001 From: datluongductuan Date: Tue, 16 Jun 2026 14:58:41 +0700 Subject: [PATCH 1/4] feat(types): add BloxrouteBackrunConfig on Message Add BloxrouteBackrunConfig (the per-tx BackRunMe profit split) as a typed field on Message, mirroring FlashbotMevshareEvent, plus ParseBloxrouteBackrunConfig (the 5-element arbOnlyMEV array parser) + Validate + BlxrSplit. mempool-explorer parses the stream array once and attaches it here, so consumers read Message.BloxrouteBackrunConfig directly (nil when absent) instead of re-unmarshalling raw event bytes. Co-Authored-By: Claude Opus 4.8 --- pkg/types/bloxroute_backrun_config.go | 91 +++++++++++++++++++++++++++ pkg/types/pendingtx.go | 3 + 2 files changed, 94 insertions(+) create mode 100644 pkg/types/bloxroute_backrun_config.go diff --git a/pkg/types/bloxroute_backrun_config.go b/pkg/types/bloxroute_backrun_config.go new file mode 100644 index 0000000..e5cc1c8 --- /dev/null +++ b/pkg/types/bloxroute_backrun_config.go @@ -0,0 +1,91 @@ +package types + +import ( + "errors" + "fmt" + "strconv" + + "github.com/ethereum/go-ethereum/common" +) + +var ErrInvalidBloxrouteBackrunConfig = errors.New("invalid bloxroute backrun config") + +// BloxrouteBackrunConfig is the per-tx BackRunMe profit-split config from bloxroute's +// arbOnlyMEV stream. On the wire from bloxroute it arrives as a 5-element string array +// [contractSplit, targetRewardAddress, targetSplit, blxrRewardAddress, searcherSplit]; +// mempool-explorer parses it once via ParseBloxrouteBackrunConfig and attaches the typed +// value to Message.BloxrouteBackrunConfig, so downstream consumers read it directly (nil +// when the tx is not a bloxroute backrun). +type BloxrouteBackrunConfig struct { + ContractSplit float64 `json:"contract_split"` + TargetRewardAddress string `json:"target_reward_address"` + TargetSplit float64 `json:"target_split"` + BlxrRewardAddress string `json:"blxr_reward_address"` + SearcherSplit float64 `json:"searcher_split"` +} + +// ParseBloxrouteBackrunConfig parses the 5-element backrunConfig array from the arbOnlyMEV +// stream into a validated BloxrouteBackrunConfig. Element order is +// [contractSplit, targetRewardAddress, targetSplit, blxrRewardAddress, searcherSplit]. +func ParseBloxrouteBackrunConfig(parts []string) (*BloxrouteBackrunConfig, error) { + if len(parts) != 5 { + return nil, fmt.Errorf("%w: expected 5 elements, got %d", ErrInvalidBloxrouteBackrunConfig, len(parts)) + } + + contractSplit, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, fmt.Errorf("%w: contract split: %w", ErrInvalidBloxrouteBackrunConfig, err) + } + targetSplit, err := strconv.ParseFloat(parts[2], 64) + if err != nil { + return nil, fmt.Errorf("%w: target split: %w", ErrInvalidBloxrouteBackrunConfig, err) + } + searcherSplit, err := strconv.ParseFloat(parts[4], 64) + if err != nil { + return nil, fmt.Errorf("%w: searcher split: %w", ErrInvalidBloxrouteBackrunConfig, err) + } + + cfg := BloxrouteBackrunConfig{ + ContractSplit: contractSplit, + TargetRewardAddress: parts[1], + TargetSplit: targetSplit, + BlxrRewardAddress: parts[3], + SearcherSplit: searcherSplit, + } + if err := cfg.Validate(); err != nil { + return nil, err + } + + return &cfg, nil +} + +// Validate checks the split percentages are non-negative, sum to <= 100, and the reward +// addresses are valid non-zero addresses. +func (c BloxrouteBackrunConfig) Validate() error { + if c.ContractSplit < 0 || c.TargetSplit < 0 || c.SearcherSplit < 0 { + return fmt.Errorf("%w: splits must be non-negative", ErrInvalidBloxrouteBackrunConfig) + } + if _, err := c.BlxrSplit(); err != nil { + return err + } + if !isValidNonZeroAddress(c.TargetRewardAddress) { + return fmt.Errorf("%w: invalid target reward address", ErrInvalidBloxrouteBackrunConfig) + } + if !isValidNonZeroAddress(c.BlxrRewardAddress) { + return fmt.Errorf("%w: invalid blxr reward address", ErrInvalidBloxrouteBackrunConfig) + } + return nil +} + +// BlxrSplit returns bloxroute's reward share: 100 - contract - target - searcher. +func (c BloxrouteBackrunConfig) BlxrSplit() (float64, error) { + blxrSplit := 100 - c.ContractSplit - c.TargetSplit - c.SearcherSplit + if blxrSplit < 0 { + return 0, fmt.Errorf("%w: split sum exceeds 100", ErrInvalidBloxrouteBackrunConfig) + } + return blxrSplit, nil +} + +func isValidNonZeroAddress(addr string) bool { + return common.IsHexAddress(addr) && common.HexToAddress(addr) != (common.Address{}) +} diff --git a/pkg/types/pendingtx.go b/pkg/types/pendingtx.go index 5781e84..624110b 100644 --- a/pkg/types/pendingtx.go +++ b/pkg/types/pendingtx.go @@ -44,6 +44,9 @@ type Message struct { ExtraData string `json:"extra_data"` RawTx string `json:"raw_tx"` BlockNumber uint64 `json:"block_number"` // valid in block + // BloxrouteBackrunConfig is the per-tx BackRunMe profit-split config, set only for + // bloxroute arbOnlyMEV-sourced txs (nil otherwise). + BloxrouteBackrunConfig *BloxrouteBackrunConfig `json:"bloxroute_backrun_config,omitempty"` } type Prestate struct { From 9c02ff7dc1a6bd7cc8e2fc538d0be38ca9ef4b97 Mon Sep 17 00:00:00 2001 From: datluongductuan Date: Tue, 16 Jun 2026 15:05:10 +0700 Subject: [PATCH 2/4] test(types): cover ParseBloxrouteBackrunConfig validation Co-Authored-By: Claude Opus 4.8 --- pkg/types/bloxroute_backrun_config_test.go | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 pkg/types/bloxroute_backrun_config_test.go diff --git a/pkg/types/bloxroute_backrun_config_test.go b/pkg/types/bloxroute_backrun_config_test.go new file mode 100644 index 0000000..0973198 --- /dev/null +++ b/pkg/types/bloxroute_backrun_config_test.go @@ -0,0 +1,52 @@ +package types + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseBloxrouteBackrunConfig(t *testing.T) { + const ( + targetAddress = "0x435D8cB64ba4B68f6A28405D3C19E7C169B01917" + blxrAddress = "0xF1Ce037Cc1d02c046e800C3265D2Efa91940864E" + ) + + cfg, err := ParseBloxrouteBackrunConfig([]string{"50", targetAddress, "15", blxrAddress, "20"}) + require.NoError(t, err) + require.Equal(t, 50.0, cfg.ContractSplit) + require.Equal(t, targetAddress, cfg.TargetRewardAddress) + require.Equal(t, 15.0, cfg.TargetSplit) + require.Equal(t, blxrAddress, cfg.BlxrRewardAddress) + require.Equal(t, 20.0, cfg.SearcherSplit) + + blxr, err := cfg.BlxrSplit() + require.NoError(t, err) + require.Equal(t, 15.0, blxr) // 100 - 50 - 15 - 20 +} + +func TestParseBloxrouteBackrunConfigErrors(t *testing.T) { + const ( + targetAddress = "0x435D8cB64ba4B68f6A28405D3C19E7C169B01917" + blxrAddress = "0xF1Ce037Cc1d02c046e800C3265D2Efa91940864E" + ) + + tests := []struct { + name string + parts []string + }{ + {"wrong length", []string{"50", targetAddress, "15", blxrAddress}}, + {"non-numeric split", []string{"x", targetAddress, "15", blxrAddress, "20"}}, + {"sum exceeds 100", []string{"60", targetAddress, "30", blxrAddress, "20"}}, + {"zero target address", []string{"50", "0x0000000000000000000000000000000000000000", "15", blxrAddress, "20"}}, + {"invalid blxr address", []string{"50", targetAddress, "15", "not-an-address", "20"}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := ParseBloxrouteBackrunConfig(tc.parts) + require.Error(t, err) + require.True(t, errors.Is(err, ErrInvalidBloxrouteBackrunConfig)) + }) + } +} From 3cb856f1a27b93b3592653ffceeafef0f2394abe Mon Sep 17 00:00:00 2001 From: datluongductuan Date: Tue, 16 Jun 2026 15:18:02 +0700 Subject: [PATCH 3/4] test(types): use external test package (testpackage lint) Co-Authored-By: Claude Opus 4.8 --- pkg/types/bloxroute_backrun_config_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/types/bloxroute_backrun_config_test.go b/pkg/types/bloxroute_backrun_config_test.go index 0973198..56c5355 100644 --- a/pkg/types/bloxroute_backrun_config_test.go +++ b/pkg/types/bloxroute_backrun_config_test.go @@ -1,9 +1,10 @@ -package types +package types_test import ( "errors" "testing" + "github.com/KyberNetwork/tradinglib/pkg/types" "github.com/stretchr/testify/require" ) @@ -13,7 +14,7 @@ func TestParseBloxrouteBackrunConfig(t *testing.T) { blxrAddress = "0xF1Ce037Cc1d02c046e800C3265D2Efa91940864E" ) - cfg, err := ParseBloxrouteBackrunConfig([]string{"50", targetAddress, "15", blxrAddress, "20"}) + cfg, err := types.ParseBloxrouteBackrunConfig([]string{"50", targetAddress, "15", blxrAddress, "20"}) require.NoError(t, err) require.Equal(t, 50.0, cfg.ContractSplit) require.Equal(t, targetAddress, cfg.TargetRewardAddress) @@ -44,9 +45,9 @@ func TestParseBloxrouteBackrunConfigErrors(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - _, err := ParseBloxrouteBackrunConfig(tc.parts) + _, err := types.ParseBloxrouteBackrunConfig(tc.parts) require.Error(t, err) - require.True(t, errors.Is(err, ErrInvalidBloxrouteBackrunConfig)) + require.True(t, errors.Is(err, types.ErrInvalidBloxrouteBackrunConfig)) }) } } From 84af154dc08c114960e2bed6d84a52a0e2020145 Mon Sep 17 00:00:00 2001 From: datluongductuan Date: Tue, 16 Jun 2026 16:02:18 +0700 Subject: [PATCH 4/4] feat(mev): thread coinbase_profit into BackRunMe submit_arb_only_bundle bloxroute's submit_arb_only_bundle requires coinbase_profit (the wei paid to the BackRunMe contract). Add a coinbaseProfit *big.Int param to IBackrunSender.SendBackrunBundle; the BloxrouteBackrunmeSender sets it on the request params (JSON number, omitempty), the other senders ignore it. Test call sites pass nil. Co-Authored-By: Claude Opus 4.8 --- pkg/mev/backrun_public_sender.go | 2 ++ pkg/mev/backrun_public_sender_test.go | 2 +- pkg/mev/bloxroute_backrunme_sender.go | 5 +++++ pkg/mev/bloxroute_backrunme_sender_test.go | 2 +- pkg/mev/merkle_sender.go | 2 ++ pkg/mev/mevshare_sender.go | 2 ++ pkg/mev/mevshare_sender_test.go | 2 +- pkg/mev/pkg.go | 4 ++++ 8 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pkg/mev/backrun_public_sender.go b/pkg/mev/backrun_public_sender.go index ed2005e..791dfc1 100644 --- a/pkg/mev/backrun_public_sender.go +++ b/pkg/mev/backrun_public_sender.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "encoding/json" "fmt" + "math/big" "net/http" "github.com/ethereum/go-ethereum/common" @@ -44,6 +45,7 @@ func (b BackrunPublicClient) SendBackrunBundle( _ uint64, pendingTxHashes []common.Hash, _ []string, + _ *big.Int, txs ...*types.Transaction, ) (SendBundleResponse, error) { req := SendRequest{ diff --git a/pkg/mev/backrun_public_sender_test.go b/pkg/mev/backrun_public_sender_test.go index 9822a89..5708f5f 100644 --- a/pkg/mev/backrun_public_sender_test.go +++ b/pkg/mev/backrun_public_sender_test.go @@ -41,7 +41,7 @@ func TestNewBackrunPublicClient(t *testing.T) { pendingTXhash := common.HexToHash("0x79d48b1a25d7af0d815997d2ce3a127560080971c5ea98ca5a32424f604e09fb") resp, err := senderClient.SendBackrunBundle(context.Background(), nil, - blockNumber, blockNumber, []common.Hash{pendingTXhash}, []string{}, tx) + blockNumber, blockNumber, []common.Hash{pendingTXhash}, []string{}, nil, tx) t.Log("resp", resp) t.Log("err", err) } diff --git a/pkg/mev/bloxroute_backrunme_sender.go b/pkg/mev/bloxroute_backrunme_sender.go index bb3eda8..7f50923 100644 --- a/pkg/mev/bloxroute_backrunme_sender.go +++ b/pkg/mev/bloxroute_backrunme_sender.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "math/big" "net/http" "strings" @@ -35,6 +36,8 @@ type backrunmeRequestParams struct { Timestamp *uint64 `json:"timestamp,omitempty"` MinTimestamp *uint64 `json:"min_timestamp,omitempty"` MaxTimestamp *uint64 `json:"max_timestamp,omitempty"` + // CoinbaseProfit is the wei paid to the BackRunMe contract (a JSON number). + CoinbaseProfit *big.Int `json:"coinbase_profit,omitempty"` } type backrunmeResponse struct { @@ -130,6 +133,7 @@ func (s *BloxrouteBackrunmeSender) SendBackrunBundle( maxBlockNumber uint64, pendingTxHashes []common.Hash, targetBuilders []string, + coinbaseProfit *big.Int, txs ...*types.Transaction, ) (SendBundleResponse, error) { // Validate inputs @@ -165,6 +169,7 @@ func (s *BloxrouteBackrunmeSender) SendBackrunBundle( TransactionHash: pendingTxHashes[0].Hex(), Transaction: transactions, BlockNumber: fmt.Sprintf("0x%x", blockNumber), + CoinbaseProfit: coinbaseProfit, } // Build request diff --git a/pkg/mev/bloxroute_backrunme_sender_test.go b/pkg/mev/bloxroute_backrunme_sender_test.go index cb7285e..767be1b 100644 --- a/pkg/mev/bloxroute_backrunme_sender_test.go +++ b/pkg/mev/bloxroute_backrunme_sender_test.go @@ -33,7 +33,7 @@ func TestBloxrouteBackrunmeSender_SendBackrunBundle(t *testing.T) { pendingTXhash := common.HexToHash("0x79d48b1a25d7af0d815997d2ce3a127560080971c5ea98ca5a32424f604e09fb") - resp, err := sender.SendBackrunBundle(context.Background(), nil, 1, 1, []common.Hash{pendingTXhash}, []string{}, tx) + resp, err := sender.SendBackrunBundle(context.Background(), nil, 1, 1, []common.Hash{pendingTXhash}, []string{}, nil, tx) require.NoError(t, err) t.Log("resp", resp.Result.BundleHash) } diff --git a/pkg/mev/merkle_sender.go b/pkg/mev/merkle_sender.go index 1a74f05..fbd9785 100644 --- a/pkg/mev/merkle_sender.go +++ b/pkg/mev/merkle_sender.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "encoding/json" "fmt" + "math/big" "net/http" "github.com/ethereum/go-ethereum/common" @@ -44,6 +45,7 @@ func (b MerkleClient) SendBackrunBundle( _ uint64, pendingTxHashes []common.Hash, _ []string, + _ *big.Int, txs ...*types.Transaction, ) (SendBundleResponse, error) { req := SendRequest{ diff --git a/pkg/mev/mevshare_sender.go b/pkg/mev/mevshare_sender.go index 6190177..a94d374 100644 --- a/pkg/mev/mevshare_sender.go +++ b/pkg/mev/mevshare_sender.go @@ -3,6 +3,7 @@ package mev import ( "context" "crypto/ecdsa" + "math/big" "github.com/duoxehyon/mev-share-go/rpc" "github.com/ethereum/go-ethereum/common" @@ -39,6 +40,7 @@ func (m FlashbotMevShareSender) SendBackrunBundle( maxBlockNumber uint64, pendingTxHashes []common.Hash, targetBuilders []string, + _ *big.Int, txs ...*types.Transaction, ) (SendBundleResponse, error) { if m.client == nil { diff --git a/pkg/mev/mevshare_sender_test.go b/pkg/mev/mevshare_sender_test.go index fface98..5699d15 100644 --- a/pkg/mev/mevshare_sender_test.go +++ b/pkg/mev/mevshare_sender_test.go @@ -45,7 +45,7 @@ func TestSendBackrunBundle(t *testing.T) { // Send bundle res, err := rpcClient.SendBackrunBundle(context.Background(), nil, - blockNumber+1, blockNumber+1, []common.Hash{pendingTxHash}, nil, tx) + blockNumber+1, blockNumber+1, []common.Hash{pendingTxHash}, nil, nil, tx) assert.Nil(t, err) t.Log(res, "result") diff --git a/pkg/mev/pkg.go b/pkg/mev/pkg.go index dbbb98c..a718004 100644 --- a/pkg/mev/pkg.go +++ b/pkg/mev/pkg.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "math/big" "net/http" "strings" "time" @@ -91,6 +92,9 @@ type IBackrunSender interface { maxBlockNumber uint64, pendingTxHashes []common.Hash, targetBuilders []string, + // coinbaseProfit is the wei amount paid to the BackRunMe contract, reported to + // bloxroute via submit_arb_only_bundle. nil for non-bloxroute senders (ignored). + coinbaseProfit *big.Int, tx ...*types.Transaction, ) (SendBundleResponse, error) // MevSimulateBundle only use for backrun simulate with pending tx hash