Parse xpub descriptors
This commit is contained in:
parent
c4128e5c9c
commit
e500d6873d
64
api/xpub.go
64
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)
|
||||
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
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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'/1'/0']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'/1'/0']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}]}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user