Files
ynab-portfolio-monitor/providers/bitcoinElectrum/bitcoin.go

92 lines
2.8 KiB
Go
Raw Normal View History

2025-09-30 22:49:16 -06:00
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
}