diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..71b970a
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: nanmu42
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..fc72752
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,32 @@
+name: CI
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  schedule:
+    - cron: "42 6 * * 0"
+
+jobs:
+  build:
+    environment: "CI Test"
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ">=1.17.4"
+
+      - name: Test
+        env:
+          ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
+        run: go test -v -coverprofile=coverage.txt -covermode=count ./...
+
+      - name: Codecov
+        uses: codecov/codecov-action@v2.1.0
+
+      - name: golangci-lint
+        uses: golangci/golangci-lint-action@v2
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e0ab53c..c7dafde 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@
 
 # Output of the go coverage tool, specifically when used with LiteIDE
 *.out
+coverage.txt
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e9a863d..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-language: go
-
-script:
-- go test -v -coverprofile=coverage.txt -covermode=count ./...
-
-after_success:
-- bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
diff --git a/README.md b/README.md
index 4087c3a..56acd1f 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,21 @@
+**English** | [中文](https://github.com/nanmu42/etherscan-api/blob/master/README_ZH.md)
+
 # etherscan-api
 
-[![Build Status](https://travis-ci.org/nanmu42/etherscan-api.svg?branch=master)](https://travis-ci.org/nanmu42/etherscan-api)
-[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api)
-[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api)
 [![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api)
-[中文文档](https://github.com/nanmu42/etherscan-api/blob/master/README_ZH.md)
+[![CI status](https://github.com/nanmu42/etherscan-api/actions/workflows/ci.yaml/badge.svg)](https://github.com/nanmu42/etherscan-api/actions)
+[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api)
+[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api)
 
-Go bindings to the Etherscan.io API, with nearly Full implementation(accounts, transactions, tokens, contracts, blocks, stats), full network support(Mainnet, Ropsten, Kovan, Rinkby, Tobalaba), and only depending on standard library. :wink:
+Golang client for the Etherscan.io API(and its families like BscScan), with nearly full implementation(accounts, transactions, tokens, contracts, blocks, stats), full network support(Mainnet, Ropsten, Kovan, Rinkby, Goerli, Tobalaba), and only depending on standard library. :wink:
 
 # Usage
 
-Create a API instance and off you go. :rocket:
+```bash
+go get github.com/nanmu42/etherscan-api
+```
+
+Create an API instance and off you go. :rocket:
 
 ```go
 import (
@@ -22,6 +27,15 @@ func main() {
 	// create a API client for specified ethereum net
 	// there are many pre-defined network in package
 	client := etherscan.New(etherscan.Mainnet, "[your API key]")
+	
+	// or, if you are working with etherscan-family API like BscScan
+	//
+	// client := etherscan.NewCustomized(etherscan.Customization{
+	// Timeout:       15 * time.Second,
+	// Key:           "You key here",
+	// BaseURL:       "https://api.bscscan.com/api?",
+	// Verbose:       false,
+	// })
 
 	// (optional) add hooks, e.g. for rate limit
 	client.BeforeRequest = func(module, action string, param map[string]interface{}) error {
@@ -61,6 +75,6 @@ I am not from Etherscan and I just find their service really useful, so I implem
 
 # License
 
-Use of this work is governed by a MIT License.
+Use of this work is governed by an MIT License.
 
 You may find a license copy in project root.
diff --git a/README_ZH.md b/README_ZH.md
index 1e38dae..905a770 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -1,17 +1,22 @@
+[English](https://github.com/nanmu42/etherscan-api/blob/master/README.md) | **中文**
+
 # etherscan-api
 
-[![Build Status](https://travis-ci.org/nanmu42/etherscan-api.svg?branch=master)](https://travis-ci.org/nanmu42/etherscan-api)
-[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api)
-[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api)
 [![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api)
-[English Readme](https://github.com/nanmu42/etherscan-api/blob/master/README.md)
+[![CI status](https://github.com/nanmu42/etherscan-api/actions/workflows/ci.yaml/badge.svg)](https://github.com/nanmu42/etherscan-api/actions)
+[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api)
+[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api)
 
-Etherscan.io的Golang实现，
+Etherscan API的Golang客户端，
 支持几乎所有功能（accounts, transactions, tokens, contracts, blocks, stats），
-所有公共网络（Mainnet, Ropsten, Kovan, Rinkby, Tobalaba）。
+所有公共网络（Mainnet, Ropsten, Kovan, Rinkby, Goerli, Tobalaba）。
 本项目只依赖于官方库。 :wink:
 
-# Usage
+# 使用方法
+
+```bash
+go get github.com/nanmu42/etherscan-api
+```
 
 填入网络选项和API Key即可开始使用。 :rocket:
 
@@ -24,7 +29,16 @@ import (
 func main() {
 	// 创建连接指定网络的客户端
 	client := etherscan.New(etherscan.Mainnet, "[your API key]")
-
+	
+	// 或者，如果你要调用的是EtherScan家族的BscScan：
+	//
+	// client := etherscan.NewCustomized(etherscan.Customization{
+	// Timeout:       15 * time.Second,
+	// Key:           "You key here",
+	// BaseURL:       "https://api.bscscan.com/api?",
+	// Verbose:       false,
+	// })	
+	
 	// （可选）按需注册钩子函数，例如用于速率控制
 	client.BeforeRequest = func(module, action string, param map[string]interface{}) error {
 		// ...
diff --git a/account.go b/account.go
index 85d48fa..e6a9ac1 100644
--- a/account.go
+++ b/account.go
@@ -79,7 +79,15 @@ func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock *
 // contract address and/or from/to address.
 //
 // leave undesired condition to nil.
-func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int) (txs []ERC20Transfer, err error) {
+//
+// Note on a Etherscan bug:
+// Some ERC20 contract does not have valid decimals information in Etherscan.
+// When that happens, TokenName, TokenSymbol are empty strings,
+// and TokenDecimal is 0.
+//
+// More information can be found at:
+// https://github.com/nanmu42/etherscan-api/issues/8
+func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC20Transfer, err error) {
 	param := M{
 		"page":   page,
 		"offset": offset,
@@ -89,10 +97,64 @@ func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *in
 	compose(param, "startblock", startBlock)
 	compose(param, "endblock", endBlock)
 
+	if desc {
+		param["sort"] = "desc"
+	} else {
+		param["sort"] = "asc"
+	}
+
 	err = c.call("account", "tokentx", param, &txs)
 	return
 }
 
+// ERC721Transfers get a list of "erc721 - token transfer events" by
+// contract address and/or from/to address.
+//
+// leave undesired condition to nil.
+func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC721Transfer, err error) {
+	param := M{
+		"page":   page,
+		"offset": offset,
+	}
+	compose(param, "contractaddress", contractAddress)
+	compose(param, "address", address)
+	compose(param, "startblock", startBlock)
+	compose(param, "endblock", endBlock)
+
+	if desc {
+		param["sort"] = "desc"
+	} else {
+		param["sort"] = "asc"
+	}
+
+	err = c.call("account", "tokennfttx", param, &txs)
+	return
+}
+
+// ERC1155Transfers get a list of "erc1155 - token transfer events" by
+// contract address and/or from/to address.
+//
+// leave undesired condition to nil.
+func (c *Client) ERC1155Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC1155Transfer, err error) {
+	param := M{
+		"page":   page,
+		"offset": offset,
+	}
+	compose(param, "contractaddress", contractAddress)
+	compose(param, "address", address)
+	compose(param, "startblock", startBlock)
+	compose(param, "endblock", endBlock)
+
+	if desc {
+		param["sort"] = "desc"
+	} else {
+		param["sort"] = "asc"
+	}
+
+	err = c.call("account", "token1155tx", param, &txs)
+	return
+}
+
 // BlocksMinedByAddress gets list of blocks mined by address
 func (c *Client) BlocksMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) {
 	param := M{
diff --git a/account_e2e_test.go b/account_e2e_test.go
index c489697..f7688e3 100644
--- a/account_e2e_test.go
+++ b/account_e2e_test.go
@@ -8,12 +8,14 @@
 package etherscan
 
 import (
+	"encoding/json"
+	"fmt"
 	"math/big"
 	"testing"
 )
 
 func TestClient_AccountBalance(t *testing.T) {
-	balance, err := api.AccountBalance("0x281055afc982d96fab65b3a49cac8b878184cb16")
+	balance, err := api.AccountBalance("0x0000000000000000000000000000000000000000")
 	noError(t, err, "api.AccountBalance")
 
 	if balance.Int().Cmp(big.NewInt(0)) != 1 {
@@ -23,10 +25,10 @@ func TestClient_AccountBalance(t *testing.T) {
 
 func TestClient_MultiAccountBalance(t *testing.T) {
 	balances, err := api.MultiAccountBalance(
-		"0x281055afc982d96fab65b3a49cac8b878184cb16",
-		"0x6f46cf5569aefa1acc1009290c8e043747172d89",
-		"0x90e63c3d53e0ea496845b7a03ec7548b70014a91",
-		"0x53d284357ec70ce289d6d64134dfac8e511c8a3d")
+		"0x0000000000000000000000000000000000000000",
+		"0x0000000000000000000000000000000000000001",
+		"0x0000000000000000000000000000000000000002",
+		"0x0000000000000000000000000000000000000003")
 	noError(t, err, "api.MultiAccountBalance")
 
 	for i, item := range balances {
@@ -73,11 +75,12 @@ func TestClient_ERC20Transfers(t *testing.T) {
 	const (
 		wantLen1 = 3
 		wantLen2 = 458
+		wantLen3 = 2
 	)
 
 	var a, b = 3273004, 3328071
 	var contract, address = "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a", "0x4e83362442b8d1bec281594cea3050c8eb01311c"
-	txs, err := api.ERC20Transfers(&contract, &address, &a, &b, 1, 500)
+	txs, err := api.ERC20Transfers(&contract, &address, &a, &b, 1, 500, false)
 	noError(t, err, "api.ERC20Transfers 1")
 
 	//j, _ := json.MarshalIndent(txs, "", "  ")
@@ -87,11 +90,29 @@ func TestClient_ERC20Transfers(t *testing.T) {
 		t.Errorf("got txs length %v, want %v", len(txs), wantLen1)
 	}
 
-	txs, err = api.ERC20Transfers(nil, &address, nil, &b, 1, 500)
-	noError(t, err, "api.ERC20Transfers 2")
+	txs, err = api.ERC20Transfers(nil, &address, nil, &b, 1, 500, false)
+	noError(t, err, "api.ERC20Transfers 2 asc")
+	if len(txs) != wantLen2 {
+		t.Errorf("got txs length %v, want %v", len(txs), wantLen2)
+	}
+
+	txs, err = api.ERC20Transfers(nil, &address, nil, &b, 1, 500, true)
+	noError(t, err, "api.ERC20Transfers 2 desc")
+
 	if len(txs) != wantLen2 {
 		t.Errorf("got txs length %v, want %v", len(txs), wantLen2)
 	}
+
+	// some ERC20 contract does not have valid decimals information in Etherscan,
+	// which brings errors like `json: invalid use of ,string struct tag, trying to unmarshal "" into uint8`
+	var specialContract = "0x5eac95ad5b287cf44e058dcf694419333b796123"
+	var specialStartHeight = 6024142
+	var specialEndHeight = 6485274
+	txs, err = api.ERC20Transfers(&specialContract, nil, &specialStartHeight, &specialEndHeight, 1, 500, false)
+	noError(t, err, "api.ERC20Transfers 2")
+	if len(txs) != wantLen3 {
+		t.Errorf("got txs length %v, want %v", len(txs), wantLen3)
+	}
 }
 
 func TestClient_BlocksMinedByAddress(t *testing.T) {
@@ -130,3 +151,39 @@ func TestClient_TokenBalance(t *testing.T) {
 		t.Errorf("api.TokenBalance not working, got balance %s", balance.Int().String())
 	}
 }
+
+func TestClient_ERC721Transfers(t *testing.T) {
+	const (
+		wantLen = 351
+	)
+
+	var a, b = 4708442, 9231168
+	var contract, address = "0x06012c8cf97bead5deae237070f9587f8e7a266d", "0x6975be450864c02b4613023c2152ee0743572325"
+	txs, err := api.ERC721Transfers(&contract, &address, &a, &b, 1, 500, true)
+	noError(t, err, "api.ERC721Transfers")
+
+	j, _ := json.MarshalIndent(txs, "", "  ")
+	fmt.Printf("%s\n", j)
+
+	if len(txs) != wantLen {
+		t.Errorf("got txs length %v, want %v", len(txs), wantLen)
+	}
+}
+
+func TestClient_ERC1155Transfers(t *testing.T) {
+	const (
+		wantLen = 1
+	)
+
+	var a, b = 128135633, 1802672
+	var contract, address = "0x3edf71a31b80Ff6a45Fdb0858eC54DE98dF047AA", "0x4b986EF20Bb83532911521FB4F6F5605122a0721"
+	txs, err := api.ERC1155Transfers(&contract, &address, &b, &a, 0, 0, true)
+	noError(t, err, "api.ERC721Transfers")
+
+	j, _ := json.MarshalIndent(txs, "", "  ")
+	fmt.Printf("%s\n", j)
+
+	if len(txs) != wantLen {
+		t.Errorf("got txs length %v, want %v", len(txs), wantLen)
+	}
+}
diff --git a/block.go b/block.go
index 8c3736b..aea4ea1 100644
--- a/block.go
+++ b/block.go
@@ -7,6 +7,11 @@
 
 package etherscan
 
+import (
+	"fmt"
+	"strconv"
+)
+
 // BlockReward gets block and uncle rewards by block number
 func (c *Client) BlockReward(blockNum int) (rewards BlockRewards, err error) {
 	param := M{
@@ -16,3 +21,29 @@ func (c *Client) BlockReward(blockNum int) (rewards BlockRewards, err error) {
 	err = c.call("block", "getblockreward", param, &rewards)
 	return
 }
+
+// BlockNumber gets the closest block number by UNIX timestamp
+//
+// valid closest option: before, after
+func (c *Client) BlockNumber(timestamp int64, closest string) (blockNumber int, err error) {
+	var blockNumberStr string
+
+	param := M{
+		"timestamp": strconv.FormatInt(timestamp, 10),
+		"closest":   closest,
+	}
+
+	err = c.call("block", "getblocknobytime", param, &blockNumberStr)
+
+	if err != nil {
+		return
+	}
+
+	blockNumber, err = strconv.Atoi(blockNumberStr)
+	if err != nil {
+		err = fmt.Errorf("parsing block number %q: %w", blockNumberStr, err)
+		return
+	}
+
+	return
+}
diff --git a/block_e2e_test.go b/block_e2e_test.go
index 26b603e..c49a8ac 100644
--- a/block_e2e_test.go
+++ b/block_e2e_test.go
@@ -24,3 +24,23 @@ func TestClient_BlockReward(t *testing.T) {
 		t.Errorf("api.BlockReward not working, got %s, want %s", j, ans)
 	}
 }
+
+func TestClient_BlockNumber(t *testing.T) {
+	// Note: All values taken from docs.etherscan.io/api-endpoints/blocks
+	const ansBefore = 9251482
+	const ansAfter = 9251483
+
+	blockNumber, err := api.BlockNumber(1578638524, "before")
+	noError(t, err, "api.BlockNumber")
+
+	if blockNumber != ansBefore {
+		t.Errorf(`api.BlockNumber(1578638524, "before") not working, got %d, want %d`, blockNumber, ansBefore)
+	}
+
+	blockNumber, err = api.BlockNumber(1578638524, "after")
+	noError(t, err, "api.BlockNumber")
+
+	if blockNumber != ansAfter {
+		t.Errorf(`api.BlockNumber(1578638524,"after") not working, got %d, want %d`, blockNumber, ansAfter)
+	}
+}
diff --git a/client.go b/client.go
index e7422cd..e69679a 100644
--- a/client.go
+++ b/client.go
@@ -15,13 +15,13 @@ import (
 	"net/http"
 	"net/http/httputil"
 	"net/url"
+	"time"
 )
 
 // Client etherscan API client
 // Clients are safe for concurrent use by multiple goroutines.
 type Client struct {
 	coon    *http.Client
-	network Network
 	key     string
 	baseURL string
 
@@ -40,11 +40,54 @@ type Client struct {
 // New initialize a new etherscan API client
 // please use pre-defined network value
 func New(network Network, APIKey string) *Client {
+	return NewCustomized(Customization{
+		Timeout: 30 * time.Second,
+		Key:     APIKey,
+		BaseURL: fmt.Sprintf(`https://%s.etherscan.io/api?`, network.SubDomain()),
+	})
+}
+
+// Customization is used in NewCustomized()
+type Customization struct {
+	// Timeout for API call
+	Timeout time.Duration
+	// API key applied from Etherscan
+	Key string
+	// Base URL like `https://api.etherscan.io/api?`
+	BaseURL string
+	// When true, talks a lot
+	Verbose bool
+	// HTTP Client to be used. Specifying this value will ignore the Timeout value set
+	// Set your own timeout.
+	Client *http.Client
+
+	// BeforeRequest runs before every client request, in the same goroutine.
+	// May be used in rate limit.
+	// Request will be aborted, if BeforeRequest returns non-nil err.
+	BeforeRequest func(module, action string, param map[string]interface{}) error
+
+	// AfterRequest runs after every client request, even when there is an error.
+	AfterRequest func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error)
+}
+
+// NewCustomized initialize a customized API client,
+// useful when calling against etherscan-family API like BscScan.
+func NewCustomized(config Customization) *Client {
+	var httpClient *http.Client
+	if config.Client != nil {
+		httpClient = config.Client
+	} else {
+		httpClient = &http.Client{
+			Timeout: config.Timeout,
+		}
+	}
 	return &Client{
-		coon:    &http.Client{},
-		network: network,
-		key:     APIKey,
-		baseURL: fmt.Sprintf(`https://%s.etherscan.io/api?`, network.SubDomain()),
+		coon:          httpClient,
+		key:           config.Key,
+		baseURL:       config.BaseURL,
+		Verbose:       config.Verbose,
+		BeforeRequest: config.BeforeRequest,
+		AfterRequest:  config.AfterRequest,
 	}
 }
 
@@ -134,7 +177,12 @@ func (c *Client) call(module, action string, param map[string]interface{}, outco
 		return
 	}
 
-	err = json.Unmarshal(envelope.Result, outcome)
+	// workaround for missing tokenDecimal for some tokentx calls
+	if action == "tokentx" {
+		err = json.Unmarshal(bytes.Replace(envelope.Result, []byte(`"tokenDecimal":""`), []byte(`"tokenDecimal":"0"`), -1), outcome)
+	} else {
+		err = json.Unmarshal(envelope.Result, outcome)
+	}
 	if err != nil {
 		err = wrapErr(err, "json unmarshal outcome")
 		return
diff --git a/gas_tracker.go b/gas_tracker.go
new file mode 100644
index 0000000..02fe5ca
--- /dev/null
+++ b/gas_tracker.go
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 Avi Misra
+ *
+ * Use of this work is governed by a MIT License.
+ * You may find a license copy in project root.
+ */
+
+package etherscan
+
+import "time"
+
+// GasEstiamte gets estiamted confirmation time (in seconds) at the given gas price
+func (c *Client) GasEstimate(gasPrice int) (confirmationTimeInSec time.Duration, err error) {
+	params := M{"gasPrice": gasPrice}
+	var confTime string
+	err = c.call("gastracker", "gasestimate", params, &confTime)
+	if err != nil {
+		return
+	}
+	return time.ParseDuration(confTime + "s")
+}
+
+// GasOracle gets suggested gas prices (in Gwei)
+func (c *Client) GasOracle() (gasPrices GasPrices, err error) {
+	err = c.call("gastracker", "gasoracle", M{}, &gasPrices)
+	return
+}
diff --git a/gas_tracker_e2e_test.go b/gas_tracker_e2e_test.go
new file mode 100644
index 0000000..3905bc3
--- /dev/null
+++ b/gas_tracker_e2e_test.go
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2018 LI Zhennan
+ *
+ * Use of this work is governed by a MIT License.
+ * You may find a license copy in project root.
+ */
+
+package etherscan
+
+import (
+	"testing"
+)
+
+//GasEstiamte generates dynamic data. Best we can do is ensure all fields are populated
+func TestClient_GasEstimate(t *testing.T) {
+	_, err := api.GasEstimate(20000000)
+	noError(t, err, "api.GasEstimate")
+}
+
+//GasOracle generates dynamic data. Best we can do is ensure all fields are populated
+func TestClient_GasOracle(t *testing.T) {
+	gasPrice, err := api.GasOracle()
+	noError(t, err, "api.GasOrcale")
+
+	if 0 == len(gasPrice.GasUsedRatio) {
+		t.Errorf("gasPrice.GasUsedRatio empty")
+	}
+
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..5837de6
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module github.com/nanmu42/etherscan-api
+
+go 1.13
+
+require github.com/google/go-cmp v0.5.7
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..a6ca3a4
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/logs.go b/logs.go
new file mode 100644
index 0000000..0b21124
--- /dev/null
+++ b/logs.go
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022 Avi Misra
+ *
+ * Use of this work is governed by a MIT License.
+ * You may find a license copy in project root.
+ */
+
+package etherscan
+
+// GetLogs gets logs that match "topic" emitted by the specified "address" between the "fromBlock" and "toBlock"
+func (c *Client) GetLogs(fromBlock, toBlock int, address, topic string) (logs []Log, err error) {
+	param := M{
+		"fromBlock": fromBlock,
+		"toBlock":   toBlock,
+		"topic0":    topic,
+		"address":   address,
+	}
+
+	err = c.call("logs", "getLogs", param, &logs)
+	return
+}
diff --git a/logs_e2e_test.go b/logs_e2e_test.go
new file mode 100644
index 0000000..e4490d2
--- /dev/null
+++ b/logs_e2e_test.go
@@ -0,0 +1,31 @@
+package etherscan
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestClient_GetLogs(t *testing.T) {
+	expectedLogs := []Log{
+		Log{
+			Address:         "0x33990122638b9132ca29c723bdf037f1a891a70c",
+			Topics:          []string{"0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545", "0x72657075746174696f6e00000000000000000000000000000000000000000000", "0x000000000000000000000000d9b2f59f3b5c7b3c67047d2f03c3e8052470be92"},
+			Data:            "0x",
+			BlockNumber:     "0x5c958",
+			BlockHash:       "0xe32a9cac27f823b18454e8d69437d2af41a1b81179c6af2601f1040a72ad444b",
+			TransactionHash: "0x0b03498648ae2da924f961dda00dc6bb0a8df15519262b7e012b7d67f4bb7e83",
+			LogIndex:        "0x",
+		},
+	}
+
+	actualLogs, err := api.GetLogs(379224, 379225, "0x33990122638b9132ca29c723bdf037f1a891a70c", "0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545")
+
+	noError(t, err, "api.GetLogs")
+
+	equal := cmp.Equal(expectedLogs, actualLogs)
+
+	if !equal {
+		t.Errorf("api.GetLogs not working\n: %s\n", cmp.Diff(expectedLogs, actualLogs))
+	}
+}
diff --git a/network.go b/network.go
index 39f0f56..32372ca 100644
--- a/network.go
+++ b/network.go
@@ -18,6 +18,8 @@ const (
 	Kovan Network = "api-kovan"
 	// Rinkby Testnet(CLIQUE)
 	Rinkby Network = "api-rinkeby"
+	// Goerli Testnet(CLIQUE)
+	Goerli Network = "api-goerli"
 	// Tobalaba Testnet
 	Tobalaba Network = "api-tobalaba"
 )
@@ -27,6 +29,6 @@ type Network string
 
 // SubDomain returns the subdomain of  etherscan API
 // via n provided.
-func (n Network) SubDomain() (sub string)  {
+func (n Network) SubDomain() (sub string) {
 	return string(n)
-}
\ No newline at end of file
+}
diff --git a/reflect.go b/reflect.go
index 328684a..ba0d54a 100644
--- a/reflect.go
+++ b/reflect.go
@@ -27,7 +27,7 @@ func extractValue(input interface{}) (output []string) {
 			output[i] = valueToStr(v.Index(i))
 		}
 	} else {
-		output = make([]string, 1, 1)
+		output = make([]string, 1)
 		output[0] = valueToStr(v)
 	}
 
diff --git a/response.go b/response.go
index b1338d3..5c418cf 100644
--- a/response.go
+++ b/response.go
@@ -7,7 +7,12 @@
 
 package etherscan
 
-import "encoding/json"
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+)
 
 // Envelope is the carrier of nearly every response
 type Envelope struct {
@@ -45,6 +50,8 @@ type NormalTx struct {
 	CumulativeGasUsed int     `json:"cumulativeGasUsed,string"`
 	GasUsed           int     `json:"gasUsed,string"`
 	Confirmations     int     `json:"confirmations,string"`
+	FunctionName      string  `json:"functionName"`
+	MethodId          string  `json:"methodId"`
 }
 
 // InternalTx holds info from internal tx query
@@ -88,6 +95,53 @@ type ERC20Transfer struct {
 	Confirmations     int     `json:"confirmations,string"`
 }
 
+// ERC721Transfer holds info from ERC721 token transfer event query
+type ERC721Transfer struct {
+	BlockNumber       int     `json:"blockNumber,string"`
+	TimeStamp         Time    `json:"timeStamp"`
+	Hash              string  `json:"hash"`
+	Nonce             int     `json:"nonce,string"`
+	BlockHash         string  `json:"blockHash"`
+	From              string  `json:"from"`
+	ContractAddress   string  `json:"contractAddress"`
+	To                string  `json:"to"`
+	TokenID           *BigInt `json:"tokenID"`
+	TokenName         string  `json:"tokenName"`
+	TokenSymbol       string  `json:"tokenSymbol"`
+	TokenDecimal      uint8   `json:"tokenDecimal,string"`
+	TransactionIndex  int     `json:"transactionIndex,string"`
+	Gas               int     `json:"gas,string"`
+	GasPrice          *BigInt `json:"gasPrice"`
+	GasUsed           int     `json:"gasUsed,string"`
+	CumulativeGasUsed int     `json:"cumulativeGasUsed,string"`
+	Input             string  `json:"input"`
+	Confirmations     int     `json:"confirmations,string"`
+}
+
+// ERC1155Transfer holds info from ERC1155 token transfer event query
+type ERC1155Transfer struct {
+	BlockNumber       int     `json:"blockNumber,string"`
+	TimeStamp         Time    `json:"timeStamp"`
+	Hash              string  `json:"hash"`
+	Nonce             int     `json:"nonce,string"`
+	BlockHash         string  `json:"blockHash"`
+	From              string  `json:"from"`
+	ContractAddress   string  `json:"contractAddress"`
+	To                string  `json:"to"`
+	TokenID           *BigInt `json:"tokenID"`
+	TokenName         string  `json:"tokenName"`
+	TokenSymbol       string  `json:"tokenSymbol"`
+	TokenDecimal      uint8   `json:"tokenDecimal,string"`
+	TokenValue        uint8   `json:"tokenValue,string"`
+	TransactionIndex  int     `json:"transactionIndex,string"`
+	Gas               int     `json:"gas,string"`
+	GasPrice          *BigInt `json:"gasPrice"`
+	GasUsed           int     `json:"gasUsed,string"`
+	CumulativeGasUsed int     `json:"cumulativeGasUsed,string"`
+	Input             string  `json:"input"`
+	Confirmations     int     `json:"confirmations,string"`
+}
+
 // MinedBlock holds info from query for mined block by address
 type MinedBlock struct {
 	BlockNumber int     `json:"blockNumber,string"`
@@ -104,7 +158,11 @@ type ContractSource struct {
 	OptimizationUsed     int    `json:"OptimizationUsed,string"`
 	Runs                 int    `json:"Runs,string"`
 	ConstructorArguments string `json:"ConstructorArguments"`
+	EVMVersion           string `json:"EVMVersion"`
 	Library              string `json:"Library"`
+	LicenseType          string `json:"LicenseType"`
+	Proxy                string `json:"Proxy"`
+	Implementation       string `json:"Implementation"`
 	SwarmSource          string `json:"SwarmSource"`
 }
 
@@ -136,3 +194,77 @@ type LatestPrice struct {
 	ETHUSD          float64 `json:"ethusd,string"`
 	ETHUSDTimestamp Time    `json:"ethusd_timestamp"`
 }
+
+type Log struct {
+	Address         string   `json:"address"`
+	Topics          []string `json:"topics"`
+	Data            string   `json:"data"`
+	BlockNumber     string   `json:"blockNumber"`
+	TransactionHash string   `json:"transactionHash"`
+	BlockHash       string   `json:"blockHash"`
+	LogIndex        string   `json:"logIndex"`
+	Removed         bool     `json:"removed"`
+}
+
+// GasPrices holds info for Gas Oracle queries
+// Gas Prices are returned in Gwei
+type GasPrices struct {
+	LastBlock            int
+	SafeGasPrice         float64
+	ProposeGasPrice      float64
+	FastGasPrice         float64
+	SuggestBaseFeeInGwei float64   `json:"suggestBaseFee"`
+	GasUsedRatio         []float64 `json:"gasUsedRatio"`
+}
+
+func (gp *GasPrices) UnmarshalJSON(data []byte) error {
+	_gp := struct {
+		LastBlock            string
+		SafeGasPrice         string
+		ProposeGasPrice      string
+		FastGasPrice         string
+		SuggestBaseFeeInGwei string `json:"suggestBaseFee"`
+		GasUsedRatio         string `json:"gasUsedRatio"`
+	}{}
+
+	err := json.Unmarshal(data, &_gp)
+	if err != nil {
+		return err
+	}
+
+	gp.LastBlock, err = strconv.Atoi(_gp.LastBlock)
+	if err != nil {
+		return fmt.Errorf("Unable to convert LastBlock %s to int: %w", _gp.LastBlock, err)
+	}
+
+	gp.SafeGasPrice, err = strconv.ParseFloat(_gp.SafeGasPrice, 64)
+	if err != nil {
+		return fmt.Errorf("Unable to convert SafeGasPrice %s to float64: %w", _gp.SafeGasPrice, err)
+	}
+
+	gp.ProposeGasPrice, err = strconv.ParseFloat(_gp.ProposeGasPrice, 64)
+	if err != nil {
+		return fmt.Errorf("Unable to convert ProposeGasPrice %s to float64: %w", _gp.ProposeGasPrice, err)
+	}
+
+	gp.FastGasPrice, err = strconv.ParseFloat(_gp.FastGasPrice, 64)
+	if err != nil {
+		return fmt.Errorf("Unable to convert FastGasPrice %s to float64: %w", _gp.FastGasPrice, err)
+	}
+
+	gp.SuggestBaseFeeInGwei, err = strconv.ParseFloat(_gp.SuggestBaseFeeInGwei, 64)
+	if err != nil {
+		return fmt.Errorf("Unable to convert SuggestBaseFeeInGwei %s to float64: %w", _gp.SuggestBaseFeeInGwei, err)
+	}
+
+	gasRatios := strings.Split(_gp.GasUsedRatio, ",")
+	gp.GasUsedRatio = make([]float64, len(gasRatios))
+	for i, gasRatio := range gasRatios {
+		gp.GasUsedRatio[i], err = strconv.ParseFloat(gasRatio, 64)
+		if err != nil {
+			return fmt.Errorf("Unable to convert gasRatio %s to float64: %w", gasRatio, err)
+		}
+	}
+
+	return nil
+}
diff --git a/setup_e2e_test.go b/setup_e2e_test.go
index 5712171..64f2702 100644
--- a/setup_e2e_test.go
+++ b/setup_e2e_test.go
@@ -8,21 +8,31 @@
 package etherscan
 
 import (
+	"fmt"
+	"os"
 	"testing"
 	"time"
 )
 
+const apiKeyEnvName = "ETHERSCAN_API_KEY"
+
 var (
 	// api test client for many test cases
 	api *Client
 	// bucket default rate limiter
 	bucket *Bucket
+	// apiKey etherscan API key
+	apiKey string
 )
 
 func init() {
-	bucket = NewBucket(200 * time.Millisecond)
+	apiKey = os.Getenv(apiKeyEnvName)
+	if apiKey == "" {
+		panic(fmt.Sprintf("API key is empty, set env variable %q with a valid API key to proceed.", apiKeyEnvName))
+	}
+	bucket = NewBucket(500 * time.Millisecond)
 
-	api = New(Mainnet, "etherscan-api-e2e-test")
+	api = New(Mainnet, apiKey)
 	api.Verbose = true
 	api.BeforeRequest = func(module string, action string, param map[string]interface{}) error {
 		bucket.Take()
