Skip to content

Commit f72cfa2

Browse files
feat: add support for custom drip amounts (#23)
* Add support for custom drip amounts * Minor tidy * Fix typo * Revert go mod changes from previous versions
1 parent db68cdf commit f72cfa2

File tree

11 files changed

+199
-40
lines changed

11 files changed

+199
-40
lines changed

cmd/serve.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,10 @@ func (c *faucetCfg) registerRootFlags(fs *flag.FlagSet) {
112112
)
113113

114114
fs.StringVar(
115-
&c.config.SendAmount,
115+
&c.config.MaxSendAmount,
116116
"send-amount",
117-
config.DefaultSendAmount,
118-
"the static send amount (native currency)",
117+
config.DefaultMaxSendAmount,
118+
"the static max send amount per drip (native currency)",
119119
)
120120

121121
fs.StringVar(

config/config.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
const (
1212
DefaultListenAddress = "0.0.0.0:8545"
1313
DefaultChainID = "dev"
14-
DefaultSendAmount = "1000000ugnot"
14+
DefaultMaxSendAmount = "1000000ugnot"
1515
//nolint:lll // Mnemonic is naturally long
1616
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"
1717
DefaultNumAccounts = uint64(1)
@@ -45,9 +45,9 @@ type Config struct {
4545
// The mnemonic for the faucet
4646
Mnemonic string `toml:"mnemonic"`
4747

48-
// The static send amount (native currency).
48+
// The static max send amount (native currency).
4949
// Format should be: <AMOUNT>ugnot
50-
SendAmount string `toml:"send_amount"`
50+
MaxSendAmount string `toml:"send_amount"`
5151

5252
// The number of faucet accounts,
5353
// based on the mnemonic (account 0, index x)
@@ -59,7 +59,7 @@ func DefaultConfig() *Config {
5959
return &Config{
6060
ListenAddress: DefaultListenAddress,
6161
ChainID: DefaultChainID,
62-
SendAmount: DefaultSendAmount,
62+
MaxSendAmount: DefaultMaxSendAmount,
6363
Mnemonic: DefaultMnemonic,
6464
NumAccounts: DefaultNumAccounts,
6565
CORSConfig: DefaultCORSConfig(),
@@ -79,7 +79,7 @@ func ValidateConfig(config *Config) error {
7979
}
8080

8181
// validate the send amount
82-
if !amountRegex.MatchString(config.SendAmount) {
82+
if !amountRegex.MatchString(config.MaxSendAmount) {
8383
return ErrInvalidSendAmount
8484
}
8585

config/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestConfig_ValidateConfig(t *testing.T) {
3131
t.Parallel()
3232

3333
cfg := DefaultConfig()
34-
cfg.SendAmount = "1000goo" // invalid denom
34+
cfg.MaxSendAmount = "1000goo" // invalid denom
3535

3636
assert.ErrorIs(t, ValidateConfig(cfg), ErrInvalidSendAmount)
3737
})

faucet.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type Faucet struct {
3535
handlers []Handler // request handlers
3636
prepareTxMsgFn PrepareTxMessageFn // transaction message creator
3737

38-
sendAmount std.Coins // for fast lookup
38+
maxSendAmount std.Coins // the max send amount per drip
3939
}
4040

4141
var noopLogger = slog.New(slog.NewTextHandler(io.Discard, nil))
@@ -76,8 +76,8 @@ func NewFaucet(
7676
}
7777

7878
// Set the send amount
79-
//nolint:errcheck // SendAmount is validated beforehand
80-
f.sendAmount, _ = std.ParseCoins(f.config.SendAmount)
79+
//nolint:errcheck // MaxSendAmount is validated beforehand
80+
f.maxSendAmount, _ = std.ParseCoins(f.config.MaxSendAmount)
8181

8282
// Generate the in-memory keyring
8383
f.keyring = memory.New(f.config.Mnemonic, f.config.NumAccounts)

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ module github.com/gnolang/faucet
33
go 1.21
44

55
require (
6-
github.com/gnolang/gno v0.0.0-20240308113041-45c8f900a1a3
6+
github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc
77
github.com/go-chi/chi/v5 v5.0.12
88
github.com/pelletier/go-toml v1.9.5
99
github.com/peterbourgon/ff/v3 v3.4.0
1010
github.com/rs/cors v1.10.1
11-
github.com/stretchr/testify v1.9.0
11+
github.com/stretchr/testify v1.8.4
1212
golang.org/x/sync v0.6.0
1313
)
1414

@@ -22,8 +22,8 @@ require (
2222
github.com/gorilla/websocket v1.5.1 // indirect
2323
github.com/jaekwon/testify v1.6.1 // indirect
2424
github.com/kr/pretty v0.1.0 // indirect
25+
github.com/kr/text v0.2.0 // indirect
2526
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
26-
github.com/linxGnu/grocksdb v1.8.4 // indirect
2727
github.com/pmezard/go-difflib v1.0.0 // indirect
2828
golang.org/x/crypto v0.19.0 // indirect
2929
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect

go.sum

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE
2828
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
2929
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
3030
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
31+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
3132
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3233
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3334
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
4344
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
4445
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
4546
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
46-
github.com/gnolang/gno v0.0.0-20240308113041-45c8f900a1a3 h1:jD9i4n582op16xHU7aKPtp3Oo7rJ8Q5+jpR6nE/ad6U=
47-
github.com/gnolang/gno v0.0.0-20240308113041-45c8f900a1a3/go.mod h1:jDARzJA+/H5YwCGpWuouqo4D0LMSNZVVgFQK/r/R7As=
47+
github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc h1:aYkkNfumtt9z8DeI7ZiFC+vMgFFadaGY0A97pXpOqZU=
48+
github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc/go.mod h1:jDARzJA+/H5YwCGpWuouqo4D0LMSNZVVgFQK/r/R7As=
4849
github.com/gnolang/goleveldb v0.0.9 h1:Q7rGko9oXMKtQA+Apeeed5a3sjba/mcDhzJGoTVLCKE=
4950
github.com/gnolang/goleveldb v0.0.9/go.mod h1:Dz6p9bmpy/FBESTgduiThZt5mToVDipcHGzj/zUOo8E=
5051
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
8384
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
8485
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
8586
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
86-
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
8787
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
88+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
89+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
8890
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
8991
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
9092
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
9193
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
92-
github.com/linxGnu/grocksdb v1.8.4 h1:ZMsBpPpJNtRLHiKKp0mI7gW+NT4s7UgfD5xHxx1jVRo=
93-
github.com/linxGnu/grocksdb v1.8.4/go.mod h1:xZCIb5Muw+nhbDK4Y5UJuOrin5MceOuiXkVUR7vp4WY=
94+
github.com/linxGnu/grocksdb v1.6.20 h1:C0SNv12/OBr/zOdGw6reXS+mKpIdQGb/AkZWjHYnO64=
95+
github.com/linxGnu/grocksdb v1.6.20/go.mod h1:IbTMGpmWg/1pg2hcG9LlxkqyqiJymdCweaUrzsLRFmg=
9496
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
9597
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
9698
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
111113
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
112114
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
113115
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
114-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
115-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
116+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
117+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
116118
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
117119
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
118120
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=

handler.go

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,25 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"regexp"
910

1011
"github.com/gnolang/faucet/writer"
1112
httpWriter "github.com/gnolang/faucet/writer/http"
1213
"github.com/gnolang/gno/tm2/pkg/crypto"
14+
"github.com/gnolang/gno/tm2/pkg/std"
1315
)
1416

1517
const (
1618
unableToHandleRequest = "unable to handle faucet request"
1719
faucetSuccess = "successfully executed faucet transfer"
1820
)
1921

20-
var errInvalidBeneficiary = errors.New("invalid beneficiary address")
22+
var (
23+
errInvalidBeneficiary = errors.New("invalid beneficiary address")
24+
errInvalidSendAmount = errors.New("invalid send amount")
25+
)
26+
27+
var amountRegex = regexp.MustCompile(`^\d+ugnot$`)
2128

2229
// defaultHTTPHandler is the default faucet transfer handler
2330
func (f *Faucet) defaultHTTPHandler(w http.ResponseWriter, r *http.Request) {
@@ -74,8 +81,39 @@ func (f *Faucet) handleRequest(writer writer.ResponseWriter, requests Requests)
7481
continue
7582
}
7683

84+
// Extract the send amount
85+
amount, err := extractSendAmount(baseRequest)
86+
if err != nil {
87+
// Save the error response
88+
responses[i] = Response{
89+
Result: unableToHandleRequest,
90+
Error: err.Error(),
91+
}
92+
93+
continue
94+
}
95+
96+
// Check if the amount is set
97+
if amount.IsZero() {
98+
// Drip amount is not set, use
99+
// the max faucet drip amount
100+
amount = f.maxSendAmount
101+
}
102+
103+
// Check if the amount exceeds the max
104+
// drip amount for the faucet
105+
if amount.IsAllGT(f.maxSendAmount) {
106+
// Save the error response
107+
responses[i] = Response{
108+
Result: unableToHandleRequest,
109+
Error: errInvalidSendAmount.Error(),
110+
}
111+
112+
continue
113+
}
114+
77115
// Run the method handler
78-
if err := f.transferFunds(beneficiary); err != nil {
116+
if err := f.transferFunds(beneficiary, amount); err != nil {
79117
f.logger.Debug(
80118
unableToHandleRequest,
81119
"request",
@@ -144,3 +182,23 @@ func extractBeneficiary(request Request) (crypto.Address, error) {
144182

145183
return beneficiary, nil
146184
}
185+
186+
// extractSendAmount extracts the drip amount from the base faucet request, if any
187+
func extractSendAmount(request Request) (std.Coins, error) {
188+
// Check if the amount is set
189+
if request.Amount == "" {
190+
return std.Coins{}, nil
191+
}
192+
193+
// Validate the send amount is valid
194+
if !amountRegex.MatchString(request.Amount) {
195+
return std.Coins{}, errInvalidSendAmount
196+
}
197+
198+
amount, err := std.ParseCoins(request.Amount)
199+
if err != nil {
200+
return std.Coins{}, fmt.Errorf("%w, %w", errInvalidSendAmount, err)
201+
}
202+
203+
return amount, nil
204+
}

handler_test.go

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func TestFaucet_Serve_ValidRequests(t *testing.T) {
8282

8383
var (
8484
gasFee = std.MustParseCoin("1ugnot")
85-
sendAmount = std.MustParseCoins(config.DefaultSendAmount)
85+
sendAmount = std.MustParseCoins(config.DefaultMaxSendAmount)
8686
)
8787

8888
var (
@@ -295,7 +295,7 @@ func TestFaucet_Serve_InvalidRequests(t *testing.T) {
295295

296296
var (
297297
gasFee = std.MustParseCoin("1ugnot")
298-
sendAmount = std.MustParseCoins(config.DefaultSendAmount)
298+
sendAmount = std.MustParseCoins(config.DefaultMaxSendAmount)
299299
)
300300

301301
var (
@@ -569,7 +569,7 @@ func TestFaucet_Serve_NoFundedAccounts(t *testing.T) {
569569

570570
var (
571571
gasFee = std.MustParseCoin("1ugnot")
572-
sendAmount = std.MustParseCoins(config.DefaultSendAmount)
572+
sendAmount = std.MustParseCoins(config.DefaultMaxSendAmount)
573573
)
574574

575575
var (
@@ -713,3 +713,100 @@ func TestFaucet_Serve_NoFundedAccounts(t *testing.T) {
713713
// Validate the broadcast tx
714714
assert.Nil(t, capturedTxs)
715715
}
716+
717+
func TestFaucet_Serve_InvalidSendAmount(t *testing.T) {
718+
t.Parallel()
719+
720+
// Extract the default send amount
721+
maxSendAmount := std.MustParseCoins(config.DefaultMaxSendAmount)
722+
723+
testTable := []struct {
724+
name string
725+
sendAmount std.Coins
726+
}{
727+
{
728+
"invalid send amount",
729+
std.NewCoins(std.NewCoin("atom", 10)),
730+
},
731+
{
732+
"excessive send amount",
733+
maxSendAmount.Add(std.MustParseCoins("100ugnot")),
734+
},
735+
}
736+
737+
for _, testCase := range testTable {
738+
testCase := testCase
739+
740+
t.Run(testCase.name, func(t *testing.T) {
741+
t.Parallel()
742+
743+
var (
744+
validAddress = crypto.MustAddressFromString("g155n659f89cfak0zgy575yqma64sm4tv6exqk99")
745+
gasFee = std.MustParseCoin("1ugnot")
746+
747+
singleInvalidRequest = Request{
748+
To: validAddress.String(),
749+
Amount: testCase.sendAmount.String(),
750+
}
751+
)
752+
753+
encodedSingleInvalidRequest, err := json.Marshal(
754+
singleInvalidRequest,
755+
)
756+
require.NoError(t, err)
757+
758+
getFaucetURL := func(address string) string {
759+
return fmt.Sprintf("http://%s", address)
760+
}
761+
762+
// Create a new faucet with default params
763+
cfg := config.DefaultConfig()
764+
cfg.ListenAddress = fmt.Sprintf("127.0.0.1:%d", getFreePort(t))
765+
cfg.MaxSendAmount = maxSendAmount.String()
766+
767+
f, err := NewFaucet(
768+
static.New(gasFee, 100000),
769+
&mockClient{},
770+
WithConfig(cfg),
771+
)
772+
773+
require.NoError(t, err)
774+
require.NotNil(t, f)
775+
776+
// Start the faucet
777+
ctx, cancelFn := context.WithCancel(context.Background())
778+
defer cancelFn()
779+
780+
g, gCtx := errgroup.WithContext(ctx)
781+
782+
g.Go(func() error {
783+
return f.Serve(gCtx)
784+
})
785+
786+
url := getFaucetURL(f.config.ListenAddress)
787+
788+
// Wait for the faucet to be started
789+
waitForServer(t, url)
790+
791+
// Execute the request
792+
respRaw, err := http.Post(
793+
url,
794+
jsonMimeType,
795+
bytes.NewBuffer(encodedSingleInvalidRequest),
796+
)
797+
require.NoError(t, err)
798+
799+
respBytes, err := io.ReadAll(respRaw.Body)
800+
require.NoError(t, err)
801+
802+
response := decodeResponse[Response](t, respBytes)
803+
804+
assert.Contains(t, response.Error, errInvalidSendAmount.Error())
805+
assert.Equal(t, unableToHandleRequest, response.Result)
806+
807+
// Stop the faucet and wait for it to finish
808+
cancelFn()
809+
assert.NoError(t, g.Wait())
810+
})
811+
}
812+
}

0 commit comments

Comments
 (0)