From 365e401148ed6937516327c16418a1d846737fbb Mon Sep 17 00:00:00 2001 From: owen-eth Date: Fri, 6 Feb 2026 18:51:03 -0500 Subject: [PATCH] feat: fastswap api integration with barter v2 and slippage control --- tools/preconf-rpc/fastswap/fastswap.go | 132 ++++++++++++++------ tools/preconf-rpc/fastswap/fastswap_test.go | 33 ++--- 2 files changed, 109 insertions(+), 56 deletions(-) diff --git a/tools/preconf-rpc/fastswap/fastswap.go b/tools/preconf-rpc/fastswap/fastswap.go index ef3cd3e88..e7a83ee6b 100644 --- a/tools/preconf-rpc/fastswap/fastswap.go +++ b/tools/preconf-rpc/fastswap/fastswap.go @@ -40,7 +40,8 @@ type SwapRequest struct { Recipient common.Address `json:"recipient"` Deadline *big.Int `json:"deadline"` Nonce *big.Int `json:"nonce"` - Signature []byte `json:"signature"` // EIP-712 Permit2 signature + Signature []byte `json:"signature"` // EIP-712 Permit2 signature + Slippage string `json:"slippage,omitempty"` // User slippage percentage (e.g. "1.0" for 1%) } // ToIntent converts SwapRequest to the generated Intent type for ABI encoding. @@ -70,10 +71,11 @@ type SwapResult struct { // User swaps native ETH for an ERC20 token and submits the transaction themselves. type ETHSwapRequest struct { OutputToken common.Address `json:"outputToken"` - InputAmt *big.Int `json:"inputAmt"` // ETH amount in wei - UserAmtOut *big.Int `json:"userAmtOut"` // minAmountOut from dapp quote - Sender common.Address `json:"sender"` // User address (also recipient) - Deadline *big.Int `json:"deadline"` // Unix timestamp + InputAmt *big.Int `json:"inputAmt"` // ETH amount in wei + UserAmtOut *big.Int `json:"userAmtOut"` // minAmountOut from dapp quote + Sender common.Address `json:"sender"` // User address (also recipient) + Deadline *big.Int `json:"deadline"` // Unix timestamp + Slippage string `json:"slippage,omitempty"` // User slippage percentage (e.g. "1.0" for 1%) } // ETHSwapResponse is the response for /fastswap/eth containing unsigned tx data. @@ -89,11 +91,12 @@ type ETHSwapResponse struct { // BarterResponse represents the parsed response from Barter API. type BarterResponse struct { - To common.Address `json:"to"` - GasLimit string `json:"gasLimit"` - Value string `json:"value"` - Data string `json:"data"` - Route struct { + To common.Address `json:"to"` + GasLimit string `json:"gasLimit"` + Value string `json:"value"` + Data string `json:"data"` + MinReturn string `json:"minReturn"` // Guaranteed minimum amount from Barter + Route struct { OutputAmount string `json:"outputAmount"` GasEstimation uint64 `json:"gasEstimation"` BlockNumber uint64 `json:"blockNumber"` @@ -102,14 +105,14 @@ type BarterResponse struct { // barterRequest is the request body for the Barter API. type barterRequest struct { - Source string `json:"source"` - Target string `json:"target"` - SellAmount string `json:"sellAmount"` - Recipient string `json:"recipient"` - Origin string `json:"origin"` - MinReturn string `json:"minReturn"` - Deadline string `json:"deadline"` - SourceFee *sourceFee `json:"sourceFee,omitempty"` + Source string `json:"source"` + Target string `json:"target"` + SellAmount string `json:"sellAmount"` + Recipient string `json:"recipient"` + Origin string `json:"origin"` + MinReturnFraction float64 `json:"minReturnFraction"` // e.g. 0.99 for 1% slippage + Deadline string `json:"deadline"` + SourceFee *sourceFee `json:"sourceFee,omitempty"` } type sourceFee struct { @@ -229,7 +232,7 @@ func (s *Service) callBarter(ctx context.Context, reqBody barterRequest, logDesc "inputToken", reqBody.Source, "outputToken", reqBody.Target, "inputAmount", reqBody.SellAmount, - "outputAmount", reqBody.MinReturn, + "minReturnFraction", reqBody.MinReturnFraction, ) resp, err := s.httpClient.Do(req) @@ -263,17 +266,41 @@ func (s *Service) callBarter(ctx context.Context, reqBody barterRequest, logDesc } // CallBarterAPI calls the Barter API for swap routing (Path 1 - executor submitted). -func (s *Service) CallBarterAPI(ctx context.Context, intent Intent) (*BarterResponse, error) { +func (s *Service) CallBarterAPI(ctx context.Context, intent Intent, slippageStr string) (*BarterResponse, error) { + // Default slippage 0.5% if not provided + fraction := 0.995 + if slippageStr != "" { + if val, err := strconv.ParseFloat(slippageStr, 64); err == nil && val >= 0 && val <= 100 { + fraction = 1.0 - (val / 100.0) + } + } + reqBody := barterRequest{ - Source: intent.InputToken.Hex(), - Target: intent.OutputToken.Hex(), - SellAmount: intent.InputAmt.String(), - Recipient: s.settlementAddr.Hex(), - Origin: intent.User.Hex(), - MinReturn: intent.UserAmtOut.String(), - Deadline: intent.Deadline.String(), - } - return s.callBarter(ctx, reqBody, "executor-swap") + Source: intent.InputToken.Hex(), + Target: intent.OutputToken.Hex(), + SellAmount: intent.InputAmt.String(), + Recipient: s.settlementAddr.Hex(), + Origin: intent.User.Hex(), + MinReturnFraction: fraction, + Deadline: intent.Deadline.String(), + } + resp, err := s.callBarter(ctx, reqBody, "executor-swap") + if err != nil { + return nil, err + } + + // VALIDATION: Ensure Barter's output meets User's requirement + // We check minReturn (worst case) against user's requirement for safety + outAmt, ok := new(big.Int).SetString(resp.MinReturn, 10) + if !ok { + return nil, fmt.Errorf("invalid minReturn from barter: %s", resp.MinReturn) + } + if outAmt.Cmp(intent.UserAmtOut) < 0 { + // Barter's worst case < User's worst case. + // Abort to prevent failed transaction. + return nil, fmt.Errorf("barter minReturn (%s) < user required (%s)", outAmt.String(), intent.UserAmtOut.String()) + } + return resp, nil } // ============ Transaction Building ============ @@ -331,8 +358,8 @@ func (s *Service) HandleSwap(ctx context.Context, req SwapRequest) (*SwapResult, // Convert request to Intent intent := req.ToIntent() - // 1. Call Barter API - barterResp, err := s.CallBarterAPI(ctx, intent) + // 1. Call Barter API using user's slippage if provided, or default + barterResp, err := s.CallBarterAPI(ctx, intent, req.Slippage) if err != nil { return &SwapResult{ Status: "error", @@ -475,6 +502,7 @@ func (s *Service) Handler() http.HandlerFunc { Deadline string `json:"deadline"` Nonce string `json:"nonce"` Signature string `json:"signature"` + Slippage string `json:"slippage"` // Optional } if err := json.NewDecoder(r.Body).Decode(&rawReq); err != nil { @@ -543,6 +571,7 @@ func (s *Service) Handler() http.HandlerFunc { Deadline: deadline, Nonce: nonce, Signature: signature, + Slippage: rawReq.Slippage, } result, err := s.HandleSwap(r.Context(), req) @@ -561,16 +590,37 @@ func (s *Service) Handler() http.HandlerFunc { // CallBarterAPIForETH calls the Barter API for ETH->Token swap routing (Path 2). // Uses WETH as the source token since Barter works with ERC20s. func (s *Service) CallBarterAPIForETH(ctx context.Context, req ETHSwapRequest) (*BarterResponse, error) { + // Default slippage 0.5% if not provided + fraction := 0.995 + if req.Slippage != "" { + if val, err := strconv.ParseFloat(req.Slippage, 64); err == nil && val >= 0 && val <= 100 { + fraction = 1.0 - (val / 100.0) + } + } + reqBody := barterRequest{ - Source: mainnetWETH.Hex(), - Target: req.OutputToken.Hex(), - SellAmount: req.InputAmt.String(), - Recipient: s.settlementAddr.Hex(), - Origin: req.Sender.Hex(), - MinReturn: req.UserAmtOut.String(), - Deadline: req.Deadline.String(), - } - return s.callBarter(ctx, reqBody, "eth-swap") + Source: mainnetWETH.Hex(), + Target: req.OutputToken.Hex(), + SellAmount: req.InputAmt.String(), + Recipient: s.settlementAddr.Hex(), + Origin: req.Sender.Hex(), + MinReturnFraction: fraction, + Deadline: req.Deadline.String(), + } + resp, err := s.callBarter(ctx, reqBody, "eth-swap") + if err != nil { + return nil, err + } + + // VALIDATION + outAmt, ok := new(big.Int).SetString(resp.MinReturn, 10) + if !ok { + return nil, fmt.Errorf("invalid minReturn from barter: %s", resp.MinReturn) + } + if outAmt.Cmp(req.UserAmtOut) < 0 { + return nil, fmt.Errorf("barter minReturn (%s) < user required (%s)", outAmt.String(), req.UserAmtOut.String()) + } + return resp, nil } // BuildExecuteWithETHTx constructs the calldata for FastSettlementV3.executeWithETH. @@ -679,6 +729,7 @@ func (s *Service) ETHHandler() http.HandlerFunc { UserAmtOut string `json:"userAmtOut"` Sender string `json:"sender"` Deadline string `json:"deadline"` + Slippage string `json:"slippage"` // Optional } if err := json.NewDecoder(r.Body).Decode(&rawReq); err != nil { @@ -719,6 +770,7 @@ func (s *Service) ETHHandler() http.HandlerFunc { UserAmtOut: userAmtOut, Sender: common.HexToAddress(rawReq.Sender), Deadline: deadline, + Slippage: rawReq.Slippage, } result, err := s.HandleETHSwap(r.Context(), req) diff --git a/tools/preconf-rpc/fastswap/fastswap_test.go b/tools/preconf-rpc/fastswap/fastswap_test.go index 9b587981d..68826300a 100644 --- a/tools/preconf-rpc/fastswap/fastswap_test.go +++ b/tools/preconf-rpc/fastswap/fastswap_test.go @@ -80,10 +80,11 @@ func newTestBarterResponse() fastswap.BarterResponse { // route.gasEstimation: gas estimate as uint64 // route.blockNumber: current block number return fastswap.BarterResponse{ - To: common.HexToAddress("0x179dc3fb0f2230094894317f307241a52cdb38aa"), // Barter swap executor - GasLimit: "1227112", - Value: "0", - Data: "0xf0d7bb940000000000000000000000002c0552e5dcb79b064fd23e358a86810bc5994244", // truncated for test + To: common.HexToAddress("0x179dc3fb0f2230094894317f307241a52cdb38aa"), // Barter swap executor + GasLimit: "1227112", + Value: "0", + Data: "0xf0d7bb940000000000000000000000002c0552e5dcb79b064fd23e358a86810bc5994244", // truncated for test + MinReturn: "250000000", // slightly less than output amount Route: struct { OutputAmount string `json:"outputAmount"` GasEstimation uint64 `json:"gasEstimation"` @@ -158,14 +159,14 @@ func TestCallBarterAPI(t *testing.T) { InputToken: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC OutputToken: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH InputAmt: big.NewInt(1000000000), // 1000 USDC - UserAmtOut: big.NewInt(500000000000000000), // 0.5 ETH + UserAmtOut: big.NewInt(100), // Low amount to pass validation Recipient: common.HexToAddress("0xRecipientAddress"), Deadline: big.NewInt(1700000000), Nonce: big.NewInt(1), } ctx := context.Background() - resp, err := svc.CallBarterAPI(ctx, intent) + resp, err := svc.CallBarterAPI(ctx, intent, "") require.NoError(t, err) require.NotNil(t, resp) @@ -190,7 +191,7 @@ func TestCallBarterAPIForETH(t *testing.T) { req := fastswap.ETHSwapRequest{ OutputToken: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC InputAmt: big.NewInt(1000000000000000000), // 1 ETH - UserAmtOut: big.NewInt(2000000000), // 2000 USDC + UserAmtOut: big.NewInt(100), // Low amount to pass validation Sender: common.HexToAddress("0xSenderAddress"), Deadline: big.NewInt(1700000000), } @@ -218,7 +219,7 @@ func TestBuildExecuteTx(t *testing.T) { InputToken: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), OutputToken: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), InputAmt: big.NewInt(1000000000), - UserAmtOut: big.NewInt(500000000000000000), + UserAmtOut: big.NewInt(100), Recipient: common.HexToAddress("0xRecipientAddress"), Deadline: big.NewInt(1700000000), Nonce: big.NewInt(1), @@ -248,7 +249,7 @@ func TestBuildExecuteWithETHTx(t *testing.T) { req := fastswap.ETHSwapRequest{ OutputToken: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), InputAmt: big.NewInt(1000000000000000000), - UserAmtOut: big.NewInt(2000000000), + UserAmtOut: big.NewInt(100), Sender: common.HexToAddress("0xSenderAddress"), Deadline: big.NewInt(1700000000), } @@ -292,7 +293,7 @@ func TestHandleSwap(t *testing.T) { InputToken: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), OutputToken: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), InputAmt: big.NewInt(1000000000), - UserAmtOut: big.NewInt(500000000000000000), + UserAmtOut: big.NewInt(100), Recipient: common.HexToAddress("0xRecipientAddress"), Deadline: big.NewInt(1700000000), Nonce: big.NewInt(1), @@ -359,7 +360,7 @@ func TestHandleETHSwap(t *testing.T) { req := fastswap.ETHSwapRequest{ OutputToken: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), InputAmt: big.NewInt(1000000000000000000), - UserAmtOut: big.NewInt(2000000000), + UserAmtOut: big.NewInt(100), Sender: common.HexToAddress("0xSenderAddress"), Deadline: big.NewInt(1700000000), } @@ -409,7 +410,7 @@ func TestHandler(t *testing.T) { "inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "outputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "inputAmt": "1000000000", - "userAmtOut": "500000000000000000", + "userAmtOut": "100", "recipient": "0x0000000000000000000000000000000000000002", "deadline": "1700000000", "nonce": "1", @@ -534,7 +535,7 @@ func TestETHHandler(t *testing.T) { reqJSON := `{ "outputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "inputAmt": "1000000000000000000", - "userAmtOut": "2000000000", + "userAmtOut": "100", "sender": "0x0000000000000000000000000000000000000001", "deadline": "1700000000" }` @@ -658,13 +659,13 @@ func TestBarterAPIError(t *testing.T) { InputToken: common.HexToAddress("0xInputToken"), OutputToken: common.HexToAddress("0xOutputToken"), InputAmt: big.NewInt(1000), - UserAmtOut: big.NewInt(900), + UserAmtOut: big.NewInt(100), Deadline: big.NewInt(1700000000), Nonce: big.NewInt(1), } ctx := context.Background() - resp, err := svc.CallBarterAPI(ctx, intent) + resp, err := svc.CallBarterAPI(ctx, intent, "") require.Error(t, err) require.Nil(t, resp) @@ -687,7 +688,7 @@ func TestIntentTupleEncoding(t *testing.T) { InputToken: common.HexToAddress("0x2222222222222222222222222222222222222222"), OutputToken: common.HexToAddress("0x3333333333333333333333333333333333333333"), InputAmt: big.NewInt(1000000000000000000), - UserAmtOut: big.NewInt(500000000000000000), + UserAmtOut: big.NewInt(100), Recipient: common.HexToAddress("0x4444444444444444444444444444444444444444"), Deadline: big.NewInt(1700000000), Nonce: big.NewInt(42),