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
106 changes: 92 additions & 14 deletions tools/preconf-rpc/fastswap/fastswap.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ type BlockTracker interface {
NextBaseFee() *big.Int
}

// NonceStore interface for getting the current nonce from internal store
type NonceStore interface {
GetCurrentNonce(ctx context.Context, sender common.Address) (uint64, bool)
}

// Service handles FastSwap operations.
type Service struct {
barterBaseURL string
Expand All @@ -164,6 +169,7 @@ type Service struct {
signer Signer
txEnqueuer TxEnqueuer
blockTracker BlockTracker
nonceStore NonceStore
}

// NewService creates a new FastSwap service.
Expand All @@ -189,10 +195,11 @@ func NewService(

// SetExecutorDeps sets the dependencies needed for Path 1 executor transaction submission.
// This is called after TxSender is created since there's a circular dependency.
func (s *Service) SetExecutorDeps(signer Signer, txEnqueuer TxEnqueuer, blockTracker BlockTracker) {
func (s *Service) SetExecutorDeps(signer Signer, txEnqueuer TxEnqueuer, blockTracker BlockTracker, nonceStore NonceStore) {
s.signer = signer
s.txEnqueuer = txEnqueuer
s.blockTracker = blockTracker
s.nonceStore = nonceStore
}

// ============ Barter API ============
Expand Down Expand Up @@ -347,15 +354,30 @@ func (s *Service) HandleSwap(ctx context.Context, req SwapRequest) (*SwapResult,
gasLimit += 100000 // Buffer for settlement contract overhead

// 4. Get nonce for executor wallet
// Use same logic as sender's hasCorrectNonce
executorAddr := s.signer.GetAddress()
nonce, err := s.blockTracker.AccountNonce(ctx, executorAddr)
maxNonce, hasTxs := s.nonceStore.GetCurrentNonce(ctx, executorAddr)
chainNonce, err := s.blockTracker.AccountNonce(ctx, executorAddr)
if err != nil {
return &SwapResult{
Status: "error",
Error: fmt.Sprintf("failed to get nonce: %v", err),
Error: fmt.Sprintf("failed to get chain nonce: %v", err),
}, nil
}

var nonce uint64
if hasTxs {
// Has transactions in store, next nonce is max + 1
nonce = maxNonce + 1
} else {
// No transactions in store, use chain nonce
nonce = chainNonce
}
// If chain has advanced beyond our tracking, use chain nonce
if chainNonce > nonce {
nonce = chainNonce
}

// 5. Calculate gas pricing: GasFeeCap = NextBaseFee only (no tip needed, mev-commit bid handles inclusion)
nextBaseFee := s.blockTracker.NextBaseFee()
if nextBaseFee == nil || nextBaseFee.Sign() == 0 {
Expand Down Expand Up @@ -396,12 +418,12 @@ func (s *Service) HandleSwap(ctx context.Context, req SwapRequest) (*SwapResult,
}
rawTxHex := "0x" + hex.EncodeToString(rawTxBytes)

// 9. Enqueue the transaction
// 9. Enqueue the transaction (uses TxTypeFastSwap to skip balance check)
senderTx := &sender.Transaction{
Transaction: signedTx,
Sender: executorAddr,
Raw: rawTxHex,
Type: sender.TxTypeRegular,
Type: sender.TxTypeFastSwap,
}

if err := s.txEnqueuer.Enqueue(ctx, senderTx); err != nil {
Expand Down Expand Up @@ -442,30 +464,86 @@ func (s *Service) Handler() http.HandlerFunc {
return
}

var req SwapRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
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"`
}

if err := json.NewDecoder(r.Body).Decode(&rawReq); err != nil {
http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
return
}

// Validate required fields
if req.User == (common.Address{}) {
http.Error(w, "missing user address", http.StatusBadRequest)
if rawReq.User == "" || !common.IsHexAddress(rawReq.User) {
http.Error(w, "missing or invalid user address", http.StatusBadRequest)
return
}
if rawReq.InputToken == "" || !common.IsHexAddress(rawReq.InputToken) {
http.Error(w, "missing or invalid inputToken", http.StatusBadRequest)
return
}
if req.InputToken == (common.Address{}) {
http.Error(w, "missing inputToken", http.StatusBadRequest)
if rawReq.OutputToken == "" || !common.IsHexAddress(rawReq.OutputToken) {
http.Error(w, "missing or invalid outputToken", http.StatusBadRequest)
return
}
if req.OutputToken == (common.Address{}) {
http.Error(w, "missing outputToken", http.StatusBadRequest)
if rawReq.Recipient == "" || !common.IsHexAddress(rawReq.Recipient) {
http.Error(w, "missing or invalid recipient", http.StatusBadRequest)
return
}
if len(req.Signature) == 0 {
if rawReq.Signature == "" {
http.Error(w, "missing signature", http.StatusBadRequest)
return
}

// Parse big.Int fields
inputAmt, ok := new(big.Int).SetString(rawReq.InputAmt, 10)
if !ok || inputAmt.Sign() <= 0 {
http.Error(w, "invalid inputAmt", http.StatusBadRequest)
return
}
userAmtOut, ok := new(big.Int).SetString(rawReq.UserAmtOut, 10)
if !ok {
http.Error(w, "invalid userAmtOut", http.StatusBadRequest)
return
}
deadline, ok := new(big.Int).SetString(rawReq.Deadline, 10)
if !ok || deadline.Sign() <= 0 {
http.Error(w, "invalid deadline", http.StatusBadRequest)
return
}
nonce, ok := new(big.Int).SetString(rawReq.Nonce, 10)
if !ok {
http.Error(w, "invalid nonce", http.StatusBadRequest)
return
}

// Decode signature from hex
signature, err := hex.DecodeString(strings.TrimPrefix(rawReq.Signature, "0x"))
if err != nil {
http.Error(w, "invalid signature hex", 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,
}

result, err := s.HandleSwap(r.Context(), req)
if err != nil {
http.Error(w, fmt.Sprintf("swap failed: %v", err), http.StatusInternalServerError)
Expand Down
49 changes: 37 additions & 12 deletions tools/preconf-rpc/fastswap/fastswap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ func (m *mockBlockTracker) NextBaseFee() *big.Int {
return m.nextBaseFee
}

// mockNonceStore implements fastswap.NonceStore interface
type mockNonceStore struct {
nonce uint64
hasTxs bool
}

func (m *mockNonceStore) GetCurrentNonce(_ context.Context, _ common.Address) (uint64, bool) {
return m.nonce, m.hasTxs
}

// ============ Test Helpers ============

func newTestBarterResponse() fastswap.BarterResponse {
Expand Down Expand Up @@ -274,7 +284,8 @@ func TestHandleSwap(t *testing.T) {
nonce: 5,
nextBaseFee: big.NewInt(30000000000), // 30 gwei
}
svc.SetExecutorDeps(mockSigner, mockEnqueuer, mockTracker)
mockStore := &mockNonceStore{nonce: 4, hasTxs: true} // store nonce + 1 should match tracker nonce
svc.SetExecutorDeps(mockSigner, mockEnqueuer, mockTracker, mockStore)

req := fastswap.SwapRequest{
User: common.HexToAddress("0xUserAddress"),
Expand Down Expand Up @@ -387,21 +398,22 @@ func TestHandler(t *testing.T) {
nonce: 0,
nextBaseFee: big.NewInt(30000000000),
}
svc.SetExecutorDeps(mockSignerInst, mockEnqueuer, mockTracker)
mockStore := &mockNonceStore{nonce: 0, hasTxs: false}
svc.SetExecutorDeps(mockSignerInst, mockEnqueuer, mockTracker, mockStore)

handler := svc.Handler()

// Use raw JSON with flattened request structure
// Use raw JSON with string values for numeric fields (new handler format)
reqJSON := `{
"user": "0x0000000000000000000000000000000000000001",
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"outputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"inputAmt": 1000000000,
"userAmtOut": 500000000000000000,
"inputAmt": "1000000000",
"userAmtOut": "500000000000000000",
"recipient": "0x0000000000000000000000000000000000000002",
"deadline": 1700000000,
"nonce": 1,
"signature": "AQIDBA=="
"deadline": "1700000000",
"nonce": "1",
"signature": "0x01020304"
}`

req := httptest.NewRequest(http.MethodPost, "/fastswap", strings.NewReader(reqJSON))
Expand Down Expand Up @@ -444,30 +456,43 @@ func TestHandler_MissingFields(t *testing.T) {
{
name: "missing user",
reqBody: map[string]interface{}{},
expected: "missing user address",
expected: "missing or invalid user address",
},
{
name: "missing inputToken",
reqBody: map[string]interface{}{
"user": "0x1234567890123456789012345678901234567890",
},
expected: "missing inputToken",
expected: "missing or invalid inputToken",
},
{
name: "missing outputToken",
reqBody: map[string]interface{}{
"user": "0x1234567890123456789012345678901234567890",
"inputToken": "0x1234567890123456789012345678901234567890",
},
expected: "missing outputToken",
expected: "missing or invalid outputToken",
},
{
name: "missing recipient",
reqBody: map[string]interface{}{
"user": "0x1234567890123456789012345678901234567890",
"inputToken": "0x1234567890123456789012345678901234567890",
"outputToken": "0x1234567890123456789012345678901234567890",
},
expected: "missing or invalid recipient",
},
{
name: "missing signature",
reqBody: map[string]interface{}{
"user": "0x1234567890123456789012345678901234567890",
"inputToken": "0x1234567890123456789012345678901234567890",
"outputToken": "0x1234567890123456789012345678901234567890",
"inputAmt": 1000,
"recipient": "0x1234567890123456789012345678901234567890",
"inputAmt": "1000",
"userAmtOut": "900",
"deadline": "1700000000",
"nonce": "0",
},
expected: "missing signature",
},
Expand Down
33 changes: 23 additions & 10 deletions tools/preconf-rpc/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type Store interface {
GetTransactionCommitments(ctx context.Context, txnHash common.Hash) ([]*bidderapiv1.Commitment, error)
GetTransactionLogs(ctx context.Context, txnHash common.Hash) ([]*types.Log, error)
GetBalance(ctx context.Context, account common.Address) (*big.Int, error)
GetCurrentNonce(ctx context.Context, account common.Address) uint64
GetCurrentNonce(ctx context.Context, account common.Address) (uint64, bool)
HasBalance(ctx context.Context, account common.Address, amount *big.Int) bool
AlreadySubsidized(ctx context.Context, account common.Address) bool
AddSubsidy(ctx context.Context, account common.Address, amount *big.Int) error
Expand Down Expand Up @@ -687,18 +687,31 @@ func (h *rpcMethodHandler) handleGetTxCount(ctx context.Context, params ...any)
)
}

accNonce := h.store.GetCurrentNonce(ctx, common.HexToAddress(account))
if accNonce == 0 {
return nil, true, nil
}

accNonce += 1
maxNonce, hasTxs := h.store.GetCurrentNonce(ctx, common.HexToAddress(account))

// Get backend nonce
backendNonce, err := h.blockTracker.AccountNonce(ctx, common.HexToAddress(account))
if err == nil {
if backendNonce > accNonce {
accNonce = backendNonce
if err != nil {
// If backend fails and no store txs, return nil (proxy will handle)
if !hasTxs {
return nil, true, nil
}
// Otherwise use store nonce + 1
backendNonce = 0
}

var accNonce uint64
if hasTxs {
// Has transactions in store, next nonce is max + 1
accNonce = maxNonce + 1
} else {
// No transactions in store, use chain nonce
accNonce = backendNonce
}

// If chain has advanced beyond our tracking, use chain nonce
if backendNonce > accNonce {
accNonce = backendNonce
}

nonceJSON, err := json.Marshal(accNonce)
Expand Down
Loading
Loading