package bitcoinElectrum import ( "crypto/sha256" "encoding/hex" "log" "sync/atomic" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" ) // Derives a P2WPKH address at a specific index from a branch key. func deriveAddress(branchKey *hdkeychain.ExtendedKey, index uint32) (btcutil.Address, error) { childKey, err := branchKey.Derive(index) if err != nil { return nil, err } pubKey, err := childKey.ECPubKey() if err != nil { return nil, err } // Note: For zpub/vpub, use NewAddressWitnessPubKeyHash. // For ypub/upub, use NewAddressScriptHash with a P2WPKH script. // For xpub/tpub, use NewAddressPubKeyHash. return btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(pubKey.SerializeCompressed()), &chaincfg.MainNetParams) } // Converts a btcutil.Address to an Electrum scripthash. func addressToScriptHash(address btcutil.Address) (string, error) { script, err := txscript.PayToAddrScript(address) if err != nil { return "", err } hash := sha256.Sum256(script) // Reverse byte order for Electrum protocol for i, j := 0, len(hash)-1; i < j; i, j = i+1, j-1 { hash[i], hash[j] = hash[j], hash[i] } return hex.EncodeToString(hash[:]), nil } // Scans a derivation branch (m/0 or m/1) for balances until the gap limit is reached. func scanBranch(masterKey *hdkeychain.ExtendedKey, branch uint32, gapLimit int, spvServer string) int64 { log.Printf("Scanning branch m/%d/k...", branch) branchKey, err := masterKey.Derive(branch) if err != nil { log.Printf("Error deriving branch %d: %v", branch, err) return 0 } var branchTotalBalance int64 unusedAddressCount := 0 for i := uint32(0); ; i++ { if unusedAddressCount >= gapLimit { log.Printf("Gap limit of %d reached on branch m/%d. Stopping scan.", gapLimit, branch) break } // 3. Derive child address address, err := deriveAddress(branchKey, i) if err != nil { log.Printf("Could not derive address at index %d on branch %d: %v", i, branch, err) continue } // 4. Get balance from Electrum server balance, err := getAddressBalance(spvServer, address) if err != nil { log.Printf("Error getting balance for %s: %v", address.EncodeAddress(), err) // On error, we can't know if it's used, so we continue scanning. unusedAddressCount = 0 continue } total := balance.Result.Confirmed + balance.Result.Unconfirmed if total > 0 { log.Printf("Found balance for m/%d/%d (%s): %.8f BTC", branch, i, address.EncodeAddress(), btcutil.Amount(total).ToBTC()) atomic.AddInt64(&branchTotalBalance, total) unusedAddressCount = 0 // Reset gap counter on finding a used address } else { log.Printf("No balance for m/%d/%d (%s)", branch, i, address.EncodeAddress()) unusedAddressCount++ } } return branchTotalBalance }