From e500d6873dba05153a0c163fc43647f904b7dcd0 Mon Sep 17 00:00:00 2001 From: Martin Boehm Date: Mon, 18 Oct 2021 13:58:45 +0200 Subject: [PATCH] Parse xpub descriptors --- api/xpub.go | 70 +++++--- bchain/baseparser.go | 11 +- bchain/coins/btc/bitcoinlikeparser.go | 204 ++++++++++++++------- bchain/coins/btc/bitcoinparser_test.go | 236 ++++++++++++++++++++++++- bchain/coins/dcr/decredparser.go | 35 ++-- bchain/coins/dcr/decredparser_test.go | 22 ++- bchain/coins/nuls/nulsparser.go | 8 +- bchain/coins/nuls/nulsparser_test.go | 7 +- bchain/types.go | 31 +++- server/public_test.go | 4 +- tests/dbtestdata/dbtestdata.go | 2 +- 11 files changed, 502 insertions(+), 128 deletions(-) diff --git a/api/xpub.go b/api/xpub.go index 66b00687..153eaa97 100644 --- a/api/xpub.go +++ b/api/xpub.go @@ -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) - 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 + 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 + } } } 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 diff --git a/bchain/baseparser.go b/bchain/baseparser.go index b16c1f9a..0f1ebe57 100644 --- a/bchain/baseparser.go +++ b/bchain/baseparser.go @@ -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") } diff --git a/bchain/coins/btc/bitcoinlikeparser.go b/bchain/coins/btc/bitcoinlikeparser.go index 60f5707e..3f168239 100644 --- a/bchain/coins/btc/bitcoinlikeparser.go +++ b/bchain/coins/btc/bitcoinlikeparser.go @@ -8,6 +8,7 @@ import ( "math/big" "regexp" "strconv" + "strings" "unicode/utf8" vlq "github.com/bsm/go-vlq" @@ -360,28 +361,27 @@ 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 != "" { + switch descriptor.Type { + case bchain.P2PKH: + a, err = extKey.Address(p.Params) + case bchain.P2SHWPKH: + // redeemScript <20-byte-pubKeyHash> + pubKeyHash := btcutil.Hash160(extKey.PubKeyBytes()) + redeemScript := make([]byte, len(pubKeyHash)+2) + redeemScript[0] = 0 + redeemScript[1] = byte(len(pubKeyHash)) + copy(redeemScript[2:], pubKeyHash) + hash := btcutil.Hash160(redeemScript) + a, err = btcutil.NewAddressScriptHashFromHash(hash, p.Params) + case bchain.P2WPKH: + a, err = btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(extKey.PubKeyBytes()), p.Params) + case bchain.P2TR: a, err = p.taprootAddrFromExtKey(extKey) - } else { - version := extKey.Version() - if version == p.XPubMagicSegwitP2sh { - // redeemScript <20-byte-pubKeyHash> - pubKeyHash := btcutil.Hash160(extKey.PubKeyBytes()) - redeemScript := make([]byte, len(pubKeyHash)+2) - redeemScript[0] = 0 - redeemScript[1] = byte(len(pubKeyHash)) - copy(redeemScript[2:], pubKeyHash) - hash := btcutil.Hash160(redeemScript) - a, err = btcutil.NewAddressScriptHashFromHash(hash, p.Params) - } else if version == p.XPubMagicSegwitNative { - a, err = btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(extKey.PubKeyBytes()), p.Params) - } else { - // default to P2PKH address - a, err = extKey.Address(p.Params) - } + 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(sh\(wpkh|wpkh|pk|pkh|wpkh|wsh|tr))\((\[\w+/(?P\d+)'/\d+'?/\d+'?\])?(?P\w+)(/(({(?P\d+(,\d+)*)})|(<(?P\d+(;\d+)*)>)|(?P\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 } diff --git a/bchain/coins/btc/bitcoinparser_test.go b/bchain/coins/btc/bitcoinparser_test.go index 8bce4ef3..cfa8bb5e 100644 --- a/bchain/coins/btc/bitcoinparser_test.go +++ b/bchain/coins/btc/bitcoinparser_test.go @@ -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 diff --git a/bchain/coins/dcr/decredparser.go b/bchain/coins/dcr/decredparser.go index 9868266c..8d20269d 100644 --- a/bchain/coins/dcr/decredparser.go +++ b/bchain/coins/dcr/decredparser.go @@ -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'/'/'//
. This function only // returns a path up to m/44'/'/'/ whereby the rest of the // other details (/
) 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 } diff --git a/bchain/coins/dcr/decredparser_test.go b/bchain/coins/dcr/decredparser_test.go index eaff0978..67a3641d 100644 --- a/bchain/coins/dcr/decredparser_test.go +++ b/bchain/coins/dcr/decredparser_test.go @@ -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 diff --git a/bchain/coins/nuls/nulsparser.go b/bchain/coins/nuls/nulsparser.go index b8531e60..3c3f97d8 100644 --- a/bchain/coins/nuls/nulsparser.go +++ b/bchain/coins/nuls/nulsparser.go @@ -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 } diff --git a/bchain/coins/nuls/nulsparser_test.go b/bchain/coins/nuls/nulsparser_test.go index 15e90526..82d9ea9b 100644 --- a/bchain/coins/nuls/nulsparser_test.go +++ b/bchain/coins/nuls/nulsparser_test.go @@ -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 diff --git a/bchain/types.go b/bchain/types.go index 78c8a531..dd7e7cd6 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -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) } diff --git a/server/public_test.go b/server/public_test.go index 96013a81..2967e4a5 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -413,7 +413,7 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) { contentType: "text/html; charset=utf-8", body: []string{ `Fake Coin Explorer`, - `

XPUB 0 FAKE

tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/0/*)#4rqwxvej

Confirmed

`, + `

XPUB 0 FAKE

tr([5c9e228d/86'/1'/0']tpubDC88gkaZi5HvJGxGDNLADkvtdpni3mLmx6vr2KnXmWMG8zfkBRggsxHVBkUpgcwPe2KKpkyvTJCdXHb1UHEWE64vczyyPQfHr1skBcsRedN/{0,1}/*)#4rqwxvej

Confirmed

`, `Total Received0 FAKE`, `Total Sent0 FAKE`, `Used XPUB Addresses0`, @@ -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}]}`, }, }, { diff --git a/tests/dbtestdata/dbtestdata.go b/tests/dbtestdata/dbtestdata.go index f79a9f2a..4f9f74a4 100644 --- a/tests/dbtestdata/dbtestdata.go +++ b/tests/dbtestdata/dbtestdata.go @@ -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