├── .github ├── FUNDING.yml └── workflows │ └── ci.yaml ├── go.mod ├── .gitignore ├── go.sum ├── doc.go ├── error_wrap_test.go ├── logs.go ├── client_test.go ├── contract.go ├── gas_tracker_e2e_test.go ├── error_wrap.go ├── gas_tracker.go ├── network.go ├── stat.go ├── LICENSE ├── logs_e2e_test.go ├── block.go ├── helper_test.go ├── stat_e2e_test.go ├── reflect.go ├── transaction.go ├── block_e2e_test.go ├── transaction_e2e_test.go ├── setup_e2e_test.go ├── helper.go ├── README_ZH.md ├── README.md ├── account.go ├── account_e2e_test.go ├── client.go ├── contract_e2e_test.go └── response.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nanmu42 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nanmu42/etherscan-api 2 | 3 | go 1.13 4 | 5 | require github.com/google/go-cmp v0.5.7 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # IDEs 9 | /.idea 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | coverage.txt -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 2 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package etherscan provides Go bindings to the Etherscan.io API. 2 | // 3 | // This work is a nearly Full implementation 4 | // (accounts, transactions, tokens, contracts, blocks, stats), 5 | // with full network support(Mainnet, Ropsten, Kovan, Rinkby, Tobalaba), 6 | // and only depending on standard library. 7 | // 8 | // Example can be found at https://github.com/nanmu42/etherscan-api 9 | package etherscan 10 | -------------------------------------------------------------------------------- /error_wrap_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "errors" 12 | "testing" 13 | ) 14 | 15 | func Test_wrapfErr(t *testing.T) { 16 | const ans = "status 100: continue test" 17 | 18 | err := errors.New("continue test") 19 | err = wrapfErr(err, "%s %v", "status", "100") 20 | 21 | if err.Error() != ans { 22 | t.Fatalf("got %v, want %s", err, ans) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /logs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Avi Misra 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | // GetLogs gets logs that match "topic" emitted by the specified "address" between the "fromBlock" and "toBlock" 11 | func (c *Client) GetLogs(fromBlock, toBlock int, address, topic string) (logs []Log, err error) { 12 | param := M{ 13 | "fromBlock": fromBlock, 14 | "toBlock": toBlock, 15 | "topic0": topic, 16 | "address": address, 17 | } 18 | 19 | err = c.call("logs", "getLogs", param, &logs) 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func TestClient_craftURL(t *testing.T) { 15 | c := New(Ropsten, "abc123") 16 | 17 | 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` 18 | output := c.craftURL("testing", "craftURL", M{ 19 | "one": 1, 20 | "two": "2", 21 | "three": []int{1, 2, 3}, 22 | "four": []string{"d", "e", "f"}, 23 | }) 24 | 25 | if output != expected { 26 | t.Fatalf("output != expected, got %s, want %s", output, expected) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contract.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | // ContractABI gets contract abi for verified contract source codes 11 | func (c *Client) ContractABI(address string) (abi string, err error) { 12 | param := M{ 13 | "address": address, 14 | } 15 | 16 | err = c.call("contract", "getabi", param, &abi) 17 | return 18 | } 19 | 20 | // ContractSource gets contract source code for verified contract source codes 21 | func (c *Client) ContractSource(address string) (source []ContractSource, err error) { 22 | param := M{ 23 | "address": address, 24 | } 25 | 26 | err = c.call("contract", "getsourcecode", param, &source) 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: "42 6 * * 0" 10 | 11 | jobs: 12 | build: 13 | environment: "CI Test" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: ">=1.17.4" 22 | 23 | - name: Test 24 | env: 25 | ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} 26 | run: go test -v -coverprofile=coverage.txt -covermode=count ./... 27 | 28 | - name: Codecov 29 | uses: codecov/codecov-action@v2.1.0 30 | 31 | - name: golangci-lint 32 | uses: golangci/golangci-lint-action@v2 -------------------------------------------------------------------------------- /gas_tracker_e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | //GasEstiamte generates dynamic data. Best we can do is ensure all fields are populated 15 | func TestClient_GasEstimate(t *testing.T) { 16 | _, err := api.GasEstimate(20000000) 17 | noError(t, err, "api.GasEstimate") 18 | } 19 | 20 | //GasOracle generates dynamic data. Best we can do is ensure all fields are populated 21 | func TestClient_GasOracle(t *testing.T) { 22 | gasPrice, err := api.GasOracle() 23 | noError(t, err, "api.GasOrcale") 24 | 25 | if 0 == len(gasPrice.GasUsedRatio) { 26 | t.Errorf("gasPrice.GasUsedRatio empty") 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /error_wrap.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "fmt" 12 | ) 13 | 14 | // wrapErr gives error some context msg 15 | // returns nil if err is nil 16 | func wrapErr(err error, msg string) (errWithContext error) { 17 | if err == nil { 18 | return 19 | } 20 | 21 | errWithContext = fmt.Errorf("%s: %v", msg, err) 22 | return 23 | } 24 | 25 | // wrapfErr gives error some context msg 26 | // with desired format and content 27 | // returns nil if err is nil 28 | func wrapfErr(err error, format string, a ...interface{}) (errWithContext error) { 29 | if err == nil { 30 | return 31 | } 32 | 33 | errWithContext = wrapErr(err, fmt.Sprintf(format, a...)) 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /gas_tracker.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Avi Misra 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import "time" 11 | 12 | // GasEstiamte gets estiamted confirmation time (in seconds) at the given gas price 13 | func (c *Client) GasEstimate(gasPrice int) (confirmationTimeInSec time.Duration, err error) { 14 | params := M{"gasPrice": gasPrice} 15 | var confTime string 16 | err = c.call("gastracker", "gasestimate", params, &confTime) 17 | if err != nil { 18 | return 19 | } 20 | return time.ParseDuration(confTime + "s") 21 | } 22 | 23 | // GasOracle gets suggested gas prices (in Gwei) 24 | func (c *Client) GasOracle() (gasPrices GasPrices, err error) { 25 | err = c.call("gastracker", "gasoracle", M{}, &gasPrices) 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | const ( 11 | //// Ethereum public networks 12 | 13 | // Mainnet Ethereum mainnet for production 14 | Mainnet Network = "api" 15 | // Ropsten Testnet(POW) 16 | Ropsten Network = "api-ropsten" 17 | // Kovan Testnet(POA) 18 | Kovan Network = "api-kovan" 19 | // Rinkby Testnet(CLIQUE) 20 | Rinkby Network = "api-rinkeby" 21 | // Goerli Testnet(CLIQUE) 22 | Goerli Network = "api-goerli" 23 | // Tobalaba Testnet 24 | Tobalaba Network = "api-tobalaba" 25 | ) 26 | 27 | // Network is ethereum network type (mainnet, ropsten, etc) 28 | type Network string 29 | 30 | // SubDomain returns the subdomain of etherscan API 31 | // via n provided. 32 | func (n Network) SubDomain() (sub string) { 33 | return string(n) 34 | } 35 | -------------------------------------------------------------------------------- /stat.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | // EtherTotalSupply gets total supply of ether 11 | func (c *Client) EtherTotalSupply() (totalSupply *BigInt, err error) { 12 | err = c.call("stats", "ethsupply", nil, &totalSupply) 13 | return 14 | } 15 | 16 | // EtherLatestPrice gets the latest ether price, in BTC and USD 17 | func (c *Client) EtherLatestPrice() (price LatestPrice, err error) { 18 | err = c.call("stats", "ethprice", nil, &price) 19 | return 20 | } 21 | 22 | // TokenTotalSupply gets total supply of token on specified contract address 23 | func (c *Client) TokenTotalSupply(contractAddress string) (totalSupply *BigInt, err error) { 24 | param := M{ 25 | "contractaddress": contractAddress, 26 | } 27 | 28 | err = c.call("stats", "tokensupply", param, &totalSupply) 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 LI Zhennan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /logs_e2e_test.go: -------------------------------------------------------------------------------- 1 | package etherscan 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestClient_GetLogs(t *testing.T) { 10 | expectedLogs := []Log{ 11 | Log{ 12 | Address: "0x33990122638b9132ca29c723bdf037f1a891a70c", 13 | Topics: []string{"0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545", "0x72657075746174696f6e00000000000000000000000000000000000000000000", "0x000000000000000000000000d9b2f59f3b5c7b3c67047d2f03c3e8052470be92"}, 14 | Data: "0x", 15 | BlockNumber: "0x5c958", 16 | BlockHash: "0xe32a9cac27f823b18454e8d69437d2af41a1b81179c6af2601f1040a72ad444b", 17 | TransactionHash: "0x0b03498648ae2da924f961dda00dc6bb0a8df15519262b7e012b7d67f4bb7e83", 18 | LogIndex: "0x", 19 | }, 20 | } 21 | 22 | actualLogs, err := api.GetLogs(379224, 379225, "0x33990122638b9132ca29c723bdf037f1a891a70c", "0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545") 23 | 24 | noError(t, err, "api.GetLogs") 25 | 26 | equal := cmp.Equal(expectedLogs, actualLogs) 27 | 28 | if !equal { 29 | t.Errorf("api.GetLogs not working\n: %s\n", cmp.Diff(expectedLogs, actualLogs)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /block.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "fmt" 12 | "strconv" 13 | ) 14 | 15 | // BlockReward gets block and uncle rewards by block number 16 | func (c *Client) BlockReward(blockNum int) (rewards BlockRewards, err error) { 17 | param := M{ 18 | "blockno": blockNum, 19 | } 20 | 21 | err = c.call("block", "getblockreward", param, &rewards) 22 | return 23 | } 24 | 25 | // BlockNumber gets the closest block number by UNIX timestamp 26 | // 27 | // valid closest option: before, after 28 | func (c *Client) BlockNumber(timestamp int64, closest string) (blockNumber int, err error) { 29 | var blockNumberStr string 30 | 31 | param := M{ 32 | "timestamp": strconv.FormatInt(timestamp, 10), 33 | "closest": closest, 34 | } 35 | 36 | err = c.call("block", "getblocknobytime", param, &blockNumberStr) 37 | 38 | if err != nil { 39 | return 40 | } 41 | 42 | blockNumber, err = strconv.Atoi(blockNumberStr) 43 | if err != nil { 44 | err = fmt.Errorf("parsing block number %q: %w", blockNumberStr, err) 45 | return 46 | } 47 | 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "math/big" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestBigInt(t *testing.T) { 17 | const ansStr = "255" 18 | var ans = big.NewInt(255) 19 | 20 | b := new(BigInt) 21 | err := b.UnmarshalText([]byte(ansStr)) 22 | noError(t, err, "BigInt.UnmarshalText") 23 | 24 | if b.Int().Cmp(ans) != 0 { 25 | t.Fatalf("BigInt.UnmarshalText not working, got %v, want %v", b.Int(), ans) 26 | } 27 | textBytes, err := b.MarshalText() 28 | noError(t, err, "BigInt.MarshalText") 29 | 30 | if string(textBytes) != ansStr { 31 | t.Fatalf("BigInt.MarshalText not working, got %s, want %s", textBytes, ansStr) 32 | } 33 | } 34 | 35 | func TestTime(t *testing.T) { 36 | const ansStr = "1533396289" 37 | var ans = time.Unix(1533396289, 0) 38 | 39 | b := new(Time) 40 | err := b.UnmarshalText([]byte(ansStr)) 41 | noError(t, err, "Time.UnmarshalText") 42 | 43 | if !b.Time().Equal(ans) { 44 | t.Fatalf("Time.UnmarshalText not working, got %v, want %v", b, ans) 45 | } 46 | textBytes, err := b.MarshalText() 47 | noError(t, err, "BigInt.MarshalText") 48 | 49 | if string(textBytes) != ansStr { 50 | t.Fatalf("Time.MarshalText not working, got %s, want %s", textBytes, ansStr) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /stat_e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "math/big" 12 | "testing" 13 | ) 14 | 15 | func TestClient_EtherTotalSupply(t *testing.T) { 16 | totalSupply, err := api.EtherTotalSupply() 17 | noError(t, err, "api.EtherTotalSupply") 18 | 19 | if totalSupply.Int().Cmp(big.NewInt(100)) != 1 { 20 | t.Errorf("api.EtherTotalSupply not working, totalSupply is %s", totalSupply.Int().String()) 21 | } 22 | } 23 | 24 | func TestClient_EtherLatestPrice(t *testing.T) { 25 | latest, err := api.EtherLatestPrice() 26 | noError(t, err, "api.EtherLatestPrice") 27 | 28 | if latest.ETHBTC == 0 { 29 | t.Errorf("ETHBTC got 0") 30 | } 31 | if latest.ETHBTCTimestamp.Time().IsZero() { 32 | t.Errorf("ETHBTCTimestamp is zero") 33 | } 34 | if latest.ETHUSD == 0 { 35 | t.Errorf("ETHUSD got 0") 36 | } 37 | if latest.ETHUSDTimestamp.Time().IsZero() { 38 | t.Errorf("ETHUSDTimestamp is zero") 39 | } 40 | } 41 | 42 | func TestClient_TokenTotalSupply(t *testing.T) { 43 | totalSupply, err := api.TokenTotalSupply("0x57d90b64a1a57749b0f932f1a3395792e12e7055") 44 | noError(t, err, "api.TokenTotalSupply") 45 | 46 | if totalSupply.Int().Cmp(big.NewInt(100)) != 1 { 47 | t.Errorf("api.TokenTotalSupply not working, totalSupply is %s", totalSupply.Int().String()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /reflect.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "fmt" 12 | "reflect" 13 | "strconv" 14 | ) 15 | 16 | // extractValue obtains string-formed slice representation via 17 | // input. It only handles string, int, and their slice form, and 18 | // panics otherwise. 19 | func extractValue(input interface{}) (output []string) { 20 | v := direct(reflect.ValueOf(input)) 21 | 22 | if v.Kind() == reflect.Slice { 23 | length := v.Len() 24 | output = make([]string, length) 25 | 26 | for i := 0; i < length; i++ { 27 | output[i] = valueToStr(v.Index(i)) 28 | } 29 | } else { 30 | output = make([]string, 1) 31 | output[0] = valueToStr(v) 32 | } 33 | 34 | return 35 | } 36 | 37 | // valueToStr convert v into proper string representation 38 | // Only handles int and string, panic otherwise. 39 | func valueToStr(v reflect.Value) (str string) { 40 | switch v.Kind() { 41 | case reflect.String: 42 | str = v.String() 43 | case reflect.Int: 44 | str = strconv.FormatInt(v.Int(), 10) 45 | default: 46 | panic(fmt.Sprintf("valueToStr: %v is of unexpected kind %q", v, v.Kind())) 47 | } 48 | return 49 | } 50 | 51 | // direct traverses the pointer chain to fetch 52 | // the solid value 53 | func direct(v reflect.Value) reflect.Value { 54 | for ; v.Kind() == reflect.Ptr; v = v.Elem() { 55 | // relax 56 | } 57 | return v 58 | } 59 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import "errors" 11 | 12 | // ErrPreByzantiumTx transaction before 4,370,000 does not support receipt status check 13 | var ErrPreByzantiumTx = errors.New("pre-byzantium transaction does not support receipt status check") 14 | 15 | // ExecutionStatus checks contract execution status (if there was an error during contract execution) 16 | // 17 | // note on IsError: 0 = pass, 1 = error 18 | func (c *Client) ExecutionStatus(txHash string) (status ExecutionStatus, err error) { 19 | param := M{ 20 | "txhash": txHash, 21 | } 22 | 23 | err = c.call("transaction", "getstatus", param, &status) 24 | return 25 | } 26 | 27 | // ReceiptStatus checks transaction receipt status 28 | // 29 | // only applicable for post byzantium fork transactions, i.e. after block 4,370,000 30 | // 31 | // An special err ErrPreByzantiumTx raises for the transaction before byzantium fork. 32 | // 33 | // Note: receiptStatus: 0 = Fail, 1 = Pass. 34 | func (c *Client) ReceiptStatus(txHash string) (receiptStatus int, err error) { 35 | param := M{ 36 | "txhash": txHash, 37 | } 38 | 39 | var rawStatus = struct { 40 | Status string `json:"status"` 41 | }{} 42 | 43 | err = c.call("transaction", "gettxreceiptstatus", param, &rawStatus) 44 | if err != nil { 45 | return 46 | } 47 | 48 | switch rawStatus.Status { 49 | case "0": 50 | receiptStatus = 0 51 | case "1": 52 | receiptStatus = 1 53 | default: 54 | receiptStatus = -1 55 | err = ErrPreByzantiumTx 56 | } 57 | 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /block_e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "encoding/json" 12 | "testing" 13 | ) 14 | 15 | func TestClient_BlockReward(t *testing.T) { 16 | 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"}` 17 | 18 | reward, err := api.BlockReward(2165403) 19 | noError(t, err, "api.BlockReward") 20 | 21 | j, err := json.Marshal(reward) 22 | noError(t, err, "json.Marshal") 23 | if string(j) != ans { 24 | t.Errorf("api.BlockReward not working, got %s, want %s", j, ans) 25 | } 26 | } 27 | 28 | func TestClient_BlockNumber(t *testing.T) { 29 | // Note: All values taken from docs.etherscan.io/api-endpoints/blocks 30 | const ansBefore = 9251482 31 | const ansAfter = 9251483 32 | 33 | blockNumber, err := api.BlockNumber(1578638524, "before") 34 | noError(t, err, "api.BlockNumber") 35 | 36 | if blockNumber != ansBefore { 37 | t.Errorf(`api.BlockNumber(1578638524, "before") not working, got %d, want %d`, blockNumber, ansBefore) 38 | } 39 | 40 | blockNumber, err = api.BlockNumber(1578638524, "after") 41 | noError(t, err, "api.BlockNumber") 42 | 43 | if blockNumber != ansAfter { 44 | t.Errorf(`api.BlockNumber(1578638524,"after") not working, got %d, want %d`, blockNumber, ansAfter) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /transaction_e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func TestClient_ExecutionStatus(t *testing.T) { 15 | var err error 16 | 17 | // bad execution 18 | bad, err := api.ExecutionStatus("0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a") 19 | noError(t, err, "api.ExecutionStatus") 20 | if bad.IsError != 1 && bad.ErrDescription != "Bad jump destination" { 21 | t.Errorf("api.ExecutionStatus not working, got\n%+v", bad) 22 | } 23 | 24 | // good execution 25 | good, err := api.ExecutionStatus("0xe8253035f1a1e93be24f43a3592a2c6cdbe3360e6f738ff40d46305252b44f5c") 26 | noError(t, err, "api.ExecutionStatus") 27 | if good.IsError != 0 && good.ErrDescription != "" { 28 | t.Errorf("api.ExecutionStatus not working, got\n%+v", good) 29 | } 30 | } 31 | 32 | func TestClient_ReceiptStatus(t *testing.T) { 33 | var err error 34 | 35 | // bad execution 36 | bad, err := api.ReceiptStatus("0xe7bbbeb43cf863e20ec865021d63005149c133d7822e8edc1e6cb746d6728d4e") 37 | noError(t, err, "api.ReceiptStatus") 38 | if bad != 0 { 39 | t.Errorf("api.ExecutionStatus not working, got %v, want 0", bad) 40 | } 41 | 42 | // good execution 43 | good, err := api.ReceiptStatus("0xe8253035f1a1e93be24f43a3592a2c6cdbe3360e6f738ff40d46305252b44f5c") 44 | noError(t, err, "api.ReceiptStatus") 45 | if good != 1 { 46 | t.Errorf("api.ExecutionStatus not working, got %v, want 1", good) 47 | } 48 | 49 | // tx before byzantium fork 50 | before, err := api.ReceiptStatus("0x836b403cc1516eb1337c151ff3660c3ebd528d850e6ac20a75652c705ea769f4") 51 | if err != ErrPreByzantiumTx { 52 | t.Errorf("api.ReceiptStatus does not throw error for tx before byzantium fork") 53 | } 54 | if before != -1 { 55 | t.Errorf("api.ExecutionStatus not working, got %v, want -1", before) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /setup_e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | const apiKeyEnvName = "ETHERSCAN_API_KEY" 18 | 19 | var ( 20 | // api test client for many test cases 21 | api *Client 22 | // bucket default rate limiter 23 | bucket *Bucket 24 | // apiKey etherscan API key 25 | apiKey string 26 | ) 27 | 28 | func init() { 29 | apiKey = os.Getenv(apiKeyEnvName) 30 | if apiKey == "" { 31 | panic(fmt.Sprintf("API key is empty, set env variable %q with a valid API key to proceed.", apiKeyEnvName)) 32 | } 33 | bucket = NewBucket(500 * time.Millisecond) 34 | 35 | api = New(Mainnet, apiKey) 36 | api.Verbose = true 37 | api.BeforeRequest = func(module string, action string, param map[string]interface{}) error { 38 | bucket.Take() 39 | return nil 40 | } 41 | } 42 | 43 | // Bucket is a simple and easy rate limiter 44 | // Use NewBucket() to construct one. 45 | type Bucket struct { 46 | bucket chan bool 47 | refillTime time.Duration 48 | } 49 | 50 | // NewBucket factory of Bucket 51 | func NewBucket(refillTime time.Duration) (b *Bucket) { 52 | b = &Bucket{ 53 | bucket: make(chan bool), 54 | refillTime: refillTime, 55 | } 56 | 57 | go b.fillRoutine() 58 | 59 | return 60 | } 61 | 62 | // Take a action token from bucket, 63 | // blocks if there is currently no token left. 64 | func (b *Bucket) Take() { 65 | <-b.bucket 66 | } 67 | 68 | // fill a action token into bucket, 69 | // no-op if the bucket is currently full 70 | func (b *Bucket) fill() { 71 | b.bucket <- true 72 | } 73 | 74 | func (b *Bucket) fillRoutine() { 75 | ticker := time.NewTicker(b.refillTime) 76 | 77 | for range ticker.C { 78 | b.fill() 79 | } 80 | } 81 | 82 | // noError checks for testing error 83 | func noError(t *testing.T, err error, msg string) { 84 | if err != nil { 85 | t.Fatalf("%s: %v", msg, err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "math/big" 12 | "reflect" 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | // compose adds input to param, whose key is tag 18 | // if input is nil or nil of some type, compose is a no-op. 19 | func compose(param map[string]interface{}, tag string, input interface{}) { 20 | // simple situation 21 | if input == nil { 22 | return 23 | } 24 | 25 | // needs dig further 26 | v := reflect.ValueOf(input) 27 | switch v.Kind() { 28 | case reflect.Ptr, reflect.Slice, reflect.Interface: 29 | if v.IsNil() { 30 | return 31 | } 32 | } 33 | 34 | param[tag] = input 35 | } 36 | 37 | // M is a type shorthand for param input 38 | type M map[string]interface{} 39 | 40 | // BigInt is a wrapper over big.Int to implement only unmarshalText 41 | // for json decoding. 42 | type BigInt big.Int 43 | 44 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 45 | func (b *BigInt) UnmarshalText(text []byte) (err error) { 46 | var bigInt = new(big.Int) 47 | err = bigInt.UnmarshalText(text) 48 | if err != nil { 49 | return 50 | } 51 | 52 | *b = BigInt(*bigInt) 53 | return nil 54 | } 55 | 56 | // MarshalText implements the encoding.TextMarshaler 57 | func (b *BigInt) MarshalText() (text []byte, err error) { 58 | return []byte(b.Int().String()), nil 59 | } 60 | 61 | // Int returns b's *big.Int form 62 | func (b *BigInt) Int() *big.Int { 63 | return (*big.Int)(b) 64 | } 65 | 66 | // Time is a wrapper over big.Int to implement only unmarshalText 67 | // for json decoding. 68 | type Time time.Time 69 | 70 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 71 | func (t *Time) UnmarshalText(text []byte) (err error) { 72 | input, err := strconv.ParseInt(string(text), 10, 64) 73 | if err != nil { 74 | err = wrapErr(err, "strconv.ParseInt") 75 | return 76 | } 77 | 78 | var timestamp = time.Unix(input, 0) 79 | *t = Time(timestamp) 80 | 81 | return nil 82 | } 83 | 84 | // Time returns t's time.Time form 85 | func (t Time) Time() time.Time { 86 | return time.Time(t) 87 | } 88 | 89 | // MarshalText implements the encoding.TextMarshaler 90 | func (t Time) MarshalText() (text []byte, err error) { 91 | return []byte(strconv.FormatInt(t.Time().Unix(), 10)), nil 92 | } 93 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | [English](https://github.com/nanmu42/etherscan-api/blob/master/README.md) | **中文** 2 | 3 | # etherscan-api 4 | 5 | [![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api) 6 | [![CI status](https://github.com/nanmu42/etherscan-api/actions/workflows/ci.yaml/badge.svg)](https://github.com/nanmu42/etherscan-api/actions) 7 | [![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api) 9 | 10 | Etherscan API的Golang客户端, 11 | 支持几乎所有功能(accounts, transactions, tokens, contracts, blocks, stats), 12 | 所有公共网络(Mainnet, Ropsten, Kovan, Rinkby, Goerli, Tobalaba)。 13 | 本项目只依赖于官方库。 :wink: 14 | 15 | # 使用方法 16 | 17 | ```bash 18 | go get github.com/nanmu42/etherscan-api 19 | ``` 20 | 21 | 填入网络选项和API Key即可开始使用。 :rocket: 22 | 23 | ```go 24 | import ( 25 | "github.com/nanmu42/etherscan-api" 26 | "fmt" 27 | ) 28 | 29 | func main() { 30 | // 创建连接指定网络的客户端 31 | client := etherscan.New(etherscan.Mainnet, "[your API key]") 32 | 33 | // 或者,如果你要调用的是EtherScan家族的BscScan: 34 | // 35 | // client := etherscan.NewCustomized(etherscan.Customization{ 36 | // Timeout: 15 * time.Second, 37 | // Key: "You key here", 38 | // BaseURL: "https://api.bscscan.com/api?", 39 | // Verbose: false, 40 | // }) 41 | 42 | // (可选)按需注册钩子函数,例如用于速率控制 43 | client.BeforeRequest = func(module, action string, param map[string]interface{}) error { 44 | // ... 45 | } 46 | client.AfterRequest = func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) { 47 | // ... 48 | } 49 | 50 | // 查询账户以太坊余额 51 | balance, err := client.AccountBalance("0x281055afc982d96fab65b3a49cac8b878184cb16") 52 | if err != nil { 53 | panic(err) 54 | } 55 | // 余额以 *big.Int 的类型呈现,单位为 wei 56 | fmt.Println(balance.Int()) 57 | 58 | // 查询token余额 59 | tokenBalance, err := client.TokenBalance("contractAddress", "holderAddress") 60 | 61 | // 查询出入指定地址的ERC20转账列表 62 | transfers, err := client.ERC20Transfers("contractAddress", "address", startBlock, endBlock, page, offset) 63 | } 64 | ``` 65 | 66 | 客户端方法列表可在[GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api)查询。 67 | 68 | # Etherscan API Key 69 | 70 | API Key可以在[etherscan](https://etherscan.io/apis)申请。 71 | 72 | Etherscan的API服务是一个公开的社区无偿服务,请避免滥用。 73 | API的调用速率不能高于5次/秒,否则会遭到封禁。 74 | 75 | # 利益声明 76 | 77 | 我和Etherscan没有任何联系。我仅仅是觉得他们的服务很棒,而自己又恰好需要这样一个库。 :smile: 78 | 79 | # 许可证 80 | 81 | MIT 82 | 83 | 请自由享受开源,欢迎贡献开源。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **English** | [中文](https://github.com/nanmu42/etherscan-api/blob/master/README_ZH.md) 2 | 3 | # etherscan-api 4 | 5 | [![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api) 6 | [![CI status](https://github.com/nanmu42/etherscan-api/actions/workflows/ci.yaml/badge.svg)](https://github.com/nanmu42/etherscan-api/actions) 7 | [![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api) 9 | 10 | 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: 11 | 12 | # Usage 13 | 14 | ```bash 15 | go get github.com/nanmu42/etherscan-api 16 | ``` 17 | 18 | Create an API instance and off you go. :rocket: 19 | 20 | ```go 21 | import ( 22 | "github.com/nanmu42/etherscan-api" 23 | "fmt" 24 | ) 25 | 26 | func main() { 27 | // create a API client for specified ethereum net 28 | // there are many pre-defined network in package 29 | client := etherscan.New(etherscan.Mainnet, "[your API key]") 30 | 31 | // or, if you are working with etherscan-family API like BscScan 32 | // 33 | // client := etherscan.NewCustomized(etherscan.Customization{ 34 | // Timeout: 15 * time.Second, 35 | // Key: "You key here", 36 | // BaseURL: "https://api.bscscan.com/api?", 37 | // Verbose: false, 38 | // }) 39 | 40 | // (optional) add hooks, e.g. for rate limit 41 | client.BeforeRequest = func(module, action string, param map[string]interface{}) error { 42 | // ... 43 | } 44 | client.AfterRequest = func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) { 45 | // ... 46 | } 47 | 48 | // check account balance 49 | balance, err := client.AccountBalance("0x281055afc982d96fab65b3a49cac8b878184cb16") 50 | if err != nil { 51 | panic(err) 52 | } 53 | // balance in wei, in *big.Int type 54 | fmt.Println(balance.Int()) 55 | 56 | // check token balance 57 | tokenBalance, err := client.TokenBalance("contractAddress", "holderAddress") 58 | 59 | // check ERC20 transactions from/to a specified address 60 | transfers, err := client.ERC20Transfers("contractAddress", "address", startBlock, endBlock, page, offset) 61 | } 62 | ``` 63 | 64 | You may find full method list at [GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api). 65 | 66 | # Etherscan API Key 67 | 68 | You may apply for an API key on [etherscan](https://etherscan.io/apis). 69 | 70 | > 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). 71 | 72 | # Paperwork Things 73 | 74 | I am not from Etherscan and I just find their service really useful, so I implement this. :smile: 75 | 76 | # License 77 | 78 | Use of this work is governed by an MIT License. 79 | 80 | You may find a license copy in project root. 81 | -------------------------------------------------------------------------------- /account.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | // AccountBalance gets ether balance for a single address 11 | func (c *Client) AccountBalance(address string) (balance *BigInt, err error) { 12 | param := M{ 13 | "tag": "latest", 14 | "address": address, 15 | } 16 | balance = new(BigInt) 17 | err = c.call("account", "balance", param, balance) 18 | return 19 | } 20 | 21 | // MultiAccountBalance gets ether balance for multiple addresses in a single call 22 | func (c *Client) MultiAccountBalance(addresses ...string) (balances []AccountBalance, err error) { 23 | param := M{ 24 | "tag": "latest", 25 | "address": addresses, 26 | } 27 | balances = make([]AccountBalance, 0, len(addresses)) 28 | err = c.call("account", "balancemulti", param, &balances) 29 | return 30 | } 31 | 32 | // NormalTxByAddress gets a list of "normal" transactions by address 33 | // 34 | // startBlock and endBlock can be nil 35 | // 36 | // if desc is true, result will be sorted in blockNum descendant order. 37 | func (c *Client) NormalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []NormalTx, err error) { 38 | param := M{ 39 | "address": address, 40 | "page": page, 41 | "offset": offset, 42 | } 43 | compose(param, "startblock", startBlock) 44 | compose(param, "endblock", endBlock) 45 | if desc { 46 | param["sort"] = "desc" 47 | } else { 48 | param["sort"] = "asc" 49 | } 50 | 51 | err = c.call("account", "txlist", param, &txs) 52 | return 53 | } 54 | 55 | // InternalTxByAddress gets a list of "internal" transactions by address 56 | // 57 | // startBlock and endBlock can be nil 58 | // 59 | // if desc is true, result will be sorted in descendant order. 60 | func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []InternalTx, err error) { 61 | param := M{ 62 | "address": address, 63 | "page": page, 64 | "offset": offset, 65 | } 66 | compose(param, "startblock", startBlock) 67 | compose(param, "endblock", endBlock) 68 | if desc { 69 | param["sort"] = "desc" 70 | } else { 71 | param["sort"] = "asc" 72 | } 73 | 74 | err = c.call("account", "txlistinternal", param, &txs) 75 | return 76 | } 77 | 78 | // ERC20Transfers get a list of "erc20 - token transfer events" by 79 | // contract address and/or from/to address. 80 | // 81 | // leave undesired condition to nil. 82 | // 83 | // Note on a Etherscan bug: 84 | // Some ERC20 contract does not have valid decimals information in Etherscan. 85 | // When that happens, TokenName, TokenSymbol are empty strings, 86 | // and TokenDecimal is 0. 87 | // 88 | // More information can be found at: 89 | // https://github.com/nanmu42/etherscan-api/issues/8 90 | func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC20Transfer, err error) { 91 | param := M{ 92 | "page": page, 93 | "offset": offset, 94 | } 95 | compose(param, "contractaddress", contractAddress) 96 | compose(param, "address", address) 97 | compose(param, "startblock", startBlock) 98 | compose(param, "endblock", endBlock) 99 | 100 | if desc { 101 | param["sort"] = "desc" 102 | } else { 103 | param["sort"] = "asc" 104 | } 105 | 106 | err = c.call("account", "tokentx", param, &txs) 107 | return 108 | } 109 | 110 | // ERC721Transfers get a list of "erc721 - token transfer events" by 111 | // contract address and/or from/to address. 112 | // 113 | // leave undesired condition to nil. 114 | func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC721Transfer, err error) { 115 | param := M{ 116 | "page": page, 117 | "offset": offset, 118 | } 119 | compose(param, "contractaddress", contractAddress) 120 | compose(param, "address", address) 121 | compose(param, "startblock", startBlock) 122 | compose(param, "endblock", endBlock) 123 | 124 | if desc { 125 | param["sort"] = "desc" 126 | } else { 127 | param["sort"] = "asc" 128 | } 129 | 130 | err = c.call("account", "tokennfttx", param, &txs) 131 | return 132 | } 133 | 134 | // ERC1155Transfers get a list of "erc1155 - token transfer events" by 135 | // contract address and/or from/to address. 136 | // 137 | // leave undesired condition to nil. 138 | func (c *Client) ERC1155Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC1155Transfer, err error) { 139 | param := M{ 140 | "page": page, 141 | "offset": offset, 142 | } 143 | compose(param, "contractaddress", contractAddress) 144 | compose(param, "address", address) 145 | compose(param, "startblock", startBlock) 146 | compose(param, "endblock", endBlock) 147 | 148 | if desc { 149 | param["sort"] = "desc" 150 | } else { 151 | param["sort"] = "asc" 152 | } 153 | 154 | err = c.call("account", "token1155tx", param, &txs) 155 | return 156 | } 157 | 158 | // BlocksMinedByAddress gets list of blocks mined by address 159 | func (c *Client) BlocksMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { 160 | param := M{ 161 | "address": address, 162 | "blocktype": "blocks", 163 | "page": page, 164 | "offset": offset, 165 | } 166 | 167 | err = c.call("account", "getminedblocks", param, &mined) 168 | return 169 | } 170 | 171 | // UnclesMinedByAddress gets list of uncles mined by address 172 | func (c *Client) UnclesMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { 173 | param := M{ 174 | "address": address, 175 | "blocktype": "uncles", 176 | "page": page, 177 | "offset": offset, 178 | } 179 | 180 | err = c.call("account", "getminedblocks", param, &mined) 181 | return 182 | } 183 | 184 | // TokenBalance get erc20-token account balance of address for contractAddress 185 | func (c *Client) TokenBalance(contractAddress, address string) (balance *BigInt, err error) { 186 | param := M{ 187 | "contractaddress": contractAddress, 188 | "address": address, 189 | "tag": "latest", 190 | } 191 | 192 | err = c.call("account", "tokenbalance", param, &balance) 193 | return 194 | } 195 | -------------------------------------------------------------------------------- /account_e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "math/big" 14 | "testing" 15 | ) 16 | 17 | func TestClient_AccountBalance(t *testing.T) { 18 | balance, err := api.AccountBalance("0x0000000000000000000000000000000000000000") 19 | noError(t, err, "api.AccountBalance") 20 | 21 | if balance.Int().Cmp(big.NewInt(0)) != 1 { 22 | t.Fatalf("rich man is no longer rich") 23 | } 24 | } 25 | 26 | func TestClient_MultiAccountBalance(t *testing.T) { 27 | balances, err := api.MultiAccountBalance( 28 | "0x0000000000000000000000000000000000000000", 29 | "0x0000000000000000000000000000000000000001", 30 | "0x0000000000000000000000000000000000000002", 31 | "0x0000000000000000000000000000000000000003") 32 | noError(t, err, "api.MultiAccountBalance") 33 | 34 | for i, item := range balances { 35 | if item.Account == "" { 36 | t.Errorf("bound error on index %v", i) 37 | } 38 | if item.Balance.Int().Cmp(big.NewInt(0)) != 1 { 39 | t.Errorf("rich man %s at index %v is no longer rich.", item.Account, i) 40 | } 41 | } 42 | } 43 | 44 | func TestClient_NormalTxByAddress(t *testing.T) { 45 | const wantLen = 19 46 | 47 | var a, b = 54092, 79728 48 | txs, err := api.NormalTxByAddress("0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", &a, &b, 1, 500, false) 49 | noError(t, err, "api.NormalTxByAddress") 50 | 51 | //j, _ := json.MarshalIndent(txs, "", " ") 52 | //fmt.Printf("%s\n", j) 53 | 54 | if len(txs) != wantLen { 55 | t.Errorf("got txs length %v, want %v", len(txs), wantLen) 56 | } 57 | } 58 | 59 | func TestClient_InternalTxByAddress(t *testing.T) { 60 | const wantLen = 66 61 | 62 | var a, b = 0, 2702578 63 | txs, err := api.InternalTxByAddress("0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3", &a, &b, 1, 500, false) 64 | noError(t, err, "api.InternalTxByAddress") 65 | 66 | //j, _ := json.MarshalIndent(txs, "", " ") 67 | //fmt.Printf("%s\n", j) 68 | 69 | if len(txs) != wantLen { 70 | t.Errorf("got txs length %v, want %v", len(txs), wantLen) 71 | } 72 | } 73 | 74 | func TestClient_ERC20Transfers(t *testing.T) { 75 | const ( 76 | wantLen1 = 3 77 | wantLen2 = 458 78 | wantLen3 = 2 79 | ) 80 | 81 | var a, b = 3273004, 3328071 82 | var contract, address = "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a", "0x4e83362442b8d1bec281594cea3050c8eb01311c" 83 | txs, err := api.ERC20Transfers(&contract, &address, &a, &b, 1, 500, false) 84 | noError(t, err, "api.ERC20Transfers 1") 85 | 86 | //j, _ := json.MarshalIndent(txs, "", " ") 87 | //fmt.Printf("%s\n", j) 88 | 89 | if len(txs) != wantLen1 { 90 | t.Errorf("got txs length %v, want %v", len(txs), wantLen1) 91 | } 92 | 93 | txs, err = api.ERC20Transfers(nil, &address, nil, &b, 1, 500, false) 94 | noError(t, err, "api.ERC20Transfers 2 asc") 95 | if len(txs) != wantLen2 { 96 | t.Errorf("got txs length %v, want %v", len(txs), wantLen2) 97 | } 98 | 99 | txs, err = api.ERC20Transfers(nil, &address, nil, &b, 1, 500, true) 100 | noError(t, err, "api.ERC20Transfers 2 desc") 101 | 102 | if len(txs) != wantLen2 { 103 | t.Errorf("got txs length %v, want %v", len(txs), wantLen2) 104 | } 105 | 106 | // some ERC20 contract does not have valid decimals information in Etherscan, 107 | // which brings errors like `json: invalid use of ,string struct tag, trying to unmarshal "" into uint8` 108 | var specialContract = "0x5eac95ad5b287cf44e058dcf694419333b796123" 109 | var specialStartHeight = 6024142 110 | var specialEndHeight = 6485274 111 | txs, err = api.ERC20Transfers(&specialContract, nil, &specialStartHeight, &specialEndHeight, 1, 500, false) 112 | noError(t, err, "api.ERC20Transfers 2") 113 | if len(txs) != wantLen3 { 114 | t.Errorf("got txs length %v, want %v", len(txs), wantLen3) 115 | } 116 | } 117 | 118 | func TestClient_BlocksMinedByAddress(t *testing.T) { 119 | const wantLen = 10 120 | 121 | blocks, err := api.BlocksMinedByAddress("0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b", 1, wantLen) 122 | noError(t, err, "api.BlocksMinedByAddress") 123 | 124 | //j, _ := json.MarshalIndent(blocks, "", " ") 125 | //fmt.Printf("%s\n", j) 126 | 127 | if len(blocks) != wantLen { 128 | t.Errorf("got txs length %v, want %v", len(blocks), wantLen) 129 | } 130 | } 131 | 132 | func TestClient_UnclesMinedByAddress(t *testing.T) { 133 | const wantLen = 10 134 | 135 | blocks, err := api.UnclesMinedByAddress("0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b", 1, wantLen) 136 | noError(t, err, "api.UnclesMinedByAddress") 137 | 138 | //j, _ := json.MarshalIndent(blocks, "", " ") 139 | //fmt.Printf("%s\n", j) 140 | 141 | if len(blocks) != wantLen { 142 | t.Errorf("got txs length %v, want %v", len(blocks), wantLen) 143 | } 144 | } 145 | 146 | func TestClient_TokenBalance(t *testing.T) { 147 | balance, err := api.TokenBalance("0x57d90b64a1a57749b0f932f1a3395792e12e7055", "0xe04f27eb70e025b78871a2ad7eabe85e61212761") 148 | noError(t, err, "api.TokenBalance") 149 | 150 | if balance.Int().Cmp(big.NewInt(0)) != 1 { 151 | t.Errorf("api.TokenBalance not working, got balance %s", balance.Int().String()) 152 | } 153 | } 154 | 155 | func TestClient_ERC721Transfers(t *testing.T) { 156 | const ( 157 | wantLen = 351 158 | ) 159 | 160 | var a, b = 4708442, 9231168 161 | var contract, address = "0x06012c8cf97bead5deae237070f9587f8e7a266d", "0x6975be450864c02b4613023c2152ee0743572325" 162 | txs, err := api.ERC721Transfers(&contract, &address, &a, &b, 1, 500, true) 163 | noError(t, err, "api.ERC721Transfers") 164 | 165 | j, _ := json.MarshalIndent(txs, "", " ") 166 | fmt.Printf("%s\n", j) 167 | 168 | if len(txs) != wantLen { 169 | t.Errorf("got txs length %v, want %v", len(txs), wantLen) 170 | } 171 | } 172 | 173 | func TestClient_ERC1155Transfers(t *testing.T) { 174 | const ( 175 | wantLen = 1 176 | ) 177 | 178 | var a, b = 128135633, 1802672 179 | var contract, address = "0x3edf71a31b80Ff6a45Fdb0858eC54DE98dF047AA", "0x4b986EF20Bb83532911521FB4F6F5605122a0721" 180 | txs, err := api.ERC1155Transfers(&contract, &address, &b, &a, 0, 0, true) 181 | noError(t, err, "api.ERC721Transfers") 182 | 183 | j, _ := json.MarshalIndent(txs, "", " ") 184 | fmt.Printf("%s\n", j) 185 | 186 | if len(txs) != wantLen { 187 | t.Errorf("got txs length %v, want %v", len(txs), wantLen) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "bytes" 12 | "encoding/json" 13 | "fmt" 14 | "io" 15 | "net/http" 16 | "net/http/httputil" 17 | "net/url" 18 | "time" 19 | ) 20 | 21 | // Client etherscan API client 22 | // Clients are safe for concurrent use by multiple goroutines. 23 | type Client struct { 24 | coon *http.Client 25 | key string 26 | baseURL string 27 | 28 | // Verbose when true, talks a lot 29 | Verbose bool 30 | 31 | // BeforeRequest runs before every client request, in the same goroutine. 32 | // May be used in rate limit. 33 | // Request will be aborted, if BeforeRequest returns non-nil err. 34 | BeforeRequest func(module, action string, param map[string]interface{}) error 35 | 36 | // AfterRequest runs after every client request, even when there is an error. 37 | AfterRequest func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) 38 | } 39 | 40 | // New initialize a new etherscan API client 41 | // please use pre-defined network value 42 | func New(network Network, APIKey string) *Client { 43 | return NewCustomized(Customization{ 44 | Timeout: 30 * time.Second, 45 | Key: APIKey, 46 | BaseURL: fmt.Sprintf(`https://%s.etherscan.io/api?`, network.SubDomain()), 47 | }) 48 | } 49 | 50 | // Customization is used in NewCustomized() 51 | type Customization struct { 52 | // Timeout for API call 53 | Timeout time.Duration 54 | // API key applied from Etherscan 55 | Key string 56 | // Base URL like `https://api.etherscan.io/api?` 57 | BaseURL string 58 | // When true, talks a lot 59 | Verbose bool 60 | // HTTP Client to be used. Specifying this value will ignore the Timeout value set 61 | // Set your own timeout. 62 | Client *http.Client 63 | 64 | // BeforeRequest runs before every client request, in the same goroutine. 65 | // May be used in rate limit. 66 | // Request will be aborted, if BeforeRequest returns non-nil err. 67 | BeforeRequest func(module, action string, param map[string]interface{}) error 68 | 69 | // AfterRequest runs after every client request, even when there is an error. 70 | AfterRequest func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) 71 | } 72 | 73 | // NewCustomized initialize a customized API client, 74 | // useful when calling against etherscan-family API like BscScan. 75 | func NewCustomized(config Customization) *Client { 76 | var httpClient *http.Client 77 | if config.Client != nil { 78 | httpClient = config.Client 79 | } else { 80 | httpClient = &http.Client{ 81 | Timeout: config.Timeout, 82 | } 83 | } 84 | return &Client{ 85 | coon: httpClient, 86 | key: config.Key, 87 | baseURL: config.BaseURL, 88 | Verbose: config.Verbose, 89 | BeforeRequest: config.BeforeRequest, 90 | AfterRequest: config.AfterRequest, 91 | } 92 | } 93 | 94 | // call does almost all the dirty work. 95 | func (c *Client) call(module, action string, param map[string]interface{}, outcome interface{}) (err error) { 96 | // fire hooks if in need 97 | if c.BeforeRequest != nil { 98 | err = c.BeforeRequest(module, action, param) 99 | if err != nil { 100 | err = wrapErr(err, "beforeRequest") 101 | return 102 | } 103 | } 104 | if c.AfterRequest != nil { 105 | defer c.AfterRequest(module, action, param, outcome, err) 106 | } 107 | 108 | // recover if there shall be an panic 109 | defer func() { 110 | if r := recover(); r != nil { 111 | err = fmt.Errorf("[ouch! panic recovered] please report this with what you did and what you expected, panic detail: %v", r) 112 | } 113 | }() 114 | 115 | req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, param), http.NoBody) 116 | if err != nil { 117 | err = wrapErr(err, "http.NewRequest") 118 | return 119 | } 120 | req.Header.Set("User-Agent", "etherscan-api(Go)") 121 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 122 | 123 | if c.Verbose { 124 | var reqDump []byte 125 | reqDump, err = httputil.DumpRequestOut(req, false) 126 | if err != nil { 127 | err = wrapErr(err, "verbose mode req dump failed") 128 | return 129 | } 130 | 131 | fmt.Printf("\n%s\n", reqDump) 132 | 133 | defer func() { 134 | if err != nil { 135 | fmt.Printf("[Error] %v\n", err) 136 | } 137 | }() 138 | } 139 | 140 | res, err := c.coon.Do(req) 141 | if err != nil { 142 | err = wrapErr(err, "sending request") 143 | return 144 | } 145 | defer res.Body.Close() 146 | 147 | if c.Verbose { 148 | var resDump []byte 149 | resDump, err = httputil.DumpResponse(res, true) 150 | if err != nil { 151 | err = wrapErr(err, "verbose mode res dump failed") 152 | return 153 | } 154 | 155 | fmt.Printf("%s\n", resDump) 156 | } 157 | 158 | var content bytes.Buffer 159 | if _, err = io.Copy(&content, res.Body); err != nil { 160 | err = wrapErr(err, "reading response") 161 | return 162 | } 163 | 164 | if res.StatusCode != http.StatusOK { 165 | err = fmt.Errorf("response status %v %s, response body: %s", res.StatusCode, res.Status, content.String()) 166 | return 167 | } 168 | 169 | var envelope Envelope 170 | err = json.Unmarshal(content.Bytes(), &envelope) 171 | if err != nil { 172 | err = wrapErr(err, "json unmarshal envelope") 173 | return 174 | } 175 | if envelope.Status != 1 { 176 | err = fmt.Errorf("etherscan server: %s", envelope.Message) 177 | return 178 | } 179 | 180 | // workaround for missing tokenDecimal for some tokentx calls 181 | if action == "tokentx" { 182 | err = json.Unmarshal(bytes.Replace(envelope.Result, []byte(`"tokenDecimal":""`), []byte(`"tokenDecimal":"0"`), -1), outcome) 183 | } else { 184 | err = json.Unmarshal(envelope.Result, outcome) 185 | } 186 | if err != nil { 187 | err = wrapErr(err, "json unmarshal outcome") 188 | return 189 | } 190 | 191 | return 192 | } 193 | 194 | // craftURL returns desired URL via param provided 195 | func (c *Client) craftURL(module, action string, param map[string]interface{}) (URL string) { 196 | q := url.Values{ 197 | "module": []string{module}, 198 | "action": []string{action}, 199 | "apikey": []string{c.key}, 200 | } 201 | 202 | for k, v := range param { 203 | q[k] = extractValue(v) 204 | } 205 | 206 | URL = c.baseURL + q.Encode() 207 | return 208 | } 209 | -------------------------------------------------------------------------------- /contract_e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func TestClient_ContractABI(t *testing.T) { 15 | 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"}]` 16 | 17 | abi, err := api.ContractABI("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413") 18 | noError(t, err, "api.ContractABI") 19 | 20 | //fmt.Println(abi) 21 | 22 | if abi != answer { 23 | t.Fatalf("api.ContractABI not working, got %s, want %s", abi, answer) 24 | } 25 | } 26 | 27 | func TestClient_ContractSource(t *testing.T) { 28 | source, err := api.ContractSource("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413") 29 | noError(t, err, "api.ContractSource") 30 | 31 | if len(source) != 1 { 32 | t.Fatalf("api.ContractSource not working, got len %v, expect 1", len(source)) 33 | } 34 | s := source[0] 35 | if s.CompilerVersion != "v0.3.1-2016-04-12-3ad5e82" || 36 | s.Runs != 200 || 37 | s.OptimizationUsed != 1 { 38 | t.Fatalf("api.ContractSource not working, content match failed, got\n%+v", s) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018 LI Zhennan 3 | * 4 | * Use of this work is governed by a MIT License. 5 | * You may find a license copy in project root. 6 | */ 7 | 8 | package etherscan 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | // Envelope is the carrier of nearly every response 18 | type Envelope struct { 19 | // 1 for good, 0 for error 20 | Status int `json:"status,string"` 21 | // OK for good, other words when Status equals 0 22 | Message string `json:"message"` 23 | // where response lies 24 | Result json.RawMessage `json:"result"` 25 | } 26 | 27 | // AccountBalance account and its balance in pair 28 | type AccountBalance struct { 29 | Account string `json:"account"` 30 | Balance *BigInt `json:"balance"` 31 | } 32 | 33 | // NormalTx holds info from normal tx query 34 | type NormalTx struct { 35 | BlockNumber int `json:"blockNumber,string"` 36 | TimeStamp Time `json:"timeStamp"` 37 | Hash string `json:"hash"` 38 | Nonce int `json:"nonce,string"` 39 | BlockHash string `json:"blockHash"` 40 | TransactionIndex int `json:"transactionIndex,string"` 41 | From string `json:"from"` 42 | To string `json:"to"` 43 | Value *BigInt `json:"value"` 44 | Gas int `json:"gas,string"` 45 | GasPrice *BigInt `json:"gasPrice"` 46 | IsError int `json:"isError,string"` 47 | TxReceiptStatus string `json:"txreceipt_status"` 48 | Input string `json:"input"` 49 | ContractAddress string `json:"contractAddress"` 50 | CumulativeGasUsed int `json:"cumulativeGasUsed,string"` 51 | GasUsed int `json:"gasUsed,string"` 52 | Confirmations int `json:"confirmations,string"` 53 | FunctionName string `json:"functionName"` 54 | MethodId string `json:"methodId"` 55 | } 56 | 57 | // InternalTx holds info from internal tx query 58 | type InternalTx struct { 59 | BlockNumber int `json:"blockNumber,string"` 60 | TimeStamp Time `json:"timeStamp"` 61 | Hash string `json:"hash"` 62 | From string `json:"from"` 63 | To string `json:"to"` 64 | Value *BigInt `json:"value"` 65 | ContractAddress string `json:"contractAddress"` 66 | Input string `json:"input"` 67 | Type string `json:"type"` 68 | Gas int `json:"gas,string"` 69 | GasUsed int `json:"gasUsed,string"` 70 | TraceID string `json:"traceId"` 71 | IsError int `json:"isError,string"` 72 | ErrCode string `json:"errCode"` 73 | } 74 | 75 | // ERC20Transfer holds info from ERC20 token transfer event query 76 | type ERC20Transfer struct { 77 | BlockNumber int `json:"blockNumber,string"` 78 | TimeStamp Time `json:"timeStamp"` 79 | Hash string `json:"hash"` 80 | Nonce int `json:"nonce,string"` 81 | BlockHash string `json:"blockHash"` 82 | From string `json:"from"` 83 | ContractAddress string `json:"contractAddress"` 84 | To string `json:"to"` 85 | Value *BigInt `json:"value"` 86 | TokenName string `json:"tokenName"` 87 | TokenSymbol string `json:"tokenSymbol"` 88 | TokenDecimal uint8 `json:"tokenDecimal,string"` 89 | TransactionIndex int `json:"transactionIndex,string"` 90 | Gas int `json:"gas,string"` 91 | GasPrice *BigInt `json:"gasPrice"` 92 | GasUsed int `json:"gasUsed,string"` 93 | CumulativeGasUsed int `json:"cumulativeGasUsed,string"` 94 | Input string `json:"input"` 95 | Confirmations int `json:"confirmations,string"` 96 | } 97 | 98 | // ERC721Transfer holds info from ERC721 token transfer event query 99 | type ERC721Transfer struct { 100 | BlockNumber int `json:"blockNumber,string"` 101 | TimeStamp Time `json:"timeStamp"` 102 | Hash string `json:"hash"` 103 | Nonce int `json:"nonce,string"` 104 | BlockHash string `json:"blockHash"` 105 | From string `json:"from"` 106 | ContractAddress string `json:"contractAddress"` 107 | To string `json:"to"` 108 | TokenID *BigInt `json:"tokenID"` 109 | TokenName string `json:"tokenName"` 110 | TokenSymbol string `json:"tokenSymbol"` 111 | TokenDecimal uint8 `json:"tokenDecimal,string"` 112 | TransactionIndex int `json:"transactionIndex,string"` 113 | Gas int `json:"gas,string"` 114 | GasPrice *BigInt `json:"gasPrice"` 115 | GasUsed int `json:"gasUsed,string"` 116 | CumulativeGasUsed int `json:"cumulativeGasUsed,string"` 117 | Input string `json:"input"` 118 | Confirmations int `json:"confirmations,string"` 119 | } 120 | 121 | // ERC1155Transfer holds info from ERC1155 token transfer event query 122 | type ERC1155Transfer struct { 123 | BlockNumber int `json:"blockNumber,string"` 124 | TimeStamp Time `json:"timeStamp"` 125 | Hash string `json:"hash"` 126 | Nonce int `json:"nonce,string"` 127 | BlockHash string `json:"blockHash"` 128 | From string `json:"from"` 129 | ContractAddress string `json:"contractAddress"` 130 | To string `json:"to"` 131 | TokenID *BigInt `json:"tokenID"` 132 | TokenName string `json:"tokenName"` 133 | TokenSymbol string `json:"tokenSymbol"` 134 | TokenDecimal uint8 `json:"tokenDecimal,string"` 135 | TokenValue uint8 `json:"tokenValue,string"` 136 | TransactionIndex int `json:"transactionIndex,string"` 137 | Gas int `json:"gas,string"` 138 | GasPrice *BigInt `json:"gasPrice"` 139 | GasUsed int `json:"gasUsed,string"` 140 | CumulativeGasUsed int `json:"cumulativeGasUsed,string"` 141 | Input string `json:"input"` 142 | Confirmations int `json:"confirmations,string"` 143 | } 144 | 145 | // MinedBlock holds info from query for mined block by address 146 | type MinedBlock struct { 147 | BlockNumber int `json:"blockNumber,string"` 148 | TimeStamp Time `json:"timeStamp"` 149 | BlockReward *BigInt `json:"blockReward"` 150 | } 151 | 152 | // ContractSource holds info from query for contract source code 153 | type ContractSource struct { 154 | SourceCode string `json:"SourceCode"` 155 | ABI string `json:"ABI"` 156 | ContractName string `json:"ContractName"` 157 | CompilerVersion string `json:"CompilerVersion"` 158 | OptimizationUsed int `json:"OptimizationUsed,string"` 159 | Runs int `json:"Runs,string"` 160 | ConstructorArguments string `json:"ConstructorArguments"` 161 | EVMVersion string `json:"EVMVersion"` 162 | Library string `json:"Library"` 163 | LicenseType string `json:"LicenseType"` 164 | Proxy string `json:"Proxy"` 165 | Implementation string `json:"Implementation"` 166 | SwarmSource string `json:"SwarmSource"` 167 | } 168 | 169 | // ExecutionStatus holds info from query for transaction execution status 170 | type ExecutionStatus struct { 171 | // 0 = pass, 1 = error 172 | IsError int `json:"isError,string"` 173 | ErrDescription string `json:"errDescription"` 174 | } 175 | 176 | // BlockRewards holds info from query for block and uncle rewards 177 | type BlockRewards struct { 178 | BlockNumber int `json:"blockNumber,string"` 179 | TimeStamp Time `json:"timeStamp"` 180 | BlockMiner string `json:"blockMiner"` 181 | BlockReward *BigInt `json:"blockReward"` 182 | Uncles []struct { 183 | Miner string `json:"miner"` 184 | UnclePosition int `json:"unclePosition,string"` 185 | BlockReward *BigInt `json:"blockreward"` 186 | } `json:"uncles"` 187 | UncleInclusionReward *BigInt `json:"uncleInclusionReward"` 188 | } 189 | 190 | // LatestPrice holds info from query for latest ether price 191 | type LatestPrice struct { 192 | ETHBTC float64 `json:"ethbtc,string"` 193 | ETHBTCTimestamp Time `json:"ethbtc_timestamp"` 194 | ETHUSD float64 `json:"ethusd,string"` 195 | ETHUSDTimestamp Time `json:"ethusd_timestamp"` 196 | } 197 | 198 | type Log struct { 199 | Address string `json:"address"` 200 | Topics []string `json:"topics"` 201 | Data string `json:"data"` 202 | BlockNumber string `json:"blockNumber"` 203 | TransactionHash string `json:"transactionHash"` 204 | BlockHash string `json:"blockHash"` 205 | LogIndex string `json:"logIndex"` 206 | Removed bool `json:"removed"` 207 | } 208 | 209 | // GasPrices holds info for Gas Oracle queries 210 | // Gas Prices are returned in Gwei 211 | type GasPrices struct { 212 | LastBlock int 213 | SafeGasPrice float64 214 | ProposeGasPrice float64 215 | FastGasPrice float64 216 | SuggestBaseFeeInGwei float64 `json:"suggestBaseFee"` 217 | GasUsedRatio []float64 `json:"gasUsedRatio"` 218 | } 219 | 220 | func (gp *GasPrices) UnmarshalJSON(data []byte) error { 221 | _gp := struct { 222 | LastBlock string 223 | SafeGasPrice string 224 | ProposeGasPrice string 225 | FastGasPrice string 226 | SuggestBaseFeeInGwei string `json:"suggestBaseFee"` 227 | GasUsedRatio string `json:"gasUsedRatio"` 228 | }{} 229 | 230 | err := json.Unmarshal(data, &_gp) 231 | if err != nil { 232 | return err 233 | } 234 | 235 | gp.LastBlock, err = strconv.Atoi(_gp.LastBlock) 236 | if err != nil { 237 | return fmt.Errorf("Unable to convert LastBlock %s to int: %w", _gp.LastBlock, err) 238 | } 239 | 240 | gp.SafeGasPrice, err = strconv.ParseFloat(_gp.SafeGasPrice, 64) 241 | if err != nil { 242 | return fmt.Errorf("Unable to convert SafeGasPrice %s to float64: %w", _gp.SafeGasPrice, err) 243 | } 244 | 245 | gp.ProposeGasPrice, err = strconv.ParseFloat(_gp.ProposeGasPrice, 64) 246 | if err != nil { 247 | return fmt.Errorf("Unable to convert ProposeGasPrice %s to float64: %w", _gp.ProposeGasPrice, err) 248 | } 249 | 250 | gp.FastGasPrice, err = strconv.ParseFloat(_gp.FastGasPrice, 64) 251 | if err != nil { 252 | return fmt.Errorf("Unable to convert FastGasPrice %s to float64: %w", _gp.FastGasPrice, err) 253 | } 254 | 255 | gp.SuggestBaseFeeInGwei, err = strconv.ParseFloat(_gp.SuggestBaseFeeInGwei, 64) 256 | if err != nil { 257 | return fmt.Errorf("Unable to convert SuggestBaseFeeInGwei %s to float64: %w", _gp.SuggestBaseFeeInGwei, err) 258 | } 259 | 260 | gasRatios := strings.Split(_gp.GasUsedRatio, ",") 261 | gp.GasUsedRatio = make([]float64, len(gasRatios)) 262 | for i, gasRatio := range gasRatios { 263 | gp.GasUsedRatio[i], err = strconv.ParseFloat(gasRatio, 64) 264 | if err != nil { 265 | return fmt.Errorf("Unable to convert gasRatio %s to float64: %w", gasRatio, err) 266 | } 267 | } 268 | 269 | return nil 270 | } 271 | --------------------------------------------------------------------------------