diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 7c8963dc69..5b410b8868 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -53,9 +53,10 @@ import ( ) var ( - genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with") - apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection") - wsEndpoint = flag.String("ws", "http://127.0.0.1:7777/", "Url to ws endpoint") + genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with") + apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection") + wsEndpoint = flag.String("ws", "http://127.0.0.1:7777/", "Url to ws endpoint") + wsEndpointMainnet = flag.String("ws.mainnet", "", "Url to ws endpoint of BSC mainnet") netnameFlag = flag.String("faucet.name", "", "Network name to assign to the faucet") payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to pay out per user request") @@ -82,6 +83,7 @@ var ( resendBatchSize = 3 resendMaxGasPrice = big.NewInt(50 * params.GWei) wsReadTimeout = 5 * time.Minute + minMainnetBalance = big.NewInt(5 * 1e7 * params.GWei) // 0.05 bnb ) var ( @@ -92,6 +94,11 @@ var ( //go:embed faucet.html var websiteTmpl string +func weiToEtherStringF2(wei *big.Int) string { + f, _ := new(big.Float).Quo(new(big.Float).SetInt(wei), big.NewFloat(params.Ether)).Float64() + return strconv.FormatFloat(f, 'f', 2, 64) +} + func main() { // Parse the flags and set up the logger to print everything requested flag.Parse() @@ -175,7 +182,7 @@ func main() { log.Crit("Failed to unlock faucet signer account", "err", err) } // Assemble and start the faucet light service - faucet, err := newFaucet(genesis, *wsEndpoint, ks, website.Bytes(), bep2eInfos) + faucet, err := newFaucet(genesis, *wsEndpoint, *wsEndpointMainnet, ks, website.Bytes(), bep2eInfos) if err != nil { log.Crit("Failed to start faucet", "err", err) } @@ -202,9 +209,10 @@ type bep2eInfo struct { // faucet represents a crypto faucet backed by an Ethereum light client. type faucet struct { - config *params.ChainConfig // Chain configurations for signing - client *ethclient.Client // Client connection to the Ethereum chain - index []byte // Index page to serve up on the web + config *params.ChainConfig // Chain configurations for signing + client *ethclient.Client // Client connection to the Ethereum chain + clientMainnet *ethclient.Client // Client connection to BSC mainnet for balance check + index []byte // Index page to serve up on the web keystore *keystore.KeyStore // Keystore containing the single signer account accounts.Account // Account funding user faucet requests @@ -233,7 +241,7 @@ type wsConn struct { wlock sync.Mutex } -func newFaucet(genesis *core.Genesis, url string, ks *keystore.KeyStore, index []byte, bep2eInfos map[string]bep2eInfo) (*faucet, error) { +func newFaucet(genesis *core.Genesis, url string, mainnetUrl string, ks *keystore.KeyStore, index []byte, bep2eInfos map[string]bep2eInfo) (*faucet, error) { bep2eAbi, err := abi.JSON(strings.NewReader(bep2eAbiJson)) if err != nil { return nil, err @@ -242,6 +250,11 @@ func newFaucet(genesis *core.Genesis, url string, ks *keystore.KeyStore, index [ if err != nil { return nil, err } + clientMainnet, err := ethclient.Dial(mainnetUrl) + if err != nil { + // skip mainnet balance check if it there is no available mainnet endpoint + log.Warn("dail mainnet endpoint failed", "mainnetUrl", mainnetUrl, "err", err) + } // Allow 1 request per minute with burst of 5, and cache up to 1000 IPs limiter, err := NewIPRateLimiter(rate.Limit(1.0), 5, 1000) @@ -250,16 +263,17 @@ func newFaucet(genesis *core.Genesis, url string, ks *keystore.KeyStore, index [ } return &faucet{ - config: genesis.Config, - client: client, - index: index, - keystore: ks, - account: ks.Accounts()[0], - timeouts: make(map[string]time.Time), - update: make(chan struct{}, 1), - bep2eInfos: bep2eInfos, - bep2eAbi: bep2eAbi, - limiter: limiter, + config: genesis.Config, + client: client, + clientMainnet: clientMainnet, + index: index, + keystore: ks, + account: ks.Accounts()[0], + timeouts: make(map[string]time.Time), + update: make(chan struct{}, 1), + bep2eInfos: bep2eInfos, + bep2eAbi: bep2eAbi, + limiter: limiter, }, nil } @@ -407,7 +421,7 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { } log.Info("Faucet funds requested", "url", msg.URL, "tier", msg.Tier, "ip", ip) - // If captcha verifications are enabled, make sure we're not dealing with a robot + // check #1: captcha verifications to exclude robot if *captchaToken != "" { form := url.Values{} form.Add("secret", *captchaSecret) @@ -486,7 +500,7 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { } log.Info("Faucet request valid", "url", msg.URL, "tier", msg.Tier, "user", username, "address", address, "ip", ip) - // Ensure the user didn't request funds too recently + // check #2: check IP and ID(address) to ensure the user didn't request funds too recently, f.lock.Lock() var ( fund bool @@ -494,68 +508,101 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { ) if ipTimeout := f.timeouts[ips[len(ips)-2]]; time.Now().Before(ipTimeout) { + f.lock.Unlock() if err = sendError(wsconn, fmt.Errorf("%s left until next allowance", common.PrettyDuration(time.Until(ipTimeout)))); err != nil { // nolint: gosimple log.Warn("Failed to send funding error to client", "err", err) + return } + continue + } + if timeout = f.timeouts[id]; time.Now().Before(timeout) { f.lock.Unlock() + // Send an error if too frequent funding, otherwise a success + if err = sendError(wsconn, fmt.Errorf("%s left until next allowance", common.PrettyDuration(time.Until(timeout)))); err != nil { // nolint: gosimple + log.Warn("Failed to send funding error to client", "err", err) + return + } continue } - - if timeout = f.timeouts[id]; time.Now().After(timeout) { - var tx *types.Transaction - if msg.Symbol == "BNB" { - // User wasn't funded recently, create the funding transaction - amount := new(big.Int).Div(new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether), big.NewInt(10)) - amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil)) - amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil)) - - tx = types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil) + // check #3: minimum mainnet balance check, internal error will bypass the check to avoid blocking the faucet service + if f.clientMainnet != nil { + mainnetAddr := address + balanceMainnet, err := f.clientMainnet.BalanceAt(context.Background(), mainnetAddr, nil) + if err != nil { + log.Warn("check balance failed, call BalanceAt", "err", err) + } else if balanceMainnet == nil { + log.Warn("check balance failed, balanceMainnet is nil") } else { - tokenInfo, ok := f.bep2eInfos[msg.Symbol] - if !ok { + if balanceMainnet.Cmp(minMainnetBalance) < 0 { f.lock.Unlock() - log.Warn("Failed to find symbol", "symbol", msg.Symbol) - continue - } - input, err := f.bep2eAbi.Pack("transfer", address, &tokenInfo.Amount) - if err != nil { - f.lock.Unlock() - log.Warn("Failed to pack transfer transaction", "err", err) + log.Warn("insufficient enough BNB on BSC mainnet", "address", mainnetAddr, + "balanceMainnet", balanceMainnet, "minimum mainnet BNB request", minMainnetBalance) + // Send an error if failed to meet the minimum balance requirement + if err = sendError(wsconn, fmt.Errorf("%s, insufficient BNB on BSC mainnet(should >=%sBNB)", + address.Hex(), weiToEtherStringF2(minMainnetBalance))); err != nil { + log.Warn("Failed to send mainnet minimum balance error to client", "err", err) + return + } continue } - tx = types.NewTransaction(f.nonce+uint64(len(f.reqs)), tokenInfo.Contract, nil, 420000, f.price, input) } - signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID) - if err != nil { + } + + // now, it is ok to send tBNB or other tokens + var tx *types.Transaction + if msg.Symbol == "BNB" { + // User wasn't funded recently, create the funding transaction + amount := new(big.Int).Div(new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether), big.NewInt(10)) + amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil)) + amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil)) + + tx = types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil) + } else { + tokenInfo, ok := f.bep2eInfos[msg.Symbol] + if !ok { f.lock.Unlock() - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send transaction creation error to client", "err", err) - return - } + log.Warn("Failed to find symbol", "symbol", msg.Symbol) continue } - // Submit the transaction and mark as funded if successful - if err := f.client.SendTransaction(context.Background(), signed); err != nil { + input, err := f.bep2eAbi.Pack("transfer", address, &tokenInfo.Amount) + if err != nil { f.lock.Unlock() - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send transaction transmission error to client", "err", err) - return - } + log.Warn("Failed to pack transfer transaction", "err", err) continue } - f.reqs = append(f.reqs, &request{ - Avatar: avatar, - Account: address, - Time: time.Now(), - Tx: signed, - }) - timeout := time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute - grace := timeout / 288 // 24h timeout => 5m grace - - f.timeouts[id] = time.Now().Add(timeout - grace) - f.timeouts[ips[len(ips)-2]] = time.Now().Add(timeout - grace) - fund = true + tx = types.NewTransaction(f.nonce+uint64(len(f.reqs)), tokenInfo.Contract, nil, 420000, f.price, input) + } + signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID) + if err != nil { + f.lock.Unlock() + if err = sendError(wsconn, err); err != nil { + log.Warn("Failed to send transaction creation error to client", "err", err) + return + } + continue } + // Submit the transaction and mark as funded if successful + if err := f.client.SendTransaction(context.Background(), signed); err != nil { + f.lock.Unlock() + if err = sendError(wsconn, err); err != nil { + log.Warn("Failed to send transaction transmission error to client", "err", err) + return + } + continue + } + f.reqs = append(f.reqs, &request{ + Avatar: avatar, + Account: address, + Time: time.Now(), + Tx: signed, + }) + timeoutInt64 := time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute + grace := timeoutInt64 / 288 // 24h timeout => 5m grace + + f.timeouts[id] = time.Now().Add(timeoutInt64 - grace) + f.timeouts[ips[len(ips)-2]] = time.Now().Add(timeoutInt64 - grace) + fund = true + f.lock.Unlock() // Send an error if too frequent funding, otherwise a success