Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: maintain nonce locally #22

Merged
merged 8 commits into from
Sep 18, 2023
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
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
Loading