/* * 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 }