├── README.md ├── eth.go ├── main.go └── phrase.go /README.md: -------------------------------------------------------------------------------- 1 | ## 12-Word Recovery Phrase Activity Scanner 2 | 3 | This simple tool takes a 12-word phrase, such as those used by the BRD cryptocurrency wallet, and scans for wallet activity. It will search for both BIP32 and BIP44 wallets, however 24-word phrases are not currently supported. 4 | 5 | It will list out the following: 6 | 7 | 1. If the phrase has ever been used for for BTC, BCH, or ETH. 8 | 1. If a remaining balance exists for BTC, BCH, or ETH. 9 | 1. If the Ethereum address has any remaining ERC20 token balances (if not, nothing will be displayed) 10 | 11 | **Note 1:** This tool is also capable of scanning for Testnet BTC and ETH, however this feature is currently disabled to API unavailability. If you want to try it out, download the source code and change "showTestnet" to "true" at the top of main.go. 12 | 13 | **Note 2:** BIP44 support only looks at account 0. 14 | 15 | ### Usage 16 | 17 | Either build the project with Go or download the binary, then... 18 | 19 | **Single phrase:** Run the binary followed by a 12-word phrase: `scan-phrase one two [...] twelve` 20 | 21 | **Multiple phrases:** List up multiple phrases, one per line, in a file titled `phrase.txt`. In the same directory as this file, run the binary with no arguments: `phrase-scan` 22 | 23 | **Single currency:** By default this tool will search for BTC and ETH. If you want to search for just one of these, use the `-coin` flag followed by the currency you want to search for. For example, `scan-phrase -coin=btc` will only look for bitcoin. This will work with either single- or multi-phrase mode, but in the case of single-phrase mode, the flag must always come before the phrase (e.g. `scan-phrase -coin=btc [12 word phrase]`). You can also search BCH balances by specifying `bch` as the coin to search for, but this functionality was dropped from default operation due to a lack of high quality APIs. 24 | 25 | If you downloaded the binary, remember to give the file execure permissions with `chmod` and add `./` before the commands listed above. 26 | 27 | ### Block explorers 28 | 29 | This tool uses the following block explorers: 30 | 31 | * Blockchain.info: Bitcoin, Bitcoin Testnet 32 | * Etherscan.io: Ethereum, Ethereum Testnet (Ropsten - not implemented for this tool yet) 33 | * btc.com: Bitcoin Cash (Mainnet only) 34 | -------------------------------------------------------------------------------- /eth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/ethereum/go-ethereum/crypto" 11 | ) 12 | 13 | // LookupETH looks up the details for this phrase's ethereum address. Returns an array of Addresses to be consistent 14 | // with other methods, but only populates the first item in the array. 15 | func (p Phrase) LookupETH(isTestnet bool) (addresses []*Address, err error) { 16 | 17 | // Get our ethereum xpub 18 | ethxpub, err := deriveHDKey(p.xprv, 44, 60, 0, 0, 0) 19 | if err != nil { 20 | return 21 | } 22 | 23 | // Get our ethereum key 24 | ethkey, err := ethxpub.ECPubKey() 25 | if err != nil { 26 | fmt.Println(err) 27 | return 28 | } 29 | 30 | // Generate our ethereum address for the request 31 | pubBytes := ethkey.SerializeUncompressed() 32 | ethadd := common.BytesToAddress(crypto.Keccak256(pubBytes[1:])[12:]) 33 | 34 | addresses = []*Address{ 35 | {Address: ethadd.String(), IsTest: isTestnet}, 36 | } 37 | 38 | domain := "api" 39 | if isTestnet { 40 | domain = "api-ropsten" 41 | } 42 | 43 | // Lookup activity 44 | var ethact struct { 45 | Status string `json:"status"` 46 | Message string `json:"message"` 47 | Result []struct { 48 | Hash string `json:"hash"` 49 | } `json:"result"` 50 | } 51 | 52 | url := "https://" + domain + ".etherscan.io/api?module=account&action=txlist&address=" + addresses[0].Address 53 | err = callAPI(url, ðact, etherscanRate) 54 | if err != nil { 55 | return 56 | } 57 | 58 | if ethact.Status != "1" && ethact.Message != "No transactions found" { 59 | err = errors.New("etherscan error: " + ethact.Message) 60 | return 61 | } 62 | 63 | addresses[0].TxCount = len(ethact.Result) 64 | 65 | if len(ethact.Result) > 0 { 66 | // Ethereumn balance lookup (only if we've seen activity) 67 | var ethbal struct { 68 | Status string `json:"status"` 69 | Message string `json:"message"` 70 | Result string `json:"result"` 71 | } 72 | 73 | url = "https://" + domain + ".etherscan.io/api?module=account&action=balance&address=" + addresses[0].Address 74 | err = callAPI(url, ðbal, etherscanRate) 75 | if err != nil { 76 | return 77 | } 78 | 79 | if ethbal.Status != "1" { 80 | fmt.Printf("%#v\n", ethbal) 81 | err = errors.New("etherscan error: " + ethbal.Message) 82 | return 83 | } 84 | 85 | addresses[0].Balance, err = snipEth(ethbal.Result, 18) 86 | if err != nil { 87 | return 88 | } 89 | } 90 | 91 | // ERC20 token lookup 92 | var erc20bal struct { 93 | Status string `json:"status"` 94 | Message string `json:"message"` 95 | Result []struct { 96 | Address string `json:"contractAddress"` 97 | To string `json:"to"` 98 | Value string `json:"value"` 99 | Name string `json:"tokenName"` 100 | Ticker string `json:"tokenSymbol"` 101 | Decimal string `json:"tokenDecimal"` 102 | Hash string `json:"hash"` 103 | } `json:"result"` 104 | } 105 | 106 | err = callAPI("https://"+domain+".etherscan.io/api?module=account&action=tokentx&address="+addresses[0].Address, &erc20bal, etherscanRate) 107 | if err != nil { 108 | return 109 | } 110 | 111 | if erc20bal.Status != "1" { 112 | return 113 | } 114 | 115 | if len(erc20bal.Result) >= 10000 { 116 | fmt.Println("WARN: Eth address has at least 10k transactions--any more than that will be omitted.") 117 | } 118 | 119 | txlist := make(map[string]int) 120 | for _, v := range erc20bal.Result { 121 | if v.Ticker == "" { 122 | continue 123 | } 124 | 125 | idx, e := txlist[v.Ticker] 126 | 127 | if !e { 128 | // Create a new entry if we haven't seen it yet 129 | txlist[v.Ticker] = len(addresses[0].Tokens) 130 | addresses[0].Tokens = append(addresses[0].Tokens, Token{ 131 | Name: v.Name, 132 | Ticker: v.Ticker, 133 | Address: v.Address, 134 | }) 135 | } 136 | 137 | // Convert number of decimal places this token uses 138 | var dec int 139 | dec, err = strconv.Atoi(v.Decimal) 140 | if err != nil { 141 | return 142 | } 143 | 144 | // Parse the value and snip it down based on decimal places 145 | var val float64 146 | val, err = snipEth(v.Value, dec) 147 | if err != nil { 148 | return 149 | } 150 | 151 | // Etherscan returns addresses all lower case... 152 | if v.To != strings.ToLower(addresses[0].Address) { 153 | // This must be a send, not a receive 154 | val *= -1 155 | } 156 | 157 | nicebal := float64(val) 158 | if dec > 0 { 159 | nicebal = float64(val / float64(10^dec)) 160 | } 161 | 162 | addresses[0].Tokens[idx].Balance += nicebal 163 | addresses[0].Tokens[idx].TxCount++ 164 | } 165 | return 166 | } 167 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | bip39 "github.com/tyler-smith/go-bip39" 12 | ) 13 | 14 | var showTestnet = false 15 | var showBTC = false 16 | var showBCH = false 17 | var showETH = false 18 | 19 | var gfx = map[string]string{ 20 | "start": "╒═════════════════════════════════════╕\n", 21 | "phrase1": "╒═══╧═════════════════════════════════╕\n", 22 | "phrase2": "│ %s... │\n", // Show first 28 characters of phrase 23 | "phrase3": "╘═══╤═════════════════════════════════╛\n", 24 | "crypto": " ┝ %s : %s\n", 25 | "subcrypto1": " │ ┝ %s : %s\n", 26 | "subcrypto2": " │ ┕ %s : %s\n", 27 | "end": " ╘═══════════════════════════☐ Done!\n", 28 | } 29 | 30 | // Lastcall is used as a timestamp for the last api call (for rate limiting) 31 | var lastcall = time.Now() 32 | var etherscanRate = int64(5000) 33 | var btcRate = int64(200) 34 | 35 | func main() { 36 | 37 | // Check our command line flags 38 | cflag := flag.String("coin", "default", "which coins to search for (btc, bch, eth, all)") 39 | flag.Parse() 40 | 41 | switch strings.ToLower(*cflag) { 42 | case "btc": 43 | showBTC = true 44 | case "bch": 45 | showBCH = true 46 | case "eth": 47 | showETH = true 48 | case "default": 49 | showBTC = true 50 | showETH = true 51 | default: 52 | fmt.Println("Invalid -coin flag.") 53 | return 54 | } 55 | 56 | var phrases []string 57 | 58 | // If a phrase is provided, load that in as the only one. Otherwise, load up the "phrases.txt" file. 59 | // All phrases will later be validated by the phrase library 60 | if len(flag.Args()) == 0 { 61 | if _, e := os.Stat("phrases.txt"); os.IsNotExist(e) { 62 | fmt.Println("Please create a phrases.txt file or call this command followed by a valid 12-word phrase.") 63 | return 64 | } 65 | 66 | data, err := ioutil.ReadFile("phrases.txt") 67 | if err != nil { 68 | fmt.Println("data load error: ", err) 69 | } 70 | 71 | splits := strings.Split(string(data), "\n") 72 | for _, v := range splits { 73 | // Skip any invalid phrases so we can number them accurately in the UI 74 | if bip39.IsMnemonicValid(v) { 75 | phrases = append(phrases, v) 76 | } 77 | } 78 | 79 | } else if len(flag.Args()) == 12 { 80 | var phrase string 81 | for i, v := range flag.Args() { 82 | phrase += v 83 | if i < 11 { 84 | phrase += " " 85 | } 86 | } 87 | phrases = []string{phrase} 88 | } else { 89 | fmt.Println("Please create a phrases.txt file or call this command followed by a valid 12-word phrase.") 90 | return 91 | } 92 | 93 | fmt.Println() 94 | 95 | if showETH { 96 | fmt.Printf("Ethereum balances will take a long time to look up (sorry).\n\n") 97 | } 98 | 99 | // Process each phrase 100 | for i, v := range phrases { 101 | 102 | // Prepare phrase 103 | p, err := NewPhrase(v) 104 | if err != nil { 105 | fmt.Println("phrase error: ", err) 106 | return 107 | } 108 | 109 | // Display phrase header 110 | if i == 0 { 111 | fmt.Printf(gfx["start"]) 112 | } else { 113 | fmt.Printf(gfx["phrase1"]) 114 | } 115 | fmt.Printf(gfx["phrase2"], v[:28]) 116 | fmt.Printf(gfx["phrase3"]) 117 | 118 | // Print each currency 119 | if showBTC { 120 | p.printBTCBalances("BTC", []BTCFormat{{Coin: "btc32", Type: "BIP32"}, {Coin: "btc44", Type: "BIP44"}}) 121 | if showTestnet { 122 | p.printBTCBalances("TBT", []BTCFormat{{Coin: "tbt32", Type: "BIP32"}, {Coin: "tbt44", Type: "BIP44"}}) 123 | } 124 | } 125 | 126 | if showBCH { 127 | p.printBTCBalances("BCH", []BTCFormat{ 128 | {Coin: "bch32", Type: "BIP32"}, 129 | {Coin: "bch440", Type: "BIP44-coin0"}, 130 | {Coin: "bch44145", Type: "BIP44-coin145"}, 131 | }) 132 | } 133 | 134 | if showETH { 135 | p.printETHBalances("ETH", false) 136 | if showTestnet { 137 | p.printETHBalances("TET", true) 138 | } 139 | } 140 | } 141 | 142 | // End 143 | fmt.Printf(gfx["end"]) 144 | fmt.Println() 145 | } 146 | 147 | // BTCFormat defines the specific flavor of bitcoin 148 | type BTCFormat struct { 149 | Coin string 150 | Type string 151 | isUsed bool 152 | balance float64 153 | } 154 | 155 | func (p Phrase) printBTCBalances(label string, coins []BTCFormat) { 156 | numused := 0 157 | for i, v := range coins { 158 | var err error 159 | coins[i].balance, coins[i].isUsed, _, err = p.LookupBTCBal(v.Coin) 160 | if err != nil { 161 | fmt.Printf(gfx["crypto"], "There was a problem with "+v.Coin, err) 162 | } 163 | 164 | if coins[i].isUsed { 165 | numused++ 166 | } 167 | } 168 | 169 | var output string 170 | 171 | if numused == 0 { 172 | output = "Unused" 173 | } else { 174 | output = "** Used ** Balance: " 175 | done := 0 176 | for _, v := range coins { 177 | if v.isUsed { 178 | output += fmt.Sprintf("%.5f", v.balance) + label + " (" + v.Type + ")" 179 | done++ 180 | if done < numused { 181 | output += ", " 182 | } 183 | } 184 | } 185 | } 186 | 187 | fmt.Printf(gfx["crypto"], label, output) 188 | } 189 | 190 | func (p Phrase) printETHBalances(label string, testnet bool) { 191 | 192 | addslice, err := p.LookupETH(testnet) 193 | if err != nil { 194 | fmt.Printf(gfx["crypto"], "There was a problem with "+label, err) 195 | return 196 | } 197 | 198 | add := addslice[0] 199 | 200 | var output string 201 | if add.TxCount == 0 { 202 | output = "Unused" 203 | } else { 204 | output = fmt.Sprintf("** Used ** Balance: %.5f%s", add.Balance, label) 205 | } 206 | 207 | fmt.Printf(gfx["crypto"], label, output) 208 | 209 | //If we had any tokens... 210 | if len(add.Tokens) == 0 { 211 | return 212 | } 213 | 214 | for i, v := range add.Tokens { 215 | output = fmt.Sprintf("%.5f%s", v.Balance, v.Ticker) 216 | 217 | if i < len(add.Tokens)-1 { 218 | fmt.Printf(gfx["subcrypto1"], v.Name, output) 219 | } else { 220 | fmt.Printf(gfx["subcrypto2"], v.Name, output) 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /phrase.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //TODO: add in checking for bip44 btc 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "math" 11 | "net/http" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "github.com/btcsuite/btcd/chaincfg" 17 | "github.com/btcsuite/btcutil/hdkeychain" 18 | bip32 "github.com/tyler-smith/go-bip32" 19 | bip39 "github.com/tyler-smith/go-bip39" 20 | ) 21 | 22 | // Phrase represents a phrase we are examining 23 | type Phrase struct { 24 | master string 25 | xprv *hdkeychain.ExtendedKey 26 | } 27 | 28 | // Address represents a crypto address generated from a phrase, including details about how/if it has been used 29 | type Address struct { 30 | Address string 31 | TxCount int 32 | Balance float64 33 | IsTest bool 34 | Tokens []Token 35 | } 36 | 37 | // Token represents a balance of ERC20 tokens tied to an Ethereum address. Name is the ticker of the token, and 38 | // Address is the contract address for the token. 39 | type Token struct { 40 | Name string 41 | Ticker string 42 | Address string 43 | Balance float64 44 | TxCount int 45 | } 46 | 47 | // NewPhrase returns a phrase object with the master key generated (and the eth address, since that is static) 48 | func NewPhrase(phrase string) (p *Phrase, err error) { 49 | p = new(Phrase) 50 | 51 | // Populate our BIP39 seed 52 | seed := bip39.NewSeed(phrase, "") 53 | masterKey, _ := bip32.NewMasterKey(seed) 54 | p.master = masterKey.String() 55 | 56 | // Get our master xprv 57 | // Path: m 58 | p.xprv, err = hdkeychain.NewKeyFromString(p.master) 59 | 60 | return 61 | } 62 | 63 | // getBitcoinAddress by specifying the chain number (0 for normal, 1 for change), child number (address number), 64 | // and whether testnet or not. Count specifies how many addresses to return. 65 | func (p Phrase) getBitcoinAddresses(purpose uint32, coin uint32, chainNo uint32, childNo uint32, count int, testnet bool) (addresses []*Address) { 66 | if count < 1 { 67 | count = 1 68 | } 69 | 70 | if count > 100 { 71 | count = 100 72 | } 73 | 74 | for i := 0; i < count; i++ { 75 | // Path: m/0H/[chain]/[child] (e.g. m/0H/0/0) 76 | child, err := deriveHDKey(p.xprv, purpose, coin, 0, chainNo, childNo) 77 | if err != nil { 78 | fmt.Println("Uh-oh! HD derivation error:", err) 79 | return 80 | } 81 | 82 | // Generate address based on testnet or mainnet 83 | params := &chaincfg.MainNetParams 84 | 85 | if testnet { 86 | params = &chaincfg.TestNet3Params 87 | } 88 | 89 | pkh, err := child.Address(params) 90 | if err != nil { 91 | fmt.Println("Uh-oh! HD derivation error:", err) 92 | return 93 | } 94 | addresses = append(addresses, &Address{Address: pkh.EncodeAddress()}) 95 | 96 | childNo++ 97 | } 98 | 99 | return 100 | } 101 | 102 | // LookupBTC takes a slice of addresses and fills in the details 103 | func (p Phrase) LookupBTC(addresses []*Address, isTestnet bool) (err error) { 104 | 105 | domain := "" 106 | if isTestnet { 107 | domain = "testnet." 108 | } 109 | 110 | var addylist string 111 | 112 | for i, v := range addresses { 113 | if isTestnet { 114 | addresses[i].IsTest = true 115 | } 116 | addylist += v.Address + "," 117 | } 118 | 119 | var BCi map[string]struct { 120 | Balance int64 `json:"final_balance"` 121 | TxCount int `json:"n_tx"` 122 | Received int64 `json:"total_received"` 123 | } 124 | 125 | err = callAPI("https://"+domain+"blockchain.info/balance?active="+addylist, &BCi, btcRate) 126 | if err != nil { 127 | return 128 | } 129 | 130 | for i, v := range addresses { 131 | addresses[i].TxCount = BCi[v.Address].TxCount 132 | addresses[i].Balance = float64(float64(BCi[v.Address].Balance) / 100000000) 133 | } 134 | return 135 | } 136 | 137 | // LookupBCH takes a slice of addresses and fills in the details 138 | func (p Phrase) LookupBCH(addresses []*Address) (err error) { 139 | 140 | // NOTE: the API no longer supports looking up multiple addresses at once, so we will have to do them one at a time 141 | 142 | for i, v := range addresses { 143 | var BTCcom struct { 144 | Error int `json:"err_no"` 145 | Data struct { 146 | Address string `json:"address"` 147 | Balance int64 `json:"balance"` 148 | TxCount int `json:"tx_count"` 149 | Received int64 `json:"received"` 150 | } `json:"data"` 151 | } 152 | 153 | url := "https://bch-chain.api.btc.com/v3/address/" + v.Address 154 | err = callAPI(url, &BTCcom, btcRate) 155 | if err != nil { 156 | return 157 | } 158 | 159 | if BTCcom.Error != 0 { 160 | err = errors.New("BTC.com error: " + strconv.Itoa(BTCcom.Error)) 161 | return 162 | } 163 | 164 | // We'll stop at the first unused address 165 | if BTCcom.Data.TxCount == 0 { 166 | return 167 | } 168 | 169 | addresses[i].TxCount = BTCcom.Data.TxCount 170 | addresses[i].Balance = float64(float64(BTCcom.Data.Balance) / 100000000) 171 | } 172 | return 173 | } 174 | 175 | // LookupBTCBal follows the entire btc/bch chain and finds out the remaining balance for the entire wallet. 176 | func (p Phrase) LookupBTCBal(coin string) (balance float64, isUsed bool, addresses []*Address, err error) { 177 | 178 | batch := 50 // How many addresses to fetch at one time 179 | skips := 0 // How many empty addresses in a row we've found 180 | 181 | // chain 0 = main, chain 1 = change 182 | for chain := uint32(0); chain < 2; chain++ { 183 | 184 | child := uint32(0) 185 | 186 | for skips < 10 { // Go until we find 10 in a row that are unused 187 | var addr []*Address 188 | switch coin { 189 | case "btc32": 190 | addr = p.getBitcoinAddresses(0, 0, chain, child, batch, false) 191 | err = p.LookupBTC(addr, false) 192 | case "btc44": 193 | addr = p.getBitcoinAddresses(44, 0, chain, child, batch, false) 194 | err = p.LookupBTC(addr, false) 195 | case "tbt32": 196 | addr = p.getBitcoinAddresses(0, 0, chain, child, batch, true) 197 | err = p.LookupBTC(addr, true) 198 | case "tbt44": 199 | addr = p.getBitcoinAddresses(44, 1, chain, child, batch, true) 200 | err = p.LookupBTC(addr, true) 201 | case "bch32": 202 | addr = p.getBitcoinAddresses(0, 0, chain, child, batch, false) 203 | err = p.LookupBCH(addr) 204 | case "bch440": 205 | addr = p.getBitcoinAddresses(44, 0, chain, child, batch, false) 206 | err = p.LookupBCH(addr) 207 | case "bch44145": 208 | addr = p.getBitcoinAddresses(44, 145, chain, child, batch, false) 209 | err = p.LookupBCH(addr) 210 | } 211 | 212 | for _, v := range addr { 213 | balance += v.Balance 214 | if v.TxCount > 0 { 215 | isUsed = true 216 | skips = 0 217 | } else { 218 | skips++ 219 | } 220 | } 221 | 222 | addresses = append(addresses, addr...) 223 | 224 | child += uint32(batch) 225 | } 226 | } 227 | return 228 | } 229 | 230 | // Derive a key for a specific location in a BIP44 wallet 231 | func deriveHDKey(xprv *hdkeychain.ExtendedKey, purpose uint32, coin uint32, account uint32, chain uint32, address uint32) (pubkey *hdkeychain.ExtendedKey, err error) { 232 | 233 | // Path: m/44H 234 | purp, err := xprv.Child(hdkeychain.HardenedKeyStart + purpose) 235 | if err != nil { 236 | return 237 | } 238 | 239 | if purpose == 0 { 240 | // This is a bip32 path 241 | // Path: m/0H/[chain] 242 | var cha *hdkeychain.ExtendedKey 243 | cha, err = purp.Child(chain) 244 | if err != nil { 245 | return 246 | } 247 | 248 | // Path: m/0H/[chain]/[child] (e.g. m/0H/0/0) 249 | pubkey, err = cha.Child(address) 250 | return 251 | } 252 | 253 | // Coin number 254 | // Path: m/44H/60H 255 | co, err := purp.Child(hdkeychain.HardenedKeyStart + coin) 256 | if err != nil { 257 | return 258 | } 259 | 260 | // Path: m/44H/60H/0H 261 | acc, err := co.Child(hdkeychain.HardenedKeyStart + account) 262 | if err != nil { 263 | return 264 | } 265 | 266 | // Path: m/44H/60H/0H/0 267 | cha, err := acc.Child(chain) 268 | if err != nil { 269 | fmt.Println(err) 270 | return 271 | } 272 | 273 | // Path: m/44H/60H/0H/0/0 274 | pubkey, err = cha.Child(address) 275 | if err != nil { 276 | fmt.Println(err) 277 | return 278 | } 279 | 280 | return 281 | } 282 | 283 | func callAPI(url string, target interface{}, rate int64) (err error) { 284 | 285 | prnt := fmt.Sprintf("Please wait: %s...%s", url[8:30], url[len(url)-10:]) 286 | fmt.Printf("%s\r", prnt) 287 | 288 | //rate limit 289 | time.Sleep(time.Until(lastcall.Add(time.Millisecond * time.Duration(rate)))) 290 | 291 | var res *http.Response 292 | client := http.Client{ 293 | Timeout: 5 * time.Second, 294 | } 295 | res, err = client.Get(url) 296 | if err != nil { 297 | return 298 | } 299 | 300 | var data []byte 301 | data, err = ioutil.ReadAll(res.Body) 302 | if err != nil { 303 | return 304 | } 305 | 306 | err = json.Unmarshal(data, target) 307 | if err != nil { 308 | return errors.New("Invalid server response: " + string(data)) 309 | } 310 | lastcall = time.Now() 311 | 312 | fmt.Printf("%s\r", strings.Repeat(" ", len(prnt))) 313 | return 314 | } 315 | 316 | // Truncate any eth value to 8 decimals of precision (make large numbers easier) 317 | func snipEth(input string, decimal int) (output float64, err error) { 318 | tocut := decimal - 8 319 | 320 | if tocut > 0 && len(input)-tocut > 0 { 321 | start := len(input) - tocut 322 | input = input[:start] 323 | decimal = 8 324 | } 325 | 326 | output, err = strconv.ParseFloat(input, 64) 327 | if decimal > 0 { 328 | output = output / math.Pow(10, float64(decimal)) 329 | } 330 | return 331 | } 332 | --------------------------------------------------------------------------------