Skip to content

Commit 8531ef1

Browse files
committed
fix: resolve nonce race condition and eliminate redundant body parsing
1 parent b501a12 commit 8531ef1

5 files changed

Lines changed: 26 additions & 19 deletions

File tree

internal/chain/transaction.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"fmt"
77
"math/big"
88
"strings"
9-
"sync/atomic"
9+
"sync"
1010

1111
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1212
"github.com/ethereum/go-ethereum/common"
@@ -22,6 +22,7 @@ type TxBuilder interface {
2222
}
2323

2424
type TxBuild struct {
25+
mu sync.Mutex
2526
client bind.ContractTransactor
2627
privateKey *ecdsa.PrivateKey
2728
signer types.Signer
@@ -69,10 +70,12 @@ func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (comm
6970
return common.Hash{}, fmt.Errorf("invalid transfer value: must be positive")
7071
}
7172

73+
b.mu.Lock()
74+
defer b.mu.Unlock()
75+
7276
gasLimit := uint64(21000)
7377
toAddress := common.HexToAddress(to)
74-
75-
nonce := b.getAndIncrementNonce()
78+
nonce := b.nonce
7679

7780
var err error
7881
var unsignedTx *types.Transaction
@@ -99,6 +102,7 @@ func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (comm
99102
return common.Hash{}, err
100103
}
101104

105+
b.nonce++
102106
return signedTx.Hash(), nil
103107
}
104108

@@ -143,10 +147,6 @@ func (b *TxBuild) buildLegacyTx(ctx context.Context, to *common.Address, value *
143147
}), nil
144148
}
145149

146-
func (b *TxBuild) getAndIncrementNonce() uint64 {
147-
return atomic.AddUint64(&b.nonce, 1) - 1
148-
}
149-
150150
func (b *TxBuild) refreshNonce(ctx context.Context) {
151151
nonce, err := b.client.PendingNonceAt(ctx, b.Sender())
152152
if err != nil {
@@ -157,7 +157,7 @@ func (b *TxBuild) refreshNonce(ctx context.Context) {
157157
return
158158
}
159159

160-
atomic.StoreUint64(&b.nonce, nonce)
160+
b.nonce = nonce
161161
log.WithField("nonce", nonce).Info("Nonce refreshed successfully")
162162
}
163163

internal/server/dto.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ func decodeJSONBody(r *http.Request, dst interface{}) error {
8282
}
8383
}
8484

85-
r.Body = io.NopCloser(bytes.NewReader(body))
8685
return nil
8786
}
8887

internal/server/middleware.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package server
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"net"
@@ -15,6 +16,10 @@ import (
1516
"github.com/urfave/negroni/v3"
1617
)
1718

19+
type contextKey int
20+
21+
const addressContextKey contextKey = iota
22+
1823
type Limiter struct {
1924
mutex sync.Mutex
2025
cache *ttlcache.Cache
@@ -44,6 +49,8 @@ func (l *Limiter) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.Ha
4449
return
4550
}
4651

52+
r = r.WithContext(context.WithValue(r.Context(), addressContextKey, address))
53+
4754
if l.ttl <= 0 {
4855
next.ServeHTTP(w, r)
4956
return

internal/server/server.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ import (
1717
)
1818

1919
type Server struct {
20-
chain.TxBuilder
20+
txBuilder chain.TxBuilder
2121
cfg *Config
2222
server *http.Server
2323
payoutWei *big.Int
2424
}
2525

2626
func NewServer(builder chain.TxBuilder, cfg *Config) *Server {
2727
return &Server{
28-
TxBuilder: builder,
28+
txBuilder: builder,
2929
cfg: cfg,
3030
payoutWei: chain.EtherToWei(cfg.payout),
3131
}
@@ -78,13 +78,16 @@ func (s *Server) handleClaim() http.HandlerFunc {
7878
return
7979
}
8080

81-
// Address has already been validated by limiter
82-
address, _ := readAddress(r)
81+
address, ok := r.Context().Value(addressContextKey).(string)
82+
if !ok || address == "" {
83+
renderJSON(w, claimResponse{Message: "invalid request"}, http.StatusBadRequest)
84+
return
85+
}
8386

8487
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
8588
defer cancel()
8689

87-
txHash, err := s.Transfer(ctx, address, new(big.Int).Set(s.payoutWei))
90+
txHash, err := s.txBuilder.Transfer(ctx, address, new(big.Int).Set(s.payoutWei))
8891
if err != nil {
8992
log.WithFields(log.Fields{
9093
"error": err,
@@ -110,7 +113,7 @@ func (s *Server) handleInfo() http.HandlerFunc {
110113
return
111114
}
112115
renderJSON(w, infoResponse{
113-
Account: s.Sender().String(),
116+
Account: s.txBuilder.Sender().String(),
114117
Network: s.cfg.network,
115118
Symbol: s.cfg.symbol,
116119
Payout: strconv.FormatFloat(s.cfg.payout, 'f', -1, 64),

internal/server/server_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package server
33
import (
44
"context"
55
"encoding/json"
6-
"fmt"
76
"math/big"
87
"net/http"
98
"net/http/httptest"
10-
"strings"
119
"testing"
1210

1311
"github.com/ethereum/go-ethereum/common"
@@ -49,11 +47,11 @@ func TestHandleClaim(t *testing.T) {
4947
mockBuilder.On("Transfer", mock.Anything, expectedAddress, expectedAmount).Return(common.Hash{1}, nil)
5048

5149
server := setupTestServer(mockBuilder)
52-
reqBody := strings.NewReader(fmt.Sprintf(`{"address": "%s"}`, expectedAddress))
53-
req, err := http.NewRequest("POST", "/api/claim", reqBody)
50+
req, err := http.NewRequest("POST", "/api/claim", nil)
5451
if err != nil {
5552
t.Fatal(err)
5653
}
54+
req = req.WithContext(context.WithValue(req.Context(), addressContextKey, expectedAddress))
5755

5856
rr := httptest.NewRecorder()
5957
handler := server.handleClaim()

0 commit comments

Comments
 (0)