Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 47 additions & 30 deletions tools/preconf-rpc/fastswap/fastswap.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
fastsettlementv3 "github.com/primev/mev-commit/contracts-abi/clients/FastSettlementV3"
bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1"
"github.com/primev/mev-commit/tools/preconf-rpc/sender"
)

Expand All @@ -32,16 +33,17 @@ type SwapCall = fastsettlementv3.IFastSettlementV3SwapCall

// SwapRequest is the HTTP request body for the /fastswap endpoint.
type SwapRequest struct {
User common.Address `json:"user"`
InputToken common.Address `json:"inputToken"`
OutputToken common.Address `json:"outputToken"`
InputAmt *big.Int `json:"inputAmt"`
UserAmtOut *big.Int `json:"userAmtOut"`
Recipient common.Address `json:"recipient"`
Deadline *big.Int `json:"deadline"`
Nonce *big.Int `json:"nonce"`
Signature []byte `json:"signature"` // EIP-712 Permit2 signature
Slippage string `json:"slippage,omitempty"` // User slippage percentage (e.g. "1.0" for 1%)
User common.Address `json:"user"`
InputToken common.Address `json:"inputToken"`
OutputToken common.Address `json:"outputToken"`
InputAmt *big.Int `json:"inputAmt"`
UserAmtOut *big.Int `json:"userAmtOut"`
Recipient common.Address `json:"recipient"`
Deadline *big.Int `json:"deadline"`
Nonce *big.Int `json:"nonce"`
Signature []byte `json:"signature"` // EIP-712 Permit2 signature
Slippage string `json:"slippage,omitempty"` // User slippage percentage (e.g. "1.0" for 1%)
TopPercentile int32 `json:"topPercentile,omitempty"` // If 1-100, apply top-of-block PositionConstraint at this percentile. 0 = disabled.
}

// ToIntent converts SwapRequest to the generated Intent type for ABI encoding.
Expand Down Expand Up @@ -455,6 +457,13 @@ func (s *Service) HandleSwap(ctx context.Context, req SwapRequest) (*SwapResult,
Raw: rawTxHex,
Type: sender.TxTypeFastSwap,
}
if req.TopPercentile > 0 {
senderTx.Constraint = &bidderapiv1.PositionConstraint{
Anchor: bidderapiv1.PositionConstraint_ANCHOR_TOP,
Basis: bidderapiv1.PositionConstraint_BASIS_PERCENTILE,
Value: req.TopPercentile,
}
}

