diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..df6083f --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: "42 6 * * 0" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: "1.x" + + - name: Install Dependencies + run: | + go mod download github.com/davecgh/go-spew + go mod download github.com/google/go-cmp + + - name: Test + env: + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + run: go test -v -coverprofile=coverage.txt -covermode=count ./... + + - name: Codecov + uses: codecov/codecov-action@v2.1.0 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 297a5cc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: go - -env: - - GO111MODULE=on - -script: - - go test -v -coverprofile=coverage.txt -covermode=count ./... - -after_success: - - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/README.md b/README.md index 38b149a..56acd1f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,21 @@ +**English** | [中文](https://github.com/nanmu42/etherscan-api/blob/master/README_ZH.md) + # etherscan-api -[![Build Status](https://travis-ci.org/nanmu42/etherscan-api.svg?branch=master)](https://travis-ci.org/nanmu42/etherscan-api) -[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api) -[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api) [![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api) -[中文文档](https://github.com/nanmu42/etherscan-api/blob/master/README_ZH.md) +[![CI status](https://github.com/nanmu42/etherscan-api/actions/workflows/ci.yaml/badge.svg)](https://github.com/nanmu42/etherscan-api/actions) +[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api) +[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api) -Go bindings to the Etherscan.io API(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 API(and its families like BscScan), with nearly full implementation(accounts, transactions, tokens, contracts, blocks, stats), full network support(Mainnet, Ropsten, Kovan, Rinkby, Goerli, Tobalaba), and only depending on standard library. :wink: # Usage -Create a API instance and off you go. :rocket: +```bash +go get github.com/nanmu42/etherscan-api +``` + +Create an API instance and off you go. :rocket: ```go import ( @@ -70,6 +75,6 @@ I am not from Etherscan and I just find their service really useful, so I implem # License -Use of this work is governed by a MIT License. +Use of this work is governed by an MIT License. You may find a license copy in project root. diff --git a/README_ZH.md b/README_ZH.md index 72ace44..905a770 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -1,17 +1,22 @@ +[English](https://github.com/nanmu42/etherscan-api/blob/master/README.md) | **中文** + # etherscan-api -[![Build Status](https://travis-ci.org/nanmu42/etherscan-api.svg?branch=master)](https://travis-ci.org/nanmu42/etherscan-api) -[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api) -[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api) [![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api) -[English Readme](https://github.com/nanmu42/etherscan-api/blob/master/README.md) +[![CI status](https://github.com/nanmu42/etherscan-api/actions/workflows/ci.yaml/badge.svg)](https://github.com/nanmu42/etherscan-api/actions) +[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api) +[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api) -Etherscan.io的Golang实现, +Etherscan API的Golang客户端, 支持几乎所有功能(accounts, transactions, tokens, contracts, blocks, stats), 所有公共网络(Mainnet, Ropsten, Kovan, Rinkby, Goerli, Tobalaba)。 本项目只依赖于官方库。 :wink: -# Usage +# 使用方法 + +```bash +go get github.com/nanmu42/etherscan-api +``` 填入网络选项和API Key即可开始使用。 :rocket: diff --git a/block.go b/block.go index 9da0efa..aea4ea1 100644 --- a/block.go +++ b/block.go @@ -7,7 +7,10 @@ package etherscan -import "strconv" +import ( + "fmt" + "strconv" +) // BlockReward gets block and uncle rewards by block number func (c *Client) BlockReward(blockNum int) (rewards BlockRewards, err error) { @@ -19,21 +22,28 @@ func (c *Client) BlockReward(blockNum int) (rewards BlockRewards, err error) { return } -// BlockNumber gets closest block number by UNIX timestamp +// 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 result string + var blockNumberStr string + param := M{ - "timestamp": strconv.Itoa(int(timestamp)), + "timestamp": strconv.FormatInt(timestamp, 10), "closest": closest, } - err = c.call("block", "getblocknobytime", param, &result) + 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 } - blockNum, err := strconv.ParseInt(result, 10, 64) - blockNumber = int(blockNum) return } diff --git a/block_e2e_test.go b/block_e2e_test.go index e05ff05..c49a8ac 100644 --- a/block_e2e_test.go +++ b/block_e2e_test.go @@ -26,21 +26,21 @@ func TestClient_BlockReward(t *testing.T) { } func TestClient_BlockNumber(t *testing.T) { - //Note: All values taken from docs.etherscan.io/api-endpoints/blocks - const ans_before = 9251482 - const ans_after = 9251483 + // Note: All values taken from docs.etherscan.io/api-endpoints/blocks + const ansBefore = 9251482 + const ansAfter = 9251483 blockNumber, err := api.BlockNumber(1578638524, "before") noError(t, err, "api.BlockNumber") - if blockNumber != ans_before { - t.Errorf(`api.BlockNumber(1578638524, "before") not working, got %d, want %d`, blockNumber, ans_before) + if blockNumber != ansBefore { + t.Errorf(`api.BlockNumber(1578638524, "before") not working, got %d, want %d`, blockNumber, ansBefore) } blockNumber, err = api.BlockNumber(1578638524, "after") noError(t, err, "api.BlockNumber") - if blockNumber != ans_after { - t.Errorf(`api.BlockNumber(1578638524,"after") not working, got %d, want %d`, blockNumber, ans_after) + if blockNumber != ansAfter { + t.Errorf(`api.BlockNumber(1578638524,"after") not working, got %d, want %d`, blockNumber, ansAfter) } } diff --git a/client.go b/client.go index e69679a..7e11a0c 100644 --- a/client.go +++ b/client.go @@ -172,8 +172,14 @@ func (c *Client) call(module, action string, param map[string]interface{}, outco err = wrapErr(err, "json unmarshal envelope") return } - if envelope.Status != 1 { - err = fmt.Errorf("etherscan server: %s", envelope.Message) + + if module != "proxy" { //proxy calls use jsonRPC + if envelope.Status != 1 { + err = fmt.Errorf("etherscan server: %s", envelope.Message) + return + } + } else if envelope.Error != nil { + err = fmt.Errorf("etherscan server: %s", envelope.Error) return } diff --git a/go.mod b/go.mod index 0a30134..71da4ac 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module github.com/nanmu42/etherscan-api go 1.13 + +require ( + github.com/davecgh/go-spew v1.1.1 + github.com/google/go-cmp v0.5.6 +) diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..ab87dea --- /dev/null +++ b/proxy.go @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2021 LI Zhennan + * + * Use of this work is governed by a MIT License. + * You may find a license copy in project root. + */ + +package etherscan + +// GetTransactionReceipt gets ETH Transaction Receipt for a given transaction hash +func (c *Client) GetTransactionReceipt(txHash string) (receipt TxReceipt, err error) { + param := M{ + "txhash": txHash, + } + + err = c.call("proxy", "eth_getTransactionReceipt", param, &receipt) + return +} diff --git a/proxy_e2e_test.go b/proxy_e2e_test.go new file mode 100644 index 0000000..96eb36d --- /dev/null +++ b/proxy_e2e_test.go @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 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" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" +) + +func TestClient_GetTransactionReceipt(t *testing.T) { + var expectedReceipt TxReceipt + //Response grabbed from random transaction on Etherscan.io. This is a good test since it has a couple of embedded Logs + cannedResp := []byte(`{"blockHash":"0x9d4d4c6bab3dcd3b0b3a65f3b1880a9414990090da9315183b81a1b9a309b79e","blockNumber":"0xccc210","contractAddress":null,"cumulativeGasUsed":"0x35365c","effectiveGasPrice":"0x26d7dd3724","from":"0xf878c632317b5c58c88e8b7e8de6aa17bb3dfe83","gasUsed":"0x200ea","logs":[{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","topics":["0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c","0x00000000000000000000000003f7724180aa6b939894b5ca4314783b0b36b329"],"data":"0x000000000000000000000000000000000000000000000000008e1bc9bf040000","blockNumber":"0xccc210","transactionHash":"0x162681c37d4e5db904b85a9953cbbe213e9631d5c928ad6b772f696aac9f67f5","transactionIndex":"0x12","blockHash":"0x9d4d4c6bab3dcd3b0b3a65f3b1880a9414990090da9315183b81a1b9a309b79e","logIndex":"0x37","removed":false},{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x00000000000000000000000003f7724180aa6b939894b5ca4314783b0b36b329","0x000000000000000000000000d0dcb7a4f8cfcdb29364d621ca5d997b7eddbc46"],"data":"0x000000000000000000000000000000000000000000000000008e1bc9bf040000","blockNumber":"0xccc210","transactionHash":"0x162681c37d4e5db904b85a9953cbbe213e9631d5c928ad6b772f696aac9f67f5","transactionIndex":"0x12","blockHash":"0x9d4d4c6bab3dcd3b0b3a65f3b1880a9414990090da9315183b81a1b9a309b79e","logIndex":"0x38","removed":false},{"address":"0x8b3192f5eebd8579568a2ed41e6feb402f93f73f","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000d0dcb7a4f8cfcdb29364d621ca5d997b7eddbc46","0x000000000000000000000000f878c632317b5c58c88e8b7e8de6aa17bb3dfe83"],"data":"0x000000000000000000000000000000000000000000000000932a9ca104b8bd31","blockNumber":"0xccc210","transactionHash":"0x162681c37d4e5db904b85a9953cbbe213e9631d5c928ad6b772f696aac9f67f5","transactionIndex":"0x12","blockHash":"0x9d4d4c6bab3dcd3b0b3a65f3b1880a9414990090da9315183b81a1b9a309b79e","logIndex":"0x39","removed":false},{"address":"0xd0dcb7a4f8cfcdb29364d621ca5d997b7eddbc46","topics":["0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1"],"data":"0x0000000000000000000000000000000000000000000000f2ce932331c81806b2000000000000000000000000000000000000000000000000e5a3780b0117856d","blockNumber":"0xccc210","transactionHash":"0x162681c37d4e5db904b85a9953cbbe213e9631d5c928ad6b772f696aac9f67f5","transactionIndex":"0x12","blockHash":"0x9d4d4c6bab3dcd3b0b3a65f3b1880a9414990090da9315183b81a1b9a309b79e","logIndex":"0x3a","removed":false},{"address":"0xd0dcb7a4f8cfcdb29364d621ca5d997b7eddbc46","topics":["0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822","0x00000000000000000000000003f7724180aa6b939894b5ca4314783b0b36b329","0x000000000000000000000000f878c632317b5c58c88e8b7e8de6aa17bb3dfe83"],"data":"0x000000000000000000000000000000000000000000000000000008d0e801213b000000000000000000000000000000000000000000000000008e1bc9bf040000000000000000000000000000000000000000000000000000962b7b410a0ae0650000000000000000000000000000000000000000000000000000000000000000","blockNumber":"0xccc210","transactionHash":"0x162681c37d4e5db904b85a9953cbbe213e9631d5c928ad6b772f696aac9f67f5","transactionIndex":"0x12","blockHash":"0x9d4d4c6bab3dcd3b0b3a65f3b1880a9414990090da9315183b81a1b9a309b79e","logIndex":"0x3b","removed":false}],"logsBloom":"0x00200000000000000000000080000004000000000800000000000000000040000000000000000000000000000000000003000000080001000000000000000000000000000000000000000008000000200000000000000000000020008000000000000000000000000000000000000000000210000000000000000010000000000000000000000000100000000000000400000001000000080000004000000000000000000000000000000000000200000000001000000000000000000000000100000002001000000000000000000000000000000000001000000000000000000000200000000080000000000000000000000000000000400000000000000000","status":"0x1","to":"0x03f7724180aa6b939894b5ca4314783b0b36b329","transactionHash":"0x162681c37d4e5db904b85a9953cbbe213e9631d5c928ad6b772f696aac9f67f5","transactionIndex":"0x12","type":"0x2"}`) + + json.Unmarshal(cannedResp, &expectedReceipt) + + actualReceipt, err := api.GetTransactionReceipt("0x162681c37d4e5db904b85a9953cbbe213e9631d5c928ad6b772f696aac9f67f5") + noError(t, err, "api.GetTransactionReceipt") + + if !cmp.Equal(expectedReceipt, actualReceipt) { + t.Errorf(`api.GetTransactionReceipt("0x162681c37d4e5db904b85a9953cbbe213e9631d5c928ad6b772f696aac9f67f5" not working, got %s, want %s`, spew.Sdump(actualReceipt), spew.Sdump(expectedReceipt)) + } +} diff --git a/response.go b/response.go index b1338d3..61fa08b 100644 --- a/response.go +++ b/response.go @@ -14,7 +14,8 @@ 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"` + Message string `json:"message"` + Error json.RawMessage `json:"error"` //Used for JSON RPC calls (i.e. when module==proxy) // where response lies Result json.RawMessage `json:"result"` } @@ -136,3 +137,31 @@ type LatestPrice struct { ETHUSD float64 `json:"ethusd,string"` ETHUSDTimestamp Time `json:"ethusd_timestamp"` } + +type Log struct { + Address string `json:"address"` + Topics []string `json:"topics"` + Data string `json:"data"` + BlockNumber string `json:"blockNumber"` + TransactionHash string `json:"transactionHash"` + BlockHash string `json:"blockHash"` + LogIndex string `json:"logIndex"` + Removed bool `json:"removed"` +} + +type TxReceipt struct { + BlockHash string `json:"blockHash"` + BlockNumber string `json:"blockNumber"` + ContractAddress string `json:"contractAddress"` + CumulativeGasUsed string `json:"cumulativeGasUsed"` + EffectiveGasPrice string `json:"effectiveGasPrice"` + From string `json:"from"` + GasUsed string `json:"gasUsed"` + Logs []Log `json:"logs"` + LogsBloom string `json:"logsBloom"` + Status string `json:"status"` + To string `json:"to"` + TransactionHash string `json:"transactionHash"` + TransactionIndex string `json:"transactionIndex"` + Type string `json:"type"` +}