From 72c4058ef4df1e1f7e61839d7c6339b9e5275422 Mon Sep 17 00:00:00 2001 From: nfel Date: Thu, 21 Sep 2023 16:28:12 +0330 Subject: [PATCH 1/6] Added support for multiple keys --- client.go | 33 +++++++++++++++++++++++---------- client_test.go | 2 +- setup_e2e_test.go | 2 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index e69679a..2e842eb 100644 --- a/client.go +++ b/client.go @@ -15,15 +15,18 @@ import ( "net/http" "net/http/httputil" "net/url" + "sync" "time" ) // Client etherscan API client // Clients are safe for concurrent use by multiple goroutines. type Client struct { - coon *http.Client - key string - baseURL string + coon *http.Client + baseURL string + keys []string + keySelectMutex sync.Mutex + keySelectIdx int // Verbose when true, talks a lot Verbose bool @@ -37,12 +40,22 @@ type Client struct { AfterRequest func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) } +func (c *Client) getKey() string { + c.keySelectMutex.Lock() + defer c.keySelectMutex.Unlock() + c.keySelectIdx++ + if c.keySelectIdx >= len(c.keys) { + c.keySelectIdx = 0 + } + return c.keys[c.keySelectIdx] +} + // New initialize a new etherscan API client // please use pre-defined network value -func New(network Network, APIKey string) *Client { +func New(network Network, APIKeys []string) *Client { return NewCustomized(Customization{ Timeout: 30 * time.Second, - Key: APIKey, + Keys: APIKeys, BaseURL: fmt.Sprintf(`https://%s.etherscan.io/api?`, network.SubDomain()), }) } @@ -51,8 +64,8 @@ func New(network Network, APIKey string) *Client { type Customization struct { // Timeout for API call Timeout time.Duration - // API key applied from Etherscan - Key string + // API keys applied from Etherscan + Keys []string // Base URL like `https://api.etherscan.io/api?` BaseURL string // When true, talks a lot @@ -83,7 +96,7 @@ func NewCustomized(config Customization) *Client { } return &Client{ coon: httpClient, - key: config.Key, + keys: config.Keys, baseURL: config.BaseURL, Verbose: config.Verbose, BeforeRequest: config.BeforeRequest, @@ -117,7 +130,7 @@ func (c *Client) call(module, action string, param map[string]interface{}, outco err = wrapErr(err, "http.NewRequest") return } - req.Header.Set("User-Agent", "etherscan-api(Go)") + req.Header.Set("User-Agent", "etherscan-api-multikey(Go)") req.Header.Set("Content-Type", "application/json; charset=utf-8") if c.Verbose { @@ -196,7 +209,7 @@ func (c *Client) craftURL(module, action string, param map[string]interface{}) ( q := url.Values{ "module": []string{module}, "action": []string{action}, - "apikey": []string{c.key}, + "apikey": []string{c.getKey()}, } for k, v := range param { diff --git a/client_test.go b/client_test.go index a8d045c..4f34b1f 100644 --- a/client_test.go +++ b/client_test.go @@ -12,7 +12,7 @@ import ( ) func TestClient_craftURL(t *testing.T) { - c := New(Ropsten, "abc123") + c := New(Ropsten, []string{"abc123"}) const expected = `https://api-ropsten.etherscan.io/api?action=craftURL&apikey=abc123&four=d&four=e&four=f&module=testing&one=1&three=1&three=2&three=3&two=2` output := c.craftURL("testing", "craftURL", M{ diff --git a/setup_e2e_test.go b/setup_e2e_test.go index 64f2702..66599b7 100644 --- a/setup_e2e_test.go +++ b/setup_e2e_test.go @@ -32,7 +32,7 @@ func init() { } bucket = NewBucket(500 * time.Millisecond) - api = New(Mainnet, apiKey) + api = New(Mainnet, []string{apiKey}) api.Verbose = true api.BeforeRequest = func(module string, action string, param map[string]interface{}) error { bucket.Take() From 61ba84f2ba7c022d1046240e658311f940cb9696 Mon Sep 17 00:00:00 2001 From: nfel Date: Thu, 21 Sep 2023 16:39:57 +0330 Subject: [PATCH 2/6] Test Added --- multi_key_test.go | 20 ++++++++++++++++++++ setup_e2e_test.go | 15 ++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 multi_key_test.go diff --git a/multi_key_test.go b/multi_key_test.go new file mode 100644 index 0000000..84151f4 --- /dev/null +++ b/multi_key_test.go @@ -0,0 +1,20 @@ +package etherscan + +import "testing" + +// TestgetKey may fail is you run test parallel +func TestgetKey(t *testing.T) { + countApiKey, countBackupApiKey, k := 0, 0, "" + for i := 0; i < 10; i++ { + k = api.getKey() + if apiKey == k { + countApiKey++ + } else if backupApiKey == k { + countBackupApiKey++ + } + } + equal := countApiKey == 5 && countBackupApiKey == 5 + if !equal { + t.Error("api.getKey not working") + } +} diff --git a/setup_e2e_test.go b/setup_e2e_test.go index 66599b7..f7af078 100644 --- a/setup_e2e_test.go +++ b/setup_e2e_test.go @@ -9,12 +9,16 @@ package etherscan import ( "fmt" + "log" "os" "testing" "time" ) -const apiKeyEnvName = "ETHERSCAN_API_KEY" +const ( + apiKeyEnvName = "ETHERSCAN_API_KEY" + backupApiKeyEnvName = "BACKUP_ETHERSCAN_API_KEY" +) var ( // api test client for many test cases @@ -22,7 +26,8 @@ var ( // bucket default rate limiter bucket *Bucket // apiKey etherscan API key - apiKey string + apiKey string + backupApiKey string ) func init() { @@ -30,9 +35,13 @@ func init() { if apiKey == "" { panic(fmt.Sprintf("API key is empty, set env variable %q with a valid API key to proceed.", apiKeyEnvName)) } + backupApiKey = os.Getenv(apiKeyEnvName) + if backupApiKey == "" { + log.Printf("WARN: Backup API key is empty, set env variable %q with a valid API key to proceed.", backupApiKeyEnvName) + } bucket = NewBucket(500 * time.Millisecond) - api = New(Mainnet, []string{apiKey}) + api = New(Mainnet, []string{apiKey, backupApiKey}) api.Verbose = true api.BeforeRequest = func(module string, action string, param map[string]interface{}) error { bucket.Take() From 217c1e2bba4f182c3ef000e525d360d33d7743bc Mon Sep 17 00:00:00 2001 From: nfel Date: Thu, 21 Sep 2023 16:56:23 +0330 Subject: [PATCH 3/6] bad test fixed --- multi_key_test.go | 7 ++++--- setup_e2e_test.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/multi_key_test.go b/multi_key_test.go index 84151f4..4c6736c 100644 --- a/multi_key_test.go +++ b/multi_key_test.go @@ -2,11 +2,12 @@ package etherscan import "testing" -// TestgetKey may fail is you run test parallel -func TestgetKey(t *testing.T) { +// TestGetKey may fail is you run test parallel +func TestGetKey(t *testing.T) { countApiKey, countBackupApiKey, k := 0, 0, "" for i := 0; i < 10; i++ { k = api.getKey() + t.Logf("key: %s", k) if apiKey == k { countApiKey++ } else if backupApiKey == k { @@ -15,6 +16,6 @@ func TestgetKey(t *testing.T) { } equal := countApiKey == 5 && countBackupApiKey == 5 if !equal { - t.Error("api.getKey not working") + t.Errorf("api.getKey not working, expected 5 for each key, got main:%d , backup %d", countApiKey, countBackupApiKey) } } diff --git a/setup_e2e_test.go b/setup_e2e_test.go index f7af078..3573d26 100644 --- a/setup_e2e_test.go +++ b/setup_e2e_test.go @@ -35,7 +35,7 @@ func init() { if apiKey == "" { panic(fmt.Sprintf("API key is empty, set env variable %q with a valid API key to proceed.", apiKeyEnvName)) } - backupApiKey = os.Getenv(apiKeyEnvName) + backupApiKey = os.Getenv(backupApiKeyEnvName) if backupApiKey == "" { log.Printf("WARN: Backup API key is empty, set env variable %q with a valid API key to proceed.", backupApiKeyEnvName) } From f644dc7b9160c9c7dd2d97c8dc60e8947d7b5e10 Mon Sep 17 00:00:00 2001 From: nfel Date: Thu, 21 Sep 2023 17:02:13 +0330 Subject: [PATCH 4/6] v1 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5837de6..db8ee5b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/nanmu42/etherscan-api +module github.com/NFEL/etherscan-api-multikey go 1.13 From f0fcbd001fbbf3ebf4b10bc05b6a0301a67179ed Mon Sep 17 00:00:00 2001 From: nfel Date: Thu, 21 Sep 2023 17:10:02 +0330 Subject: [PATCH 5/6] Fixed Broken Method of New -> NewMultiKey [Added] --- client.go | 12 +++++++++++- client_test.go | 2 +- setup_e2e_test.go | 3 ++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 2e842eb..dc42968 100644 --- a/client.go +++ b/client.go @@ -52,7 +52,17 @@ func (c *Client) getKey() string { // New initialize a new etherscan API client // please use pre-defined network value -func New(network Network, APIKeys []string) *Client { +func New(network Network, APIKeys string) *Client { + return NewCustomized(Customization{ + Timeout: 30 * time.Second, + Keys: []string{APIKeys}, + BaseURL: fmt.Sprintf(`https://%s.etherscan.io/api?`, network.SubDomain()), + }) +} + +// New initialize a new etherscan API client +// please use pre-defined network value +func NewMultiKey(network Network, APIKeys []string) *Client { return NewCustomized(Customization{ Timeout: 30 * time.Second, Keys: APIKeys, diff --git a/client_test.go b/client_test.go index 4f34b1f..a8d045c 100644 --- a/client_test.go +++ b/client_test.go @@ -12,7 +12,7 @@ import ( ) func TestClient_craftURL(t *testing.T) { - c := New(Ropsten, []string{"abc123"}) + c := New(Ropsten, "abc123") const expected = `https://api-ropsten.etherscan.io/api?action=craftURL&apikey=abc123&four=d&four=e&four=f&module=testing&one=1&three=1&three=2&three=3&two=2` output := c.craftURL("testing", "craftURL", M{ diff --git a/setup_e2e_test.go b/setup_e2e_test.go index 3573d26..c262e8f 100644 --- a/setup_e2e_test.go +++ b/setup_e2e_test.go @@ -41,7 +41,8 @@ func init() { } bucket = NewBucket(500 * time.Millisecond) - api = New(Mainnet, []string{apiKey, backupApiKey}) + api = New(Mainnet, apiKey) + api = NewMultiKey(Mainnet, []string{apiKey, backupApiKey}) api.Verbose = true api.BeforeRequest = func(module string, action string, param map[string]interface{}) error { bucket.Take() From 088f22e0284300c9c0231ef2956acf9111d5b6cb Mon Sep 17 00:00:00 2001 From: NFEL <37847623+NFEL@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:15:53 +0330 Subject: [PATCH 6/6] Update go.mod Tagged v1.11 for personal use --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index db8ee5b..5837de6 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/NFEL/etherscan-api-multikey +module github.com/nanmu42/etherscan-api go 1.13