diff --git a/README.md b/README.md index 2914180..179f20f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ ## 12-Word Recovery Phrase Activity Scanner -This simple tool takes a 12-word phrase, such as those used by the BRD cryptocurrency wallet, and scans for wallet activity. It will list out, in order, the following: +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. -1. If the phrase has ever been used for for BTC, Test BTC, BCH, or ETH. -1. If a remaining balance exists for BTC, Test BTC, BCH, or ETH. +It will list out the following: + +1. If the phrase has ever been used for for BTC, BCH, or ETH. +1. If a remaining balance exists for BTC, BCH, or ETH. 1. If the Ethereum address has any remaining ERC20 token balances (if not, nothing will be displayed) +**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. + +**Note 2:** BIP44 support only looks at account 0. + ### Usage Either build the project with Go or download the binary, then... diff --git a/eth.go b/eth.go new file mode 100644 index 0000000..5196a39 --- /dev/null +++ b/eth.go @@ -0,0 +1,163 @@ +package main + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// LookupETH looks up the details for this phrase's ethereum address. Returns an array of Addresses to be consistent +// with other methods, but only populates the first item in the array. +func (p Phrase) LookupETH(isTestnet bool) (addresses []*Address, err error) { + + // Get our ethereum xpub + ethxpub, err := deriveHDKey(p.xprv, 44, 60, 0, 0, 0) + if err != nil { + return + } + + // Get our ethereum key + ethkey, err := ethxpub.ECPubKey() + if err != nil { + fmt.Println(err) + return + } + + // Generate our ethereum address for the request + pubBytes := ethkey.SerializeUncompressed() + ethadd := common.BytesToAddress(crypto.Keccak256(pubBytes[1:])[12:]) + + addresses = []*Address{ + &Address{Address: ethadd.String(), IsTest: isTestnet}, + } + + domain := "api" + if isTestnet { + domain = "api-ropsten" + } + + // Lookup activity + var ethact struct { + Status string `json:"status"` + Message string `json:"message"` + Result []struct { + Hash string `json:"hash"` + } `json:"result"` + } + + err = callAPI("https://"+domain+".etherscan.io/api?module=account&action=txlist&address="+addresses[0].Address, ðact) + if err != nil { + return + } + + if ethact.Status != "1" && ethact.Message != "No transactions found" { + err = errors.New("etherscan error: " + ethact.Message) + return + } + + addresses[0].TxCount = len(ethact.Result) + + if len(ethact.Result) > 0 { + // Ethereumn balance lookup (only if we've seen activity) + var ethbal struct { + Status string `json:"status"` + Message string `json:"message"` + Result string `json:"result"` + } + + err = callAPI("https://"+domain+".etherscan.io/api?module=account&action=balance&address="+addresses[0].Address, ðbal) + if err != nil { + return + } + + if ethbal.Status != "1" { + err = errors.New("etherscan error: " + ethbal.Message) + return + } + + addresses[0].Balance, err = snipEth(ethbal.Result, 18) + if err != nil { + return + } + } + + // ERC20 token lookup + var erc20bal struct { + Status string `json:"status"` + Message string `json:"message"` + Result []struct { + Address string `json:"contractAddress"` + To string `json:"to"` + Value string `json:"value"` + Name string `json:"tokenName"` + Ticker string `json:"tokenSymbol"` + Decimal string `json:"tokenDecimal"` + Hash string `json:"hash"` + } `json:"result"` + } + + err = callAPI("https://"+domain+".etherscan.io/api?module=account&action=tokentx&address="+addresses[0].Address, &erc20bal) + if err != nil { + return + } + + if erc20bal.Status != "1" { + return + } + + if len(erc20bal.Result) >= 10000 { + fmt.Println("WARN: Eth address has at least 10k transactions--any more than that will be omitted.") + } + + txlist := make(map[string]int) + for _, v := range erc20bal.Result { + if v.Ticker == "" { + continue + } + + idx, e := txlist[v.Ticker] + + if !e { + // Create a new entry if we haven't seen it yet + txlist[v.Ticker] = len(addresses[0].Tokens) + addresses[0].Tokens = append(addresses[0].Tokens, Token{ + Name: v.Name, + Ticker: v.Ticker, + Address: v.Address, + }) + } + + // Convert number of decimal places this token uses + var dec int + dec, err = strconv.Atoi(v.Decimal) + if err != nil { + return + } + + // Parse the value and snip it down based on decimal places + var val float64 + val, err = snipEth(v.Value, dec) + if err != nil { + return + } + + // Etherscan returns addresses all lower case... + if v.To != strings.ToLower(addresses[0].Address) { + // This must be a send, not a receive + val *= -1 + } + + nicebal := float64(val) + if dec > 0 { + nicebal = float64(val / float64(10^dec)) + } + + addresses[0].Tokens[idx].Balance += nicebal + addresses[0].Tokens[idx].TxCount++ + } + return +} diff --git a/main.go b/main.go index cde4d88..d96fc95 100644 --- a/main.go +++ b/main.go @@ -4,28 +4,28 @@ import ( "fmt" "io/ioutil" "os" - "strconv" "strings" "time" bip39 "github.com/tyler-smith/go-bip39" ) +var showTestnet = false + var gfx = map[string]string{ - "head1": "╒═══════════════════════════════════╤═════════════╤═════════════╤═════════════╤═════════════╕", - "head2": "│ Phrase │ BTC │ T.BTC │ BCH │ ETH │", - "div": "╞═══════════════════════════════════╪═════════════╪═════════════╪═════════════╪═════════════╡", - "ercstart": "╞═══════════════════════════════════╧═════════════╧═════════════╧═════════════╛ │", - "ercend": "╞═══════════════════════════════════╤═════════════╤═════════════╤═════════════╤═════════════╡", - "end": "╘═══════════════════════════════════╧═════════════╧═════════════╧═════════════╧═════════════╛", + "start": "╒═════════════════════════════════════╕\n", + "phrase1": "╒═══╧═════════════════════════════════╕\n", + "phrase2": "│ %s... │\n", // Show first 28 characters of phrase + "phrase3": "╘═══╤═════════════════════════════════╛\n", + "crypto": " ┝ %s : %s\n", + "subcrypto1": " │ ┝ %s : %s\n", + "subcrypto2": " │ ┕ %s : %s\n", + "end": " ╘═══════════════════════════☐ Done!\n", } // Lastcall is used as a timestamp for the last api call (for rate limiting) var lastcall = time.Now() -// Save lists of addresses for verbose logging if in single address mode -var addlists = make(map[string][]Address) - func main() { var phrases []string @@ -61,19 +61,11 @@ func main() { } } - // Write out the UI header fmt.Println() - fmt.Println(gfx["head1"]) - fmt.Println(gfx["head2"]) - fmt.Println(gfx["div"]) // Process each phrase for i, v := range phrases { - // Record which ones have been used, then look up balances - currencies := []string{"btc", "tbt", "bch", "eth"} - firstrun := make(map[string]Address) - // Prepare phrase p, err := NewPhrase(v) if err != nil { @@ -81,140 +73,110 @@ func main() { return } - // Display address (width incl. pipes: 37) - fmt.Printf("│ #%-4v %s... │", strconv.Itoa(i+1), v[:24]) - - // Lookup which currencies have been used - for _, v := range currencies { - - var addr []Address - var err error - switch v { - case "btc": - addr, err = p.LookupBTC(0, 0, 1, false) - case "tbt": - addr, err = p.LookupBTC(0, 0, 1, true) - case "bch": - addr, err = p.LookupBCH(0, 0, 1) - case "eth": - addr, err = p.LookupETH(false) - case "tet": - addr, err = p.LookupETH(true) - } - if err != nil { - fmt.Println("lookup error: ", err) - return - } - - if addr[0].TxCount > 0 { - fmt.Print(centerText("Used", 13)) - } else { - fmt.Print(centerText("Not used", 13)) - } - - firstrun[v] = addr[0] - fmt.Print("│") - time.Sleep(time.Millisecond * 100) + // Display phrase header + if i == 0 { + fmt.Printf(gfx["start"]) + } else { + fmt.Printf(gfx["phrase1"]) } + fmt.Printf(gfx["phrase2"], v[:28]) + fmt.Printf(gfx["phrase3"]) - fmt.Printf("\n│ │") - - // Lookup balances - for _, v := range currencies { - if firstrun[v].TxCount > 0 || firstrun[v].Balance > 0 { - - var bal float64 - if v == "eth" { - // No need to do all that again - bal = firstrun[v].Balance - } else { - var err error - bal, addlists[v], err = p.LookupBTCBal(v) - if err != nil { - fmt.Println("full bal lookup error: ", err) - return - } - } - - fmt.Print(centerBalance(bal, 13)) - fmt.Print("│") - } else { - fmt.Print(" │") - } + // Print each currency + p.printBTCBalances("BTC", []BTCFormat{BTCFormat{Coin: "btc32", Type: "BIP32"}, BTCFormat{Coin: "btc44", Type: "BIP44"}}) + if showTestnet { + p.printBTCBalances("TBT", []BTCFormat{BTCFormat{Coin: "tbt32", Type: "BIP32"}, BTCFormat{Coin: "tbt44", Type: "BIP44"}}) + } + p.printBTCBalances("BCH", []BTCFormat{ + BTCFormat{Coin: "bch32", Type: "BIP32"}, + BTCFormat{Coin: "bch440", Type: "BIP44-coin0"}, + BTCFormat{Coin: "bch44145", Type: "BIP44-coin145"}, + }) + p.printETHBalances("ETH", false) + if showTestnet { + p.printETHBalances("TET", true) } - // End of balance row - fmt.Printf("\n") + } - //If we had any tokens... - if len(firstrun["eth"].Tokens) > 0 { - fmt.Println(gfx["ercstart"]) + // End + fmt.Printf(gfx["end"]) + fmt.Println() +} - for _, v := range firstrun["eth"].Tokens { - fmt.Println("│" + strings.Repeat(" ", 40) + centerText(v.Name, 20) + ":" + rightBalance(v.Balance, 15) + " " + v.Ticker + strings.Repeat(" ", 11) + "│") - } +type BTCFormat struct { + Coin string + Type string + isUsed bool + balance float64 +} - fmt.Println(gfx["ercend"]) +func (p Phrase) printBTCBalances(label string, coins []BTCFormat) { + numused := 0 + for i, v := range coins { + var err error + coins[i].balance, coins[i].isUsed, _, err = p.LookupBTCBal(v.Coin) + if err != nil { + fmt.Printf(gfx["crypto"], "There was a problem with "+v.Coin, err) + } - } else if i < len(phrases)-1 { - // Start a new line if this isn't the last phrase - fmt.Println(gfx["div"]) + if coins[i].isUsed { + numused++ } } - // End - fmt.Println(gfx["end"]) - fmt.Println() + var output string - if len(os.Args) > 1 { - // Show some verbose balance logging if only looking up a single phrase - listAddBals(addlists) + if numused == 0 { + output = "Unused" + } else { + output = "** Used ** Balance: " + done := 0 + for _, v := range coins { + if v.isUsed { + output += fmt.Sprintf("%.5f", v.balance) + label + " (" + v.Type + ")" + done++ + if done < numused { + output += ", " + } + } + } } - fmt.Println(centerText("You're welcome!", 76)) - fmt.Println() + fmt.Printf(gfx["crypto"], label, output) } -// Center some text inside a given width -func centerText(label string, width int) string { - if len(label) >= width { - return label[:width] - } - l := len(label) - n := (width - l) / 2 +func (p Phrase) printETHBalances(label string, testnet bool) { - return strings.Repeat(" ", n+((width-len(label))%2)) + label + strings.Repeat(" ", n) -} + addslice, err := p.LookupETH(testnet) + if err != nil { + fmt.Printf(gfx["crypto"], "There was a problem with "+label, err) + return + } -// Center a number (balance) inside a given width -func centerBalance(amount float64, width int) string { - return centerText(fmt.Sprintf("%.8g", amount), width) -} + add := addslice[0] -// Right-align a number (balance) inside a given width -func rightBalance(amount float64, width int) string { - v := fmt.Sprintf("%f", amount) - if len(v) >= width { - return v[:width] + var output string + if add.TxCount == 0 { + output = "Unused" + } else { + output = fmt.Sprintf("** Used ** Balance: %.5f%s", add.Balance, label) } - return strings.Repeat(" ", width-len(v)) + v -} + fmt.Printf(gfx["crypto"], label, output) -func listAddBals(adds map[string][]Address) { - order := []string{"btc", "tbt", "bch"} - - for _, cur := range order { + //If we had any tokens... + if len(add.Tokens) == 0 { + return + } - var line string - for i, v := range adds[cur] { - if v.Balance > 0 { - line += fmt.Sprintf("Child %d (%s) balance: %f\n", i, v.Address, v.Balance) - } - } + for i, v := range add.Tokens { + output = fmt.Sprintf("%.5f%s", v.Balance, v.Ticker) - if line != "" { - fmt.Printf("\n %s:\n%s\n", strings.ToUpper(cur), line) + if i < len(add.Tokens)-1 { + fmt.Printf(gfx["subcrypto1"], v.Name, output) + } else { + fmt.Printf(gfx["subcrypto2"], v.Name, output) } } } diff --git a/phrase.go b/phrase.go index a6fec69..725a091 100644 --- a/phrase.go +++ b/phrase.go @@ -1,5 +1,7 @@ package main +//TODO: add in checking for bip44 btc + import ( "encoding/json" "errors" @@ -8,22 +10,18 @@ import ( "math" "net/http" "strconv" - "strings" "time" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcutil/hdkeychain" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" bip32 "github.com/tyler-smith/go-bip32" bip39 "github.com/tyler-smith/go-bip39" ) // Phrase represents a phrase we are examining type Phrase struct { - master string - brdBtc *hdkeychain.ExtendedKey - brdEthAddr string + master string + xprv *hdkeychain.ExtendedKey } // Address represents a crypto address generated from a phrase, including details about how/if it has been used @@ -56,68 +54,14 @@ func NewPhrase(phrase string) (p *Phrase, err error) { // Get our master xprv // Path: m - xprv, err := hdkeychain.NewKeyFromString(p.master) - if err != nil { - return - } - - // Set up our BIP32 bitcoin path - // Path: m/0H - p.brdBtc, err = xprv.Child(hdkeychain.HardenedKeyStart + 0) - if err != nil { - return - } - - // Set up our BIP44 Eth path - // Path: m/44H - purpose, err := xprv.Child(hdkeychain.HardenedKeyStart + 44) - if err != nil { - return - } - - // Path: m/44H/60H - coin, err := purpose.Child(hdkeychain.HardenedKeyStart + 60) - if err != nil { - return - } - - // Path: m/44H/60H/0H - account, err := coin.Child(hdkeychain.HardenedKeyStart) - if err != nil { - return - } - - // Path: m/44H/60H/0H/0 - change, err := account.Child(0) - if err != nil { - fmt.Println(err) - return - } - - // Path: m/44H/60H/0H/0/0 - addidx, err := change.Child(0) - if err != nil { - fmt.Println(err) - return - } - - // Generate our Ethereum address - xpub, err := addidx.ECPubKey() - if err != nil { - fmt.Println(err) - return - } - - pubBytes := xpub.SerializeUncompressed() - ethadd := common.BytesToAddress(crypto.Keccak256(pubBytes[1:])[12:]) - p.brdEthAddr = ethadd.String() + p.xprv, err = hdkeychain.NewKeyFromString(p.master) return } // getBitcoinAddress by specifying the chain number (0 for normal, 1 for change), child number (address number), // and whether testnet or not. Count specifies how many addresses to return. -func (p Phrase) getBitcoinAddresses(chainNo uint32, childNo uint32, count int, testnet bool) (addresses []Address, err error) { +func (p Phrase) getBitcoinAddresses(purpose uint32, coin uint32, chainNo uint32, childNo uint32, count int, testnet bool) (addresses []*Address) { if count < 1 { count = 1 } @@ -126,18 +70,12 @@ func (p Phrase) getBitcoinAddresses(chainNo uint32, childNo uint32, count int, t count = 100 } - // Using our saved BIP32 starting point, generate the target address - // Path: m/0H/[chain] - chain, err := p.brdBtc.Child(chainNo) - if err != nil { - return - } - for i := 0; i < count; i++ { // Path: m/0H/[chain]/[child] (e.g. m/0H/0/0) - child, err := chain.Child(childNo) + child, err := deriveHDKey(p.xprv, purpose, coin, 0, chainNo, childNo) if err != nil { - return nil, err + fmt.Println("Uh-oh! HD derivation error:", err) + return } // Generate address based on testnet or mainnet @@ -149,9 +87,10 @@ func (p Phrase) getBitcoinAddresses(chainNo uint32, childNo uint32, count int, t pkh, err := child.Address(params) if err != nil { - return nil, err + fmt.Println("Uh-oh! HD derivation error:", err) + return } - addresses = append(addresses, Address{Address: pkh.EncodeAddress()}) + addresses = append(addresses, &Address{Address: pkh.EncodeAddress()}) childNo++ } @@ -159,14 +98,8 @@ func (p Phrase) getBitcoinAddresses(chainNo uint32, childNo uint32, count int, t return } -// LookupBTC looks up one or more bitcoin addresses, starting with the child specified and continuing for the number of -// addresses specified with "count" -func (p Phrase) LookupBTC(chain uint32, child uint32, count int, isTestnet bool) (addresses []Address, err error) { - - addresses, err = p.getBitcoinAddresses(chain, child, count, isTestnet) - if err != nil { - return - } +// LookupBTC takes a slice of addresses and fills in the details +func (p Phrase) LookupBTC(addresses []*Address, isTestnet bool) (err error) { domain := "" if isTestnet { @@ -198,17 +131,10 @@ func (p Phrase) LookupBTC(chain uint32, child uint32, count int, isTestnet bool) addresses[i].Balance = float64(float64(BCi[v.Address].Balance) / 100000000) } return - } -// LookupBCH looks up one or more bitcoin addresses, starting with the child specified and continuing for the number of -// addresses specified with "count" -func (p Phrase) LookupBCH(chain uint32, child uint32, count int) (addresses []Address, err error) { - - addresses, err = p.getBitcoinAddresses(chain, child, count, false) - if err != nil { - return - } +// LookupBCH takes a slice of addresses and fills in the details +func (p Phrase) LookupBCH(addresses []*Address) (err error) { // Hack: the API will return an array if we request 2+ addresses but only return the single object if we request only 1. // So, to avoid defining two structures, we will just add another address to force the response to be an array. @@ -256,177 +182,111 @@ func (p Phrase) LookupBCH(chain uint32, child uint32, count int) (addresses []Ad return } -// LookupETH looks up the details for this phrase's ethereum address. Returns an array of Addresses to be consistent -// with other methods, but only populates the first item in the array. -func (p Phrase) LookupETH(isTestnet bool) (addresses []Address, err error) { +// LookupBTCBal follows the entire btc/bch chain and finds out the remaining balance for the entire wallet. +func (p Phrase) LookupBTCBal(coin string) (balance float64, isUsed bool, addresses []*Address, err error) { - addresses = []Address{ - Address{Address: p.brdEthAddr, IsTest: isTestnet}, - } + batch := 50 // How many addresses to fetch at one time + skips := 0 // How many empty addresses in a row we've found - domain := "api" - if isTestnet { - domain = "api-ropsten" - } + // chain 0 = main, chain 1 = change + for chain := uint32(0); chain < 2; chain++ { - // Lookup activity - var ethact struct { - Status string `json:"status"` - Message string `json:"message"` - Result []struct { - Hash string `json:"hash"` - } `json:"result"` - } + child := uint32(0) - err = callAPI("https://"+domain+".etherscan.io/api?module=account&action=txlist&address="+addresses[0].Address, ðact) - if err != nil { - return - } + for skips < 10 { // Go until we find 10 in a row that are unused + var addr []*Address + switch coin { + case "btc32": + addr = p.getBitcoinAddresses(0, 0, chain, child, batch, false) + err = p.LookupBTC(addr, false) + case "btc44": + addr = p.getBitcoinAddresses(44, 0, chain, child, batch, false) + err = p.LookupBTC(addr, false) + case "tbt32": + addr = p.getBitcoinAddresses(0, 0, chain, child, batch, true) + err = p.LookupBTC(addr, true) + case "tbt44": + addr = p.getBitcoinAddresses(44, 1, chain, child, batch, true) + err = p.LookupBTC(addr, true) + case "bch32": + addr = p.getBitcoinAddresses(0, 0, chain, child, batch, false) + err = p.LookupBCH(addr) + case "bch440": + addr = p.getBitcoinAddresses(44, 0, chain, child, batch, false) + err = p.LookupBCH(addr) + case "bch44145": + addr = p.getBitcoinAddresses(44, 145, chain, child, batch, false) + err = p.LookupBCH(addr) + } - if ethact.Status != "1" && ethact.Message != "No transactions found" { - err = errors.New("etherscan error: " + ethact.Message) - return - } + for _, v := range addr { + balance += v.Balance + if v.TxCount > 0 { + isUsed = true + skips = 0 + } else { + skips++ + } + } - addresses[0].TxCount = len(ethact.Result) + addresses = append(addresses, addr...) - if len(ethact.Result) > 0 { - // Ethereumn balance lookup (only if we've seen activity) - var ethbal struct { - Status string `json:"status"` - Message string `json:"message"` - Result string `json:"result"` + child += uint32(batch) } + } + return +} - err = callAPI("https://"+domain+".etherscan.io/api?module=account&action=balance&address="+addresses[0].Address, ðbal) - if err != nil { - return - } +// Derive a key for a specific location in a BIP44 wallet +func deriveHDKey(xprv *hdkeychain.ExtendedKey, purpose uint32, coin uint32, account uint32, chain uint32, address uint32) (pubkey *hdkeychain.ExtendedKey, err error) { - if ethbal.Status != "1" { - err = errors.New("etherscan error: " + ethbal.Message) - return - } + // Path: m/44H + purp, err := xprv.Child(hdkeychain.HardenedKeyStart + purpose) + if err != nil { + return + } - addresses[0].Balance, err = snipEth(ethbal.Result, 18) + if purpose == 0 { + // This is a bip32 path + // Path: m/0H/[chain] + var cha *hdkeychain.ExtendedKey + cha, err = purp.Child(chain) if err != nil { return } - } - // ERC20 token lookup - var erc20bal struct { - Status string `json:"status"` - Message string `json:"message"` - Result []struct { - Address string `json:"contractAddress"` - To string `json:"to"` - Value string `json:"value"` - Name string `json:"tokenName"` - Ticker string `json:"tokenSymbol"` - Decimal string `json:"tokenDecimal"` - Hash string `json:"hash"` - } `json:"result"` + // Path: m/0H/[chain]/[child] (e.g. m/0H/0/0) + pubkey, err = cha.Child(address) + return } - err = callAPI("https://"+domain+".etherscan.io/api?module=account&action=tokentx&address="+addresses[0].Address, &erc20bal) + // Coin number + // Path: m/44H/60H + co, err := purp.Child(hdkeychain.HardenedKeyStart + coin) if err != nil { return } - if erc20bal.Status != "1" { + // Path: m/44H/60H/0H + acc, err := co.Child(hdkeychain.HardenedKeyStart + account) + if err != nil { return } - if len(erc20bal.Result) >= 10000 { - fmt.Println("WARN: Eth address has at least 10k transactions--any more than that will be omitted.") + // Path: m/44H/60H/0H/0 + cha, err := acc.Child(chain) + if err != nil { + fmt.Println(err) + return } - txlist := make(map[string]int) - for _, v := range erc20bal.Result { - if v.Ticker == "" { - continue - } - - idx, e := txlist[v.Ticker] - - if !e { - // Create a new entry if we haven't seen it yet - txlist[v.Ticker] = len(addresses[0].Tokens) - addresses[0].Tokens = append(addresses[0].Tokens, Token{ - Name: v.Name, - Ticker: v.Ticker, - Address: v.Address, - }) - } - - // Convert number of decimal places this token uses - var dec int - dec, err = strconv.Atoi(v.Decimal) - if err != nil { - return - } - - // Parse the value and snip it down based on decimal places - var val float64 - val, err = snipEth(v.Value, dec) - if err != nil { - return - } - - // Etherscan returns addresses all lower case... - if v.To != strings.ToLower(addresses[0].Address) { - // This must be a send, not a receive - val *= -1 - } - - nicebal := float64(val) - if dec > 0 { - nicebal = float64(val / float64(10^dec)) - } - - addresses[0].Tokens[idx].Balance += nicebal - addresses[0].Tokens[idx].TxCount++ + // Path: m/44H/60H/0H/0/0 + pubkey, err = cha.Child(address) + if err != nil { + fmt.Println(err) + return } - return -} - -// LookupBTCBal follows the entire btc/bch chain and finds out the remaining balance for the entire wallet. -func (p Phrase) LookupBTCBal(coin string) (balance float64, addresses []Address, err error) { - - batch := 50 // How many addresses to fetch at one time - skips := 0 // How many empty addresses in a row we've found - // chain 0 = main, chain 1 = change - for chain := uint32(0); chain < 2; chain++ { - - child := uint32(0) - - for skips < 10 { // Go until we find 10 in a row that are unused - var addr []Address - switch coin { - case "btc": - addr, err = p.LookupBTC(chain, child, batch, false) - case "tbt": - addr, err = p.LookupBTC(chain, child, batch, true) - case "bch": - addr, err = p.LookupBCH(chain, child, batch) - } - - addresses = append(addresses, addr...) - - for _, v := range addr { - balance += v.Balance - if v.TxCount > 0 { - skips = 0 - } else { - skips++ - } - } - - child += uint32(batch) - } - } return } @@ -451,7 +311,7 @@ func callAPI(url string, target interface{}) (err error) { lastcall = time.Now() err = json.Unmarshal(data, target) if err != nil { - fmt.Printf("\n%s\n", string(data)) + return errors.New("Invalid server response: " + string(data)) } return }