Skip to content

Commit

Permalink
refactor: maintain nonce locally (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
iczc authored Sep 18, 2023
1 parent 6bbfaf7 commit 9ecd8c4
Show file tree
Hide file tree
Showing 8 changed files with 38 additions and 70 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ The following are the available command-line flags(excluding above wallet flags)
|-------------------|--------------------------------------------------|---------------|
| -httpport | Listener port to serve HTTP connection | 8080 |
| -proxycount | Count of reverse proxies in front of the server | 0 |
| -queuecap | Maximum transactions waiting to be sent | 100 |
| -faucet.amount | Number of Ethers to transfer per user request | 1 |
| -faucet.minutes | Number of minutes to wait between funding rounds | 1440 |
| -faucet.name | Network name to display on the frontend | testnet |
Expand Down
3 changes: 1 addition & 2 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ var (

httpPortFlag = flag.Int("httpport", 8080, "Listener port to serve HTTP connection")
proxyCntFlag = flag.Int("proxycount", 0, "Count of reverse proxies in front of the server")
queueCapFlag = flag.Int("queuecap", 100, "Maximum transactions waiting to be sent")
versionFlag = flag.Bool("version", false, "Print version number")

payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to transfer per user request")
Expand Down Expand Up @@ -61,7 +60,7 @@ func Execute() {
if err != nil {
panic(fmt.Errorf("cannot connect to web3 provider: %w", err))
}
config := server.NewConfig(*netnameFlag, *symbolFlag, *httpPortFlag, *intervalFlag, *payoutFlag, *proxyCntFlag, *queueCapFlag, *hcaptchaSiteKeyFlag, *hcaptchaSecretFlag)
config := server.NewConfig(*netnameFlag, *symbolFlag, *httpPortFlag, *intervalFlag, *payoutFlag, *proxyCntFlag, *hcaptchaSiteKeyFlag, *hcaptchaSecretFlag)
go server.NewServer(txBuilder, config).Run()

c := make(chan os.Signal, 1)
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module github.com/chainflag/eth-faucet
go 1.17

require (
github.com/LK4D4/trylock v0.0.0-20191027065348-ff7e133a5c54
github.com/agiledragon/gomonkey/v2 v2.10.1
github.com/ethereum/go-ethereum v1.10.26
github.com/jellydator/ttlcache/v2 v2.11.1
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/LK4D4/trylock v0.0.0-20191027065348-ff7e133a5c54 h1:sg9CWNOhr58hMGmJ0q7x7jQ/B1RK/GyHNmeaYCJos9M=
github.com/LK4D4/trylock v0.0.0-20191027065348-ff7e133a5c54/go.mod h1:uHbOgfPowb74TKlV4AR5Az2haG6evxzM8Lmj1Xil25E=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
Expand Down
42 changes: 33 additions & 9 deletions internal/chain/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"context"
"crypto/ecdsa"
"math/big"
"strings"
"sync/atomic"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
log "github.com/sirupsen/logrus"
)

type TxBuilder interface {
Expand All @@ -22,6 +25,7 @@ type TxBuild struct {
privateKey *ecdsa.PrivateKey
signer types.Signer
fromAddress common.Address
nonce uint64
}

func NewTxBuilder(provider string, privateKey *ecdsa.PrivateKey, chainID *big.Int) (TxBuilder, error) {
Expand All @@ -37,24 +41,22 @@ func NewTxBuilder(provider string, privateKey *ecdsa.PrivateKey, chainID *big.In
}
}

return &TxBuild{
txBuilder := &TxBuild{
client: client,
privateKey: privateKey,
signer: types.NewEIP155Signer(chainID),
fromAddress: crypto.PubkeyToAddress(privateKey.PublicKey),
}, nil
}
txBuilder.refreshNonce(context.Background())

return txBuilder, nil
}

func (b *TxBuild) Sender() common.Address {
return b.fromAddress
}

func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (common.Hash, error) {
nonce, err := b.client.PendingNonceAt(ctx, b.Sender())
if err != nil {
return common.Hash{}, err
}

gasLimit := uint64(21000)
gasPrice, err := b.client.SuggestGasPrice(ctx)
if err != nil {
Expand All @@ -63,7 +65,7 @@ func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (comm

toAddress := common.HexToAddress(to)
unsignedTx := types.NewTx(&types.LegacyTx{
Nonce: nonce,
Nonce: b.getAndIncrementNonce(),
To: &toAddress,
Value: value,
Gas: gasLimit,
Expand All @@ -75,5 +77,27 @@ func (b *TxBuild) Transfer(ctx context.Context, to string, value *big.Int) (comm
return common.Hash{}, err
}

return signedTx.Hash(), b.client.SendTransaction(ctx, signedTx)
if err = b.client.SendTransaction(ctx, signedTx); err != nil {
log.Error("failed to send tx", "tx hash", signedTx.Hash().String(), "err", err)
if strings.Contains(err.Error(), "nonce") {
b.refreshNonce(context.Background())
}
return common.Hash{}, err
}

return signedTx.Hash(), nil
}

func (b *TxBuild) getAndIncrementNonce() uint64 {
return atomic.AddUint64(&b.nonce, 1) - 1
}

func (b *TxBuild) refreshNonce(ctx context.Context) {
nonce, err := b.client.PendingNonceAt(ctx, b.Sender())
if err != nil {
log.Error("failed to refresh nonce", "address", b.Sender(), "err", err)
return
}

b.nonce = nonce
}
4 changes: 1 addition & 3 deletions internal/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@ type Config struct {
interval int
payout int
proxyCount int
queueCap int
hcaptchaSiteKey string
hcaptchaSecret string
}

func NewConfig(network, symbol string, httpPort, interval, payout, proxyCount, queueCap int, hcaptchaSiteKey, hcaptchaSecret string) *Config {
func NewConfig(network, symbol string, httpPort, interval, payout, proxyCount int, hcaptchaSiteKey, hcaptchaSecret string) *Config {
return &Config{
network: network,
symbol: symbol,
httpPort: httpPort,
interval: interval,
payout: payout,
proxyCount: proxyCount,
queueCap: queueCap,
hcaptchaSiteKey: hcaptchaSiteKey,
hcaptchaSecret: hcaptchaSecret,
}
Expand Down
2 changes: 1 addition & 1 deletion internal/server/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func decodeJSONBody(r *http.Request, dst interface{}) error {
msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
return &malformedRequest{status: http.StatusBadRequest, message: msg}
case errors.Is(err, io.ErrUnexpectedEOF):
msg := fmt.Sprintf("Request body contains badly-formed JSON")
msg := "Request body contains badly-formed JSON"
return &malformedRequest{status: http.StatusBadRequest, message: msg}
case errors.As(err, &unmarshalTypeError):
msg := fmt.Sprintf("Request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset)
Expand Down
53 changes: 2 additions & 51 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"strconv"
"time"

"github.com/LK4D4/trylock"
log "github.com/sirupsen/logrus"
"github.com/urfave/negroni"

Expand All @@ -17,16 +16,13 @@ import (

type Server struct {
chain.TxBuilder
mutex trylock.Mutex
cfg *Config
queue chan string
cfg *Config
}

func NewServer(builder chain.TxBuilder, cfg *Config) *Server {
return &Server{
TxBuilder: builder,
cfg: cfg,
queue: make(chan string, cfg.queueCap),
}
}

Expand All @@ -42,40 +38,12 @@ func (s *Server) setupRouter() *http.ServeMux {
}

func (s *Server) Run() {
go func() {
ticker := time.NewTicker(time.Second)
for range ticker.C {
s.consumeQueue()
}
}()

n := negroni.New(negroni.NewRecovery(), negroni.NewLogger())
n.UseHandler(s.setupRouter())
log.Infof("Starting http server %d", s.cfg.httpPort)
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(s.cfg.httpPort), n))
}

func (s *Server) consumeQueue() {
if len(s.queue) == 0 {
return
}

s.mutex.Lock()
defer s.mutex.Unlock()
for len(s.queue) != 0 {
address := <-s.queue
txHash, err := s.Transfer(context.Background(), address, chain.EtherToWei(int64(s.cfg.payout)))
if err != nil {
log.WithError(err).Error("Failed to handle transaction in the queue")
} else {
log.WithFields(log.Fields{
"txHash": txHash,
"address": address,
}).Info("Consume from queue successfully")
}
}
}

func (s *Server) handleClaim() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
Expand All @@ -85,26 +53,9 @@ func (s *Server) handleClaim() http.HandlerFunc {

// The error always be nil since it has already been handled in limiter
address, _ := readAddress(r)
// Try to lock mutex if the work queue is empty
if len(s.queue) != 0 || !s.mutex.TryLock() {
select {
case s.queue <- address:
log.WithFields(log.Fields{
"address": address,
}).Info("Added to queue successfully")
resp := claimResponse{Message: fmt.Sprintf("Added %s to the queue", address)}
renderJSON(w, resp, http.StatusOK)
default:
log.Warn("Max queue capacity reached")
renderJSON(w, claimResponse{Message: "Faucet queue is too long, please try again later"}, http.StatusServiceUnavailable)
}
return
}

ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
txHash, err := s.Transfer(ctx, address, chain.EtherToWei(int64(s.cfg.payout)))
s.mutex.Unlock()
if err != nil {
log.WithError(err).Error("Failed to send transaction")
renderJSON(w, claimResponse{Message: err.Error()}, http.StatusInternalServerError)
Expand All @@ -114,7 +65,7 @@ func (s *Server) handleClaim() http.HandlerFunc {
log.WithFields(log.Fields{
"txHash": txHash,
"address": address,
}).Info("Funded directly successfully")
}).Info("Transaction sent successfully")
resp := claimResponse{Message: fmt.Sprintf("Txhash: %s", txHash)}
renderJSON(w, resp, http.StatusOK)
}
Expand Down

0 comments on commit 9ecd8c4

Please sign in to comment.