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 0effe2c..e6a9ac1 100644 --- a/account.go +++ b/account.go @@ -87,7 +87,7 @@ func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock * // // 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) (txs []ERC20Transfer, err error) { +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, @@ -97,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 f3272ed..f7688e3 100644 --- a/account_e2e_test.go +++ b/account_e2e_test.go @@ -8,6 +8,8 @@ package etherscan import ( + "encoding/json" + "fmt" "math/big" "testing" ) @@ -78,7 +80,7 @@ func TestClient_ERC20Transfers(t *testing.T) { 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, "", " ") @@ -88,8 +90,15 @@ 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) } @@ -99,7 +108,7 @@ func TestClient_ERC20Transfers(t *testing.T) { var specialContract = "0x5eac95ad5b287cf44e058dcf694419333b796123" var specialStartHeight = 6024142 var specialEndHeight = 6485274 - txs, err = api.ERC20Transfers(&specialContract, nil, &specialStartHeight, &specialEndHeight, 1, 500) + 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) @@ -142,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 b981d59..e69679a 100644 --- a/client.go +++ b/client.go @@ -22,7 +22,6 @@ import ( // Clients are safe for concurrent use by multiple goroutines. type Client struct { coon *http.Client - network Network key string baseURL string @@ -41,13 +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{ - Timeout: 30 * time.Second, - }, - 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, } } 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()