Skip to content

Commit c9e2b48

Browse files
committed
Merge branch 'ratelimiting'
2 parents ce95522 + e509f18 commit c9e2b48

File tree

13 files changed

+651
-209
lines changed

13 files changed

+651
-209
lines changed

backend/backend.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import (
5252
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
5353
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable"
5454
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable/action"
55-
"github.com/BitBoxSwiss/bitbox-wallet-app/util/ratelimit"
5655
"github.com/BitBoxSwiss/bitbox-wallet-app/util/socksproxy"
5756
"github.com/btcsuite/btcd/chaincfg"
5857
"github.com/ethereum/go-ethereum/params"
@@ -290,7 +289,7 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
290289
backend.notifier = notifier
291290
backend.socksProxy = backendProxy
292291
backend.httpClient = hclient
293-
backend.etherScanHTTPClient = ratelimit.FromTransport(hclient.Transport, etherscan.CallInterval)
292+
backend.etherScanHTTPClient = hclient
294293

295294
ratesCache := filepath.Join(arguments.CacheDirectoryPath(), "exchangerates")
296295
if err := os.MkdirAll(ratesCache, 0700); err != nil {

backend/coins/eth/etherscan/etherscan.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ import (
3535
"github.com/ethereum/go-ethereum/common"
3636
"github.com/ethereum/go-ethereum/common/hexutil"
3737
"github.com/ethereum/go-ethereum/core/types"
38+
"golang.org/x/time/rate"
3839
)
3940

40-
// CallInterval is the duration between etherscan requests.
41+
// callsPerSec is thenumber of etherscanr equests allowed
42+
// per second.
4143
// Etherscan rate limits to one request per 0.2 seconds.
42-
var CallInterval = 260 * time.Millisecond
44+
var callsPerSec = 3.8
4345

4446
const apiKey = "X3AFAGQT2QCAFTFPIH9VJY88H9PIQ2UWP7"
4547

@@ -50,17 +52,22 @@ const ERC20GasErr = "insufficient funds for gas * price + value"
5052
type EtherScan struct {
5153
url string
5254
httpClient *http.Client
55+
limiter *rate.Limiter
5356
}
5457

5558
// NewEtherScan creates a new instance of EtherScan.
5659
func NewEtherScan(url string, httpClient *http.Client) *EtherScan {
5760
return &EtherScan{
5861
url: url,
5962
httpClient: httpClient,
63+
limiter: rate.NewLimiter(rate.Limit(callsPerSec), 1),
6064
}
6165
}
6266

63-
func (etherScan *EtherScan) call(params url.Values, result interface{}) error {
67+
func (etherScan *EtherScan) call(ctx context.Context, params url.Values, result interface{}) error {
68+
if err := etherScan.limiter.Wait(ctx); err != nil {
69+
return errp.WithStack(err)
70+
}
6471
params.Set("apikey", apiKey)
6572
response, err := etherScan.httpClient.Get(etherScan.url + "?" + params.Encode())
6673
if err != nil {
@@ -323,7 +330,7 @@ func (etherScan *EtherScan) Transactions(
323330
result := struct {
324331
Result []*Transaction
325332
}{}
326-
if err := etherScan.call(params, &result); err != nil {
333+
if err := etherScan.call(context.TODO(), params, &result); err != nil {
327334
return nil, err
328335
}
329336
isERC20 := erc20Token != nil
@@ -338,7 +345,7 @@ func (etherScan *EtherScan) Transactions(
338345
resultInternal := struct {
339346
Result []*Transaction
340347
}{}
341-
if err := etherScan.call(params, &resultInternal); err != nil {
348+
if err := etherScan.call(context.TODO(), params, &resultInternal); err != nil {
342349
return nil, err
343350
}
344351
var err error
@@ -353,7 +360,7 @@ func (etherScan *EtherScan) Transactions(
353360

354361
// ----- RPC node proxy methods follow
355362

356-
func (etherScan *EtherScan) rpcCall(params url.Values, result interface{}) error {
363+
func (etherScan *EtherScan) rpcCall(ctx context.Context, params url.Values, result interface{}) error {
357364
params.Set("module", "proxy")
358365

359366
var wrapped struct {
@@ -364,7 +371,7 @@ func (etherScan *EtherScan) rpcCall(params url.Values, result interface{}) error
364371
} `json:"error"`
365372
Result *json.RawMessage `json:"result"`
366373
}
367-
if err := etherScan.call(params, &wrapped); err != nil {
374+
if err := etherScan.call(ctx, params, &wrapped); err != nil {
368375
return err
369376
}
370377
if wrapped.Error != nil {
@@ -389,7 +396,7 @@ func (etherScan *EtherScan) TransactionReceiptWithBlockNumber(
389396
params.Set("action", "eth_getTransactionReceipt")
390397
params.Set("txhash", hash.Hex())
391398
var result *rpcclient.RPCTransactionReceipt
392-
if err := etherScan.rpcCall(params, &result); err != nil {
399+
if err := etherScan.rpcCall(ctx, params, &result); err != nil {
393400
return nil, err
394401
}
395402
return result, nil
@@ -402,7 +409,7 @@ func (etherScan *EtherScan) TransactionByHash(
402409
params.Set("action", "eth_getTransactionByHash")
403410
params.Set("txhash", hash.Hex())
404411
var result rpcclient.RPCTransaction
405-
if err := etherScan.rpcCall(params, &result); err != nil {
412+
if err := etherScan.rpcCall(ctx, params, &result); err != nil {
406413
return nil, false, err
407414
}
408415
return &result.Transaction, result.BlockNumber == nil, nil
@@ -415,7 +422,7 @@ func (etherScan *EtherScan) BlockNumber(ctx context.Context) (*big.Int, error) {
415422
params.Set("tag", "latest")
416423
params.Set("boolean", "false")
417424
var header *types.Header
418-
if err := etherScan.rpcCall(params, &header); err != nil {
425+
if err := etherScan.rpcCall(ctx, params, &header); err != nil {
419426
return nil, err
420427
}
421428
return header.Number, nil
@@ -434,7 +441,7 @@ func (etherScan *EtherScan) Balance(ctx context.Context, account common.Address)
434441
params.Set("action", "balance")
435442
params.Set("address", account.Hex())
436443
params.Set("tag", "latest")
437-
if err := etherScan.call(params, &result); err != nil {
444+
if err := etherScan.call(ctx, params, &result); err != nil {
438445
return nil, err
439446
}
440447
if result.Status != "1" {
@@ -461,7 +468,7 @@ func (etherScan *EtherScan) ERC20Balance(account common.Address, erc20Token *erc
461468
params.Set("address", account.Hex())
462469
params.Set("contractaddress", erc20Token.ContractAddress().Hex())
463470
params.Set("tag", "latest")
464-
if err := etherScan.call(params, &result); err != nil {
471+
if err := etherScan.call(context.TODO(), params, &result); err != nil {
465472
return nil, err
466473
}
467474
if result.Status != "1" {
@@ -485,7 +492,7 @@ func (etherScan *EtherScan) CallContract(ctx context.Context, msg ethereum.CallM
485492
panic("not implemented")
486493
}
487494
var result hexutil.Bytes
488-
if err := etherScan.rpcCall(params, &result); err != nil {
495+
if err := etherScan.rpcCall(ctx, params, &result); err != nil {
489496
return nil, err
490497
}
491498
return result, nil
@@ -515,7 +522,7 @@ func (etherScan *EtherScan) EstimateGas(ctx context.Context, msg ethereum.CallMs
515522
callMsgParams(&params, msg)
516523

517524
var result hexutil.Uint64
518-
if err := etherScan.rpcCall(params, &result); err != nil {
525+
if err := etherScan.rpcCall(ctx, params, &result); err != nil {
519526
return 0, err
520527
}
521528
return uint64(result), nil
@@ -528,7 +535,7 @@ func (etherScan *EtherScan) PendingNonceAt(ctx context.Context, account common.A
528535
params.Set("address", account.Hex())
529536
params.Set("tag", "pending")
530537
var result hexutil.Uint64
531-
if err := etherScan.rpcCall(params, &result); err != nil {
538+
if err := etherScan.rpcCall(ctx, params, &result); err != nil {
532539
return 0, err
533540
}
534541
return uint64(result), nil
@@ -544,15 +551,15 @@ func (etherScan *EtherScan) SendTransaction(ctx context.Context, tx *types.Trans
544551
params := url.Values{}
545552
params.Set("action", "eth_sendRawTransaction")
546553
params.Set("hex", hexutil.Encode(encodedTx))
547-
return etherScan.rpcCall(params, nil)
554+
return etherScan.rpcCall(ctx, params, nil)
548555
}
549556

550557
// SuggestGasPrice implements rpc.Interface.
551558
func (etherScan *EtherScan) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
552559
params := url.Values{}
553560
params.Set("action", "eth_gasPrice")
554561
var result hexutil.Big
555-
if err := etherScan.rpcCall(params, &result); err != nil {
562+
if err := etherScan.rpcCall(ctx, params, &result); err != nil {
556563
return nil, err
557564
}
558565
return (*big.Int)(&result), nil
@@ -581,7 +588,7 @@ func (etherScan *EtherScan) FeeTargets(ctx context.Context) ([]*ethtypes.FeeTarg
581588
params := url.Values{}
582589
params.Set("module", "gastracker")
583590
params.Set("action", "gasoracle")
584-
if err := etherScan.call(params, &result); err != nil {
591+
if err := etherScan.call(ctx, params, &result); err != nil {
585592
return nil, err
586593
}
587594
// Convert string fields to int64

backend/rates/gecko.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package rates
22

3-
import "time"
3+
import (
4+
"time"
5+
6+
"golang.org/x/time/rate"
7+
)
48

59
const (
610
// See the following for docs and details: https://www.coingecko.com/en/api.
@@ -16,21 +20,19 @@ const (
1620
maxGeckoRange = 364 * 24 * time.Hour
1721
)
1822

19-
// apiRateLimit specifies the minimal interval between equally spaced API calls
23+
// apiRateLimit specifies the maximum number of API calls per second
2024
// to one of the supported exchange rates providers.
21-
func apiRateLimit(baseURL string) time.Duration {
25+
func apiRateLimit(baseURL string) rate.Limit {
2226
switch baseURL {
2327
default:
24-
return time.Second // arbitrary; localhost, staging, etc.
28+
return rate.Limit(1) // arbitrary; localhost, staging, etc.
2529
case coingeckoAPIV3:
2630
// API calls. From https://www.coingecko.com/en/api:
2731
// > Generous rate limits with up to 100 requests/minute
2832
// We use slightly lower value.
29-
return 2 * time.Second
33+
return rate.Limit(0.5)
3034
case shiftGeckoMirrorAPIV3:
31-
// Avoid zero to prevent unexpected panics like in time.NewTicker
32-
// and leave some room to breathe.
33-
return 10 * time.Millisecond
35+
return rate.Limit(100)
3436
}
3537
}
3638

backend/rates/history.go

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -278,38 +278,38 @@ func (updater *RateUpdater) fetchGeckoMarketRange(ctx context.Context, coin, fia
278278
}
279279

280280
// Make the call, abiding the upstream rate limits.
281-
msg := fmt.Sprintf("fetch coingecko coin=%s fiat=%s start=%s", coin, fiat, timeRange.start)
282281
var jsonBody struct{ Prices [][2]float64 } // [timestamp in milliseconds, value]
283-
callErr := updater.geckoLimiter.Call(ctx, msg, func() error {
284-
param := url.Values{
285-
"from": {strconv.FormatInt(timeRange.start.Unix(), 10)},
286-
"to": {strconv.FormatInt(timeRange.end().Unix(), 10)},
287-
"vs_currency": {gfiat},
288-
}
289-
endpoint := fmt.Sprintf("%s/coins/%s/market_chart/range?%s", updater.coingeckoURL, gcoin, param.Encode())
290-
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
291-
if err != nil {
292-
return err
293-
}
282+
if err := updater.geckoLimiter.Wait(ctx); err != nil {
283+
return nil, err
284+
}
294285

295-
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
296-
defer cancel()
297-
res, err := updater.httpClient.Do(req.WithContext(ctx))
298-
if err != nil {
299-
return err
300-
}
301-
defer res.Body.Close() //nolint:errcheck
302-
if res.StatusCode != http.StatusOK {
303-
return fmt.Errorf("fetchGeckoMarketRange: bad response code %d", res.StatusCode)
304-
}
305-
// 1Mb is more than enough for a single response, but make sure initial
306-
// download with empty cache fits here. See maxGeckoRange.
307-
return json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&jsonBody)
308-
})
309-
if callErr != nil {
310-
return nil, callErr
286+
param := url.Values{
287+
"from": {strconv.FormatInt(timeRange.start.Unix(), 10)},
288+
"to": {strconv.FormatInt(timeRange.end().Unix(), 10)},
289+
"vs_currency": {gfiat},
290+
}
291+
endpoint := fmt.Sprintf("%s/coins/%s/market_chart/range?%s", updater.coingeckoURL, gcoin, param.Encode())
292+
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
293+
if err != nil {
294+
return nil, err
295+
}
296+
297+
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
298+
defer cancel()
299+
res, err := updater.httpClient.Do(req.WithContext(ctx))
300+
if err != nil {
301+
return nil, err
302+
}
303+
defer res.Body.Close() //nolint:errcheck
304+
if res.StatusCode != http.StatusOK {
305+
return nil, fmt.Errorf("fetchGeckoMarketRange: bad response code %d", res.StatusCode)
311306
}
312307

308+
// 1Mb is more than enough for a single response, but make sure initial
309+
// download with empty cache fits here. See maxGeckoRange
310+
if err := json.NewDecoder(io.LimitReader(res.Body, 1<<20)).Decode(&jsonBody); err != nil {
311+
return nil, err
312+
}
313313
// Transform the response into a usable result.
314314
rates := make([]exchangeRate, len(jsonBody.Prices))
315315
for i, v := range jsonBody.Prices {

0 commit comments

Comments
 (0)