diff --git a/cmd/serve.go b/cmd/serve.go index 27afed4..e8f2304 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -112,10 +112,10 @@ func (c *faucetCfg) registerRootFlags(fs *flag.FlagSet) { ) fs.StringVar( - &c.config.SendAmount, + &c.config.MaxSendAmount, "send-amount", - config.DefaultSendAmount, - "the static send amount (native currency)", + config.DefaultMaxSendAmount, + "the static max send amount per drip (native currency)", ) fs.StringVar( diff --git a/config/config.go b/config/config.go index e605fb8..32ca427 100644 --- a/config/config.go +++ b/config/config.go @@ -11,7 +11,7 @@ import ( const ( DefaultListenAddress = "0.0.0.0:8545" DefaultChainID = "dev" - DefaultSendAmount = "1000000ugnot" + DefaultMaxSendAmount = "1000000ugnot" //nolint:lll // Mnemonic is naturally long DefaultMnemonic = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" DefaultNumAccounts = uint64(1) @@ -45,9 +45,9 @@ type Config struct { // The mnemonic for the faucet Mnemonic string `toml:"mnemonic"` - // The static send amount (native currency). + // The static max send amount (native currency). // Format should be: ugnot - SendAmount string `toml:"send_amount"` + MaxSendAmount string `toml:"send_amount"` // The number of faucet accounts, // based on the mnemonic (account 0, index x) @@ -59,7 +59,7 @@ func DefaultConfig() *Config { return &Config{ ListenAddress: DefaultListenAddress, ChainID: DefaultChainID, - SendAmount: DefaultSendAmount, + MaxSendAmount: DefaultMaxSendAmount, Mnemonic: DefaultMnemonic, NumAccounts: DefaultNumAccounts, CORSConfig: DefaultCORSConfig(), @@ -79,7 +79,7 @@ func ValidateConfig(config *Config) error { } // validate the send amount - if !amountRegex.MatchString(config.SendAmount) { + if !amountRegex.MatchString(config.MaxSendAmount) { return ErrInvalidSendAmount } diff --git a/config/config_test.go b/config/config_test.go index bddac1d..d6d2c18 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -31,7 +31,7 @@ func TestConfig_ValidateConfig(t *testing.T) { t.Parallel() cfg := DefaultConfig() - cfg.SendAmount = "1000goo" // invalid denom + cfg.MaxSendAmount = "1000goo" // invalid denom assert.ErrorIs(t, ValidateConfig(cfg), ErrInvalidSendAmount) }) diff --git a/faucet.go b/faucet.go index 8873704..8e5cff3 100644 --- a/faucet.go +++ b/faucet.go @@ -35,7 +35,7 @@ type Faucet struct { handlers []Handler // request handlers prepareTxMsgFn PrepareTxMessageFn // transaction message creator - sendAmount std.Coins // for fast lookup + maxSendAmount std.Coins // the max send amount per drip } var noopLogger = slog.New(slog.NewTextHandler(io.Discard, nil)) @@ -76,8 +76,8 @@ func NewFaucet( } // Set the send amount - //nolint:errcheck // SendAmount is validated beforehand - f.sendAmount, _ = std.ParseCoins(f.config.SendAmount) + //nolint:errcheck // MaxSendAmount is validated beforehand + f.maxSendAmount, _ = std.ParseCoins(f.config.MaxSendAmount) // Generate the in-memory keyring f.keyring = memory.New(f.config.Mnemonic, f.config.NumAccounts) diff --git a/go.mod b/go.mod index 2e7f240..ecbcc2a 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/gnolang/faucet go 1.21 require ( - github.com/gnolang/gno v0.0.0-20240308113041-45c8f900a1a3 + github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc github.com/go-chi/chi/v5 v5.0.12 github.com/pelletier/go-toml v1.9.5 github.com/peterbourgon/ff/v3 v3.4.0 github.com/rs/cors v1.10.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.8.4 golang.org/x/sync v0.6.0 ) @@ -22,8 +22,8 @@ require ( github.com/gorilla/websocket v1.5.1 // indirect github.com/jaekwon/testify v1.6.1 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/linxGnu/grocksdb v1.8.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect diff --git a/go.sum b/go.sum index da691ac..c8af376 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,7 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -43,8 +44,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/gno v0.0.0-20240308113041-45c8f900a1a3 h1:jD9i4n582op16xHU7aKPtp3Oo7rJ8Q5+jpR6nE/ad6U= -github.com/gnolang/gno v0.0.0-20240308113041-45c8f900a1a3/go.mod h1:jDARzJA+/H5YwCGpWuouqo4D0LMSNZVVgFQK/r/R7As= +github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc h1:aYkkNfumtt9z8DeI7ZiFC+vMgFFadaGY0A97pXpOqZU= +github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc/go.mod h1:jDARzJA+/H5YwCGpWuouqo4D0LMSNZVVgFQK/r/R7As= github.com/gnolang/goleveldb v0.0.9 h1:Q7rGko9oXMKtQA+Apeeed5a3sjba/mcDhzJGoTVLCKE= github.com/gnolang/goleveldb v0.0.9/go.mod h1:Dz6p9bmpy/FBESTgduiThZt5mToVDipcHGzj/zUOo8E= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= @@ -83,14 +84,15 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/linxGnu/grocksdb v1.8.4 h1:ZMsBpPpJNtRLHiKKp0mI7gW+NT4s7UgfD5xHxx1jVRo= -github.com/linxGnu/grocksdb v1.8.4/go.mod h1:xZCIb5Muw+nhbDK4Y5UJuOrin5MceOuiXkVUR7vp4WY= +github.com/linxGnu/grocksdb v1.6.20 h1:C0SNv12/OBr/zOdGw6reXS+mKpIdQGb/AkZWjHYnO64= +github.com/linxGnu/grocksdb v1.6.20/go.mod h1:IbTMGpmWg/1pg2hcG9LlxkqyqiJymdCweaUrzsLRFmg= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -111,8 +113,8 @@ github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= diff --git a/handler.go b/handler.go index e1b9828..90ba4d6 100644 --- a/handler.go +++ b/handler.go @@ -6,10 +6,12 @@ import ( "fmt" "io" "net/http" + "regexp" "github.com/gnolang/faucet/writer" httpWriter "github.com/gnolang/faucet/writer/http" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" ) const ( @@ -17,7 +19,12 @@ const ( faucetSuccess = "successfully executed faucet transfer" ) -var errInvalidBeneficiary = errors.New("invalid beneficiary address") +var ( + errInvalidBeneficiary = errors.New("invalid beneficiary address") + errInvalidSendAmount = errors.New("invalid send amount") +) + +var amountRegex = regexp.MustCompile(`^\d+ugnot$`) // defaultHTTPHandler is the default faucet transfer handler func (f *Faucet) defaultHTTPHandler(w http.ResponseWriter, r *http.Request) { @@ -74,8 +81,39 @@ func (f *Faucet) handleRequest(writer writer.ResponseWriter, requests Requests) continue } + // Extract the send amount + amount, err := extractSendAmount(baseRequest) + if err != nil { + // Save the error response + responses[i] = Response{ + Result: unableToHandleRequest, + Error: err.Error(), + } + + continue + } + + // Check if the amount is set + if amount.IsZero() { + // Drip amount is not set, use + // the max faucet drip amount + amount = f.maxSendAmount + } + + // Check if the amount exceeds the max + // drip amount for the faucet + if amount.IsAllGT(f.maxSendAmount) { + // Save the error response + responses[i] = Response{ + Result: unableToHandleRequest, + Error: errInvalidSendAmount.Error(), + } + + continue + } + // Run the method handler - if err := f.transferFunds(beneficiary); err != nil { + if err := f.transferFunds(beneficiary, amount); err != nil { f.logger.Debug( unableToHandleRequest, "request", @@ -144,3 +182,23 @@ func extractBeneficiary(request Request) (crypto.Address, error) { return beneficiary, nil } + +// extractSendAmount extracts the drip amount from the base faucet request, if any +func extractSendAmount(request Request) (std.Coins, error) { + // Check if the amount is set + if request.Amount == "" { + return std.Coins{}, nil + } + + // Validate the send amount is valid + if !amountRegex.MatchString(request.Amount) { + return std.Coins{}, errInvalidSendAmount + } + + amount, err := std.ParseCoins(request.Amount) + if err != nil { + return std.Coins{}, fmt.Errorf("%w, %w", errInvalidSendAmount, err) + } + + return amount, nil +} diff --git a/handler_test.go b/handler_test.go index c7a6ada..391f6c2 100644 --- a/handler_test.go +++ b/handler_test.go @@ -82,7 +82,7 @@ func TestFaucet_Serve_ValidRequests(t *testing.T) { var ( gasFee = std.MustParseCoin("1ugnot") - sendAmount = std.MustParseCoins(config.DefaultSendAmount) + sendAmount = std.MustParseCoins(config.DefaultMaxSendAmount) ) var ( @@ -295,7 +295,7 @@ func TestFaucet_Serve_InvalidRequests(t *testing.T) { var ( gasFee = std.MustParseCoin("1ugnot") - sendAmount = std.MustParseCoins(config.DefaultSendAmount) + sendAmount = std.MustParseCoins(config.DefaultMaxSendAmount) ) var ( @@ -569,7 +569,7 @@ func TestFaucet_Serve_NoFundedAccounts(t *testing.T) { var ( gasFee = std.MustParseCoin("1ugnot") - sendAmount = std.MustParseCoins(config.DefaultSendAmount) + sendAmount = std.MustParseCoins(config.DefaultMaxSendAmount) ) var ( @@ -713,3 +713,100 @@ func TestFaucet_Serve_NoFundedAccounts(t *testing.T) { // Validate the broadcast tx assert.Nil(t, capturedTxs) } + +func TestFaucet_Serve_InvalidSendAmount(t *testing.T) { + t.Parallel() + + // Extract the default send amount + maxSendAmount := std.MustParseCoins(config.DefaultMaxSendAmount) + + testTable := []struct { + name string + sendAmount std.Coins + }{ + { + "invalid send amount", + std.NewCoins(std.NewCoin("atom", 10)), + }, + { + "excessive send amount", + maxSendAmount.Add(std.MustParseCoins("100ugnot")), + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + var ( + validAddress = crypto.MustAddressFromString("g155n659f89cfak0zgy575yqma64sm4tv6exqk99") + gasFee = std.MustParseCoin("1ugnot") + + singleInvalidRequest = Request{ + To: validAddress.String(), + Amount: testCase.sendAmount.String(), + } + ) + + encodedSingleInvalidRequest, err := json.Marshal( + singleInvalidRequest, + ) + require.NoError(t, err) + + getFaucetURL := func(address string) string { + return fmt.Sprintf("http://%s", address) + } + + // Create a new faucet with default params + cfg := config.DefaultConfig() + cfg.ListenAddress = fmt.Sprintf("127.0.0.1:%d", getFreePort(t)) + cfg.MaxSendAmount = maxSendAmount.String() + + f, err := NewFaucet( + static.New(gasFee, 100000), + &mockClient{}, + WithConfig(cfg), + ) + + require.NoError(t, err) + require.NotNil(t, f) + + // Start the faucet + ctx, cancelFn := context.WithCancel(context.Background()) + defer cancelFn() + + g, gCtx := errgroup.WithContext(ctx) + + g.Go(func() error { + return f.Serve(gCtx) + }) + + url := getFaucetURL(f.config.ListenAddress) + + // Wait for the faucet to be started + waitForServer(t, url) + + // Execute the request + respRaw, err := http.Post( + url, + jsonMimeType, + bytes.NewBuffer(encodedSingleInvalidRequest), + ) + require.NoError(t, err) + + respBytes, err := io.ReadAll(respRaw.Body) + require.NoError(t, err) + + response := decodeResponse[Response](t, respBytes) + + assert.Contains(t, response.Error, errInvalidSendAmount.Error()) + assert.Equal(t, unableToHandleRequest, response.Result) + + // Stop the faucet and wait for it to finish + cancelFn() + assert.NoError(t, g.Wait()) + }) + } +} diff --git a/transfer.go b/transfer.go index cb62076..5ea4adf 100644 --- a/transfer.go +++ b/transfer.go @@ -10,9 +10,9 @@ import ( var errNoFundedAccount = errors.New("no funded account found") // transferFunds transfers funds to the given address -func (f *Faucet) transferFunds(address crypto.Address) error { +func (f *Faucet) transferFunds(address crypto.Address, amount std.Coins) error { // Find an account that has balance to cover the transfer - fundAccount, err := f.findFundedAccount() + fundAccount, err := f.findFundedAccount(amount) if err != nil { return err } @@ -21,7 +21,7 @@ func (f *Faucet) transferFunds(address crypto.Address) error { pCfg := PrepareCfg{ FromAddress: fundAccount.GetAddress(), ToAddress: address, - SendAmount: f.sendAmount, + SendAmount: amount, } tx := prepareTransaction(f.estimator, f.prepareTxMsgFn(pCfg)) @@ -46,12 +46,12 @@ func (f *Faucet) transferFunds(address crypto.Address) error { // findFundedAccount finds an account // whose balance is enough to cover the send amount -func (f *Faucet) findFundedAccount() (std.Account, error) { +func (f *Faucet) findFundedAccount(amount std.Coins) (std.Account, error) { // A funded account is an account that can // cover the initial transfer fee, as well // as the send amount estimatedFee := f.estimator.EstimateGasFee() - requiredFunds := f.sendAmount.Add(std.NewCoins(estimatedFee)) + requiredFunds := amount.Add(std.NewCoins(estimatedFee)) for _, address := range f.keyring.GetAddresses() { // Fetch the account diff --git a/transfer_test.go b/transfer_test.go index 71d0ec4..fdaf6ff 100644 --- a/transfer_test.go +++ b/transfer_test.go @@ -21,6 +21,7 @@ func TestFaucet_TransferFunds(t *testing.T) { var ( fetchErr = errors.New("unable to fetch account") + amount = std.NewCoins(std.NewCoin("ugnot", 1)) mockClient = &mockClient{ getAccountFn: func(_ crypto.Address) (std.Account, error) { @@ -44,7 +45,7 @@ func TestFaucet_TransferFunds(t *testing.T) { require.NotNil(t, f) // Attempt the transfer - assert.ErrorIs(t, f.transferFunds(crypto.Address{}), errNoFundedAccount) + assert.ErrorIs(t, f.transferFunds(crypto.Address{}, amount), errNoFundedAccount) }) t.Run("no funded accounts", func(t *testing.T) { @@ -72,7 +73,7 @@ func TestFaucet_TransferFunds(t *testing.T) { // Create faucet cfg := config.DefaultConfig() - cfg.SendAmount = sendAmount.String() + cfg.MaxSendAmount = sendAmount.String() f, err := NewFaucet( mockEstimator, @@ -84,7 +85,7 @@ func TestFaucet_TransferFunds(t *testing.T) { require.NotNil(t, f) // Attempt the transfer - assert.ErrorIs(t, f.transferFunds(crypto.Address{}), errNoFundedAccount) + assert.ErrorIs(t, f.transferFunds(crypto.Address{}, sendAmount), errNoFundedAccount) }) t.Run("unable to sign transaction", func(t *testing.T) { @@ -128,7 +129,7 @@ func TestFaucet_TransferFunds(t *testing.T) { // Create faucet cfg := config.DefaultConfig() - cfg.SendAmount = sendAmount.String() + cfg.MaxSendAmount = sendAmount.String() f, err := NewFaucet( mockEstimator, @@ -143,7 +144,7 @@ func TestFaucet_TransferFunds(t *testing.T) { require.NotNil(t, f) // Attempt the transfer - assert.ErrorIs(t, f.transferFunds(crypto.Address{}), signErr) + assert.ErrorIs(t, f.transferFunds(crypto.Address{}, sendAmount), signErr) }) t.Run("valid asset transfer", func(t *testing.T) { @@ -201,7 +202,7 @@ func TestFaucet_TransferFunds(t *testing.T) { // Create faucet cfg := config.DefaultConfig() - cfg.SendAmount = sendAmount.String() + cfg.MaxSendAmount = sendAmount.String() f, err := NewFaucet( mockEstimator, @@ -215,6 +216,6 @@ func TestFaucet_TransferFunds(t *testing.T) { require.NotNil(t, f) // Attempt the transfer - assert.NoError(t, f.transferFunds(crypto.Address{})) + assert.NoError(t, f.transferFunds(crypto.Address{}, sendAmount)) }) } diff --git a/types.go b/types.go index 8967282..ec948b0 100644 --- a/types.go +++ b/types.go @@ -19,7 +19,8 @@ type Requests []Request // Request is a single Faucet transfer request type Request struct { - To string `json:"to"` + To string `json:"to"` + Amount string `json:"amount"` } type Responses []Response