if err := s.txEnqueuer.Enqueue(ctx, senderTx); err != nil {
return &SwapResult{
Expand All @@ -476,6 +485,7 @@ func (s *Service) HandleSwap(ctx context.Context, req SwapRequest) (*SwapResult,
"gasFeeCap", gasFeeCap.String(),
"gasTipCap", gasTipCap.String(),
"nonce", nonce,
"topPercentile", req.TopPercentile,
)

return &SwapResult{
Expand All @@ -497,16 +507,17 @@ func (s *Service) Handler() http.HandlerFunc {
}

var rawReq struct {
User string `json:"user"`
InputToken string `json:"inputToken"`
OutputToken string `json:"outputToken"`
InputAmt string `json:"inputAmt"`
UserAmtOut string `json:"userAmtOut"`
Recipient string `json:"recipient"`
Deadline string `json:"deadline"`
Nonce string `json:"nonce"`
Signature string `json:"signature"`
Slippage string `json:"slippage"` // Optional
User string `json:"user"`
InputToken string `json:"inputToken"`
OutputToken string `json:"outputToken"`
InputAmt string `json:"inputAmt"`
UserAmtOut string `json:"userAmtOut"`
Recipient string `json:"recipient"`
Deadline string `json:"deadline"`
Nonce string `json:"nonce"`
Signature string `json:"signature"`
Slippage string `json:"slippage"` // Optional
TopPercentile int32 `json:"topPercentile"` // Optional: 1-100 = apply top-of-block constraint, 0 = off
}

if err := json.NewDecoder(r.Body).Decode(&rawReq); err != nil {
Expand Down Expand Up @@ -565,17 +576,23 @@ func (s *Service) Handler() http.HandlerFunc {
return
}

if rawReq.TopPercentile < 0 || rawReq.TopPercentile > 100 {
http.Error(w, "topPercentile must be between 0 and 100", http.StatusBadRequest)
return
}

req := SwapRequest{
User: common.HexToAddress(rawReq.User),
InputToken: common.HexToAddress(rawReq.InputToken),
OutputToken: common.HexToAddress(rawReq.OutputToken),
InputAmt: inputAmt,
UserAmtOut: userAmtOut,
Recipient: common.HexToAddress(rawReq.Recipient),
Deadline: deadline,
Nonce: nonce,
Signature: signature,
Slippage: rawReq.Slippage,
User: common.HexToAddress(rawReq.User),
InputToken: common.HexToAddress(rawReq.InputToken),
OutputToken: common.HexToAddress(rawReq.OutputToken),
InputAmt: inputAmt,
UserAmtOut: userAmtOut,
Recipient: common.HexToAddress(rawReq.Recipient),
Deadline: deadline,
Nonce: nonce,
Signature: signature,
Slippage: rawReq.Slippage,
TopPercentile: rawReq.TopPercentile,
}

result, err := s.HandleSwap(r.Context(), req)
Expand Down
138 changes: 138 additions & 0 deletions tools/preconf-rpc/fastswap/fastswap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
bidderapiv1 "github.com/primev/mev-commit/p2p/gen/go/bidderapi/v1"
"github.com/primev/mev-commit/tools/preconf-rpc/fastswap"
"github.com/primev/mev-commit/tools/preconf-rpc/sender"
"github.com/primev/mev-commit/x/util"
Expand Down Expand Up @@ -313,6 +314,48 @@ func TestHandleSwap(t *testing.T) {
require.Len(t, mockEnqueuer.enqueuedTxs, 1)
enqueuedTx := mockEnqueuer.enqueuedTxs[0]
require.Equal(t, mockSigner.address, enqueuedTx.Sender)
// Default (TopPercentile == 0): no PositionConstraint should be attached
require.Nil(t, enqueuedTx.Constraint)
}

func TestHandleSwap_TopPercentile(t *testing.T) {
barterResp := newTestBarterResponse()
srv := setupTestServer(t, barterResp)
defer srv.Close()

logger := util.NewTestLogger(os.Stdout)
settlementAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")
svc := fastswap.NewService(srv.URL, "test-api-key", settlementAddr, 1, logger)

mockSigner := &mockSigner{address: common.HexToAddress("0xExecutorAddress")}
mockEnqueuer := &mockTxEnqueuer{}
mockTracker := &mockBlockTracker{nonce: 5, nextBaseFee: big.NewInt(30000000000)}
mockStore := &mockNonceStore{nonce: 4, hasTxs: true}
svc.SetExecutorDeps(mockSigner, mockEnqueuer, mockTracker, mockStore)

req := fastswap.SwapRequest{
User: common.HexToAddress("0xUserAddress"),
InputToken: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
OutputToken: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
InputAmt: big.NewInt(1000000000),
UserAmtOut: big.NewInt(100),
Recipient: common.HexToAddress("0xRecipientAddress"),
Deadline: big.NewInt(1700000000),
Nonce: big.NewInt(1),
Signature: []byte{0x01, 0x02, 0x03, 0x04},
TopPercentile: 10,
}

result, err := svc.HandleSwap(context.Background(), req)
require.NoError(t, err)
require.Equal(t, "success", result.Status)

require.Len(t, mockEnqueuer.enqueuedTxs, 1)
enqueuedTx := mockEnqueuer.enqueuedTxs[0]
require.NotNil(t, enqueuedTx.Constraint, "Constraint should be set when TopPercentile > 0")
require.Equal(t, bidderapiv1.PositionConstraint_ANCHOR_TOP, enqueuedTx.Constraint.Anchor)
require.Equal(t, bidderapiv1.PositionConstraint_BASIS_PERCENTILE, enqueuedTx.Constraint.Basis)
require.Equal(t, int32(10), enqueuedTx.Constraint.Value)
}

func TestHandleSwap_NoExecutorDeps(t *testing.T) {
Expand Down Expand Up @@ -431,6 +474,101 @@ func TestHandler(t *testing.T) {
require.Equal(t, "success", result.Status)
}

func TestHandler_TopPercentile(t *testing.T) {
barterResp := newTestBarterResponse()
srv := setupTestServer(t, barterResp)
defer srv.Close()

logger := util.NewTestLogger(os.Stdout)
settlementAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")

baseReq := func(topPercentile string) string {
body := `{
"user": "0x0000000000000000000000000000000000000001",
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"outputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"inputAmt": "1000000000",
"userAmtOut": "100",
"recipient": "0x0000000000000000000000000000000000000002",
"deadline": "1700000000",
"nonce": "1",
"signature": "0x01020304"`
if topPercentile != "" {
body += `, "topPercentile": ` + topPercentile
}
return body + `}`
}

t.Run("valid percentile sets constraint", func(t *testing.T) {
svc := fastswap.NewService(srv.URL, "test-api-key", settlementAddr, 1, logger)
mockEnqueuer := &mockTxEnqueuer{}
svc.SetExecutorDeps(
&mockSigner{address: common.HexToAddress("0xExecutorAddress")},
mockEnqueuer,
&mockBlockTracker{nonce: 0, nextBaseFee: big.NewInt(30000000000)},
&mockNonceStore{nonce: 0, hasTxs: false},
)

req := httptest.NewRequest(http.MethodPost, "/fastswap", strings.NewReader(baseReq("10")))
w := httptest.NewRecorder()
svc.Handler()(w, req)

require.Equal(t, http.StatusOK, w.Code, "body=%s", w.Body.String())
require.Len(t, mockEnqueuer.enqueuedTxs, 1)
require.NotNil(t, mockEnqueuer.enqueuedTxs[0].Constraint)
require.Equal(t, int32(10), mockEnqueuer.enqueuedTxs[0].Constraint.Value)
})

t.Run("omitted field leaves constraint nil (backwards compat)", func(t *testing.T) {
svc := fastswap.NewService(srv.URL, "test-api-key", settlementAddr, 1, logger)
mockEnqueuer := &mockTxEnqueuer{}
svc.SetExecutorDeps(
&mockSigner{address: common.HexToAddress("0xExecutorAddress")},
mockEnqueuer,
&mockBlockTracker{nonce: 0, nextBaseFee: big.NewInt(30000000000)},
&mockNonceStore{nonce: 0, hasTxs: false},
)

req := httptest.NewRequest(http.MethodPost, "/fastswap", strings.NewReader(baseReq("")))
w := httptest.NewRecorder()
svc.Handler()(w, req)

require.Equal(t, http.StatusOK, w.Code, "body=%s", w.Body.String())
require.Len(t, mockEnqueuer.enqueuedTxs, 1)
require.Nil(t, mockEnqueuer.enqueuedTxs[0].Constraint)
})

t.Run("zero value leaves constraint nil", func(t *testing.T) {
svc := fastswap.NewService(srv.URL, "test-api-key", settlementAddr, 1, logger)
mockEnqueuer := &mockTxEnqueuer{}
svc.SetExecutorDeps(
&mockSigner{address: common.HexToAddress("0xExecutorAddress")},
mockEnqueuer,
&mockBlockTracker{nonce: 0, nextBaseFee: big.NewInt(30000000000)},
&mockNonceStore{nonce: 0, hasTxs: false},
)

req := httptest.NewRequest(http.MethodPost, "/fastswap", strings.NewReader(baseReq("0")))
w := httptest.NewRecorder()
svc.Handler()(w, req)

require.Equal(t, http.StatusOK, w.Code, "body=%s", w.Body.String())
require.Len(t, mockEnqueuer.enqueuedTxs, 1)
require.Nil(t, mockEnqueuer.enqueuedTxs[0].Constraint)
})

t.Run("out of range rejected", func(t *testing.T) {
svc := fastswap.NewService(srv.URL, "test-api-key", settlementAddr, 1, logger)
for _, val := range []string{"-1", "101", "500"} {
req := httptest.NewRequest(http.MethodPost, "/fastswap", strings.NewReader(baseReq(val)))
w := httptest.NewRecorder()
svc.Handler()(w, req)
require.Equal(t, http.StatusBadRequest, w.Code, "value=%s", val)
require.Contains(t, w.Body.String(), "topPercentile must be between 0 and 100")
}
})
}

func TestHandler_MethodNotAllowed(t *testing.T) {
logger := util.NewTestLogger(os.Stdout)
svc := fastswap.NewService("", "", common.Address{}, 1, logger)
Expand Down
4 changes: 3 additions & 1 deletion tools/preconf-rpc/pricer/pricer.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
)

var apiURL = "https://api.blocknative.com/gasprices/blockprices?chainid=1&confidenceLevels=70,75,80,85"
var apiURL = "https://api.blocknative.com/gasprices/blockprices?chainid=1&confidenceLevels=70,75,80,85,90"

type EstimatedPrice struct {
Confidence int `json:"confidence"`
Expand Down Expand Up @@ -150,6 +150,8 @@ func (b *BidPricer) syncEstimate(ctx context.Context) error {
b.currentEstimates[int64(estimatedPrice.Confidence)] = estimatedPrice.PriorityFeePerGasGwei
case 85:
b.currentEstimates[int64(estimatedPrice.Confidence)] = estimatedPrice.PriorityFeePerGasGwei
case 90:
b.currentEstimates[int64(estimatedPrice.Confidence)] = estimatedPrice.PriorityFeePerGasGwei
}
b.metrics.bidPrices.WithLabelValues(fmt.Sprintf("%d", estimatedPrice.Confidence)).Set(estimatedPrice.PriorityFeePerGasGwei)
}
Expand Down
4 changes: 2 additions & 2 deletions tools/preconf-rpc/pricer/pricer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ func TestEstimatePrice(t *testing.T) {

prices := bp.EstimatePrice(ctx)

if len(prices) != 4 {
t.Fatalf("expected 4 confidence levels, got %d", len(prices))
if len(prices) != 5 {
t.Fatalf("expected 5 confidence levels, got %d", len(prices))
}

for confidence, price := range prices {
Expand Down
10 changes: 10 additions & 0 deletions tools/preconf-rpc/sender/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const (
defaultSwapConfidence = 70 // default confidence level for fastswap transactions
confidenceSecondAttempt = 80 // confidence level for the second attempt
confidenceSubsequentAttempts = 85 // confidence level for subsequent attempts
topOfBlockSwapConfidence = 80 // confidence level for top-of-block fastswap first attempt
topOfBlockSubsequentAttempts = 90 // confidence level for top-of-block fastswap retries
transactionTimeout = 10 * time.Minute // timeout for transaction processing
maxAttemptsPerBlock = 10 // maximum attempts per block
defaultRetryDelay = 500 * time.Millisecond
Expand Down Expand Up @@ -1112,6 +1114,14 @@ func (t *TxSender) calculatePriceForNextBlock(
confidence = confidenceSubsequentAttempts
}

if txn.Constraint != nil && txn.Type == TxTypeFastSwap {
if isRetry {
confidence = topOfBlockSubsequentAttempts
} else {
confidence = topOfBlockSwapConfidence
}
}

// If this is the first attempt for the next block, we add it to the history
if !isRetry {
attempts.attempts = append(attempts.attempts, &blockAttempt{
Expand Down
Loading