加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
client.go 5.33 KB
一键复制 编辑 原始数据 按行查看 历史
/*
* Copyright (c) 2018 LI Zhennan
*
* Use of this work is governed by a MIT License.
* You may find a license copy in project root.
*/
package etherscan
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"time"
)
// Client etherscan API client
// Clients are safe for concurrent use by multiple goroutines.
type Client struct {
coon *http.Client
key string
baseURL string
// Verbose when true, talks a lot
Verbose bool
// BeforeRequest runs before every client request, in the same goroutine.
// May be used in rate limit.
// Request will be aborted, if BeforeRequest returns non-nil err.
BeforeRequest func(module, action string, param map[string]interface{}) error
// AfterRequest runs after every client request, even when there is an error.
AfterRequest func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error)
}
// New initialize a new etherscan API client
// please use pre-defined network value
func New(network Network, APIKey string) *Client {
return NewCustomized(Customization{
Timeout: 30 * time.Second,
Key: APIKey,
BaseURL: fmt.Sprintf(`https://%s.etherscan.io/api?`, network.SubDomain()),
})
}
// Customization is used in NewCustomized()
type Customization struct {
// Timeout for API call
Timeout time.Duration
// API key applied from Etherscan
Key string
// Base URL like `https://api.etherscan.io/api?`
BaseURL string
// When true, talks a lot
Verbose bool
// HTTP Client to be used. Specifying this value will ignore the Timeout value set
// Set your own timeout.
Client *http.Client
// BeforeRequest runs before every client request, in the same goroutine.
// May be used in rate limit.
// Request will be aborted, if BeforeRequest returns non-nil err.
BeforeRequest func(module, action string, param map[string]interface{}) error
// AfterRequest runs after every client request, even when there is an error.
AfterRequest func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error)
}
// NewCustomized initialize a customized API client,
// useful when calling against etherscan-family API like BscScan.
func NewCustomized(config Customization) *Client {
var httpClient *http.Client
if config.Client != nil {
httpClient = config.Client
} else {
httpClient = &http.Client{
Timeout: config.Timeout,
}
}
return &Client{
coon: httpClient,
key: config.Key,
baseURL: config.BaseURL,
Verbose: config.Verbose,
BeforeRequest: config.BeforeRequest,
AfterRequest: config.AfterRequest,
}
}
// call does almost all the dirty work.
func (c *Client) call(module, action string, param map[string]interface{}, outcome interface{}) (err error) {
// fire hooks if in need
if c.BeforeRequest != nil {
err = c.BeforeRequest(module, action, param)
if err != nil {
err = wrapErr(err, "beforeRequest")
return
}
}
if c.AfterRequest != nil {
defer c.AfterRequest(module, action, param, outcome, err)
}
// recover if there shall be an panic
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("[ouch! panic recovered] please report this with what you did and what you expected, panic detail: %v", r)
}
}()
req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, param), http.NoBody)
if err != nil {
err = wrapErr(err, "http.NewRequest")
return
}
req.Header.Set("User-Agent", "etherscan-api(Go)")
req.Header.Set("Content-Type", "application/json; charset=utf-8")
if c.Verbose {
var reqDump []byte
reqDump, err = httputil.DumpRequestOut(req, false)
if err != nil {
err = wrapErr(err, "verbose mode req dump failed")
return
}
fmt.Printf("\n%s\n", reqDump)
defer func() {
if err != nil {
fmt.Printf("[Error] %v\n", err)
}
}()
}
res, err := c.coon.Do(req)
if err != nil {
err = wrapErr(err, "sending request")
return
}
defer res.Body.Close()
if c.Verbose {
var resDump []byte
resDump, err = httputil.DumpResponse(res, true)
if err != nil {
err = wrapErr(err, "verbose mode res dump failed")
return
}
fmt.Printf("%s\n", resDump)
}
var content bytes.Buffer
if _, err = io.Copy(&content, res.Body); err != nil {
err = wrapErr(err, "reading response")
return
}
if res.StatusCode != http.StatusOK {
err = fmt.Errorf("response status %v %s, response body: %s", res.StatusCode, res.Status, content.String())
return
}
var envelope Envelope
err = json.Unmarshal(content.Bytes(), &envelope)
if err != nil {
err = wrapErr(err, "json unmarshal envelope")
return
}
if envelope.Status != 1 {
err = fmt.Errorf("etherscan server: %s", envelope.Message)
return
}
// workaround for missing tokenDecimal for some tokentx calls
if action == "tokentx" {
err = json.Unmarshal(bytes.Replace(envelope.Result, []byte(`"tokenDecimal":""`), []byte(`"tokenDecimal":"0"`), -1), outcome)
} else {
err = json.Unmarshal(envelope.Result, outcome)
}
if err != nil {
err = wrapErr(err, "json unmarshal outcome")
return
}
return
}
// craftURL returns desired URL via param provided
func (c *Client) craftURL(module, action string, param map[string]interface{}) (URL string) {
q := url.Values{
"module": []string{module},
"action": []string{action},
"apikey": []string{c.key},
}
for k, v := range param {
q[k] = extractValue(v)
}
URL = c.baseURL + q.Encode()
return
}
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化