Parse xpub descriptors

This commit is contained in:
Martin Boehm 2021-10-18 13:58:45 +02:00
parent c4128e5c9c
commit e500d6873d
11 changed files with 502 additions and 128 deletions

View File

@ -56,6 +56,7 @@ type xpubAddress struct {
}
type xpubData struct {
descriptor *bchain.XpubDescriptor
gap int
accessed int64
basePath string
@ -64,8 +65,7 @@ type xpubData struct {
txCountEstimate uint32
sentSat big.Int
balanceSat big.Int
addresses []xpubAddress
changeAddresses []xpubAddress
addresses [][]xpubAddress
}
func (w *Worker) initXpubCache() {
@ -201,7 +201,7 @@ func (w *Worker) xpubDerivedAddressBalance(data *xpubData, ad *xpubAddress) (boo
return false, nil
}
func (w *Worker) xpubScanAddresses(xpub string, data *xpubData, addresses []xpubAddress, gap int, change int, minDerivedIndex int, fork bool) (int, []xpubAddress, error) {
func (w *Worker) xpubScanAddresses(xd *bchain.XpubDescriptor, data *xpubData, addresses []xpubAddress, gap int, change uint32, minDerivedIndex int, fork bool) (int, []xpubAddress, error) {
// rescan known addresses
lastUsed := 0
for i := range addresses {
@ -229,7 +229,7 @@ func (w *Worker) xpubScanAddresses(xpub string, data *xpubData, addresses []xpub
if to < minDerivedIndex {
to = minDerivedIndex
}
descriptors, err := w.chainParser.DeriveAddressDescriptorsFromTo(xpub, uint32(change), uint32(from), uint32(to))
descriptors, err := w.chainParser.DeriveAddressDescriptorsFromTo(xd, change, uint32(from), uint32(to))
if err != nil {
return 0, nil, err
}
@ -277,7 +277,7 @@ func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeInd
}
}
func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int) (*xpubData, uint32, bool, error) {
func (w *Worker) getXpubData(xd *bchain.XpubDescriptor, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int) (*xpubData, uint32, bool, error) {
if w.chainType != bchain.ChainBitcoinType {
return nil, 0, false, ErrUnsupportedXpub
}
@ -296,7 +296,7 @@ func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option Accoun
gap++
var processedHash string
cachedXpubsMux.Lock()
data, inCache := cachedXpubs[xpub]
data, inCache := cachedXpubs[xd.XpubDescriptor]
cachedXpubsMux.Unlock()
// to load all data for xpub may take some time, do it in a loop to process a possible new block
for {
@ -309,8 +309,11 @@ func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option Accoun
}
fork := false
if !inCache || data.gap != gap {
data = xpubData{gap: gap}
data.basePath, err = w.chainParser.DerivationBasePath(xpub)
data = xpubData{
gap: gap,
addresses: make([][]xpubAddress, len(xd.ChangeIndexes)),
}
data.basePath, err = w.chainParser.DerivationBasePath(xd)
if err != nil {
return nil, 0, inCache, err
}
@ -331,18 +334,16 @@ func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option Accoun
data.balanceSat = *new(big.Int)
data.sentSat = *new(big.Int)
data.txCountEstimate = 0
var lastUsedIndex int
lastUsedIndex, data.addresses, err = w.xpubScanAddresses(xpub, &data, data.addresses, gap, 0, 0, fork)
var minDerivedIndex int
for i, change := range xd.ChangeIndexes {
minDerivedIndex, data.addresses[i], err = w.xpubScanAddresses(xd, &data, data.addresses[i], gap, change, minDerivedIndex, fork)
if err != nil {
return nil, 0, inCache, err
}
_, data.changeAddresses, err = w.xpubScanAddresses(xpub, &data, data.changeAddresses, gap, 1, lastUsedIndex, fork)
if err != nil {
return nil, 0, inCache, err
}
}
if option >= AccountDetailsTxidHistory {
for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for _, da := range data.addresses {
for i := range da {
if err = w.xpubCheckAndLoadTxids(&da[i], filter, bestheight, (page+1)*txsOnPage); err != nil {
return nil, 0, inCache, err
@ -353,7 +354,7 @@ func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option Accoun
}
data.accessed = time.Now().Unix()
cachedXpubsMux.Lock()
cachedXpubs[xpub] = data
cachedXpubs[xd.XpubDescriptor] = data
cachedXpubsMux.Unlock()
return &data, bestheight, inCache, nil
}
@ -377,11 +378,14 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
txids []string
pg Paging
filtered bool
err error
uBalSat big.Int
unconfirmedTxs int
)
data, bestheight, inCache, err := w.getXpubData(xpub, page, txsOnPage, option, filter, gap)
xd, err := w.chainParser.ParseXpub(xpub)
if err != nil {
return nil, err
}
data, bestheight, inCache, err := w.getXpubData(xd, page, txsOnPage, option, filter, gap)
if err != nil {
return nil, err
}
@ -410,7 +414,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
if filter.ToHeight == 0 && !filter.OnlyConfirmed {
txmMap = make(map[string]*Tx)
mempoolEntries := make(bchain.MempoolTxidEntries, 0)
for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for _, da := range data.addresses {
for i := range da {
ad := &da[i]
newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, true, 0, 0, maxInt)
@ -457,7 +461,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
if option >= AccountDetailsTxidHistory {
txcMap := make(map[string]bool)
txc = make(xpubTxids, 0, 32)
for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for _, da := range data.addresses {
for i := range da {
ad := &da[i]
for _, txid := range ad.txids {
@ -515,7 +519,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
tokens = make([]Token, 0, 4)
xpubAddresses = make(map[string]struct{})
}
for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for ci, da := range data.addresses {
for i := range da {
ad := &da[i]
if ad.balance != nil {
@ -549,14 +553,18 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
Tokens: tokens,
XPubAddresses: xpubAddresses,
}
glog.Info("GetXpubAddress ", xpub[:xpubLogPrefix], ", cache ", inCache, ", ", len(data.addresses)+len(data.changeAddresses), " addresses, ", txCount, " txs, ", time.Since(start))
glog.Info("GetXpubAddress ", xpub[:xpubLogPrefix], ", cache ", inCache, ", ", txCount, " txs, ", time.Since(start))
return &addr, nil
}
// GetXpubUtxo returns unspent outputs for given xpub
func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, error) {
start := time.Now()
data, _, inCache, err := w.getXpubData(xpub, 0, 1, AccountDetailsBasic, &AddressFilter{
xd, err := w.chainParser.ParseXpub(xpub)
if err != nil {
return nil, err
}
data, _, inCache, err := w.getXpubData(xd, 0, 1, AccountDetailsBasic, &AddressFilter{
Vout: AddressFilterVoutOff,
OnlyConfirmed: onlyConfirmed,
}, gap)
@ -564,7 +572,7 @@ func (w *Worker) GetXpubUtxo(xpub string, onlyConfirmed bool, gap int) (Utxos, e
return nil, err
}
r := make(Utxos, 0, 8)
for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for ci, da := range data.addresses {
for i := range da {
ad := &da[i]
onlyMempool := false
@ -602,7 +610,11 @@ func (w *Worker) GetXpubBalanceHistory(xpub string, fromTimestamp, toTimestamp i
if fromHeight >= toHeight {
return bhs, nil
}
data, _, inCache, err := w.getXpubData(xpub, 0, 1, AccountDetailsTxidHistory, &AddressFilter{
xd, err := w.chainParser.ParseXpub(xpub)
if err != nil {
return nil, err
}
data, _, inCache, err := w.getXpubData(xd, 0, 1, AccountDetailsTxidHistory, &AddressFilter{
Vout: AddressFilterVoutOff,
OnlyConfirmed: true,
FromHeight: fromHeight,
@ -612,12 +624,12 @@ func (w *Worker) GetXpubBalanceHistory(xpub string, fromTimestamp, toTimestamp i
return nil, err
}
selfAddrDesc := make(map[string]struct{})
for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for _, da := range data.addresses {
for i := range da {
selfAddrDesc[string(da[i].addrDesc)] = struct{}{}
}
}
for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} {
for _, da := range data.addresses {
for i := range da {
ad := &da[i]
txids := ad.txids

View File

@ -280,18 +280,23 @@ func (p *BaseParser) IsAddrDescIndexable(addrDesc AddressDescriptor) bool {
return true
}
// ParseXpub is unsupported
func (p *BaseParser) ParseXpub(xpub string) (*XpubDescriptor, error) {
return nil, errors.New("Not supported")
}
// DerivationBasePath is unsupported
func (p *BaseParser) DerivationBasePath(xpub string) (string, error) {
func (p *BaseParser) DerivationBasePath(descriptor *XpubDescriptor) (string, error) {
return "", errors.New("Not supported")
}
// DeriveAddressDescriptors is unsupported
func (p *BaseParser) DeriveAddressDescriptors(xpub string, change uint32, indexes []uint32) ([]AddressDescriptor, error) {
func (p *BaseParser) DeriveAddressDescriptors(descriptor *XpubDescriptor, change uint32, indexes []uint32) ([]AddressDescriptor, error) {
return nil, errors.New("Not supported")
}
// DeriveAddressDescriptorsFromTo is unsupported
func (p *BaseParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error) {
func (p *BaseParser) DeriveAddressDescriptorsFromTo(descriptor *XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error) {
return nil, errors.New("Not supported")
}

View File

@ -8,6 +8,7 @@ import (
"math/big"
"regexp"
"strconv"
"strings"
"unicode/utf8"
vlq "github.com/bsm/go-vlq"
@ -360,14 +361,13 @@ func (p *BitcoinLikeParser) taprootAddrFromExtKey(extKey *hdkeychain.ExtendedKey
return btcutil.NewAddressWitnessTaproot(b, p.Params)
}
func (p *BitcoinLikeParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey, bip string) (bchain.AddressDescriptor, error) {
func (p *BitcoinLikeParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey, descriptor *bchain.XpubDescriptor) (bchain.AddressDescriptor, error) {
var a btcutil.Address
var err error
if bip != "" {
a, err = p.taprootAddrFromExtKey(extKey)
} else {
version := extKey.Version()
if version == p.XPubMagicSegwitP2sh {
switch descriptor.Type {
case bchain.P2PKH:
a, err = extKey.Address(p.Params)
case bchain.P2SHWPKH:
// redeemScript <witness version: OP_0><len pubKeyHash: 20><20-byte-pubKeyHash>
pubKeyHash := btcutil.Hash160(extKey.PubKeyBytes())
redeemScript := make([]byte, len(pubKeyHash)+2)
@ -376,12 +376,12 @@ func (p *BitcoinLikeParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey, b
copy(redeemScript[2:], pubKeyHash)
hash := btcutil.Hash160(redeemScript)
a, err = btcutil.NewAddressScriptHashFromHash(hash, p.Params)
} else if version == p.XPubMagicSegwitNative {
case bchain.P2WPKH:
a, err = btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(extKey.PubKeyBytes()), p.Params)
} else {
// default to P2PKH address
a, err = extKey.Address(p.Params)
}
case bchain.P2TR:
a, err = p.taprootAddrFromExtKey(extKey)
default:
return nil, errors.New("Unsupported xpub descriptor type")
}
if err != nil {
return nil, err
@ -389,33 +389,135 @@ func (p *BitcoinLikeParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey, b
return txscript.PayToAddrScript(a)
}
func parseXpub(xpub string) (string, string) {
r, _ := regexp.Compile(`tr\(\[\w+/(\d+)'/.*\](\w+)`)
match := r.FindStringSubmatch(xpub)
if len(match) == 3 {
return match[2], match[1]
func (p *BitcoinLikeParser) xpubDescriptorFromXpub(xpub string) (*bchain.XpubDescriptor, error) {
var descriptor bchain.XpubDescriptor
extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher)
if err != nil {
return nil, err
}
return xpub, ""
descriptor.Xpub = xpub
descriptor.XpubDescriptor = xpub
if extKey.Version() == p.XPubMagicSegwitP2sh {
descriptor.Type = bchain.P2SHWPKH
descriptor.Bip = "49"
} else if extKey.Version() == p.XPubMagicSegwitNative {
descriptor.Type = bchain.P2WPKH
descriptor.Bip = "84"
} else {
descriptor.Type = bchain.P2PKH
descriptor.Bip = "44"
}
descriptor.ChangeIndexes = []uint32{0, 1}
descriptor.ExtKey = extKey
return &descriptor, nil
}
var (
xpubDesriptorRegex *regexp.Regexp
typeSubexpIndex int
bipSubexpIndex int
xpubSubexpIndex int
changeSubexpIndex int
changeList1SubexpIndex int
changeList2SubexpIndex int
)
func init() {
xpubDesriptorRegex, _ = regexp.Compile(`^(?P<type>(sh\(wpkh|wpkh|pk|pkh|wpkh|wsh|tr))\((\[\w+/(?P<bip>\d+)'/\d+'?/\d+'?\])?(?P<xpub>\w+)(/(({(?P<changelist1>\d+(,\d+)*)})|(<(?P<changelist2>\d+(;\d+)*)>)|(?P<change>\d+))/\*)?\)+`)
typeSubexpIndex = xpubDesriptorRegex.SubexpIndex("type")
bipSubexpIndex = xpubDesriptorRegex.SubexpIndex("bip")
xpubSubexpIndex = xpubDesriptorRegex.SubexpIndex("xpub")
changeList1SubexpIndex = xpubDesriptorRegex.SubexpIndex("changelist1")
changeList2SubexpIndex = xpubDesriptorRegex.SubexpIndex("changelist2")
changeSubexpIndex = xpubDesriptorRegex.SubexpIndex("change")
if changeSubexpIndex < 0 {
panic("Invalid bitcoinparser xpubDesriptorRegex")
}
}
// ParseXpub parses xpub (or xpub descriptor) and returns XpubDescriptor
func (p *BitcoinLikeParser) ParseXpub(xpub string) (*bchain.XpubDescriptor, error) {
match := xpubDesriptorRegex.FindStringSubmatch(xpub)
if len(match) > changeSubexpIndex {
var descriptor bchain.XpubDescriptor
descriptor.XpubDescriptor = xpub
m := match[typeSubexpIndex]
switch m {
case "pkh":
descriptor.Type = bchain.P2PKH
descriptor.Bip = "44"
case "sh(wpkh":
descriptor.Type = bchain.P2SHWPKH
descriptor.Bip = "49"
case "wpkh":
descriptor.Type = bchain.P2WPKH
descriptor.Bip = "84"
case "tr":
descriptor.Type = bchain.P2TR
descriptor.Bip = "86"
default:
return nil, errors.Errorf("Xpub descriptor %s is not supported", m)
}
if len(match[bipSubexpIndex]) > 0 {
descriptor.Bip = match[bipSubexpIndex]
}
descriptor.Xpub = match[xpubSubexpIndex]
extKey, err := hdkeychain.NewKeyFromString(descriptor.Xpub, p.Params.Base58CksumHasher)
if err != nil {
return nil, err
}
descriptor.ExtKey = extKey
if len(match[changeSubexpIndex]) > 0 {
change, err := strconv.ParseUint(match[changeSubexpIndex], 10, 32)
if err != nil {
return nil, err
}
descriptor.ChangeIndexes = []uint32{uint32(change)}
} else {
if len(match[changeList1SubexpIndex]) > 0 || len(match[changeList2SubexpIndex]) > 0 {
var changes []string
if len(match[changeList1SubexpIndex]) > 0 {
changes = strings.Split(match[changeList1SubexpIndex], ",")
} else {
changes = strings.Split(match[changeList2SubexpIndex], ";")
}
if len(changes) == 0 {
return nil, errors.New("Invalid xpub descriptor, cannot parse change")
}
descriptor.ChangeIndexes = make([]uint32, len(changes))
for i, ch := range changes {
change, err := strconv.ParseUint(ch, 10, 32)
if err != nil {
return nil, err
}
descriptor.ChangeIndexes[i] = uint32(change)
}
} else {
// default to {0,1}
descriptor.ChangeIndexes = []uint32{0, 1}
}
}
return &descriptor, nil
}
return p.xpubDescriptorFromXpub(xpub)
}
// DeriveAddressDescriptors derives address descriptors from given xpub for listed indexes
func (p *BitcoinLikeParser) DeriveAddressDescriptors(xpub string, change uint32, indexes []uint32) ([]bchain.AddressDescriptor, error) {
parsedXpub, bip := parseXpub(xpub)
extKey, err := hdkeychain.NewKeyFromString(parsedXpub, p.Params.Base58CksumHasher)
if err != nil {
return nil, err
}
changeExtKey, err := extKey.Derive(change)
if err != nil {
return nil, err
}
func (p *BitcoinLikeParser) DeriveAddressDescriptors(descriptor *bchain.XpubDescriptor, change uint32, indexes []uint32) ([]bchain.AddressDescriptor, error) {
ad := make([]bchain.AddressDescriptor, len(indexes))
changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Derive(change)
if err != nil {
return nil, err
}
for i, index := range indexes {
indexExtKey, err := changeExtKey.Derive(index)
if err != nil {
return nil, err
}
ad[i], err = p.addrDescFromExtKey(indexExtKey, bip)
ad[i], err = p.addrDescFromExtKey(indexExtKey, descriptor)
if err != nil {
return nil, err
}
@ -424,16 +526,11 @@ func (p *BitcoinLikeParser) DeriveAddressDescriptors(xpub string, change uint32,
}
// DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for addresses in index range
func (p *BitcoinLikeParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) {
func (p *BitcoinLikeParser) DeriveAddressDescriptorsFromTo(descriptor *bchain.XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) {
if toIndex <= fromIndex {
return nil, errors.New("toIndex<=fromIndex")
}
parsedXpub, bip := parseXpub(xpub)
extKey, err := hdkeychain.NewKeyFromString(parsedXpub, p.Params.Base58CksumHasher)
if err != nil {
return nil, err
}
changeExtKey, err := extKey.Derive(change)
changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Derive(change)
if err != nil {
return nil, err
}
@ -443,7 +540,7 @@ func (p *BitcoinLikeParser) DeriveAddressDescriptorsFromTo(xpub string, change u
if err != nil {
return nil, err
}
ad[index-fromIndex], err = p.addrDescFromExtKey(indexExtKey, bip)
ad[index-fromIndex], err = p.addrDescFromExtKey(indexExtKey, descriptor)
if err != nil {
return nil, err
}
@ -452,13 +549,9 @@ func (p *BitcoinLikeParser) DeriveAddressDescriptorsFromTo(xpub string, change u
}
// DerivationBasePath returns base path of xpub
func (p *BitcoinLikeParser) DerivationBasePath(xpub string) (string, error) {
parsedXpub, bip := parseXpub(xpub)
extKey, err := hdkeychain.NewKeyFromString(parsedXpub, p.Params.Base58CksumHasher)
if err != nil {
return "", err
}
func (p *BitcoinLikeParser) DerivationBasePath(descriptor *bchain.XpubDescriptor) (string, error) {
var c string
extKey := descriptor.ExtKey.(*hdkeychain.ExtendedKey)
cn := extKey.ChildNum()
if cn >= 0x80000000 {
cn -= 0x80000000
@ -468,14 +561,5 @@ func (p *BitcoinLikeParser) DerivationBasePath(xpub string) (string, error) {
if extKey.Depth() != 3 {
return "unknown/" + c, nil
}
if bip == "" {
if extKey.Version() == p.XPubMagicSegwitP2sh {
bip = "49"
} else if extKey.Version() == p.XPubMagicSegwitNative {
bip = "84"
} else {
bip = "44"
}
}
return "m/" + bip + "'/" + strconv.Itoa(int(p.Slip44)) + "'/" + c, nil
return "m/" + descriptor.Bip + "'/" + strconv.Itoa(int(p.Slip44)) + "'/" + c, nil
}

View File

@ -718,6 +718,212 @@ func TestUnpackTx(t *testing.T) {
}
}
func TestParseXpubDescriptors(t *testing.T) {
btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518})
btcTestnetParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198})
tests := []struct {
name string
xpub string
parser *BitcoinParser
want *bchain.XpubDescriptor
wantErr bool
}{
{
name: "tpub",
xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN",
parser: btcTestnetParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN",
Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN",
Type: bchain.P2PKH,
Bip: "44",
ChangeIndexes: []uint32{0, 1},
},
},
{
name: "tr(tpub)",
xpub: "tr(tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN)",
parser: btcTestnetParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "tr(tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN)",
Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN",
Type: bchain.P2TR,
Bip: "86",
ChangeIndexes: []uint32{0, 1},
},
},
{
name: "tr([5c9e228d/86'/1'/0']tpubD/{0,1,2}/*)#4rqwxvej",
xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1,2}/*)#4rqwxvej",
parser: btcTestnetParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1,2}/*)#4rqwxvej",
Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN",
Type: bchain.P2TR,
Bip: "86",
ChangeIndexes: []uint32{0, 1, 2},
},
},
{
name: "tr([5c9e228d/86'/1'/0']tpubD/<0;1;2>/*)#4rqwxvej",
xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/<0;1;2>/*)#4rqwxvej",
parser: btcTestnetParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/<0;1;2>/*)#4rqwxvej",
Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN",
Type: bchain.P2TR,
Bip: "86",
ChangeIndexes: []uint32{0, 1, 2},
},
},
{
name: "tr([5c9e228d/86'/1'/0']tpubD/3/*)#4rqwxvej",
xpub: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/3/*)#4rqwxvej",
parser: btcTestnetParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/3/*)#4rqwxvej",
Xpub: "tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN",
Type: bchain.P2TR,
Bip: "86",
ChangeIndexes: []uint32{3},
},
},
{
name: "xpub",
xpub: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj",
parser: btcMainParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj",
Xpub: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj",
Type: bchain.P2PKH,
Bip: "44",
ChangeIndexes: []uint32{0, 1},
},
},
{
name: "ypub",
xpub: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP",
parser: btcMainParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP",
Xpub: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP",
Type: bchain.P2SHWPKH,
Bip: "49",
ChangeIndexes: []uint32{0, 1},
},
},
{
name: "zpub",
xpub: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs",
parser: btcMainParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs",
Xpub: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs",
Type: bchain.P2WPKH,
Bip: "84",
ChangeIndexes: []uint32{0, 1},
},
},
{
name: "sh(wpkh([5c9e228d/99'/0'/0']xpub/{122,123,4431}/*))",
xpub: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/{122,123,4431}/*))",
parser: btcMainParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/{122,123,4431}/*))",
Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ",
Type: bchain.P2SHWPKH,
Bip: "99",
ChangeIndexes: []uint32{122, 123, 4431},
},
},
{
name: "sh(wpkh([5c9e228d/99'/0'/0']xpub/<122;123;4431>/*))",
xpub: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<122;123;4431>/*))",
parser: btcMainParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<122;123;4431>/*))",
Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ",
Type: bchain.P2SHWPKH,
Bip: "99",
ChangeIndexes: []uint32{122, 123, 4431},
},
},
{
name: "pkh(xpub)",
xpub: "pkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)",
parser: btcMainParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "pkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)",
Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ",
Type: bchain.P2PKH,
Bip: "44",
ChangeIndexes: []uint32{0, 1},
},
},
{
name: "sh(wpkh(xpub))",
xpub: "sh(wpkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ))",
parser: btcMainParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "sh(wpkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ))",
Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ",
Type: bchain.P2SHWPKH,
Bip: "49",
ChangeIndexes: []uint32{0, 1},
},
},
{
name: "wpkh(xpub)",
xpub: "wpkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)",
parser: btcMainParser,
want: &bchain.XpubDescriptor{
XpubDescriptor: "wpkh(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)",
Xpub: "xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ",
Type: bchain.P2WPKH,
Bip: "84",
ChangeIndexes: []uint32{0, 1},
},
},
{
name: "xxx(xpub) error - unknown output script",
xpub: "xxx(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ)",
parser: btcMainParser,
wantErr: true,
},
{
name: "sh(wpkh([5c9e228d/99'/0'/0']xpub/{0,123,4431}/1)) error - * in index is mandatory",
xpub: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/{122,123,4431}/1))",
parser: btcMainParser,
wantErr: true,
},
{
name: "sh(wpkh([5c9e228d/99'/0'/0']xpub/{0,123,4431}/1) error - path too long",
xpub: "sh(wpkh([5c9e228d/99'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/{122,123,4431}/1/*))",
parser: btcMainParser,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.parser.ParseXpub(tt.xpub)
if (err != nil) != tt.wantErr {
t.Errorf("ParseXpub() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil {
if got.ExtKey == nil {
t.Errorf("ParseXpub() got nil ExtKey")
return
}
got.ExtKey = nil
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseXpub() = %+v, want %+v", got, tt.want)
}
}
})
}
}
func TestDeriveAddressDescriptors(t *testing.T) {
btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518})
btcTestnetParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198})
@ -796,7 +1002,12 @@ func TestDeriveAddressDescriptors(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.args.parser.DeriveAddressDescriptors(tt.args.xpub, tt.args.change, tt.args.indexes)
descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub)
if err != nil {
t.Errorf("ParseXpub() error = %v", err)
return
}
got, err := tt.args.parser.DeriveAddressDescriptors(descriptor, tt.args.change, tt.args.indexes)
if (err != nil) != tt.wantErr {
t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr)
return
@ -891,7 +1102,12 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(tt.args.xpub, tt.args.change, tt.args.fromIndex, tt.args.toIndex)
descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub)
if err != nil {
t.Errorf("ParseXpub() error = %v", err)
return
}
got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(descriptor, tt.args.change, tt.args.fromIndex, tt.args.toIndex)
if (err != nil) != tt.wantErr {
t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr)
return
@ -915,21 +1131,24 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) {
func BenchmarkDeriveAddressDescriptorsFromToXpub(b *testing.B) {
btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518})
for i := 0; i < b.N; i++ {
btcMainParser.DeriveAddressDescriptorsFromTo("xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", 1, 0, 100)
descriptor, _ := btcMainParser.ParseXpub("xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj")
btcMainParser.DeriveAddressDescriptorsFromTo(descriptor, 1, 0, 100)
}
}
func BenchmarkDeriveAddressDescriptorsFromToYpub(b *testing.B) {
btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518})
for i := 0; i < b.N; i++ {
btcMainParser.DeriveAddressDescriptorsFromTo("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP", 1, 0, 100)
descriptor, _ := btcMainParser.ParseXpub("ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP")
btcMainParser.DeriveAddressDescriptorsFromTo(descriptor, 1, 0, 100)
}
}
func BenchmarkDeriveAddressDescriptorsFromToZpub(b *testing.B) {
btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518})
for i := 0; i < b.N; i++ {
btcMainParser.DeriveAddressDescriptorsFromTo("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", 1, 0, 100)
descriptor, _ := btcMainParser.ParseXpub("zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs")
btcMainParser.DeriveAddressDescriptorsFromTo(descriptor, 1, 0, 100)
}
}
@ -1006,7 +1225,12 @@ func TestBitcoinParser_DerivationBasePath(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.args.parser.DerivationBasePath(tt.args.xpub)
descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub)
if err != nil {
t.Errorf("ParseXpub() error = %v", err)
return
}
got, err := tt.args.parser.DerivationBasePath(descriptor)
if (err != nil) != tt.wantErr {
t.Errorf("BitcoinParser.DerivationBasePath() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@ -254,16 +254,28 @@ func (p *DecredParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey) (bchai
return p.GetAddrDescFromAddress(addr.String())
}
// DeriveAddressDescriptors derives address descriptors from given xpub for
// listed indexes
func (p *DecredParser) DeriveAddressDescriptors(xpub string, change uint32,
indexes []uint32) ([]bchain.AddressDescriptor, error) {
// ParseXpub parses xpub (or xpub descriptor) and returns XpubDescriptor
func (p *DecredParser) ParseXpub(xpub string) (*bchain.XpubDescriptor, error) {
var descriptor bchain.XpubDescriptor
extKey, err := hdkeychain.NewKeyFromString(xpub, p.netConfig)
if err != nil {
return nil, err
}
descriptor.Xpub = xpub
descriptor.XpubDescriptor = xpub
descriptor.Type = bchain.P2PKH
descriptor.Bip = "44"
descriptor.ChangeIndexes = []uint32{0, 1}
descriptor.ExtKey = extKey
return &descriptor, nil
}
changeExtKey, err := extKey.Child(change)
// DeriveAddressDescriptors derives address descriptors from given xpub for
// listed indexes
func (p *DecredParser) DeriveAddressDescriptors(descriptor *bchain.XpubDescriptor, change uint32,
indexes []uint32) ([]bchain.AddressDescriptor, error) {
changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Child(change)
if err != nil {
return nil, err
}
@ -284,16 +296,13 @@ func (p *DecredParser) DeriveAddressDescriptors(xpub string, change uint32,
// DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for
// addresses in index range
func (p *DecredParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32,
func (p *DecredParser) DeriveAddressDescriptorsFromTo(descriptor *bchain.XpubDescriptor, change uint32,
fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) {
if toIndex <= fromIndex {
return nil, errors.New("toIndex<=fromIndex")
}
extKey, err := hdkeychain.NewKeyFromString(xpub, p.netConfig)
if err != nil {
return nil, err
}
changeExtKey, err := extKey.Child(change)
changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Child(change)
if err != nil {
return nil, err
}
@ -316,9 +325,9 @@ func (p *DecredParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32
// m/44'/<coin type>'/<account>'/<branch>/<address index>. This function only
// returns a path up to m/44'/<coin type>'/<account>'/ whereby the rest of the
// other details (<branch>/<address index>) are populated automatically.
func (p *DecredParser) DerivationBasePath(xpub string) (string, error) {
func (p *DecredParser) DerivationBasePath(descriptor *bchain.XpubDescriptor) (string, error) {
var c string
cn, depth, err := p.decodeXpub(xpub)
cn, depth, err := p.decodeXpub(descriptor.Xpub)
if err != nil {
return "", err
}

View File

@ -1,3 +1,4 @@
//go:build unittest
// +build unittest
package dcr
@ -336,7 +337,12 @@ func TestDeriveAddressDescriptors(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.args.parser.DeriveAddressDescriptors(tt.args.xpub, tt.args.change, tt.args.indexes)
descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub)
if err != nil {
t.Errorf("ParseXpub() error = %v", err)
return
}
got, err := tt.args.parser.DeriveAddressDescriptors(descriptor, tt.args.change, tt.args.indexes)
if (err != nil) != tt.wantErr {
t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr)
return
@ -419,7 +425,12 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(tt.args.xpub, tt.args.change, tt.args.fromIndex, tt.args.toIndex)
descriptor, err := tt.args.parser.ParseXpub(tt.args.xpub)
if err != nil {
t.Errorf("ParseXpub() error = %v", err)
return
}
got, err := tt.args.parser.DeriveAddressDescriptorsFromTo(descriptor, tt.args.change, tt.args.fromIndex, tt.args.toIndex)
if (err != nil) != tt.wantErr {
t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr)
return
@ -470,7 +481,12 @@ func TestDerivationBasePath(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.parser.DerivationBasePath(tt.xpub)
descriptor, err := tt.parser.ParseXpub(tt.xpub)
if err != nil {
t.Errorf("ParseXpub() error = %v", err)
return
}
got, err := tt.parser.DerivationBasePath(descriptor)
if err != nil {
t.Errorf("DerivationBasePath() expected no error but got %v", err)
return

View File

@ -161,15 +161,11 @@ func (p *NulsParser) ParseTx(b []byte) (*bchain.Tx, error) {
}
// DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for addresses in index range
func (p *NulsParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) {
func (p *NulsParser) DeriveAddressDescriptorsFromTo(descriptor *bchain.XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) {
if toIndex <= fromIndex {
return nil, errors.New("toIndex<=fromIndex")
}
extKey, err := hdkeychain.NewKeyFromString(xpub, p.Params.Base58CksumHasher)
if err != nil {
return nil, err
}
changeExtKey, err := extKey.Derive(change)
changeExtKey, err := descriptor.ExtKey.(*hdkeychain.ExtendedKey).Derive(change)
if err != nil {
return nil, err
}

View File

@ -486,7 +486,12 @@ func TestDeriveAddressDescriptorsFromTo(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parser.DeriveAddressDescriptorsFromTo(tt.args.xpub, tt.args.change, tt.args.fromIndex, tt.args.toIndex)
descriptor, err := parser.ParseXpub(tt.args.xpub)
if err != nil {
t.Errorf("ParseXpub() error = %v", err)
return
}
got, err := parser.DeriveAddressDescriptorsFromTo(descriptor, tt.args.change, tt.args.fromIndex, tt.args.toIndex)
if (err != nil) != tt.wantErr {
t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@ -197,7 +197,7 @@ func AddressDescriptorFromString(s string) (AddressDescriptor, error) {
if len(s) > 3 && s[0:3] == "ad:" {
return hex.DecodeString(s[3:])
}
return nil, errors.New("Not AddressDescriptor")
return nil, errors.New("invalid address descriptor")
}
// EthereumType specific
@ -224,6 +224,28 @@ type MempoolTxidEntry struct {
Time uint32
}
// ScriptType - type of output script parsed from xpub (descriptor)
type ScriptType int
// ScriptType enumeration
const (
P2PK = ScriptType(iota)
P2PKH
P2SHWPKH
P2WPKH
P2TR
)
// XpubDescriptor contains parsed data from xpub descriptor
type XpubDescriptor struct {
XpubDescriptor string // The whole descriptor
Xpub string // Xpub part of the descriptor
Type ScriptType
Bip string
ChangeIndexes []uint32
ExtKey interface{} // extended key parsed from xpub, usually of type *hdkeychain.ExtendedKey
}
// MempoolTxidEntries is array of MempoolTxidEntry
type MempoolTxidEntries []MempoolTxidEntry
@ -317,9 +339,10 @@ type BlockChainParser interface {
UnpackBlockHash(buf []byte) (string, error)
ParseBlock(b []byte) (*Block, error)
// xpub
DerivationBasePath(xpub string) (string, error)
DeriveAddressDescriptors(xpub string, change uint32, indexes []uint32) ([]AddressDescriptor, error)
DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error)
ParseXpub(xpub string) (*XpubDescriptor, error)
DerivationBasePath(descriptor *XpubDescriptor) (string, error)
DeriveAddressDescriptors(descriptor *XpubDescriptor, change uint32, indexes []uint32) ([]AddressDescriptor, error)
DeriveAddressDescriptorsFromTo(descriptor *XpubDescriptor, change uint32, fromIndex uint32, toIndex uint32) ([]AddressDescriptor, error)
// EthereumType specific
EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error)
}

View File

@ -413,7 +413,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
contentType: "text/html; charset=utf-8",
body: []string{
`<a class="navbar-brand" href="/">Fake Coin Explorer</a>`,
`<h1>XPUB <small class="text-muted">0 FAKE</small></h1><div class="alert alert-data ellipsis"><span class="data">tr([5c9e228d/86&#39;/1&#39;/0&#39;]tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/0/*)#4rqwxvej</span></div><h3>Confirmed</h3>`,
`<h1>XPUB <small class="text-muted">0 FAKE</small></h1><div class="alert alert-data ellipsis"><span class="data">tr([5c9e228d/86&#39;/1&#39;/0&#39;]tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej</span></div><h3>Confirmed</h3>`,
`<tr><td style="width: 25%;">Total Received</td><td class="data">0 FAKE</td></tr>`,
`<tr><td>Total Sent</td><td class="data">0 FAKE</td></tr>`,
`<tr><td>Used XPUB Addresses</td><td class="data">0</td></tr>`,
@ -719,7 +719,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/0/*)#4rqwxvej","balance":"0","totalReceived":"0","totalSent":"0","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":0,"tokens":[{"type":"XPUBAddress","name":"tb1pswrqtykue8r89t9u4rprjs0gt4qzkdfuursfnvqaa3f2yql07zmq8s8a5u","path":"m/86'/1'/0'/0/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p8tvmvsvhsee73rhym86wt435qrqm92psfsyhy6a3n5gw455znnpqm8wald","path":"m/86'/1'/0'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p537ddhyuydg5c2v75xxmn6ac64yz4xns2x0gpdcwj5vzzzgrywlqlqwk43","path":"m/86'/1'/0'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1pn2d0yjeedavnkd8z8lhm566p0f2utm3lgvxrsdehnl94y34txmts5s7t4c","path":"m/86'/1'/0'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p0pnd6ue5vryymvd28aeq3kdz6rmsdjqrq6eespgtg8wdgnxjzjksujhq4u","path":"m/86'/1'/0'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p29gpmd96hhgf7wj2vs03ca7x2xx39g8t6e0p55h2d5ssqs4fsj8qtx00wc","path":"m/86'/1'/0'/1/2","transfers":0,"decimals":8}]}`,
`{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej","balance":"0","totalReceived":"0","totalSent":"0","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":0,"tokens":[{"type":"XPUBAddress","name":"tb1pswrqtykue8r89t9u4rprjs0gt4qzkdfuursfnvqaa3f2yql07zmq8s8a5u","path":"m/86'/1'/0'/0/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p8tvmvsvhsee73rhym86wt435qrqm92psfsyhy6a3n5gw455znnpqm8wald","path":"m/86'/1'/0'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p537ddhyuydg5c2v75xxmn6ac64yz4xns2x0gpdcwj5vzzzgrywlqlqwk43","path":"m/86'/1'/0'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1pn2d0yjeedavnkd8z8lhm566p0f2utm3lgvxrsdehnl94y34txmts5s7t4c","path":"m/86'/1'/0'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p0pnd6ue5vryymvd28aeq3kdz6rmsdjqrq6eespgtg8wdgnxjzjksujhq4u","path":"m/86'/1'/0'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"tb1p29gpmd96hhgf7wj2vs03ca7x2xx39g8t6e0p55h2d5ssqs4fsj8qtx00wc","path":"m/86'/1'/0'/1/2","transfers":0,"decimals":8}]}`,
},
},
{

View File

@ -18,7 +18,7 @@ const (
TxidB2T4 = "fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db"
Xpub = "upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q"
TaprootDescriptor = "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/0/*)#4rqwxvej"
TaprootDescriptor = "tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej"
Addr1 = "mfcWp7DB6NuaZsExybTTXpVgWz559Np4Ti" // 76a914010d39800f86122416e28f485029acf77507169288ac
Addr2 = "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz" // 76a9148bdf0aa3c567aa5975c2e61321b8bebbe7293df688ac