diff --git a/README.md b/README.md index 56acd1f..af9277d 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,46 @@ -**English** | [中文](https://github.com/nanmu42/etherscan-api/blob/master/README_ZH.md) - # etherscan-api -[![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api) -[![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) +[![GoDoc](https://godoc.org/github.com/TokenTax/etherscan-api/v2?status.svg)](https://godoc.org/github.com/TokenTax/etherscan-api) -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: +Golang client for the Etherscan.io v2 API with nearly full implementation(accounts, transactions, tokens, contracts, blocks, stats) and minimal dependencies. # Usage ```bash -go get github.com/nanmu42/etherscan-api +go get github.com/TokenTax/etherscan-api/v2 ``` Create an API instance and off you go. :rocket: ```go import ( - "github.com/nanmu42/etherscan-api" + "github.com/TokenTax/etherscan-api/v2/pkg/client" + "github.com/TokenTax/etherscan-api/v2/pkg/chain" "fmt" ) 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 := client.NewClient(chain.EthereumMainnet, "[your API key]") + + // or, if you are working with antoher chain + // client := client.NewClient(chain.OpMainnet, "[your API key]") // + // or more customized // client := etherscan.NewCustomized(etherscan.Customization{ // Timeout: 15 * time.Second, // Key: "You key here", - // BaseURL: "https://api.bscscan.com/api?", + // Chain: chain.NewChain(), + // BaseURL: "", // Verbose: false, // }) // (optional) add hooks, e.g. for rate limit - client.BeforeRequest = func(module, action string, param map[string]interface{}) error { + client.BeforeRequest = func(module, action string, values url.Values) error { // ... } - client.AfterRequest = func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) { + client.AfterRequest = func(module, action string, values url.Values, outcome interface{}, requestErr error) error { // ... } @@ -61,20 +60,18 @@ func main() { } ``` -You may find full method list at [GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api). +You may find full method list at [GoDoc](https://godoc.org/github.com/TokenTax/etherscan-api/v2). # Etherscan API Key You may apply for an API key on [etherscan](https://etherscan.io/apis). -> The Etherscan Ethereum Developer APIs are provided as a community service and without warranty, so please just use what you need and no more. They support both GET/POST requests and a rate limit of 5 requests/sec (exceed and you will be blocked). +> The Etherscan Ethereum Developer APIs are provided as a community service and without warranty, so please just use what you need and no more. They support both GET/POST requests and a rate limit of 5 requests/sec (exceed and you will be blocked). # Paperwork Things -I am not from Etherscan and I just find their service really useful, so I implement this. :smile: +This library is not affiliated with Etherscan.io, it's developed for internal use in TokenTax. # 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 deleted file mode 100644 index 905a770..0000000 --- a/README_ZH.md +++ /dev/null @@ -1,83 +0,0 @@ -[English](https://github.com/nanmu42/etherscan-api/blob/master/README.md) | **中文** - -# etherscan-api - -[![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api) -[![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 API的Golang客户端, -支持几乎所有功能(accounts, transactions, tokens, contracts, blocks, stats), -所有公共网络(Mainnet, Ropsten, Kovan, Rinkby, Goerli, Tobalaba)。 -本项目只依赖于官方库。 :wink: - -# 使用方法 - -```bash -go get github.com/nanmu42/etherscan-api -``` - -填入网络选项和API Key即可开始使用。 :rocket: - -```go -import ( - "github.com/nanmu42/etherscan-api" - "fmt" -) - -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 { - // ... - } - client.AfterRequest = func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) { - // ... - } - - // 查询账户以太坊余额 - balance, err := client.AccountBalance("0x281055afc982d96fab65b3a49cac8b878184cb16") - if err != nil { - panic(err) - } - // 余额以 *big.Int 的类型呈现,单位为 wei - fmt.Println(balance.Int()) - - // 查询token余额 - tokenBalance, err := client.TokenBalance("contractAddress", "holderAddress") - - // 查询出入指定地址的ERC20转账列表 - transfers, err := client.ERC20Transfers("contractAddress", "address", startBlock, endBlock, page, offset) -} -``` - -客户端方法列表可在[GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api)查询。 - -# Etherscan API Key - -API Key可以在[etherscan](https://etherscan.io/apis)申请。 - -Etherscan的API服务是一个公开的社区无偿服务,请避免滥用。 -API的调用速率不能高于5次/秒,否则会遭到封禁。 - -# 利益声明 - -我和Etherscan没有任何联系。我仅仅是觉得他们的服务很棒,而自己又恰好需要这样一个库。 :smile: - -# 许可证 - -MIT - -请自由享受开源,欢迎贡献开源。 \ No newline at end of file diff --git a/account.go b/account.go deleted file mode 100644 index e6a9ac1..0000000 --- a/account.go +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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 - -// AccountBalance gets ether balance for a single address -func (c *Client) AccountBalance(address string) (balance *BigInt, err error) { - param := M{ - "tag": "latest", - "address": address, - } - balance = new(BigInt) - err = c.call("account", "balance", param, balance) - return -} - -// MultiAccountBalance gets ether balance for multiple addresses in a single call -func (c *Client) MultiAccountBalance(addresses ...string) (balances []AccountBalance, err error) { - param := M{ - "tag": "latest", - "address": addresses, - } - balances = make([]AccountBalance, 0, len(addresses)) - err = c.call("account", "balancemulti", param, &balances) - return -} - -// NormalTxByAddress gets a list of "normal" transactions by address -// -// startBlock and endBlock can be nil -// -// if desc is true, result will be sorted in blockNum descendant order. -func (c *Client) NormalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []NormalTx, err error) { - param := M{ - "address": address, - "page": page, - "offset": offset, - } - compose(param, "startblock", startBlock) - compose(param, "endblock", endBlock) - if desc { - param["sort"] = "desc" - } else { - param["sort"] = "asc" - } - - err = c.call("account", "txlist", param, &txs) - return -} - -// InternalTxByAddress gets a list of "internal" transactions by address -// -// startBlock and endBlock can be nil -// -// if desc is true, result will be sorted in descendant order. -func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []InternalTx, err error) { - param := M{ - "address": address, - "page": page, - "offset": offset, - } - compose(param, "startblock", startBlock) - compose(param, "endblock", endBlock) - if desc { - param["sort"] = "desc" - } else { - param["sort"] = "asc" - } - - err = c.call("account", "txlistinternal", param, &txs) - return -} - -// ERC20Transfers get a list of "erc20 - token transfer events" by -// contract address and/or from/to address. -// -// leave undesired condition to nil. -// -// 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, - } - 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", "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{ - "address": address, - "blocktype": "blocks", - "page": page, - "offset": offset, - } - - err = c.call("account", "getminedblocks", param, &mined) - return -} - -// UnclesMinedByAddress gets list of uncles mined by address -func (c *Client) UnclesMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { - param := M{ - "address": address, - "blocktype": "uncles", - "page": page, - "offset": offset, - } - - err = c.call("account", "getminedblocks", param, &mined) - return -} - -// TokenBalance get erc20-token account balance of address for contractAddress -func (c *Client) TokenBalance(contractAddress, address string) (balance *BigInt, err error) { - param := M{ - "contractaddress": contractAddress, - "address": address, - "tag": "latest", - } - - err = c.call("account", "tokenbalance", param, &balance) - return -} diff --git a/block.go b/block.go deleted file mode 100644 index aea4ea1..0000000 --- a/block.go +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 ( - "fmt" - "strconv" -) - -// BlockReward gets block and uncle rewards by block number -func (c *Client) BlockReward(blockNum int) (rewards BlockRewards, err error) { - param := M{ - "blockno": blockNum, - } - - 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/client.go b/client.go deleted file mode 100644 index e69679a..0000000 --- a/client.go +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 ( - "bytes" - "encoding/json" - "fmt" - "io" - "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 - key string - baseURL string - - // Verbose when true, talks a lot - Verbose bool - - // 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) -} - -// 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: httpClient, - key: config.Key, - baseURL: config.BaseURL, - Verbose: config.Verbose, - BeforeRequest: config.BeforeRequest, - AfterRequest: config.AfterRequest, - } -} - -// call does almost all the dirty work. -func (c *Client) call(module, action string, param map[string]interface{}, outcome interface{}) (err error) { - // fire hooks if in need - if c.BeforeRequest != nil { - err = c.BeforeRequest(module, action, param) - if err != nil { - err = wrapErr(err, "beforeRequest") - return - } - } - if c.AfterRequest != nil { - defer c.AfterRequest(module, action, param, outcome, err) - } - - // recover if there shall be an panic - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("[ouch! panic recovered] please report this with what you did and what you expected, panic detail: %v", r) - } - }() - - req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, param), http.NoBody) - if err != nil { - err = wrapErr(err, "http.NewRequest") - return - } - req.Header.Set("User-Agent", "etherscan-api(Go)") - req.Header.Set("Content-Type", "application/json; charset=utf-8") - - if c.Verbose { - var reqDump []byte - reqDump, err = httputil.DumpRequestOut(req, false) - if err != nil { - err = wrapErr(err, "verbose mode req dump failed") - return - } - - fmt.Printf("\n%s\n", reqDump) - - defer func() { - if err != nil { - fmt.Printf("[Error] %v\n", err) - } - }() - } - - res, err := c.coon.Do(req) - if err != nil { - err = wrapErr(err, "sending request") - return - } - defer res.Body.Close() - - if c.Verbose { - var resDump []byte - resDump, err = httputil.DumpResponse(res, true) - if err != nil { - err = wrapErr(err, "verbose mode res dump failed") - return - } - - fmt.Printf("%s\n", resDump) - } - - var content bytes.Buffer - if _, err = io.Copy(&content, res.Body); err != nil { - err = wrapErr(err, "reading response") - return - } - - if res.StatusCode != http.StatusOK { - err = fmt.Errorf("response status %v %s, response body: %s", res.StatusCode, res.Status, content.String()) - return - } - - var envelope Envelope - err = json.Unmarshal(content.Bytes(), &envelope) - if err != nil { - err = wrapErr(err, "json unmarshal envelope") - return - } - if envelope.Status != 1 { - err = fmt.Errorf("etherscan server: %s", envelope.Message) - return - } - - // 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 - } - - return -} - -// craftURL returns desired URL via param provided -func (c *Client) craftURL(module, action string, param map[string]interface{}) (URL string) { - q := url.Values{ - "module": []string{module}, - "action": []string{action}, - "apikey": []string{c.key}, - } - - for k, v := range param { - q[k] = extractValue(v) - } - - URL = c.baseURL + q.Encode() - return -} diff --git a/client_test.go b/client_test.go deleted file mode 100644 index a8d045c..0000000 --- a/client_test.go +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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" -) - -func TestClient_craftURL(t *testing.T) { - 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{ - "one": 1, - "two": "2", - "three": []int{1, 2, 3}, - "four": []string{"d", "e", "f"}, - }) - - if output != expected { - t.Fatalf("output != expected, got %s, want %s", output, expected) - } -} diff --git a/contract.go b/contract.go deleted file mode 100644 index 11e9e35..0000000 --- a/contract.go +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 - -// ContractABI gets contract abi for verified contract source codes -func (c *Client) ContractABI(address string) (abi string, err error) { - param := M{ - "address": address, - } - - err = c.call("contract", "getabi", param, &abi) - return -} - -// ContractSource gets contract source code for verified contract source codes -func (c *Client) ContractSource(address string) (source []ContractSource, err error) { - param := M{ - "address": address, - } - - err = c.call("contract", "getsourcecode", param, &source) - return -} diff --git a/doc.go b/doc.go deleted file mode 100644 index d345cab..0000000 --- a/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package etherscan provides Go bindings to the Etherscan.io API. -// -// This work is a nearly Full implementation -// (accounts, transactions, tokens, contracts, blocks, stats), -// with full network support(Mainnet, Ropsten, Kovan, Rinkby, Tobalaba), -// and only depending on standard library. -// -// Example can be found at https://github.com/nanmu42/etherscan-api -package etherscan diff --git a/error_wrap.go b/error_wrap.go deleted file mode 100644 index 36ea869..0000000 --- a/error_wrap.go +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 ( - "fmt" -) - -// wrapErr gives error some context msg -// returns nil if err is nil -func wrapErr(err error, msg string) (errWithContext error) { - if err == nil { - return - } - - errWithContext = fmt.Errorf("%s: %v", msg, err) - return -} - -// wrapfErr gives error some context msg -// with desired format and content -// returns nil if err is nil -func wrapfErr(err error, format string, a ...interface{}) (errWithContext error) { - if err == nil { - return - } - - errWithContext = wrapErr(err, fmt.Sprintf(format, a...)) - return -} diff --git a/error_wrap_test.go b/error_wrap_test.go deleted file mode 100644 index 4cf437c..0000000 --- a/error_wrap_test.go +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 ( - "errors" - "testing" -) - -func Test_wrapfErr(t *testing.T) { - const ans = "status 100: continue test" - - err := errors.New("continue test") - err = wrapfErr(err, "%s %v", "status", "100") - - if err.Error() != ans { - t.Fatalf("got %v, want %s", err, ans) - } -} diff --git a/gas_tracker.go b/gas_tracker.go deleted file mode 100644 index 02fe5ca..0000000 --- a/gas_tracker.go +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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/go.mod b/go.mod index 5837de6..14cb8cb 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,14 @@ -module github.com/nanmu42/etherscan-api +module github.com/TokenTax/etherscan-api/v2 -go 1.13 +go 1.23.0 -require github.com/google/go-cmp v0.5.7 +require ( + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index a6ca3a4..33dd589 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,12 @@ -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= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helper.go b/internal/types/types.go similarity index 54% rename from helper.go rename to internal/types/types.go index 8964970..ff94fd0 100644 --- a/helper.go +++ b/internal/types/types.go @@ -5,74 +5,55 @@ * You may find a license copy in project root. */ -package etherscan +package types import ( "math/big" - "reflect" "strconv" "time" -) - -// compose adds input to param, whose key is tag -// if input is nil or nil of some type, compose is a no-op. -func compose(param map[string]interface{}, tag string, input interface{}) { - // simple situation - if input == nil { - return - } - - // needs dig further - v := reflect.ValueOf(input) - switch v.Kind() { - case reflect.Ptr, reflect.Slice, reflect.Interface: - if v.IsNil() { - return - } - } - param[tag] = input -} - -// M is a type shorthand for param input -type M map[string]interface{} + "github.com/pkg/errors" +) // BigInt is a wrapper over big.Int to implement only unmarshalText // for json decoding. type BigInt big.Int // UnmarshalText implements the encoding.TextUnmarshaler interface. -func (b *BigInt) UnmarshalText(text []byte) (err error) { +func (b *BigInt) UnmarshalText(text []byte) error { var bigInt = new(big.Int) - err = bigInt.UnmarshalText(text) - if err != nil { - return + + if string(text) == "" { + bigInt.SetInt64(0) + *b = BigInt(*bigInt) + return nil + } + + if err := bigInt.UnmarshalText(text); err != nil { + return err } *b = BigInt(*bigInt) return nil } +// Int returns b's *big.Int form +func (b *BigInt) Int() *big.Int { return (*big.Int)(b) } + // MarshalText implements the encoding.TextMarshaler -func (b *BigInt) MarshalText() (text []byte, err error) { +func (b *BigInt) MarshalText() ([]byte, error) { return []byte(b.Int().String()), nil } -// Int returns b's *big.Int form -func (b *BigInt) Int() *big.Int { - return (*big.Int)(b) -} - // Time is a wrapper over big.Int to implement only unmarshalText // for json decoding. type Time time.Time // UnmarshalText implements the encoding.TextUnmarshaler interface. -func (t *Time) UnmarshalText(text []byte) (err error) { +func (t *Time) UnmarshalText(text []byte) error { input, err := strconv.ParseInt(string(text), 10, 64) if err != nil { - err = wrapErr(err, "strconv.ParseInt") - return + return errors.Wrap(err, "strconv.ParseInt") } var timestamp = time.Unix(input, 0) @@ -82,9 +63,7 @@ func (t *Time) UnmarshalText(text []byte) (err error) { } // Time returns t's time.Time form -func (t Time) Time() time.Time { - return time.Time(t) -} +func (t Time) Time() time.Time { return time.Time(t) } // MarshalText implements the encoding.TextMarshaler func (t Time) MarshalText() (text []byte, err error) { diff --git a/helper_test.go b/internal/types/types_test.go similarity index 82% rename from helper_test.go rename to internal/types/types_test.go index 31f9ef9..72d5f36 100644 --- a/helper_test.go +++ b/internal/types/types_test.go @@ -5,12 +5,14 @@ * You may find a license copy in project root. */ -package etherscan +package types import ( "math/big" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestBigInt(t *testing.T) { @@ -19,13 +21,14 @@ func TestBigInt(t *testing.T) { b := new(BigInt) err := b.UnmarshalText([]byte(ansStr)) - noError(t, err, "BigInt.UnmarshalText") + + assert.NoError(t, err, "BigInt.UnmarshalText") if b.Int().Cmp(ans) != 0 { t.Fatalf("BigInt.UnmarshalText not working, got %v, want %v", b.Int(), ans) } textBytes, err := b.MarshalText() - noError(t, err, "BigInt.MarshalText") + assert.NoError(t, err, "BigInt.MarshalText") if string(textBytes) != ansStr { t.Fatalf("BigInt.MarshalText not working, got %s, want %s", textBytes, ansStr) @@ -38,13 +41,13 @@ func TestTime(t *testing.T) { b := new(Time) err := b.UnmarshalText([]byte(ansStr)) - noError(t, err, "Time.UnmarshalText") + assert.NoError(t, err) if !b.Time().Equal(ans) { t.Fatalf("Time.UnmarshalText not working, got %v, want %v", b, ans) } textBytes, err := b.MarshalText() - noError(t, err, "BigInt.MarshalText") + assert.NoError(t, err, "BigInt.MarshalText") if string(textBytes) != ansStr { t.Fatalf("Time.MarshalText not working, got %s, want %s", textBytes, ansStr) diff --git a/logs.go b/logs.go deleted file mode 100644 index 0b21124..0000000 --- a/logs.go +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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/network.go b/network.go deleted file mode 100644 index 32372ca..0000000 --- a/network.go +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 - -const ( - //// Ethereum public networks - - // Mainnet Ethereum mainnet for production - Mainnet Network = "api" - // Ropsten Testnet(POW) - Ropsten Network = "api-ropsten" - // Kovan Testnet(POA) - Kovan Network = "api-kovan" - // Rinkby Testnet(CLIQUE) - Rinkby Network = "api-rinkeby" - // Goerli Testnet(CLIQUE) - Goerli Network = "api-goerli" - // Tobalaba Testnet - Tobalaba Network = "api-tobalaba" -) - -// Network is ethereum network type (mainnet, ropsten, etc) -type Network string - -// SubDomain returns the subdomain of etherscan API -// via n provided. -func (n Network) SubDomain() (sub string) { - return string(n) -} diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go new file mode 100644 index 0000000..9b481c4 --- /dev/null +++ b/pkg/chain/chain.go @@ -0,0 +1,160 @@ +/* + * 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 chain + +type Chain int + +// ChainIDs supported as of 2025/01/02 +const ( + EthereumMainnet Chain = 1 + OpMainnet Chain = 10 + SepoliaTestnet Chain = 11155111 + HoleskyTestnet Chain = 17000 + CronosMainnet Chain = 25 + ApeChainCurtisTestnet Chain = 33111 + ApeChainMainnet Chain = 33139 + ArbitrumOneMainnet Chain = 42161 + ArbitrumNovaMainnet Chain = 42170 + CeloMainnet Chain = 42220 + AvalancheCChain Chain = 43114 + AvalancheFujiTestnet Chain = 43113 + XDCMainnet Chain = 50 + XDCApothemTestnet Chain = 51 + BNBSmartChainMainnet Chain = 56 + BNBSmartChainTestnet Chain = 97 + Gnosis Chain = 100 + PolygonMainnet Chain = 137 + SonicMainnet Chain = 146 + BitTorrentChainMainnet Chain = 199 + FantomOperaMainnet Chain = 250 + FraxtalMainnet Chain = 252 + KromaMainnet Chain = 255 + zkSyncMainnet Chain = 324 + zkSyncSepoliaTestnet Chain = 300 + MoonbeamMainnet Chain = 1284 + MoonbaseAlphaTestnet Chain = 1287 + MoonriverMainnet Chain = 1285 + BitTorrentChainTestnet Chain = 1028 + PolygonZkEVMMainnet Chain = 1101 + WEMIX30Mainnet Chain = 1111 + WEMIX30Testnet Chain = 1112 + FantomTestnet Chain = 4002 + WorldMainnet Chain = 480 + WorldSepoliaTestnet Chain = 4801 + MantleMainnet Chain = 5000 + MantleSepoliaTestnet Chain = 5003 + BaseMainnet Chain = 8453 + BaseSepoliaTestnet Chain = 84532 + BlastMainnet Chain = 81457 + CeloAlfajoresTestnet Chain = 44787 + PolygonAmoyTestnet Chain = 80002 + PolygonZkEVMCardona Chain = 2442 + FraxtalTestnet Chain = 2522 + KromaSepolia Chain = 2358 + ScrollMainnet Chain = 534352 + ScrollSepoliaTestnet Chain = 534351 + SonicBlazeTestnet Chain = 57054 + LineaMainnet Chain = 59144 + LineaSepoliaTestnet Chain = 59141 + SophonMainnet Chain = 50104 + SophonSepoliaTestnet Chain = 531050104 + TaikoMainnet Chain = 167000 + TaikoHeklaTestnet Chain = 167009 + XaiMainnet Chain = 660279 + XaiSepoliaTestnet Chain = 37714555429 + BlastSepoliaTestnet Chain = 168587773 + OpSepoliaTestnet Chain = 11155420 + ArbitrumSepoliaTestnet Chain = 421614 +) + +var chainNames = map[Chain]string{ + EthereumMainnet: "Ethereum Mainnet", + OpMainnet: "OP Mainnet", + SepoliaTestnet: "Sepolia Testnet", + HoleskyTestnet: "Holesky Testnet", + CronosMainnet: "Cronos Mainnet", + ApeChainCurtisTestnet: "ApeChain Curtis Testnet", + ApeChainMainnet: "ApeChain Mainnet", + ArbitrumOneMainnet: "Arbitrum One Mainnet", + ArbitrumNovaMainnet: "Arbitrum Nova Mainnet", + CeloMainnet: "Celo Mainnet", + AvalancheCChain: "Avalanche C-Chain", + AvalancheFujiTestnet: "Avalanche Fuji Testnet", + XDCMainnet: "XDC Mainnet", + XDCApothemTestnet: "XDC Apothem Testnet", + BNBSmartChainMainnet: "BNB Smart Chain Mainnet", + BNBSmartChainTestnet: "BNB Smart Chain Testnet", + Gnosis: "Gnosis", + PolygonMainnet: "Polygon Mainnet", + SonicMainnet: "Sonic Mainnet", + BitTorrentChainMainnet: "BitTorrent Chain Mainnet", + FantomOperaMainnet: "Fantom Opera Mainnet", + FraxtalMainnet: "Fraxtal Mainnet", + KromaMainnet: "Kroma Mainnet", + zkSyncMainnet: "zkSync Mainnet", + zkSyncSepoliaTestnet: "zkSync Sepolia Testnet", + MoonbeamMainnet: "Moonbeam Mainnet", + MoonbaseAlphaTestnet: "Moonbase Alpha Testnet", + MoonriverMainnet: "Moonriver Mainnet", + BitTorrentChainTestnet: "BitTorrent Chain Testnet", + PolygonZkEVMMainnet: "Polygon zkEVM Mainnet", + WEMIX30Mainnet: "WEMIX3.0 Mainnet", + WEMIX30Testnet: "WEMIX3.0 Testnet", + FantomTestnet: "Fantom Testnet", + WorldMainnet: "World Mainnet", + WorldSepoliaTestnet: "World Sepolia Testnet", + MantleMainnet: "Mantle Mainnet", + MantleSepoliaTestnet: "Mantle Sepolia Testnet", + BaseMainnet: "Base Mainnet", + BaseSepoliaTestnet: "Base Sepolia Testnet", + BlastMainnet: "Blast Mainnet", + CeloAlfajoresTestnet: "Celo Alfajores Testnet", + PolygonAmoyTestnet: "Polygon Amoy Testnet", + PolygonZkEVMCardona: "Polygon zkEVM Cardona Testnet", + FraxtalTestnet: "Fraxtal Testnet", + KromaSepolia: "Kroma Sepolia Testnet", + ScrollMainnet: "Scroll Mainnet", + ScrollSepoliaTestnet: "Scroll Sepolia Testnet", + SonicBlazeTestnet: "Sonic Blaze Testnet", + LineaMainnet: "Linea Mainnet", + LineaSepoliaTestnet: "Linea Sepolia Testnet", + SophonMainnet: "Sophon Mainnet", + SophonSepoliaTestnet: "Sophon Sepolia Testnet", + TaikoMainnet: "Taiko Mainnet", + TaikoHeklaTestnet: "Taiko Hekla L2 Testnet", + XaiMainnet: "Xai Mainnet", + XaiSepoliaTestnet: "Xai Sepolia Testnet", + BlastSepoliaTestnet: "Blast Sepolia Testnet", + OpSepoliaTestnet: "OP Sepolia Testnet", + ArbitrumSepoliaTestnet: "Arbitrum Sepolia Testnet", +} + +// String returns the name of the network +func (n Chain) String() string { + if name, ok := chainNames[n]; ok { + return name + } + return "Unknown Network" +} + +// ID returns the chain ID as an int +func (n Chain) ID() int { return int(n) } + +// NewNetwork creates a Network from a name and chain ID +func NewNetwork(name string, chainID int) Chain { + network := Chain(chainID) + + return network +} + +// GetByChainID returns a Network by its chain ID +func GetByChainID(chainID int) (Chain, bool) { + network := Chain(chainID) + _, exists := chainNames[network] + return network, exists +} diff --git a/pkg/client/account.go b/pkg/client/account.go new file mode 100644 index 0000000..e77a0d8 --- /dev/null +++ b/pkg/client/account.go @@ -0,0 +1,317 @@ +/* + * 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 client + +import ( + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/TokenTax/etherscan-api/v2/internal/types" + "github.com/TokenTax/etherscan-api/v2/pkg/response" + "github.com/pkg/errors" +) + +type AccountBalanceParams struct { + Tag string `json:"tag"` + Address string `json:"address"` +} + +type MultiAccountBalanceParams struct { + Tag string `json:"tag"` + Addresses []string `json:"address"` +} + +type TxListParams struct { + Address string `json:"address"` + StartBlock *int `json:"startblock,omitempty"` + EndBlock *int `json:"endblock,omitempty"` + Page int `json:"page"` + Offset int `json:"offset"` + Sort string `json:"sort"` +} + +type TokenTransferParams struct { + ContractAddress *string `json:"contractaddress,omitempty"` + Address *string `json:"address,omitempty"` + StartBlock *int `json:"startblock,omitempty"` + EndBlock *int `json:"endblock,omitempty"` + Page int `json:"page"` + Offset int `json:"offset"` + Sort string `json:"sort"` +} + +type MinedBlockParams struct { + Address string `json:"address"` + BlockType string `json:"blocktype"` + Page int `json:"page"` + Offset int `json:"offset"` +} + +type TokenBalanceParams struct { + ContractAddress string `json:"contractaddress"` + Address string `json:"address"` + Tag string `json:"tag"` +} + +func (p AccountBalanceParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Tag != "" { + values.Add("tag", p.Tag) + } + if p.Address != "" { + values.Add("address", p.Address) + } + return values +} + +func (p MultiAccountBalanceParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Tag != "" { + values.Add("tag", p.Tag) + } + if len(p.Addresses) > 0 { + values.Add("address", strings.Join(p.Addresses, ",")) + } + return values +} + +func (p TxListParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Address != "" { + values.Add("address", p.Address) + } + if p.StartBlock != nil { + values.Add("startblock", strconv.Itoa(*p.StartBlock)) + } + if p.EndBlock != nil { + values.Add("endblock", strconv.Itoa(*p.EndBlock)) + } + values.Add("page", strconv.Itoa(p.Page)) + values.Add("offset", strconv.Itoa(p.Offset)) + if p.Sort != "" { + values.Add("sort", p.Sort) + } + return values +} + +func (p TokenTransferParams) GetUrlValues() url.Values { + values := url.Values{} + if p.ContractAddress != nil { + values.Add("contractaddress", *p.ContractAddress) + } + if p.Address != nil { + values.Add("address", *p.Address) + } + if p.StartBlock != nil { + values.Add("startblock", strconv.Itoa(*p.StartBlock)) + } + if p.EndBlock != nil { + values.Add("endblock", strconv.Itoa(*p.EndBlock)) + } + values.Add("page", strconv.Itoa(p.Page)) + values.Add("offset", strconv.Itoa(p.Offset)) + if p.Sort != "" { + values.Add("sort", p.Sort) + } + return values +} + +func (p MinedBlockParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Address != "" { + values.Add("address", p.Address) + } + if p.BlockType != "" { + values.Add("blocktype", p.BlockType) + } + values.Add("page", strconv.Itoa(p.Page)) + values.Add("offset", strconv.Itoa(p.Offset)) + return values +} + +func (p TokenBalanceParams) GetUrlValues() url.Values { + values := url.Values{} + if p.ContractAddress != "" { + values.Add("contractaddress", p.ContractAddress) + } + if p.Address != "" { + values.Add("address", p.Address) + } + if p.Tag != "" { + values.Add("tag", p.Tag) + } + return values +} + +// Refactored methods +func (c *Client) AccountBalance(address string) (types.BigInt, error) { + param := AccountBalanceParams{ + Tag: "latest", + Address: address, + } + body, err := c.execute("account", "balance", param.GetUrlValues()) + if err != nil { + return types.BigInt{}, errors.Wrap(err, "executing AccountBalance request") + } + fmt.Printf("%s\n", body.Bytes()) + return response.ReadResponse[types.BigInt](body) +} + +func (c *Client) MultiAccountBalance(addresses ...string) ([]response.AccountBalance, error) { + param := MultiAccountBalanceParams{ + Tag: "latest", + Addresses: addresses, + } + body, err := c.execute("account", "balancemulti", param.GetUrlValues()) + if err != nil { + return []response.AccountBalance{}, errors.Wrap(err, "executing MultiAccountBalance request") + } + return response.ReadResponse[[]response.AccountBalance](body) +} + +func (c *Client) NormalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.NormalTx, error) { + param := TxListParams{ + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + body, err := c.execute("account", "txlist", param.GetUrlValues()) + if err != nil { + return []response.NormalTx{}, errors.Wrap(err, "executing NormalTxByAddress request") + } + return response.ReadResponse[[]response.NormalTx](body) +} + +func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.InternalTx, error) { + param := TxListParams{ + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + + body, err := c.execute("account", "txlistinternal", param.GetUrlValues()) + if err != nil { + return []response.InternalTx{}, errors.Wrap(err, "executing InternalTxByAddress request") + } + return response.ReadResponse[[]response.InternalTx](body) +} + +func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.ERC20Transfer, error) { + param := TokenTransferParams{ + ContractAddress: contractAddress, + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + body, err := c.execute("account", "tokentx", param.GetUrlValues()) + if err != nil { + return []response.ERC20Transfer{}, errors.Wrap(err, "executing ERC20Transfers request") + } + return response.ReadResponse[[]response.ERC20Transfer](body) +} + +func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.ERC721Transfer, error) { + param := TokenTransferParams{ + ContractAddress: contractAddress, + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + body, err := c.execute("account", "tokennfttx", param.GetUrlValues()) + if err != nil { + return []response.ERC721Transfer{}, errors.Wrap(err, "executing ERC721Transfers request") + } + return response.ReadResponse[[]response.ERC721Transfer](body) +} + +func (c *Client) ERC1155Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.ERC1155Transfer, error) { + param := TokenTransferParams{ + ContractAddress: contractAddress, + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + body, err := c.execute("account", "token1155tx", param.GetUrlValues()) + if err != nil { + return []response.ERC1155Transfer{}, errors.Wrap(err, "executing ERC1155Transfers request") + } + return response.ReadResponse[[]response.ERC1155Transfer](body) +} + +func (c *Client) BlocksMinedByAddress(address string, page int, offset int) ([]response.MinedBlock, error) { + param := MinedBlockParams{ + Address: address, + BlockType: "blocks", + Page: page, + Offset: offset, + } + body, err := c.execute("account", "getminedblocks", param.GetUrlValues()) + if err != nil { + return []response.MinedBlock{}, errors.Wrap(err, "executing BlocksMinedByAddress request") + } + return response.ReadResponse[[]response.MinedBlock](body) +} + +func (c *Client) UnclesMinedByAddress(address string, page int, offset int) ([]response.MinedBlock, error) { + param := MinedBlockParams{ + Address: address, + BlockType: "uncles", + Page: page, + Offset: offset, + } + body, err := c.execute("account", "getminedblocks", param.GetUrlValues()) + if err != nil { + return []response.MinedBlock{}, errors.Wrap(err, "executing UnclesMinedByAddress request") + } + return response.ReadResponse[[]response.MinedBlock](body) +} + +func (c *Client) TokenBalance(contractAddress, address string) (types.BigInt, error) { + param := TokenBalanceParams{ + ContractAddress: contractAddress, + Address: address, + Tag: "latest", + } + body, err := c.execute("account", "tokenbalance", param.GetUrlValues()) + if err != nil { + return types.BigInt{}, errors.Wrap(err, "executing TokenBalance request") + } + return response.ReadResponse[types.BigInt](body) +} diff --git a/account_e2e_test.go b/pkg/client/account_integration_test.go similarity index 86% rename from account_e2e_test.go rename to pkg/client/account_integration_test.go index f7688e3..4a61649 100644 --- a/account_e2e_test.go +++ b/pkg/client/account_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,18 +8,20 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "encoding/json" "fmt" "math/big" "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_AccountBalance(t *testing.T) { balance, err := api.AccountBalance("0x0000000000000000000000000000000000000000") - noError(t, err, "api.AccountBalance") + assert.NoError(t, err, "api.AccountBalance") if balance.Int().Cmp(big.NewInt(0)) != 1 { t.Fatalf("rich man is no longer rich") @@ -29,7 +34,7 @@ func TestClient_MultiAccountBalance(t *testing.T) { "0x0000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000003") - noError(t, err, "api.MultiAccountBalance") + assert.NoError(t, err, "api.MultiAccountBalance") for i, item := range balances { if item.Account == "" { @@ -46,7 +51,7 @@ func TestClient_NormalTxByAddress(t *testing.T) { var a, b = 54092, 79728 txs, err := api.NormalTxByAddress("0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", &a, &b, 1, 500, false) - noError(t, err, "api.NormalTxByAddress") + assert.NoError(t, err, "api.NormalTxByAddress") //j, _ := json.MarshalIndent(txs, "", " ") //fmt.Printf("%s\n", j) @@ -61,7 +66,7 @@ func TestClient_InternalTxByAddress(t *testing.T) { var a, b = 0, 2702578 txs, err := api.InternalTxByAddress("0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3", &a, &b, 1, 500, false) - noError(t, err, "api.InternalTxByAddress") + assert.NoError(t, err, "api.InternalTxByAddress") //j, _ := json.MarshalIndent(txs, "", " ") //fmt.Printf("%s\n", j) @@ -81,7 +86,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, false) - noError(t, err, "api.ERC20Transfers 1") + assert.NoError(t, err, "api.ERC20Transfers 1") //j, _ := json.MarshalIndent(txs, "", " ") //fmt.Printf("%s\n", j) @@ -91,13 +96,13 @@ func TestClient_ERC20Transfers(t *testing.T) { } txs, err = api.ERC20Transfers(nil, &address, nil, &b, 1, 500, false) - noError(t, err, "api.ERC20Transfers 2 asc") + assert.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") + assert.NoError(t, err, "api.ERC20Transfers 2 desc") if len(txs) != wantLen2 { t.Errorf("got txs length %v, want %v", len(txs), wantLen2) @@ -109,7 +114,7 @@ func TestClient_ERC20Transfers(t *testing.T) { var specialStartHeight = 6024142 var specialEndHeight = 6485274 txs, err = api.ERC20Transfers(&specialContract, nil, &specialStartHeight, &specialEndHeight, 1, 500, false) - noError(t, err, "api.ERC20Transfers 2") + assert.NoError(t, err, "api.ERC20Transfers 2") if len(txs) != wantLen3 { t.Errorf("got txs length %v, want %v", len(txs), wantLen3) } @@ -119,7 +124,7 @@ func TestClient_BlocksMinedByAddress(t *testing.T) { const wantLen = 10 blocks, err := api.BlocksMinedByAddress("0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b", 1, wantLen) - noError(t, err, "api.BlocksMinedByAddress") + assert.NoError(t, err, "api.BlocksMinedByAddress") //j, _ := json.MarshalIndent(blocks, "", " ") //fmt.Printf("%s\n", j) @@ -133,7 +138,7 @@ func TestClient_UnclesMinedByAddress(t *testing.T) { const wantLen = 10 blocks, err := api.UnclesMinedByAddress("0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b", 1, wantLen) - noError(t, err, "api.UnclesMinedByAddress") + assert.NoError(t, err, "api.UnclesMinedByAddress") //j, _ := json.MarshalIndent(blocks, "", " ") //fmt.Printf("%s\n", j) @@ -145,7 +150,7 @@ func TestClient_UnclesMinedByAddress(t *testing.T) { func TestClient_TokenBalance(t *testing.T) { balance, err := api.TokenBalance("0x57d90b64a1a57749b0f932f1a3395792e12e7055", "0xe04f27eb70e025b78871a2ad7eabe85e61212761") - noError(t, err, "api.TokenBalance") + assert.NoError(t, err, "api.TokenBalance") if balance.Int().Cmp(big.NewInt(0)) != 1 { t.Errorf("api.TokenBalance not working, got balance %s", balance.Int().String()) @@ -160,7 +165,7 @@ func TestClient_ERC721Transfers(t *testing.T) { 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") + assert.NoError(t, err, "api.ERC721Transfers") j, _ := json.MarshalIndent(txs, "", " ") fmt.Printf("%s\n", j) @@ -178,7 +183,7 @@ func TestClient_ERC1155Transfers(t *testing.T) { 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") + assert.NoError(t, err, "api.ERC721Transfers") j, _ := json.MarshalIndent(txs, "", " ") fmt.Printf("%s\n", j) diff --git a/pkg/client/account_test.go b/pkg/client/account_test.go new file mode 100644 index 0000000..aa59de6 --- /dev/null +++ b/pkg/client/account_test.go @@ -0,0 +1,255 @@ +package client + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAccountBalanceParams_GetUrlValues(t *testing.T) { + tests := []struct { + name string + params AccountBalanceParams + expected url.Values + }{ + { + name: "empty params", + params: AccountBalanceParams{}, + expected: url.Values{}, + }, + { + name: "full params", + params: AccountBalanceParams{ + Tag: "latest", + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + expected: url.Values{ + "tag": []string{"latest"}, + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMultiAccountBalanceParams_GetUrlValues(t *testing.T) { + tests := []struct { + name string + params MultiAccountBalanceParams + expected url.Values + }{ + { + name: "empty params", + params: MultiAccountBalanceParams{}, + expected: url.Values{}, + }, + { + name: "full params", + params: MultiAccountBalanceParams{ + Tag: "latest", + Addresses: []string{ + "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "0x742d35Cc6634C0532925a3b844Bc454e4438f44f", + }, + }, + expected: url.Values{ + "tag": []string{"latest"}, + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e,0x742d35Cc6634C0532925a3b844Bc454e4438f44f"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTxListParams_GetUrlValues(t *testing.T) { + startBlock := 12345 + endBlock := 12346 + + tests := []struct { + name string + params TxListParams + expected url.Values + }{ + { + name: "minimal params", + params: TxListParams{ + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + Page: 1, + Offset: 10, + }, + expected: url.Values{ + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "page": []string{"1"}, + "offset": []string{"10"}, + }, + }, + { + name: "full params", + params: TxListParams{ + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + StartBlock: &startBlock, + EndBlock: &endBlock, + Page: 1, + Offset: 10, + Sort: "asc", + }, + expected: url.Values{ + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "startblock": []string{"12345"}, + "endblock": []string{"12346"}, + "page": []string{"1"}, + "offset": []string{"10"}, + "sort": []string{"asc"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTokenTransferParams_GetUrlValues(t *testing.T) { + startBlock := 12345 + endBlock := 12346 + contractAddr := "0x742d35Cc6634C0532925a3b844Bc454e4438f44e" + address := "0x742d35Cc6634C0532925a3b844Bc454e4438f44f" + + tests := []struct { + name string + params TokenTransferParams + expected url.Values + }{ + { + name: "minimal params", + params: TokenTransferParams{ + Page: 1, + Offset: 10, + }, + expected: url.Values{ + "page": []string{"1"}, + "offset": []string{"10"}, + }, + }, + { + name: "full params", + params: TokenTransferParams{ + ContractAddress: &contractAddr, + Address: &address, + StartBlock: &startBlock, + EndBlock: &endBlock, + Page: 1, + Offset: 10, + Sort: "asc", + }, + expected: url.Values{ + "contractaddress": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44f"}, + "startblock": []string{"12345"}, + "endblock": []string{"12346"}, + "page": []string{"1"}, + "offset": []string{"10"}, + "sort": []string{"asc"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMinedBlockParams_GetUrlValues(t *testing.T) { + tests := []struct { + name string + params MinedBlockParams + expected url.Values + }{ + { + name: "minimal params", + params: MinedBlockParams{ + Page: 1, + Offset: 10, + }, + expected: url.Values{ + "page": []string{"1"}, + "offset": []string{"10"}, + }, + }, + { + name: "full params", + params: MinedBlockParams{ + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + BlockType: "blocks", + Page: 1, + Offset: 10, + }, + expected: url.Values{ + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "blocktype": []string{"blocks"}, + "page": []string{"1"}, + "offset": []string{"10"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTokenBalanceParams_GetUrlValues(t *testing.T) { + tests := []struct { + name string + params TokenBalanceParams + expected url.Values + }{ + { + name: "empty params", + params: TokenBalanceParams{}, + expected: url.Values{}, + }, + { + name: "full params", + params: TokenBalanceParams{ + ContractAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f", + Tag: "latest", + }, + expected: url.Values{ + "contractaddress": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44f"}, + "tag": []string{"latest"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/client/block.go b/pkg/client/block.go new file mode 100644 index 0000000..311f517 --- /dev/null +++ b/pkg/client/block.go @@ -0,0 +1,79 @@ +/* + * 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 client + +import ( + "fmt" + "net/url" + "strconv" + + "github.com/TokenTax/etherscan-api/v2/pkg/response" + "github.com/pkg/errors" +) + +type BlockRewardParams struct { + BlockNo int `json:"blockno"` +} + +type BlockNumberParams struct { + Timestamp int64 `json:"timestamp"` + Closest string `json:"closest"` +} + +func (p BlockRewardParams) GetUrlValues() url.Values { + values := url.Values{} + values.Add("blockno", strconv.Itoa(p.BlockNo)) + return values +} + +func (p BlockNumberParams) GetUrlValues() url.Values { + values := url.Values{} + values.Add("timestamp", strconv.FormatInt(p.Timestamp, 10)) + values.Add("closest", p.Closest) + return values +} + +// BlockReward gets block and uncle rewards by block number +func (c *Client) BlockReward(blockNum int) (response.BlockRewards, error) { + param := BlockRewardParams{ + BlockNo: blockNum, + } + + body, err := c.execute("block", "getblockreward", param.GetUrlValues()) + if err != nil { + return response.BlockRewards{}, errors.Wrap(err, "executing BlockReward request") + } + return response.ReadResponse[response.BlockRewards](body) +} + +// BlockNumber gets the closest block number by UNIX timestamp +// +// valid closest option: before, after +func (c *Client) BlockNumber(timestamp int64, closest string) (int, error) { + param := BlockNumberParams{ + Timestamp: timestamp, + Closest: closest, + } + + body, err := c.execute("block", "getblocknobytime", param.GetUrlValues()) + if err != nil { + return 0, errors.Wrap(err, "executing BlockNumber request") + } + + blockNumberStr, err := response.ReadResponse[string](body) + if err != nil { + return 0, errors.Wrap(err, "reading response") + } + + blockNumber, err := strconv.Atoi(blockNumberStr) + if err != nil { + return 0, fmt.Errorf("parsing block number %q: %w", blockNumberStr, err) + } + + return blockNumber, nil +} diff --git a/block_e2e_test.go b/pkg/client/block_integration_test.go similarity index 83% rename from block_e2e_test.go rename to pkg/client/block_integration_test.go index c49a8ac..498f066 100644 --- a/block_e2e_test.go +++ b/pkg/client/block_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,21 +8,23 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "encoding/json" "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_BlockReward(t *testing.T) { const ans = `{"blockNumber":"2165403","timeStamp":"1472533979","blockMiner":"0x13a06d3dfe21e0db5c016c03ea7d2509f7f8d1e3","blockReward":"5314181600000000000","uncles":[{"miner":"0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1","unclePosition":"0","blockreward":"3750000000000000000"},{"miner":"0x0d0c9855c722ff0c78f21e43aa275a5b8ea60dce","unclePosition":"1","blockreward":"3750000000000000000"}],"uncleInclusionReward":"312500000000000000"}` reward, err := api.BlockReward(2165403) - noError(t, err, "api.BlockReward") + assert.NoError(t, err, "api.BlockReward") j, err := json.Marshal(reward) - noError(t, err, "json.Marshal") + assert.NoError(t, err, "json.Marshal") if string(j) != ans { t.Errorf("api.BlockReward not working, got %s, want %s", j, ans) } @@ -31,14 +36,14 @@ func TestClient_BlockNumber(t *testing.T) { const ansAfter = 9251483 blockNumber, err := api.BlockNumber(1578638524, "before") - noError(t, err, "api.BlockNumber") + assert.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") + assert.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/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..7c0ee15 --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,252 @@ +/* + * 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 client + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "time" + + "github.com/TokenTax/etherscan-api/v2/pkg/chain" + "github.com/pkg/errors" +) + +type ( + // Client etherscan API client + // Clients are safe for concurrent use by multiple goroutines. + Client struct { + conn *http.Client + key string + baseURL string + chain chain.Chain + + // Verbose when true, talks a lot + Verbose bool + + // 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, values url.Values) error + + // AfterRequest runs after every client request, even when there is an error. + AfterRequest func(module, action string, values url.Values, outcome interface{}, requestErr error) error + } + + // Customization is used in NewCustomized() + 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 + // ChainID to be used + Chain chain.Chain + // 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, values url.Values) error + + // AfterRequest runs after every client request, even when there is an error. + AfterRequest func(module, action string, values url.Values, outcome interface{}, requestErr error) error + } +) + +// NewClient initialize a new etherscan API client +// please use pre-defined network value +func NewClient(chain chain.Chain, APIKey string) *Client { + return NewCustomized(Customization{ + Timeout: 30 * time.Second, + Key: APIKey, + Chain: chain, + BaseURL: `https://api.etherscan.io/v2/api`, + }) +} + +// 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{ + conn: httpClient, + key: config.Key, + chain: config.Chain, + baseURL: config.BaseURL, + Verbose: config.Verbose, + BeforeRequest: config.BeforeRequest, + AfterRequest: config.AfterRequest, + } +} + +func (c *Client) execute(module, action string, values url.Values) (bytes.Buffer, error) { + var content = bytes.Buffer{} + + req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, values), http.NoBody) + if err != nil { + return content, errors.Wrap(err, "creating request") + } + req.Header.Set("User-Agent", "etherscan-api(Go)") + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + if c.Verbose { + reqDump, err := httputil.DumpRequestOut(req, false) + if err != nil { + return content, errors.Wrap(err, "verbose mode: dumping request") + } + + fmt.Printf("\n%s\n", reqDump) + + defer func() { + if err != nil { + fmt.Printf("[Error] %v\n", err) + } + }() + } + + res, err := c.conn.Do(req) + if err != nil { + return content, errors.Wrap(err, "sending request") + } + defer res.Body.Close() + + if c.Verbose { + resDump, err := httputil.DumpResponse(res, true) + if err != nil { + return content, errors.Wrap(err, "verbose mode:dumping response") + } + + fmt.Printf("%s\n", resDump) + } + + if _, err = io.Copy(&content, res.Body); err != nil { + return content, errors.Wrap(err, "reading response") + } + + if res.StatusCode != http.StatusOK { + return content, errors.Errorf("got non-200 status code; status: %v, status text: %s, response body: %s", res.StatusCode, res.Status, content.String()) + } + + return content, nil +} + +/* +func (c *Client) innerCall(module, action string, values url.Values, outcome any) error { + req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, values), http.NoBody) + if err != nil { + return errors.Wrap(err, "creating request") + } + req.Header.Set("User-Agent", "etherscan-api(Go)") + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + if c.Verbose { + reqDump, err := httputil.DumpRequestOut(req, false) + if err != nil { + return errors.Wrap(err, "verbose mode: dumping request") + } + + fmt.Printf("\n%s\n", reqDump) + + defer func() { + if err != nil { + fmt.Printf("[Error] %v\n", err) + } + }() + } + + res, err := c.conn.Do(req) + if err != nil { + return errors.Wrap(err, "sending request") + } + defer res.Body.Close() + + if c.Verbose { + resDump, err := httputil.DumpResponse(res, true) + if err != nil { + return errors.Wrap(err, "verbose mode:dumping response") + } + + fmt.Printf("%s\n", resDump) + } + + resp := response.ReadResponse(res) + + if res.StatusCode != http.StatusOK { + return errors.Errorf("got non-200 status code; status: %v, status text: %s, response body: %s", res.StatusCode, res.Status, content.String()) + } + + var content bytes.Buffer + if _, err = io.Copy(&content, res.Body); err != nil { + return errors.Wrap(err, "reading response") + } + + var envelope Envelope + if err = json.Unmarshal(content.Bytes(), &envelope); err != nil { + return errors.Wrap(err, "unmarshaling response") + } + if envelope.Status != 1 { + return errors.Errorf("etherscan server: %s", envelope.Message) + } + + // 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) + } + + return errors.Wrap(err, "unmarshaling result") +} + +// call executes the Before/AfterRequest hooks and executes innerCall in between +func (c *Client) call(module, action string, values url.Values, outcome interface{}) error { + // fire hooks if in need + if c.BeforeRequest != nil { + if err := c.BeforeRequest(module, action, values); err != nil { + return errors.Wrap(err, "running beforeRequest") + } + } + + err := c.innerCall(module, action, values, outcome) + if c.AfterRequest != nil { + if err := c.AfterRequest(module, action, param, outcome, err); err != nil { + return errors.Wrapf(err, "running afterRequest with request Error: %v", err) + } + } + return err +} +*/ + +// craftURL returns desired URL via param provided +func (c *Client) craftURL(module, action string, values url.Values) string { + if values == nil { + values = url.Values{} + } + + values.Add("module", module) + values.Add("action", action) + values.Add("apikey", c.key) + values.Add("chainid", strconv.Itoa(c.chain.ID())) + + return fmt.Sprintf("%s?%s", c.baseURL, values.Encode()) +} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go new file mode 100644 index 0000000..38ad198 --- /dev/null +++ b/pkg/client/client_test.go @@ -0,0 +1,31 @@ +/* + * 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 client + +import ( + "net/url" + "testing" + + "github.com/TokenTax/etherscan-api/v2/pkg/chain" + "github.com/stretchr/testify/assert" +) + +func TestClient_craftURL(t *testing.T) { + c := NewClient(chain.EthereumMainnet, "abc123") + + const expected = `https://api.etherscan.io/v2/api?action=craftURL&apikey=abc123&chainid=1&four=d&four=e&four=f&module=testing&one=1&three=1&three=2&three=3&two=2` + + output := c.craftURL("testing", "craftURL", url.Values{ + "one": []string{"1"}, + "two": []string{"2"}, + "three": []string{"1", "2", "3"}, + "four": []string{"d", "e", "f"}, + }) + + assert.Equal(t, expected, output) +} diff --git a/pkg/client/contract.go b/pkg/client/contract.go new file mode 100644 index 0000000..3de00a9 --- /dev/null +++ b/pkg/client/contract.go @@ -0,0 +1,53 @@ +/* + * 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 client + +import ( + "net/url" + + "github.com/TokenTax/etherscan-api/v2/pkg/response" + "github.com/pkg/errors" +) + +type ContractParams struct { + Address string `json:"address"` +} + +func (p ContractParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Address != "" { + values.Add("address", p.Address) + } + return values +} + +// ContractABI gets contract abi for verified contract source codes +func (c *Client) ContractABI(address string) (string, error) { + param := ContractParams{ + Address: address, + } + + body, err := c.execute("contract", "getabi", param.GetUrlValues()) + if err != nil { + return "", errors.Wrap(err, "executing ContractABI request") + } + return response.ReadResponse[string](body) +} + +// ContractSource gets contract source code for verified contract source codes +func (c *Client) ContractSource(address string) ([]response.ContractSource, error) { + param := ContractParams{ + Address: address, + } + + body, err := c.execute("contract", "getsourcecode", param.GetUrlValues()) + if err != nil { + return nil, errors.Wrap(err, "executing ContractSource request") + } + return response.ReadResponse[[]response.ContractSource](body) +} diff --git a/contract_e2e_test.go b/pkg/client/contract_integration_test.go similarity index 98% rename from contract_e2e_test.go rename to pkg/client/contract_integration_test.go index ca577e7..a8d9a01 100644 --- a/contract_e2e_test.go +++ b/pkg/client/contract_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,17 +8,19 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_ContractABI(t *testing.T) { const answer = `[{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"proposals","outputs":[{"name":"recipient","type":"address"},{"name":"amount","type":"uint256"},{"name":"description","type":"string"},{"name":"votingDeadline","type":"uint256"},{"name":"open","type":"bool"},{"name":"proposalPassed","type":"bool"},{"name":"proposalHash","type":"bytes32"},{"name":"proposalDeposit","type":"uint256"},{"name":"newCurator","type":"bool"},{"name":"yea","type":"uint256"},{"name":"nay","type":"uint256"},{"name":"creator","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"minTokensToCreate","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"rewardAccount","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"daoCreator","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"divisor","outputs":[{"name":"divisor","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"extraBalance","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_proposalID","type":"uint256"},{"name":"_transactionData","type":"bytes"}],"name":"executeProposal","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"unblockMe","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"totalRewardToken","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"actualBalance","outputs":[{"name":"_actualBalance","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"closingTime","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"allowedRecipients","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferWithoutReward","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"refund","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"},{"name":"_description","type":"string"},{"name":"_transactionData","type":"bytes"},{"name":"_debatingPeriod","type":"uint256"},{"name":"_newCurator","type":"bool"}],"name":"newProposal","outputs":[{"name":"_proposalID","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"DAOpaidOut","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"minQuorumDivisor","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_newContract","type":"address"}],"name":"newContract","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_allowed","type":"bool"}],"name":"changeAllowedRecipients","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"halveMinQuorum","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"paidOut","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_proposalID","type":"uint256"},{"name":"_newCurator","type":"address"}],"name":"splitDAO","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"DAOrewardAccount","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"proposalDeposit","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"numberOfProposals","outputs":[{"name":"_numberOfProposals","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"lastTimeMinQuorumMet","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_toMembers","type":"bool"}],"name":"retrieveDAOReward","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"receiveEther","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"isFueled","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_tokenHolder","type":"address"}],"name":"createTokenProxy","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"_proposalID","type":"uint256"}],"name":"getNewDAOAddress","outputs":[{"name":"_newDAO","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_proposalID","type":"uint256"},{"name":"_supportsProposal","type":"bool"}],"name":"vote","outputs":[{"name":"_voteID","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[],"name":"getMyReward","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"rewardToken","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFromWithoutReward","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_proposalDeposit","type":"uint256"}],"name":"changeProposalDeposit","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"blocked","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"curator","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[{"name":"_proposalID","type":"uint256"},{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"},{"name":"_transactionData","type":"bytes"}],"name":"checkProposalCode","outputs":[{"name":"_codeChecksOut","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"privateCreation","outputs":[{"name":"","type":"address"}],"type":"function"},{"inputs":[{"name":"_curator","type":"address"},{"name":"_daoCreator","type":"address"},{"name":"_proposalDeposit","type":"uint256"},{"name":"_minTokensToCreate","type":"uint256"},{"name":"_closingTime","type":"uint256"},{"name":"_privateCreation","type":"address"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_amount","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_amount","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"FuelingToDate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"CreatedToken","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Refund","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"proposalID","type":"uint256"},{"indexed":false,"name":"recipient","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"newCurator","type":"bool"},{"indexed":false,"name":"description","type":"string"}],"name":"ProposalAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"proposalID","type":"uint256"},{"indexed":false,"name":"position","type":"bool"},{"indexed":true,"name":"voter","type":"address"}],"name":"Voted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"proposalID","type":"uint256"},{"indexed":false,"name":"result","type":"bool"},{"indexed":false,"name":"quorum","type":"uint256"}],"name":"ProposalTallied","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_newCurator","type":"address"}],"name":"NewCurator","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_recipient","type":"address"},{"indexed":false,"name":"_allowed","type":"bool"}],"name":"AllowedRecipientChanged","type":"event"}]` abi, err := api.ContractABI("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413") - noError(t, err, "api.ContractABI") + assert.NoError(t, err, "api.ContractABI") //fmt.Println(abi) @@ -26,7 +31,7 @@ func TestClient_ContractABI(t *testing.T) { func TestClient_ContractSource(t *testing.T) { source, err := api.ContractSource("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413") - noError(t, err, "api.ContractSource") + assert.NoError(t, err, "api.ContractSource") if len(source) != 1 { t.Fatalf("api.ContractSource not working, got len %v, expect 1", len(source)) diff --git a/pkg/client/gas_tracker.go b/pkg/client/gas_tracker.go new file mode 100644 index 0000000..5cf120d --- /dev/null +++ b/pkg/client/gas_tracker.go @@ -0,0 +1,55 @@ +/* + * 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 client + +import ( + "net/url" + "strconv" + "time" + + "github.com/TokenTax/etherscan-api/v2/pkg/response" + "github.com/pkg/errors" +) + +type GasEstimateParams struct { + GasPrice int `json:"gasPrice"` +} + +func (p GasEstimateParams) GetUrlValues() url.Values { + values := url.Values{} + values.Add("gasPrice", strconv.Itoa(p.GasPrice)) + return values +} + +// GasEstimate gets estimated confirmation time (in seconds) at the given gas price +func (c *Client) GasEstimate(gasPrice int) (time.Duration, error) { + param := GasEstimateParams{ + GasPrice: gasPrice, + } + + body, err := c.execute("gastracker", "gasestimate", param.GetUrlValues()) + if err != nil { + return 0, errors.Wrap(err, "executing GasEstimate request") + } + + confTime, err := response.ReadResponse[string](body) + if err != nil { + return 0, errors.Wrap(err, "reading response") + } + + return time.ParseDuration(confTime + "s") +} + +// GasOracle gets suggested gas prices (in Gwei) +func (c *Client) GasOracle() (response.GasPrices, error) { + body, err := c.execute("gastracker", "gasoracle", url.Values{}) + if err != nil { + return response.GasPrices{}, errors.Wrap(err, "executing GasOracle request") + } + return response.ReadResponse[response.GasPrices](body) +} diff --git a/gas_tracker_e2e_test.go b/pkg/client/gas_tracker_integration_test.go similarity index 53% rename from gas_tracker_e2e_test.go rename to pkg/client/gas_tracker_integration_test.go index 3905bc3..8a9bfa2 100644 --- a/gas_tracker_e2e_test.go +++ b/pkg/client/gas_tracker_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,22 +8,24 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "testing" + + "github.com/stretchr/testify/assert" ) -//GasEstiamte generates dynamic data. Best we can do is ensure all fields are populated +// 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") + assert.NoError(t, err, "api.GasEstimate") } -//GasOracle generates dynamic data. Best we can do is ensure all fields are populated +// 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") + assert.NoError(t, err, "api.GasOrcale") if 0 == len(gasPrice.GasUsedRatio) { t.Errorf("gasPrice.GasUsedRatio empty") diff --git a/pkg/client/logs.go b/pkg/client/logs.go new file mode 100644 index 0000000..51d9c3b --- /dev/null +++ b/pkg/client/logs.go @@ -0,0 +1,48 @@ +/* + * 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 client + +import ( + "net/url" + "strconv" + + "github.com/TokenTax/etherscan-api/v2/pkg/response" + "github.com/pkg/errors" +) + +type LogParams struct { + FromBlock int `json:"fromBlock"` + ToBlock int `json:"toBlock"` + Topic0 string `json:"topic0"` + Address string `json:"address"` +} + +func (p LogParams) GetUrlValues() url.Values { + values := url.Values{} + values.Add("fromBlock", strconv.Itoa(p.FromBlock)) + values.Add("toBlock", strconv.Itoa(p.ToBlock)) + values.Add("topic0", p.Topic0) + values.Add("address", p.Address) + return values +} + +// 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) ([]response.Log, error) { + param := LogParams{ + FromBlock: fromBlock, + ToBlock: toBlock, + Topic0: topic, + Address: address, + } + + body, err := c.execute("logs", "getLogs", param.GetUrlValues()) + if err != nil { + return nil, errors.Wrap(err, "executing GetLogs request") + } + return response.ReadResponse[[]response.Log](body) +} diff --git a/logs_e2e_test.go b/pkg/client/logs_integration_test.go similarity index 75% rename from logs_e2e_test.go rename to pkg/client/logs_integration_test.go index e4490d2..6a7b75a 100644 --- a/logs_e2e_test.go +++ b/pkg/client/logs_integration_test.go @@ -1,14 +1,18 @@ -package etherscan +//go:build integration +// +build integration + +package client import ( "testing" - "github.com/google/go-cmp/cmp" + "github.com/TokenTax/etherscan-api/v2/pkg/response" + "github.com/stretchr/testify/assert" ) func TestClient_GetLogs(t *testing.T) { - expectedLogs := []Log{ - Log{ + expectedLogs := []response.Log{ + { Address: "0x33990122638b9132ca29c723bdf037f1a891a70c", Topics: []string{"0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545", "0x72657075746174696f6e00000000000000000000000000000000000000000000", "0x000000000000000000000000d9b2f59f3b5c7b3c67047d2f03c3e8052470be92"}, Data: "0x", @@ -21,11 +25,7 @@ func TestClient_GetLogs(t *testing.T) { actualLogs, err := api.GetLogs(379224, 379225, "0x33990122638b9132ca29c723bdf037f1a891a70c", "0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545") - noError(t, err, "api.GetLogs") - - equal := cmp.Equal(expectedLogs, actualLogs) + assert.NoError(t, err, "api.GetLogs") - if !equal { - t.Errorf("api.GetLogs not working\n: %s\n", cmp.Diff(expectedLogs, actualLogs)) - } + assert.Equal(expectedLogs, actualLogs) } diff --git a/setup_e2e_test.go b/pkg/client/setup_integration_test.go similarity index 77% rename from setup_e2e_test.go rename to pkg/client/setup_integration_test.go index 64f2702..d02604e 100644 --- a/setup_e2e_test.go +++ b/pkg/client/setup_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,13 +8,15 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "fmt" + "net/url" "os" - "testing" "time" + + "github.com/TokenTax/etherscan-api/v2/pkg/chain" ) const apiKeyEnvName = "ETHERSCAN_API_KEY" @@ -23,18 +28,18 @@ var ( bucket *Bucket // apiKey etherscan API key apiKey string + ok bool ) func init() { - apiKey = os.Getenv(apiKeyEnvName) - if apiKey == "" { + if apiKey, ok = os.LookupEnv(apiKeyEnvName); !ok { 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, apiKey) - api.Verbose = true - api.BeforeRequest = func(module string, action string, param map[string]interface{}) error { + api = NewClient(chain.EthereumMainnet, apiKey) + //api.Verbose = true + api.BeforeRequest = func(module string, action string, values url.Values) error { bucket.Take() return nil } @@ -78,10 +83,3 @@ func (b *Bucket) fillRoutine() { b.fill() } } - -// noError checks for testing error -func noError(t *testing.T, err error, msg string) { - if err != nil { - t.Fatalf("%s: %v", msg, err) - } -} diff --git a/pkg/client/stat.go b/pkg/client/stat.go new file mode 100644 index 0000000..11099f2 --- /dev/null +++ b/pkg/client/stat.go @@ -0,0 +1,57 @@ +/* + * 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 client + +import ( + "net/url" + + "github.com/TokenTax/etherscan-api/v2/internal/types" + "github.com/TokenTax/etherscan-api/v2/pkg/response" + "github.com/pkg/errors" +) + +type TokenTotalSupplyParams struct { + ContractAddress string `json:"contractaddress"` +} + +func (p TokenTotalSupplyParams) GetUrlValues() url.Values { + values := url.Values{} + if p.ContractAddress != "" { + values.Add("contractaddress", p.ContractAddress) + } + return values +} + +// EtherTotalSupply gets total supply of ether +func (c *Client) EtherTotalSupply() (totalSupply types.BigInt, err error) { + body, err := c.execute("stats", "ethsupply", nil) + if err != nil { + return types.BigInt{}, errors.Wrap(err, "executing ExecutionStatus request") + } + return response.ReadResponse[types.BigInt](body) +} + +// EtherLatestPrice gets the latest ether price, in BTC and USD +func (c *Client) EtherLatestPrice() (price response.LatestPrice, err error) { + body, err := c.execute("stats", "ethprice", nil) + if err != nil { + return response.LatestPrice{}, errors.Wrap(err, "executing LatestPrice request") + } + return response.ReadResponse[response.LatestPrice](body) +} + +// TokenTotalSupply gets total supply of token on specified contract address +func (c *Client) TokenTotalSupply(contractAddress string) (types.BigInt, error) { + values := TokenTotalSupplyParams{ContractAddress: contractAddress} + + body, err := c.execute("stats", "tokensupply", values.GetUrlValues()) + if err != nil { + return types.BigInt{}, errors.Wrap(err, "executing TokenTotalSupply request") + } + return response.ReadResponse[types.BigInt](body) +} diff --git a/stat_e2e_test.go b/pkg/client/stat_integration_test.go similarity index 81% rename from stat_e2e_test.go rename to pkg/client/stat_integration_test.go index f24551e..ee3fb9f 100644 --- a/stat_e2e_test.go +++ b/pkg/client/stat_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,16 +8,18 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "math/big" "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_EtherTotalSupply(t *testing.T) { totalSupply, err := api.EtherTotalSupply() - noError(t, err, "api.EtherTotalSupply") + assert.NoError(t, err, "api.EtherTotalSupply") if totalSupply.Int().Cmp(big.NewInt(100)) != 1 { t.Errorf("api.EtherTotalSupply not working, totalSupply is %s", totalSupply.Int().String()) @@ -23,7 +28,7 @@ func TestClient_EtherTotalSupply(t *testing.T) { func TestClient_EtherLatestPrice(t *testing.T) { latest, err := api.EtherLatestPrice() - noError(t, err, "api.EtherLatestPrice") + assert.NoError(t, err, "api.EtherLatestPrice") if latest.ETHBTC == 0 { t.Errorf("ETHBTC got 0") @@ -41,7 +46,7 @@ func TestClient_EtherLatestPrice(t *testing.T) { func TestClient_TokenTotalSupply(t *testing.T) { totalSupply, err := api.TokenTotalSupply("0x57d90b64a1a57749b0f932f1a3395792e12e7055") - noError(t, err, "api.TokenTotalSupply") + assert.NoError(t, err, "api.TokenTotalSupply") if totalSupply.Int().Cmp(big.NewInt(100)) != 1 { t.Errorf("api.TokenTotalSupply not working, totalSupply is %s", totalSupply.Int().String()) diff --git a/pkg/client/transaction.go b/pkg/client/transaction.go new file mode 100644 index 0000000..b335fbd --- /dev/null +++ b/pkg/client/transaction.go @@ -0,0 +1,64 @@ +/* + * 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 client + +import ( + "net/url" + + "github.com/TokenTax/etherscan-api/v2/pkg/response" + "github.com/pkg/errors" +) + +// ErrPreByzantiumTx transaction before 4,370,000 does not support receipt status check +var ErrPreByzantiumTx = errors.New("pre-byzantium transaction does not support receipt status check") + +type TransactionParams struct { + TxHash string `json:"txhash"` +} + +func (p TransactionParams) GetUrlValues() url.Values { + values := url.Values{} + if p.TxHash != "" { + values.Add("txhash", p.TxHash) + } + return values +} + +// ExecutionStatus checks contract execution status +func (c *Client) ExecutionStatus(txHash string) (response.ExecutionStatus, error) { + param := TransactionParams{TxHash: txHash} + + body, err := c.execute("transaction", "getstatus", param.GetUrlValues()) + if err != nil { + return response.ExecutionStatus{}, errors.Wrap(err, "executing ExecutionStatus request") + } + return response.ReadResponse[response.ExecutionStatus](body) +} + +// ReceiptStatus checks transaction receipt status +func (c *Client) ReceiptStatus(txHash string) (int, error) { + param := TransactionParams{TxHash: txHash} + body, err := c.execute("transaction", "gettxreceiptstatus", param.GetUrlValues()) + if err != nil { + return 0, errors.Wrap(err, "executing ReceiptStatus request") + } + + rawStatus, err := response.ReadResponse[response.StatusReponse](body) + if err != nil { + return 0, err + } + + switch rawStatus.Status { + case "0": + return 0, nil + case "1": + return 1, nil + default: + return -1, ErrPreByzantiumTx + } +} diff --git a/transaction_e2e_test.go b/pkg/client/transaction_integration_test.go similarity index 84% rename from transaction_e2e_test.go rename to pkg/client/transaction_integration_test.go index 255552c..8247f8e 100644 --- a/transaction_e2e_test.go +++ b/pkg/client/transaction_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,10 +8,12 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_ExecutionStatus(t *testing.T) { @@ -16,14 +21,14 @@ func TestClient_ExecutionStatus(t *testing.T) { // bad execution bad, err := api.ExecutionStatus("0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a") - noError(t, err, "api.ExecutionStatus") + assert.NoError(t, err, "api.ExecutionStatus") if bad.IsError != 1 && bad.ErrDescription != "Bad jump destination" { t.Errorf("api.ExecutionStatus not working, got\n%+v", bad) } // good execution good, err := api.ExecutionStatus("0xe8253035f1a1e93be24f43a3592a2c6cdbe3360e6f738ff40d46305252b44f5c") - noError(t, err, "api.ExecutionStatus") + assert.NoError(t, err, "api.ExecutionStatus") if good.IsError != 0 && good.ErrDescription != "" { t.Errorf("api.ExecutionStatus not working, got\n%+v", good) } @@ -34,14 +39,14 @@ func TestClient_ReceiptStatus(t *testing.T) { // bad execution bad, err := api.ReceiptStatus("0xe7bbbeb43cf863e20ec865021d63005149c133d7822e8edc1e6cb746d6728d4e") - noError(t, err, "api.ReceiptStatus") + assert.NoError(t, err, "api.ReceiptStatus") if bad != 0 { t.Errorf("api.ExecutionStatus not working, got %v, want 0", bad) } // good execution good, err := api.ReceiptStatus("0xe8253035f1a1e93be24f43a3592a2c6cdbe3360e6f738ff40d46305252b44f5c") - noError(t, err, "api.ReceiptStatus") + assert.NoError(t, err, "api.ReceiptStatus") if good != 1 { t.Errorf("api.ExecutionStatus not working, got %v, want 1", good) } diff --git a/pkg/response/response.go b/pkg/response/response.go new file mode 100644 index 0000000..28c1da7 --- /dev/null +++ b/pkg/response/response.go @@ -0,0 +1,341 @@ +/* + * 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 response + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/TokenTax/etherscan-api/v2/internal/types" + "github.com/pkg/errors" +) + +type EtherscanResponse interface { + AccountBalance | []AccountBalance | + NormalTx | []NormalTx | + InternalTx | []InternalTx | + ERC20Transfer | []ERC20Transfer | + ERC721Transfer | []ERC721Transfer | + ERC1155Transfer | []ERC1155Transfer | + MinedBlock | []MinedBlock | + ContractSource | []ContractSource | + ExecutionStatus | []ExecutionStatus | + BlockRewards | []BlockRewards | + LatestPrice | []LatestPrice | + Log | []Log | + GasPrices | []GasPrices | + StatusReponse | []StatusReponse | + types.BigInt | []types.BigInt | types.Time | string +} + +// envelope is the carrier of nearly every response +type envelope[T EtherscanResponse] struct { + // 1 for good, 0 for error + Status int `json:"status,string"` + // OK for good, other words when Status equals 0 + Message string `json:"message"` + // where response lies + Result T `json:"result"` +} + +type statusEnvelope struct { + Status int `json:"status,string"` +} + +func ReadResponse[T EtherscanResponse](content bytes.Buffer) (T, error) { + var ret T + + var envelope envelope[T] + if err := json.Unmarshal(content.Bytes(), &envelope); err != nil { + var statusEnv statusEnvelope + if err := json.Unmarshal(content.Bytes(), &statusEnv); err == nil { + if statusEnv.Status != 1 { + return ret, errors.Errorf("unmarshaling response into %T; message=%s", ret, envelope.Message) + } + } + return ret, errors.Wrapf(err, "unmarshaling etherscan response; body=%s", content.Bytes()) + } + if envelope.Status != 1 { + return ret, errors.Errorf("etherscan server: %s", envelope.Message) + } + + return envelope.Result, nil +} + +// AccountBalance account and its balance in pair +type AccountBalance struct { + Account string `json:"account"` + Balance *types.BigInt `json:"balance"` +} + +type StatusReponse struct { + Status string `json:"status"` +} + +type ( + // EtherscanTx represents + EtherscanTx interface { + GetBlockNumber() int + GetHash() string + } + + // NormalTx holds info from normal tx query + NormalTx struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + Hash string `json:"hash"` + Nonce int `json:"nonce,string"` + BlockHash string `json:"blockHash"` + TransactionIndex int `json:"transactionIndex,string"` + From string `json:"from"` + To string `json:"to"` + Value *types.BigInt `json:"value"` + Gas int `json:"gas,string"` + GasPrice *types.BigInt `json:"gasPrice"` + IsError int `json:"isError,string"` + TxReceiptStatus string `json:"txreceipt_status"` + Input string `json:"input"` + ContractAddress string `json:"contractAddress"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + GasUsed int `json:"gasUsed,string"` + Confirmations int `json:"confirmations,string"` + } + + // InternalTx holds info from internal tx query + InternalTx struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + Hash string `json:"hash"` + From string `json:"from"` + To string `json:"to"` + Value *types.BigInt `json:"value"` + ContractAddress string `json:"contractAddress"` + Input string `json:"input"` + Type string `json:"type"` + Gas int `json:"gas,string"` + GasUsed int `json:"gasUsed,string"` + TraceID string `json:"traceId"` + IsError int `json:"isError,string"` + ErrCode string `json:"errCode"` + } + + // ERC20Transfer holds info from ERC20 token transfer event query + ERC20Transfer struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.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"` + Value *types.BigInt `json:"value"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal int `json:"tokenDecimal,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *types.BigInt `json:"gasPrice"` + GasUsed int `json:"gasUsed,string"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + Input string `json:"input"` + Confirmations int `json:"confirmations,string"` + } + + // ERC721Transfer holds info from ERC721 token transfer event query + ERC721Transfer struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.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 *types.BigInt `json:"tokenID"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal int `json:"tokenDecimal,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *types.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 + ERC1155Transfer struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.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 int `json:"tokenDecimal,string"` + //TokenValue int `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"` + } +) + +func (tx NormalTx) GetBlockNumber() int { return tx.BlockNumber } +func (tx NormalTx) GetHash() string { return tx.Hash } + +func (tx InternalTx) GetBlockNumber() int { return tx.BlockNumber } +func (tx InternalTx) GetHash() string { return tx.Hash } + +func (tx ERC20Transfer) GetBlockNumber() int { return tx.BlockNumber } +func (tx ERC20Transfer) GetHash() string { return tx.Hash } + +func (tx ERC721Transfer) GetBlockNumber() int { return tx.BlockNumber } +func (tx ERC721Transfer) GetHash() string { return tx.Hash } + +func (tx ERC1155Transfer) GetBlockNumber() int { return tx.BlockNumber } +func (tx ERC1155Transfer) GetHash() string { return tx.Hash } + +// MinedBlock holds info from query for mined block by address +type MinedBlock struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + BlockReward *types.BigInt `json:"blockReward"` +} + +// ContractSource holds info from query for contract source code +type ContractSource struct { + SourceCode string `json:"SourceCode"` + ABI string `json:"ABI"` + ContractName string `json:"ContractName"` + CompilerVersion string `json:"CompilerVersion"` + 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"` +} + +// ExecutionStatus holds info from query for transaction execution status +type ExecutionStatus struct { + // 0 = pass, 1 = error + IsError int `json:"isError,string"` + ErrDescription string `json:"errDescription"` +} + +// BlockRewards holds info from query for block and uncle rewards +type BlockRewards struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + BlockMiner string `json:"blockMiner"` + BlockReward *types.BigInt `json:"blockReward"` + Uncles []struct { + Miner string `json:"miner"` + UnclePosition int `json:"unclePosition,string"` + BlockReward *types.BigInt `json:"blockreward"` + } `json:"uncles"` + UncleInclusionReward *types.BigInt `json:"uncleInclusionReward"` +} + +// LatestPrice holds info from query for latest ether price +type LatestPrice struct { + ETHBTC float64 `json:"ethbtc,string"` + ETHBTCTimestamp types.Time `json:"ethbtc_timestamp"` + ETHUSD float64 `json:"ethusd,string"` + ETHUSDTimestamp types.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/reflect.go b/reflect.go deleted file mode 100644 index ba0d54a..0000000 --- a/reflect.go +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 ( - "fmt" - "reflect" - "strconv" -) - -// extractValue obtains string-formed slice representation via -// input. It only handles string, int, and their slice form, and -// panics otherwise. -func extractValue(input interface{}) (output []string) { - v := direct(reflect.ValueOf(input)) - - if v.Kind() == reflect.Slice { - length := v.Len() - output = make([]string, length) - - for i := 0; i < length; i++ { - output[i] = valueToStr(v.Index(i)) - } - } else { - output = make([]string, 1) - output[0] = valueToStr(v) - } - - return -} - -// valueToStr convert v into proper string representation -// Only handles int and string, panic otherwise. -func valueToStr(v reflect.Value) (str string) { - switch v.Kind() { - case reflect.String: - str = v.String() - case reflect.Int: - str = strconv.FormatInt(v.Int(), 10) - default: - panic(fmt.Sprintf("valueToStr: %v is of unexpected kind %q", v, v.Kind())) - } - return -} - -// direct traverses the pointer chain to fetch -// the solid value -func direct(v reflect.Value) reflect.Value { - for ; v.Kind() == reflect.Ptr; v = v.Elem() { - // relax - } - return v -} diff --git a/response.go b/response.go deleted file mode 100644 index d096aef..0000000 --- a/response.go +++ /dev/null @@ -1,268 +0,0 @@ -/* - * 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 ( - "encoding/json" - "fmt" - "strconv" - "strings" -) - -// Envelope is the carrier of nearly every response -type Envelope struct { - // 1 for good, 0 for error - Status int `json:"status,string"` - // OK for good, other words when Status equals 0 - Message string `json:"message"` - // where response lies - Result json.RawMessage `json:"result"` -} - -// AccountBalance account and its balance in pair -type AccountBalance struct { - Account string `json:"account"` - Balance *BigInt `json:"balance"` -} - -// NormalTx holds info from normal tx query -type NormalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - Nonce int `json:"nonce,string"` - BlockHash string `json:"blockHash"` - TransactionIndex int `json:"transactionIndex,string"` - From string `json:"from"` - To string `json:"to"` - Value *BigInt `json:"value"` - Gas int `json:"gas,string"` - GasPrice *BigInt `json:"gasPrice"` - IsError int `json:"isError,string"` - TxReceiptStatus string `json:"txreceipt_status"` - Input string `json:"input"` - ContractAddress string `json:"contractAddress"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - GasUsed int `json:"gasUsed,string"` - Confirmations int `json:"confirmations,string"` -} - -// InternalTx holds info from internal tx query -type InternalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - From string `json:"from"` - To string `json:"to"` - Value *BigInt `json:"value"` - ContractAddress string `json:"contractAddress"` - Input string `json:"input"` - Type string `json:"type"` - Gas int `json:"gas,string"` - GasUsed int `json:"gasUsed,string"` - TraceID string `json:"traceId"` - IsError int `json:"isError,string"` - ErrCode string `json:"errCode"` -} - -// ERC20Transfer holds info from ERC20 token transfer event query -type ERC20Transfer 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"` - Value *BigInt `json:"value"` - 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"` -} - -// 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"` - TimeStamp Time `json:"timeStamp"` - BlockReward *BigInt `json:"blockReward"` -} - -// ContractSource holds info from query for contract source code -type ContractSource struct { - SourceCode string `json:"SourceCode"` - ABI string `json:"ABI"` - ContractName string `json:"ContractName"` - CompilerVersion string `json:"CompilerVersion"` - 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"` -} - -// ExecutionStatus holds info from query for transaction execution status -type ExecutionStatus struct { - // 0 = pass, 1 = error - IsError int `json:"isError,string"` - ErrDescription string `json:"errDescription"` -} - -// BlockRewards holds info from query for block and uncle rewards -type BlockRewards struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - BlockMiner string `json:"blockMiner"` - BlockReward *BigInt `json:"blockReward"` - Uncles []struct { - Miner string `json:"miner"` - UnclePosition int `json:"unclePosition,string"` - BlockReward *BigInt `json:"blockreward"` - } `json:"uncles"` - UncleInclusionReward *BigInt `json:"uncleInclusionReward"` -} - -// LatestPrice holds info from query for latest ether price -type LatestPrice struct { - ETHBTC float64 `json:"ethbtc,string"` - ETHBTCTimestamp Time `json:"ethbtc_timestamp"` - 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/stat.go b/stat.go deleted file mode 100644 index d3dc156..0000000 --- a/stat.go +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 - -// EtherTotalSupply gets total supply of ether -func (c *Client) EtherTotalSupply() (totalSupply *BigInt, err error) { - err = c.call("stats", "ethsupply", nil, &totalSupply) - return -} - -// EtherLatestPrice gets the latest ether price, in BTC and USD -func (c *Client) EtherLatestPrice() (price LatestPrice, err error) { - err = c.call("stats", "ethprice", nil, &price) - return -} - -// TokenTotalSupply gets total supply of token on specified contract address -func (c *Client) TokenTotalSupply(contractAddress string) (totalSupply *BigInt, err error) { - param := M{ - "contractaddress": contractAddress, - } - - err = c.call("stats", "tokensupply", param, &totalSupply) - return -} diff --git a/transaction.go b/transaction.go deleted file mode 100644 index 8461342..0000000 --- a/transaction.go +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 "errors" - -// ErrPreByzantiumTx transaction before 4,370,000 does not support receipt status check -var ErrPreByzantiumTx = errors.New("pre-byzantium transaction does not support receipt status check") - -// ExecutionStatus checks contract execution status (if there was an error during contract execution) -// -// note on IsError: 0 = pass, 1 = error -func (c *Client) ExecutionStatus(txHash string) (status ExecutionStatus, err error) { - param := M{ - "txhash": txHash, - } - - err = c.call("transaction", "getstatus", param, &status) - return -} - -// ReceiptStatus checks transaction receipt status -// -// only applicable for post byzantium fork transactions, i.e. after block 4,370,000 -// -// An special err ErrPreByzantiumTx raises for the transaction before byzantium fork. -// -// Note: receiptStatus: 0 = Fail, 1 = Pass. -func (c *Client) ReceiptStatus(txHash string) (receiptStatus int, err error) { - param := M{ - "txhash": txHash, - } - - var rawStatus = struct { - Status string `json:"status"` - }{} - - err = c.call("transaction", "gettxreceiptstatus", param, &rawStatus) - if err != nil { - return - } - - switch rawStatus.Status { - case "0": - receiptStatus = 0 - case "1": - receiptStatus = 1 - default: - receiptStatus = -1 - err = ErrPreByzantiumTx - } - - return -}