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
132 changes: 92 additions & 40 deletions tools/preconf-rpc/fastswap/fastswap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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"`
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 ============
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 17 additions & 16 deletions tools/preconf-rpc/fastswap/fastswap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
Expand All @@ -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),
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -534,7 +535,7 @@ func TestETHHandler(t *testing.T) {
reqJSON := `{
"outputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputAmt": "1000000000000000000",
"userAmtOut": "2000000000",
"userAmtOut": "100",
"sender": "0x0000000000000000000000000000000000000001",
"deadline": "1700000000"
}`
Expand Down Expand Up @@ -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)
Expand All @@ -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),
Expand Down
Loading