diff --git a/.gitignore b/.gitignore index 4f23b5df..8d866cba 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,10 @@ notes.txt debug* .vscode docker/blockbook -build -!build/templates -!build/docker +build/pkg-defs +build/blockbook +build/ldb +build/sst_dump +build/*.deb .bin-image .deb-image \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock index f26472e8..4bbfcd31 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -94,8 +94,8 @@ [[projects]] branch = "master" name = "github.com/martinboehm/btcutil" - packages = [".","base58","bech32","chaincfg","txscript"] - revision = "613fec26904062ae125fb073762af3a77c77b6c7" + packages = [".","base58","bech32","chaincfg","hdkeychain","txscript"] + revision = "63034958e64b209cb9294128309dbaed497cde7b" [[projects]] branch = "master" diff --git a/api/types.go b/api/types.go index cd1c7450..44751c3d 100644 --- a/api/types.go +++ b/api/types.go @@ -5,28 +5,36 @@ import ( "blockbook/common" "blockbook/db" "encoding/json" + "errors" "math/big" "time" ) +const maxUint32 = ^uint32(0) const maxInt = int(^uint(0) >> 1) +const maxInt64 = int64(^uint64(0) >> 1) -// GetAddressOption specifies what data returns GetAddress api call -type GetAddressOption int +// AccountDetails specifies what data returns GetAddress and GetXpub calls +type AccountDetails int const ( - // Basic - only that address is indexed and some basic info - Basic GetAddressOption = iota - // Balance - only balances - Balance - // TxidHistory - balances and txids, subject to paging - TxidHistory - // TxHistoryLight - balances and easily obtained tx data (not requiring request to backend), subject to paging - TxHistoryLight - // TxHistory - balances and full tx data, subject to paging - TxHistory + // AccountDetailsBasic - only that address is indexed and some basic info + AccountDetailsBasic AccountDetails = iota + // AccountDetailsTokens - basic info + tokens + AccountDetailsTokens + // AccountDetailsTokenBalances - basic info + token with balance + AccountDetailsTokenBalances + // AccountDetailsTxidHistory - basic + token balances + txids, subject to paging + AccountDetailsTxidHistory + // AccountDetailsTxHistoryLight - basic + tokens + easily obtained tx data (not requiring requests to backend), subject to paging + AccountDetailsTxHistoryLight + // AccountDetailsTxHistory - basic + tokens + full tx data, subject to paging + AccountDetailsTxHistory ) +// ErrUnsupportedXpub is returned when coin type does not support xpub address derivation or provided string is not an xpub +var ErrUnsupportedXpub = errors.New("XPUB not supported") + // APIError extends error by information if the error details should be returned to the end user type APIError struct { Text string @@ -48,6 +56,11 @@ func NewAPIError(s string, public bool) error { // Amount is datatype holding amounts type Amount big.Int +// IsZeroBigInt if big int has zero value +func IsZeroBigInt(b *big.Int) bool { + return len(b.Bits()) == 0 +} + // MarshalJSON Amount serialization func (a *Amount) MarshalJSON() (out []byte, err error) { if a == nil { @@ -123,16 +136,22 @@ type TokenType string // ERC20TokenType is Ethereum ERC20 token const ERC20TokenType TokenType = "ERC20" +// XPUBAddressTokenType is address derived from xpub +const XPUBAddressTokenType TokenType = "XPUBAddress" + // Token contains info about tokens held by an address type Token struct { - Type TokenType `json:"type"` - Contract string `json:"contract"` - Transfers int `json:"transfers"` - Name string `json:"name"` - Symbol string `json:"symbol"` - Decimals int `json:"decimals"` - BalanceSat *Amount `json:"balance,omitempty"` - ContractIndex string `json:"-"` + Type TokenType `json:"type"` + Name string `json:"name"` + Path string `json:"path,omitempty"` + Contract string `json:"contract,omitempty"` + Transfers int `json:"transfers"` + Symbol string `json:"symbol,omitempty"` + Decimals int `json:"decimals,omitempty"` + BalanceSat *Amount `json:"balance,omitempty"` + TotalReceivedSat *Amount `json:"totalReceived,omitempty"` + TotalSentSat *Amount `json:"totalSent,omitempty"` + ContractIndex string `json:"-"` } // TokenTransfer contains info about a token transfer done in a transaction @@ -185,6 +204,9 @@ type Paging struct { ItemsOnPage int `json:"itemsOnPage,omitempty"` } +// TokenDetailLevel specifies detail level of tokens returned by GetAddress and GetXpubAddress +type TokenDetailLevel int + const ( // AddressFilterVoutOff disables filtering of transactions by vout AddressFilterVoutOff = -1 @@ -192,6 +214,13 @@ const ( AddressFilterVoutInputs = -2 // AddressFilterVoutOutputs specifies that only txs where the address is as output are returned AddressFilterVoutOutputs = -3 + + // TokenDetailNonzeroBalance - use to return only tokens with nonzero balance + TokenDetailNonzeroBalance TokenDetailLevel = 0 + // TokenDetailUsed - use to return tokens with some transfers (even if they have zero balance now) + TokenDetailUsed TokenDetailLevel = 1 + // TokenDetailDiscovered - use to return all discovered tokens + TokenDetailDiscovered TokenDetailLevel = 2 ) // AddressFilter is used to filter data returned from GetAddress api method @@ -200,6 +229,9 @@ type AddressFilter struct { Contract string FromHeight uint32 ToHeight uint32 + TokenLevel TokenDetailLevel + // OnlyConfirmed set to true will ignore mempool transactions; mempool is also ignored if FromHeight/ToHeight filter is specified + OnlyConfirmed bool } // Address holds information about address and its transactions @@ -216,18 +248,41 @@ type Address struct { Transactions []*Tx `json:"transactions,omitempty"` Txids []string `json:"txids,omitempty"` Nonce string `json:"nonce,omitempty"` + TotalTokens int `json:"totalTokens,omitempty"` Tokens []Token `json:"tokens,omitempty"` Erc20Contract *bchain.Erc20Contract `json:"erc20contract,omitempty"` - Filter string `json:"-"` + // helpers for explorer + Filter string `json:"-"` + XPubAddresses map[string]struct{} `json:"-"` } -// AddressUtxo holds information about address and its transactions -type AddressUtxo struct { +// Utxo is one unspent transaction output +type Utxo struct { Txid string `json:"txid"` Vout int32 `json:"vout"` AmountSat *Amount `json:"value"` Height int `json:"height,omitempty"` Confirmations int `json:"confirmations"` + Address string `json:"address,omitempty"` + Path string `json:"path,omitempty"` +} + +// Utxos is array of Utxo +type Utxos []Utxo + +func (a Utxos) Len() int { return len(a) } +func (a Utxos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Utxos) Less(i, j int) bool { + // sort in reverse order, unconfirmed (height==0) utxos on top + hi := a[i].Height + hj := a[j].Height + if hi == 0 { + hi = maxInt + } + if hj == 0 { + hj = maxInt + } + return hi >= hj } // Blocks is list of blocks with paging information diff --git a/api/typesv1.go b/api/typesv1.go index 52703d49..bcdad73f 100644 --- a/api/typesv1.go +++ b/api/typesv1.go @@ -192,7 +192,7 @@ func (w *Worker) AddressToV1(a *Address) *AddressV1 { } // AddressUtxoToV1 converts []AddressUtxo to []AddressUtxoV1 -func (w *Worker) AddressUtxoToV1(au []AddressUtxo) []AddressUtxoV1 { +func (w *Worker) AddressUtxoToV1(au Utxos) []AddressUtxoV1 { d := w.chainParser.AmountDecimals() v1 := make([]AddressUtxoV1, len(au)) for i := range au { diff --git a/api/worker.go b/api/worker.go index fd1fb9b6..208ef867 100644 --- a/api/worker.go +++ b/api/worker.go @@ -51,7 +51,7 @@ func (w *Worker) getAddressesFromVout(vout *bchain.Vout) (bchain.AddressDescript // setSpendingTxToVout is helper function, that finds transaction that spent given output and sets it to the output // there is no direct index for the operation, it must be found using addresses -> txaddresses -> tx func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) error { - err := w.db.GetAddrDescTransactions(vout.AddrDesc, height, ^uint32(0), func(t string, height uint32, indexes []int32) error { + err := w.db.GetAddrDescTransactions(vout.AddrDesc, height, maxUint32, func(t string, height uint32, indexes []int32) error { for _, index := range indexes { // take only inputs if index < 0 { @@ -364,7 +364,7 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool } else { to := filter.ToHeight if to == 0 { - to = ^uint32(0) + to = maxUint32 } err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, callback) if err != nil { @@ -479,7 +479,7 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) { }, from, to, page } -func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, option GetAddressOption, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.Erc20Contract, uint64, int, int, error) { +func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.Erc20Contract, uint64, int, int, error) { var ( ba *db.AddrBalance tokens []Token @@ -515,53 +515,55 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto return nil, nil, nil, 0, 0, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true) } } - tokens = make([]Token, len(ca.Contracts)) - var j int - for i, c := range ca.Contracts { - if len(filterDesc) > 0 { - if !bytes.Equal(filterDesc, c.Contract) { - continue + if details > AccountDetailsBasic { + tokens = make([]Token, len(ca.Contracts)) + var j int + for i, c := range ca.Contracts { + if len(filterDesc) > 0 { + if !bytes.Equal(filterDesc, c.Contract) { + continue + } + // filter only transactions of this contract + filter.Vout = i + 1 } - // filter only transactions of this contract - filter.Vout = i + 1 - } - validContract := true - ci, err := w.chain.EthereumTypeGetErc20ContractInfo(c.Contract) - if err != nil { - return nil, nil, nil, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetErc20ContractInfo %v", c.Contract) - } - if ci == nil { - ci = &bchain.Erc20Contract{} - addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(c.Contract) - if len(addresses) > 0 { - ci.Contract = addresses[0] - ci.Name = addresses[0] - } - validContract = false - } - // do not read contract balances etc in case of Basic option - if option != Basic && validContract { - b, err = w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract) + validContract := true + ci, err := w.chain.EthereumTypeGetErc20ContractInfo(c.Contract) if err != nil { - // return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractBalance %v %v", addrDesc, c.Contract) - glog.Warningf("EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v", addrDesc, c.Contract, err) + return nil, nil, nil, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetErc20ContractInfo %v", c.Contract) } - } else { - b = nil + if ci == nil { + ci = &bchain.Erc20Contract{} + addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(c.Contract) + if len(addresses) > 0 { + ci.Contract = addresses[0] + ci.Name = addresses[0] + } + validContract = false + } + // do not read contract balances etc in case of Basic option + if details >= AccountDetailsTokenBalances && validContract { + b, err = w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract) + if err != nil { + // return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractBalance %v %v", addrDesc, c.Contract) + glog.Warningf("EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v", addrDesc, c.Contract, err) + } + } else { + b = nil + } + tokens[j] = Token{ + Type: ERC20TokenType, + BalanceSat: (*Amount)(b), + Contract: ci.Contract, + Name: ci.Name, + Symbol: ci.Symbol, + Transfers: int(c.Txs), + Decimals: ci.Decimals, + ContractIndex: strconv.Itoa(i + 1), + } + j++ } - tokens[j] = Token{ - Type: ERC20TokenType, - BalanceSat: (*Amount)(b), - Contract: ci.Contract, - Name: ci.Name, - Symbol: ci.Symbol, - Transfers: int(c.Txs), - Decimals: ci.Decimals, - ContractIndex: strconv.Itoa(i + 1), - } - j++ + tokens = tokens[:j] } - tokens = tokens[:j] ci, err = w.chain.EthereumTypeGetErc20ContractInfo(addrDesc) if err != nil { return nil, nil, nil, 0, 0, 0, err @@ -588,17 +590,62 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto return ba, tokens, ci, n, nonContractTxs, totalResults, nil } +func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetails) (*Tx, error) { + var tx *Tx + var err error + // only ChainBitcoinType supports TxHistoryLight + if option == AccountDetailsTxHistoryLight && w.chainType == bchain.ChainBitcoinType { + ta, err := w.db.GetTxAddresses(txid) + if err != nil { + return nil, errors.Annotatef(err, "GetTxAddresses %v", txid) + } + if ta == nil { + glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses") + // as fallback, provide empty TxAddresses to return at least something + ta = &db.TxAddresses{} + } + bi, err := w.db.GetBlockInfo(ta.Height) + if err != nil { + return nil, errors.Annotatef(err, "GetBlockInfo %v", ta.Height) + } + if bi == nil { + glog.Warning("DB inconsistency: block height ", ta.Height, ": not found in db") + // provide empty BlockInfo to return the rest of tx data + bi = &db.BlockInfo{} + } + tx = w.txFromTxAddress(txid, ta, bi, bestheight) + } else { + tx, err = w.GetTransaction(txid, false, true) + if err != nil { + return nil, errors.Annotatef(err, "GetTransaction %v", txid) + } + } + return tx, nil +} + +func (w *Worker) getAddrDescAndNormalizeAddress(address string) (bchain.AddressDescriptor, string, error) { + addrDesc, err := w.chainParser.GetAddrDescFromAddress(address) + if err != nil { + return nil, "", NewAPIError(fmt.Sprintf("Invalid address, %v", err), true) + } + // convert the address to the format defined by the parser + addresses, _, err := w.chainParser.GetAddressesFromAddrDesc(addrDesc) + if err != nil { + glog.V(2).Infof("GetAddressesFromAddrDesc error %v, %v", err, addrDesc) + } + if len(addresses) == 1 { + address = addresses[0] + } + return addrDesc, address, nil +} + // GetAddress computes address value and gets transactions for given address -func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetAddressOption, filter *AddressFilter) (*Address, error) { +func (w *Worker) GetAddress(address string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter) (*Address, error) { start := time.Now() page-- if page < 0 { page = 0 } - addrDesc, err := w.chainParser.GetAddrDescFromAddress(address) - if err != nil { - return nil, NewAPIError(fmt.Sprintf("Invalid address, %v", err), true) - } var ( ba *db.AddrBalance tokens []Token @@ -613,6 +660,10 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA nonTokenTxs int totalResults int ) + addrDesc, address, err := w.getAddrDescAndNormalizeAddress(address) + if err != nil { + return nil, err + } if w.chainType == bchain.ChainEthereumType { var n uint64 ba, tokens, erc20c, n, nonTokenTxs, totalResults, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter) @@ -635,114 +686,67 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA } } } - // get tx history if requested by option or check mempool if there are some transactions for a new address - if option >= TxidHistory || ba == nil { - // convert the address to the format defined by the parser - addresses, _, err := w.chainParser.GetAddressesFromAddrDesc(addrDesc) + // if there are only unconfirmed transactions, there is no paging + if ba == nil { + ba = &db.AddrBalance{} + page = 0 + } + // process mempool, only if toHeight is not specified + if filter.ToHeight == 0 && !filter.OnlyConfirmed { + txm, err = w.getAddressTxids(addrDesc, true, filter, maxInt) if err != nil { - glog.V(2).Infof("GetAddressesFromAddrDesc error %v, %v", err, addrDesc) + return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc) } - if len(addresses) == 1 { - address = addresses[0] - } - // get txs from mempool only if blockheight filter is off - if filter.FromHeight == 0 && filter.ToHeight == 0 { - txm, err = w.getAddressTxids(addrDesc, true, filter, maxInt) - if err != nil { - return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc) - } - } - // if there are only unconfirmed transactions, there is no paging - if ba == nil { - ba = &db.AddrBalance{} - page = 0 - } - if option >= TxidHistory { - txc, err := w.getAddressTxids(addrDesc, false, filter, (page+1)*txsOnPage) - if err != nil { - return nil, errors.Annotatef(err, "getAddressTxids %v false", addrDesc) - } - bestheight, _, err := w.db.GetBestBlock() - if err != nil { - return nil, errors.Annotatef(err, "GetBestBlock") - } - var from, to int - pg, from, to, page = computePaging(len(txc), page, txsOnPage) - if len(txc) >= txsOnPage { - if totalResults < 0 { - pg.TotalPages = -1 - } else { - pg, _, _, _ = computePaging(totalResults, page, txsOnPage) - } - } - if option == TxidHistory { - txids = make([]string, len(txm)+to-from) + for _, txid := range txm { + tx, err := w.GetTransaction(txid, false, false) + // mempool transaction may fail + if err != nil || tx == nil { + glog.Warning("GetTransaction in mempool: ", err) } else { - txs = make([]*Tx, len(txm)+to-from) - } - txi := 0 - // get mempool transactions - for _, txid := range txm { - tx, err := w.GetTransaction(txid, false, false) - // mempool transaction may fail - if err != nil || tx == nil { - glog.Warning("GetTransaction in mempool: ", err) - } else { - // skip already confirmed txs, mempool may be out of sync - if tx.Confirmations == 0 { - uBalSat.Add(&uBalSat, tx.getAddrVoutValue(addrDesc)) - uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc)) - if page == 0 { - if option == TxidHistory { - txids[txi] = tx.Txid - } else { - txs[txi] = tx - } - txi++ + // skip already confirmed txs, mempool may be out of sync + if tx.Confirmations == 0 { + uBalSat.Add(&uBalSat, tx.getAddrVoutValue(addrDesc)) + uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc)) + if page == 0 { + if option == AccountDetailsTxidHistory { + txids = append(txids, tx.Txid) + } else if option >= AccountDetailsTxHistoryLight { + txs = append(txs, tx) } } } } - // get confirmed transactions - for i := from; i < to; i++ { - txid := txc[i] - if option == TxidHistory { - txids[txi] = txid - } else { - // only ChainBitcoinType supports TxHistoryLight - if option == TxHistoryLight && w.chainType == bchain.ChainBitcoinType { - ta, err := w.db.GetTxAddresses(txid) - if err != nil { - return nil, errors.Annotatef(err, "GetTxAddresses %v", txid) - } - if ta == nil { - glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses") - // as fallback, provide empty TxAddresses to return at least something - ta = &db.TxAddresses{} - } - bi, err := w.db.GetBlockInfo(ta.Height) - if err != nil { - return nil, errors.Annotatef(err, "GetBlockInfo %v", ta.Height) - } - if bi == nil { - glog.Warning("DB inconsistency: block height ", ta.Height, ": not found in db") - // provide empty BlockInfo to return the rest of tx data - bi = &db.BlockInfo{} - } - txs[txi] = w.txFromTxAddress(txid, ta, bi, bestheight) - } else { - txs[txi], err = w.GetTransaction(txid, false, true) - if err != nil { - return nil, errors.Annotatef(err, "GetTransaction %v", txid) - } - } - } - txi++ + } + } + // get tx history if requested by option or check mempool if there are some transactions for a new address + if option >= AccountDetailsTxidHistory { + txc, err := w.getAddressTxids(addrDesc, false, filter, (page+1)*txsOnPage) + if err != nil { + return nil, errors.Annotatef(err, "getAddressTxids %v false", addrDesc) + } + bestheight, _, err := w.db.GetBestBlock() + if err != nil { + return nil, errors.Annotatef(err, "GetBestBlock") + } + var from, to int + pg, from, to, page = computePaging(len(txc), page, txsOnPage) + if len(txc) >= txsOnPage { + if totalResults < 0 { + pg.TotalPages = -1 + } else { + pg, _, _, _ = computePaging(totalResults, page, txsOnPage) } - if option == TxidHistory { - txids = txids[:txi] - } else if option >= TxHistoryLight { - txs = txs[:txi] + } + for i := from; i < to; i++ { + txid := txc[i] + if option == AccountDetailsTxidHistory { + txids = append(txids, txid) + } else { + tx, err := w.txFromTxid(txid, bestheight, option) + if err != nil { + return nil, err + } + txs = append(txs, tx) } } } @@ -770,126 +774,138 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetA return r, nil } +func (w *Worker) getAddrDescUtxo(addrDesc bchain.AddressDescriptor, ba *db.AddrBalance, onlyConfirmed bool, onlyMempool bool) (Utxos, error) { + var err error + r := make(Utxos, 0, 8) + spentInMempool := make(map[string]struct{}) + if !onlyConfirmed { + // get utxo from mempool + txm, err := w.getAddressTxids(addrDesc, true, &AddressFilter{Vout: AddressFilterVoutOff}, maxInt) + if err != nil { + return nil, err + } + if len(txm) > 0 { + mc := make([]*bchain.Tx, len(txm)) + for i, txid := range txm { + // get mempool txs and process their inputs to detect spends between mempool txs + bchainTx, _, err := w.txCache.GetTransaction(txid) + // mempool transaction may fail + if err != nil { + glog.Error("GetTransaction in mempool ", txid, ": ", err) + } else { + mc[i] = bchainTx + // get outputs spent by the mempool tx + for i := range bchainTx.Vin { + vin := &bchainTx.Vin[i] + spentInMempool[vin.Txid+strconv.Itoa(int(vin.Vout))] = struct{}{} + } + } + } + for _, bchainTx := range mc { + if bchainTx != nil { + for i := range bchainTx.Vout { + vout := &bchainTx.Vout[i] + vad, err := w.chainParser.GetAddrDescFromVout(vout) + if err == nil && bytes.Equal(addrDesc, vad) { + // report only outpoints that are not spent in mempool + _, e := spentInMempool[bchainTx.Txid+strconv.Itoa(i)] + if !e { + r = append(r, Utxo{ + Txid: bchainTx.Txid, + Vout: int32(i), + AmountSat: (*Amount)(&vout.ValueSat), + }) + } + } + } + } + } + } + } + if !onlyMempool { + // get utxo from index + if ba == nil { + ba, err = w.db.GetAddrDescBalance(addrDesc) + if err != nil { + return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) + } + } + // ba can be nil if the address is only in mempool! + if ba != nil && !IsZeroBigInt(&ba.BalanceSat) { + outpoints := make([]bchain.Outpoint, 0, 8) + err = w.db.GetAddrDescTransactions(addrDesc, 0, maxUint32, func(txid string, height uint32, indexes []int32) error { + for _, index := range indexes { + // take only outputs + if index >= 0 { + outpoints = append(outpoints, bchain.Outpoint{Txid: txid, Vout: index}) + } + } + return nil + }) + if err != nil { + return nil, err + } + var lastTxid string + var ta *db.TxAddresses + var checksum big.Int + checksum.Set(&ba.BalanceSat) + b, _, err := w.db.GetBestBlock() + if err != nil { + return nil, err + } + bestheight := int(b) + for i := len(outpoints) - 1; i >= 0 && checksum.Int64() > 0; i-- { + o := outpoints[i] + if lastTxid != o.Txid { + ta, err = w.db.GetTxAddresses(o.Txid) + if err != nil { + return nil, err + } + lastTxid = o.Txid + } + if ta == nil { + glog.Warning("DB inconsistency: tx ", o.Txid, ": not found in txAddresses") + } else { + if len(ta.Outputs) <= int(o.Vout) { + glog.Warning("DB inconsistency: txAddresses ", o.Txid, " does not have enough outputs") + } else { + if !ta.Outputs[o.Vout].Spent { + v := ta.Outputs[o.Vout].ValueSat + // report only outpoints that are not spent in mempool + _, e := spentInMempool[o.Txid+strconv.Itoa(int(o.Vout))] + if !e { + r = append(r, Utxo{ + Txid: o.Txid, + Vout: o.Vout, + AmountSat: (*Amount)(&v), + Height: int(ta.Height), + Confirmations: bestheight - int(ta.Height) + 1, + }) + } + checksum.Sub(&checksum, &v) + } + } + } + } + if checksum.Uint64() != 0 { + glog.Warning("DB inconsistency: ", addrDesc, ": checksum is not zero") + } + } + } + return r, nil +} + // GetAddressUtxo returns unspent outputs for given address -func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) ([]AddressUtxo, error) { +func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) (Utxos, error) { if w.chainType != bchain.ChainBitcoinType { return nil, NewAPIError("Not supported", true) } start := time.Now() addrDesc, err := w.chainParser.GetAddrDescFromAddress(address) if err != nil { - return nil, NewAPIError(fmt.Sprintf("Invalid address, %v", err), true) - } - spentInMempool := make(map[string]struct{}) - r := make([]AddressUtxo, 0, 8) - if !onlyConfirmed { - // get utxo from mempool - txm, err := w.getAddressTxids(addrDesc, true, &AddressFilter{Vout: AddressFilterVoutOff}, maxInt) - if err != nil { - return nil, errors.Annotatef(err, "getAddressTxids %v true", address) - } - mc := make([]*bchain.Tx, len(txm)) - for i, txid := range txm { - // get mempool txs and process their inputs to detect spends between mempool txs - bchainTx, _, err := w.txCache.GetTransaction(txid) - // mempool transaction may fail - if err != nil { - glog.Error("GetTransaction in mempool ", txid, ": ", err) - } else { - mc[i] = bchainTx - // get outputs spent by the mempool tx - for i := range bchainTx.Vin { - vin := &bchainTx.Vin[i] - spentInMempool[vin.Txid+strconv.Itoa(int(vin.Vout))] = struct{}{} - } - } - } - for _, bchainTx := range mc { - if bchainTx != nil { - for i := range bchainTx.Vout { - vout := &bchainTx.Vout[i] - vad, err := w.chainParser.GetAddrDescFromVout(vout) - if err == nil && bytes.Equal(addrDesc, vad) { - // report only outpoints that are not spent in mempool - _, e := spentInMempool[bchainTx.Txid+strconv.Itoa(i)] - if !e { - r = append(r, AddressUtxo{ - Txid: bchainTx.Txid, - Vout: int32(i), - AmountSat: (*Amount)(&vout.ValueSat), - }) - } - } - } - } - } - } - // get utxo from index - ba, err := w.db.GetAddrDescBalance(addrDesc) - if err != nil { - return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true) - } - var checksum big.Int - // ba can be nil if the address is only in mempool! - if ba != nil && ba.BalanceSat.Uint64() > 0 { - outpoints := make([]bchain.Outpoint, 0, 8) - err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, height uint32, indexes []int32) error { - for _, index := range indexes { - // take only outputs - if index >= 0 { - outpoints = append(outpoints, bchain.Outpoint{Txid: txid, Vout: index}) - } - } - return nil - }) - if err != nil { - return nil, err - } - var lastTxid string - var ta *db.TxAddresses - checksum = ba.BalanceSat - b, _, err := w.db.GetBestBlock() - if err != nil { - return nil, err - } - bestheight := int(b) - for i := len(outpoints) - 1; i >= 0 && checksum.Int64() > 0; i-- { - o := outpoints[i] - if lastTxid != o.Txid { - ta, err = w.db.GetTxAddresses(o.Txid) - if err != nil { - return nil, err - } - lastTxid = o.Txid - } - if ta == nil { - glog.Warning("DB inconsistency: tx ", o.Txid, ": not found in txAddresses") - } else { - if len(ta.Outputs) <= int(o.Vout) { - glog.Warning("DB inconsistency: txAddresses ", o.Txid, " does not have enough outputs") - } else { - if !ta.Outputs[o.Vout].Spent { - v := ta.Outputs[o.Vout].ValueSat - // report only outpoints that are not spent in mempool - _, e := spentInMempool[o.Txid+strconv.Itoa(int(o.Vout))] - if !e { - r = append(r, AddressUtxo{ - Txid: o.Txid, - Vout: o.Vout, - AmountSat: (*Amount)(&v), - Height: int(ta.Height), - Confirmations: bestheight - int(ta.Height) + 1, - }) - } - checksum.Sub(&checksum, &v) - } - } - } - } - } - if checksum.Uint64() != 0 { - glog.Warning("DB inconsistency: ", address, ": checksum is not zero") + return nil, NewAPIError(fmt.Sprintf("Invalid address '%v', %v", address, err), true) } + r, err := w.getAddrDescUtxo(addrDesc, nil, onlyConfirmed, false) glog.Info("GetAddressUtxo ", address, ", ", len(r), " utxos, finished in ", time.Since(start)) return r, nil } @@ -935,7 +951,7 @@ func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) { // if it's a number, must be less than int32 var hash string height, err := strconv.Atoi(bid) - if err == nil && height < int(^uint32(0)) { + if err == nil && height < int(maxUint32) { hash, err = w.db.GetBlockHash(uint32(height)) if err != nil { hash = bid diff --git a/api/xpub.go b/api/xpub.go new file mode 100644 index 00000000..3419b945 --- /dev/null +++ b/api/xpub.go @@ -0,0 +1,574 @@ +package api + +import ( + "blockbook/bchain" + "blockbook/db" + "fmt" + "math/big" + "sort" + "sync" + "time" + + "github.com/golang/glog" + "github.com/juju/errors" +) + +const xpubLen = 111 +const defaultAddressesGap = 20 +const maxAddressesGap = 10000 + +const txInput = 1 +const txOutput = 2 + +const xpubCacheSize = 512 +const xpubCacheExpirationSeconds = 7200 + +var cachedXpubs = make(map[string]xpubData) +var cachedXpubsMux sync.Mutex + +type xpubTxid struct { + txid string + height uint32 + inputOutput byte +} + +type xpubTxids []xpubTxid + +func (a xpubTxids) Len() int { return len(a) } +func (a xpubTxids) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a xpubTxids) Less(i, j int) bool { return a[i].height >= a[j].height } + +type xpubAddress struct { + addrDesc bchain.AddressDescriptor + balance *db.AddrBalance + txs uint32 + maxHeight uint32 + complete bool + txids xpubTxids +} + +type xpubData struct { + gap int + accessed int64 + basePath string + dataHeight uint32 + dataHash string + txCountEstimate uint32 + sentSat big.Int + balanceSat big.Int + addresses []xpubAddress + changeAddresses []xpubAddress +} + +func (w *Worker) xpubGetAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, fromHeight, toHeight uint32, maxResults int) ([]xpubTxid, bool, error) { + var err error + complete := true + txs := make([]xpubTxid, 0, 4) + var callback db.GetTransactionsCallback + callback = func(txid string, height uint32, indexes []int32) error { + // take all txs in the last found block even if it exceeds maxResults + if len(txs) >= maxResults && txs[len(txs)-1].height != height { + complete = false + return &db.StopIteration{} + } + inputOutput := byte(0) + for _, index := range indexes { + if index < 0 { + inputOutput |= txInput + } else { + inputOutput |= txOutput + } + } + txs = append(txs, xpubTxid{txid, height, inputOutput}) + return nil + } + if mempool { + uniqueTxs := make(map[string]int) + o, err := w.chain.GetMempoolTransactionsForAddrDesc(addrDesc) + if err != nil { + return nil, false, err + } + for _, m := range o { + if l, found := uniqueTxs[m.Txid]; !found { + l = len(txs) + callback(m.Txid, 0, []int32{m.Vout}) + if len(txs) > l { + uniqueTxs[m.Txid] = l - 1 + } + } else { + if m.Vout < 0 { + txs[l].inputOutput |= txInput + } else { + txs[l].inputOutput |= txOutput + } + } + } + } else { + err = w.db.GetAddrDescTransactions(addrDesc, fromHeight, toHeight, callback) + if err != nil { + return nil, false, err + } + } + return txs, complete, nil +} + +func (w *Worker) xpubCheckAndLoadTxids(ad *xpubAddress, filter *AddressFilter, maxHeight uint32, maxResults int) error { + // skip if not used + if ad.balance == nil { + return nil + } + // if completely loaded, check if there are not some new txs and load if necessary + if ad.complete { + if ad.balance.Txs != ad.txs { + newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, false, ad.maxHeight+1, maxHeight, maxInt) + if err == nil { + ad.txids = append(newTxids, ad.txids...) + ad.maxHeight = maxHeight + ad.txs = uint32(len(ad.txids)) + if ad.txs != ad.balance.Txs { + glog.Warning("xpubCheckAndLoadTxids inconsistency ", ad.addrDesc, ", ad.txs=", ad.txs, ", ad.balance.Txs=", ad.balance.Txs) + } + } + return err + } + return nil + } + // load all txids to get paging correctly + newTxids, complete, err := w.xpubGetAddressTxids(ad.addrDesc, false, 0, maxHeight, maxInt) + if err != nil { + return err + } + ad.txids = newTxids + ad.complete = complete + ad.maxHeight = maxHeight + if complete { + ad.txs = uint32(len(ad.txids)) + if ad.txs != ad.balance.Txs { + glog.Warning("xpubCheckAndLoadTxids inconsistency ", ad.addrDesc, ", ad.txs=", ad.txs, ", ad.balance.Txs=", ad.balance.Txs) + } + } + return nil +} + +func (w *Worker) xpubDerivedAddressBalance(data *xpubData, ad *xpubAddress) (bool, error) { + var err error + if ad.balance, err = w.db.GetAddrDescBalance(ad.addrDesc); err != nil { + return false, err + } + if ad.balance != nil { + data.txCountEstimate += ad.balance.Txs + data.sentSat.Add(&data.sentSat, &ad.balance.SentSat) + data.balanceSat.Add(&data.balanceSat, &ad.balance.BalanceSat) + return true, nil + } + return false, nil +} + +func (w *Worker) xpubScanAddresses(xpub string, data *xpubData, addresses []xpubAddress, gap int, change int, minDerivedIndex int, fork bool) (int, []xpubAddress, error) { + // rescan known addresses + lastUsed := 0 + for i := range addresses { + ad := &addresses[i] + if fork { + // reset the cached data + ad.txs = 0 + ad.maxHeight = 0 + ad.complete = false + ad.txids = nil + } + used, err := w.xpubDerivedAddressBalance(data, ad) + if err != nil { + return 0, nil, err + } + if used { + lastUsed = i + } + } + // derive new addresses as necessary + missing := len(addresses) - lastUsed + for missing < gap { + from := len(addresses) + to := from + gap - missing + if to < minDerivedIndex { + to = minDerivedIndex + } + descriptors, err := w.chainParser.DeriveAddressDescriptorsFromTo(xpub, uint32(change), uint32(from), uint32(to)) + if err != nil { + return 0, nil, err + } + for i, a := range descriptors { + ad := xpubAddress{addrDesc: a} + used, err := w.xpubDerivedAddressBalance(data, &ad) + if err != nil { + return 0, nil, err + } + if used { + lastUsed = i + from + } + addresses = append(addresses, ad) + } + missing = len(addresses) - lastUsed + } + return lastUsed, addresses, nil +} + +func (w *Worker) tokenFromXpubAddress(data *xpubData, ad *xpubAddress, changeIndex int, index int, option AccountDetails) Token { + a, _, _ := w.chainParser.GetAddressesFromAddrDesc(ad.addrDesc) + var address string + if len(a) > 0 { + address = a[0] + } + var balance, totalReceived, totalSent *big.Int + var transfers int + if ad.balance != nil { + transfers = int(ad.balance.Txs) + if option >= AccountDetailsTokenBalances { + balance = &ad.balance.BalanceSat + totalSent = &ad.balance.SentSat + totalReceived = ad.balance.ReceivedSat() + } + } + return Token{ + Type: XPUBAddressTokenType, + Name: address, + Decimals: w.chainParser.AmountDecimals(), + BalanceSat: (*Amount)(balance), + TotalReceivedSat: (*Amount)(totalReceived), + TotalSentSat: (*Amount)(totalSent), + Transfers: transfers, + Path: fmt.Sprintf("%s/%d/%d", data.basePath, changeIndex, index), + } +} + +func evictXpubCacheItems() { + var oldestKey string + oldest := maxInt64 + now := time.Now().Unix() + count := 0 + for k, v := range cachedXpubs { + if v.accessed+xpubCacheExpirationSeconds < now { + delete(cachedXpubs, k) + count++ + } + if v.accessed < oldest { + oldestKey = k + oldest = v.accessed + } + } + if oldestKey != "" && oldest+xpubCacheExpirationSeconds >= now { + delete(cachedXpubs, oldestKey) + count++ + } + glog.Info("Evicted ", count, " items from xpub cache, oldest item accessed at ", time.Unix(oldest, 0), ", cache size ", len(cachedXpubs)) +} + +func (w *Worker) getXpubData(xpub string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int) (*xpubData, uint32, error) { + if w.chainType != bchain.ChainBitcoinType || len(xpub) != xpubLen { + return nil, 0, ErrUnsupportedXpub + } + var ( + err error + bestheight uint32 + besthash string + ) + if gap <= 0 { + gap = defaultAddressesGap + } else if gap > maxAddressesGap { + // limit the maximum gap to protect against unreasonably big values that could cause high load of the server + gap = maxAddressesGap + } + // gap is increased one as there must be gap of empty addresses before the derivation is stopped + gap++ + var processedHash string + cachedXpubsMux.Lock() + data, found := cachedXpubs[xpub] + cachedXpubsMux.Unlock() + // to load all data for xpub may take some time, do it in a loop to process a possible new block + for { + bestheight, besthash, err = w.db.GetBestBlock() + if err != nil { + return nil, 0, errors.Annotatef(err, "GetBestBlock") + } + if besthash == processedHash { + break + } + fork := false + if !found || data.gap != gap { + data = xpubData{gap: gap} + data.basePath, err = w.chainParser.DerivationBasePath(xpub) + if err != nil { + glog.Warning("DerivationBasePath error", err) + data.basePath = "unknown" + } + } else { + hash, err := w.db.GetBlockHash(data.dataHeight) + if err != nil { + return nil, 0, err + } + if hash != data.dataHash { + // in case of for reset all cached data + fork = true + } + } + processedHash = besthash + if data.dataHeight < bestheight || fork { + data.dataHeight = bestheight + data.dataHash = besthash + 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, err + } + _, data.changeAddresses, err = w.xpubScanAddresses(xpub, &data, data.changeAddresses, gap, 1, lastUsedIndex, fork) + if err != nil { + return nil, 0, err + } + } + if option >= AccountDetailsTxidHistory { + for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for i := range da { + if err = w.xpubCheckAndLoadTxids(&da[i], filter, bestheight, (page+1)*txsOnPage); err != nil { + return nil, 0, err + } + } + } + } + } + data.accessed = time.Now().Unix() + cachedXpubsMux.Lock() + if len(cachedXpubs) >= xpubCacheSize { + evictXpubCacheItems() + } + cachedXpubs[xpub] = data + cachedXpubsMux.Unlock() + return &data, bestheight, nil +} + +// GetXpubAddress computes address value and gets transactions for given address +func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int) (*Address, error) { + start := time.Now() + page-- + if page < 0 { + page = 0 + } + type mempoolMap struct { + tx *Tx + inputOutput byte + } + var ( + txc xpubTxids + txmMap map[string]*Tx + txCount int + txs []*Tx + txids []string + pg Paging + filtered bool + err error + uBalSat big.Int + ) + data, bestheight, err := w.getXpubData(xpub, page, txsOnPage, option, filter, gap) + if err != nil { + return nil, err + } + // setup filtering of txids + var useTxids func(txid *xpubTxid, ad *xpubAddress) bool + if !(filter.FromHeight == 0 && filter.ToHeight == 0 && filter.Vout == AddressFilterVoutOff) { + toHeight := maxUint32 + if filter.ToHeight != 0 { + toHeight = filter.ToHeight + } + useTxids = func(txid *xpubTxid, ad *xpubAddress) bool { + if txid.height < filter.FromHeight || txid.height > toHeight { + return false + } + if filter.Vout != AddressFilterVoutOff { + if filter.Vout == AddressFilterVoutInputs && txid.inputOutput&txInput == 0 || + filter.Vout == AddressFilterVoutOutputs && txid.inputOutput&txOutput == 0 { + return false + } + } + return true + } + filtered = true + } + // process mempool, only if ToHeight is not specified + if filter.ToHeight == 0 && !filter.OnlyConfirmed { + txmMap = make(map[string]*Tx) + for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for i := range da { + ad := &da[i] + newTxids, _, err := w.xpubGetAddressTxids(ad.addrDesc, true, 0, 0, maxInt) + if err != nil { + return nil, err + } + for _, txid := range newTxids { + // the same tx can have multiple addresses from the same xpub, get it from backend it only once + tx, foundTx := txmMap[txid.txid] + if !foundTx { + tx, err = w.GetTransaction(txid.txid, false, false) + // mempool transaction may fail + if err != nil || tx == nil { + glog.Warning("GetTransaction in mempool: ", err) + continue + } + txmMap[txid.txid] = tx + } + // skip already confirmed txs, mempool may be out of sync + if tx.Confirmations == 0 { + uBalSat.Add(&uBalSat, tx.getAddrVoutValue(ad.addrDesc)) + uBalSat.Sub(&uBalSat, tx.getAddrVinValue(ad.addrDesc)) + if page == 0 && !foundTx && (useTxids == nil || useTxids(&txid, ad)) { + if option == AccountDetailsTxidHistory { + txids = append(txids, tx.Txid) + } else if option >= AccountDetailsTxHistoryLight { + txs = append(txs, tx) + } + } + } + + } + } + } + } + if option >= AccountDetailsTxidHistory { + txcMap := make(map[string]bool) + txc = make(xpubTxids, 0, 32) + for _, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for i := range da { + ad := &da[i] + for _, txid := range ad.txids { + added, foundTx := txcMap[txid.txid] + // count txs regardless of filter but only once + if !foundTx { + txCount++ + } + // add tx only once + if !added { + add := useTxids == nil || useTxids(&txid, ad) + txcMap[txid.txid] = add + if add { + txc = append(txc, txid) + } + } + } + } + } + sort.Stable(txc) + txCount = len(txcMap) + totalResults := txCount + if filtered { + totalResults = -1 + } + var from, to int + pg, from, to, page = computePaging(len(txc), page, txsOnPage) + if len(txc) >= txsOnPage { + if totalResults < 0 { + pg.TotalPages = -1 + } else { + pg, _, _, _ = computePaging(totalResults, page, txsOnPage) + } + } + // get confirmed transactions + for i := from; i < to; i++ { + xpubTxid := &txc[i] + if option == AccountDetailsTxidHistory { + txids = append(txids, xpubTxid.txid) + } else { + tx, err := w.txFromTxid(xpubTxid.txid, bestheight, option) + if err != nil { + return nil, err + } + txs = append(txs, tx) + } + } + } else { + txCount = int(data.txCountEstimate) + } + totalTokens := 0 + var tokens []Token + var xpubAddresses map[string]struct{} + if option > AccountDetailsBasic { + tokens = make([]Token, 0, 4) + xpubAddresses = make(map[string]struct{}) + } + for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for i := range da { + ad := &da[i] + if ad.balance != nil { + totalTokens++ + } + if option > AccountDetailsBasic { + token := w.tokenFromXpubAddress(data, ad, ci, i, option) + if filter.TokenLevel == TokenDetailDiscovered || + filter.TokenLevel == TokenDetailUsed && ad.balance != nil || + filter.TokenLevel == TokenDetailNonzeroBalance && ad.balance != nil && !IsZeroBigInt(&ad.balance.BalanceSat) { + tokens = append(tokens, token) + } + xpubAddresses[token.Name] = struct{}{} + } + } + } + var totalReceived big.Int + totalReceived.Add(&data.balanceSat, &data.sentSat) + addr := Address{ + Paging: pg, + AddrStr: xpub, + BalanceSat: (*Amount)(&data.balanceSat), + TotalReceivedSat: (*Amount)(&totalReceived), + TotalSentSat: (*Amount)(&data.sentSat), + Txs: txCount, + UnconfirmedBalanceSat: (*Amount)(&uBalSat), + UnconfirmedTxs: len(txmMap), + Transactions: txs, + Txids: txids, + TotalTokens: totalTokens, + Tokens: tokens, + XPubAddresses: xpubAddresses, + } + glog.Info("GetXpubAddress ", xpub[:16], ", ", len(data.addresses)+len(data.changeAddresses), " derived addresses, ", txCount, " confirmed txs, finished in ", 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, _, err := w.getXpubData(xpub, 0, 1, AccountDetailsBasic, &AddressFilter{ + Vout: AddressFilterVoutOff, + OnlyConfirmed: onlyConfirmed, + }, gap) + if err != nil { + return nil, err + } + r := make(Utxos, 0, 8) + for ci, da := range [][]xpubAddress{data.addresses, data.changeAddresses} { + for i := range da { + ad := &da[i] + onlyMempool := false + if ad.balance == nil { + if onlyConfirmed { + continue + } + onlyMempool = true + } + utxos, err := w.getAddrDescUtxo(ad.addrDesc, ad.balance, onlyConfirmed, onlyMempool) + if err != nil { + return nil, err + } + if len(utxos) > 0 { + t := w.tokenFromXpubAddress(data, ad, ci, i, AccountDetailsTokens) + for j := range utxos { + a := &utxos[j] + a.Address = t.Name + a.Path = t.Path + } + r = append(r, utxos...) + } + } + } + sort.Stable(r) + glog.Info("GetXpubUtxo ", xpub[:16], ", ", len(r), " utxos, finished in ", time.Since(start)) + return r, nil +} diff --git a/bchain/baseparser.go b/bchain/baseparser.go index abe665b4..55f055e8 100644 --- a/bchain/baseparser.go +++ b/bchain/baseparser.go @@ -268,6 +268,21 @@ func (p *BaseParser) UnpackTx(buf []byte) (*Tx, uint32, error) { return &tx, pt.Height, nil } +// DerivationBasePath is unsupported +func (p *BaseParser) DerivationBasePath(xpub string) (string, error) { + return "", errors.New("Not supported") +} + +// DeriveAddressDescriptors is unsupported +func (p *BaseParser) DeriveAddressDescriptors(xpub string, 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) { + return nil, errors.New("Not supported") +} + // EthereumTypeGetErc20FromTx is unsupported func (p *BaseParser) EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error) { return nil, errors.New("Not supported") diff --git a/bchain/coins/btc/bitcoinparser.go b/bchain/coins/btc/bitcoinparser.go index f3731e0a..e6a71482 100644 --- a/bchain/coins/btc/bitcoinparser.go +++ b/bchain/coins/btc/bitcoinparser.go @@ -6,12 +6,15 @@ import ( "encoding/binary" "encoding/hex" "math/big" + "strconv" vlq "github.com/bsm/go-vlq" + "github.com/juju/errors" "github.com/martinboehm/btcd/blockchain" "github.com/martinboehm/btcd/wire" "github.com/martinboehm/btcutil" "github.com/martinboehm/btcutil/chaincfg" + "github.com/martinboehm/btcutil/hdkeychain" "github.com/martinboehm/btcutil/txscript" ) @@ -23,6 +26,10 @@ type BitcoinParser struct { *bchain.BaseParser Params *chaincfg.Params OutputScriptToAddressesFunc OutputScriptToAddressesFunc + XPubMagic uint32 + XPubMagicSegwitP2sh uint32 + XPubMagicSegwitNative uint32 + Slip44 uint32 } // NewBitcoinParser returns new BitcoinParser instance @@ -32,7 +39,11 @@ func NewBitcoinParser(params *chaincfg.Params, c *Configuration) *BitcoinParser BlockAddressesToKeep: c.BlockAddressesToKeep, AmountDecimalPoint: 8, }, - Params: params, + Params: params, + XPubMagic: c.XPubMagic, + XPubMagicSegwitP2sh: c.XPubMagicSegwitP2sh, + XPubMagicSegwitNative: c.XPubMagicSegwitNative, + Slip44: c.Slip44, } p.OutputScriptToAddressesFunc = p.outputScriptToAddresses return p @@ -266,3 +277,104 @@ func (p *BitcoinParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) { return tx, height, nil } + +func (p *BitcoinParser) addrDescFromExtKey(extKey *hdkeychain.ExtendedKey) (bchain.AddressDescriptor, error) { + var a btcutil.Address + var err error + if extKey.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 extKey.Version() == p.XPubMagicSegwitNative { + a, err = btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(extKey.PubKeyBytes()), p.Params) + } else { + // default to P2PKH address + a, err = extKey.Address(p.Params) + } + if err != nil { + return nil, err + } + return txscript.PayToAddrScript(a) +} + +// DeriveAddressDescriptors derives address descriptors from given xpub for listed indexes +func (p *BitcoinParser) DeriveAddressDescriptors(xpub string, change uint32, indexes []uint32) ([]bchain.AddressDescriptor, error) { + extKey, err := hdkeychain.NewKeyFromString(xpub) + if err != nil { + return nil, err + } + changeExtKey, err := extKey.Child(change) + if err != nil { + return nil, err + } + ad := make([]bchain.AddressDescriptor, len(indexes)) + for i, index := range indexes { + indexExtKey, err := changeExtKey.Child(index) + if err != nil { + return nil, err + } + ad[i], err = p.addrDescFromExtKey(indexExtKey) + if err != nil { + return nil, err + } + } + return ad, nil +} + +// DeriveAddressDescriptorsFromTo derives address descriptors from given xpub for addresses in index range +func (p *BitcoinParser) DeriveAddressDescriptorsFromTo(xpub string, change uint32, fromIndex uint32, toIndex uint32) ([]bchain.AddressDescriptor, error) { + if toIndex <= fromIndex { + return nil, errors.New("toIndex<=fromIndex") + } + extKey, err := hdkeychain.NewKeyFromString(xpub) + if err != nil { + return nil, err + } + changeExtKey, err := extKey.Child(change) + if err != nil { + return nil, err + } + ad := make([]bchain.AddressDescriptor, toIndex-fromIndex) + for index := fromIndex; index < toIndex; index++ { + indexExtKey, err := changeExtKey.Child(index) + if err != nil { + return nil, err + } + ad[index-fromIndex], err = p.addrDescFromExtKey(indexExtKey) + if err != nil { + return nil, err + } + } + return ad, nil +} + +// DerivationBasePath returns base path of xpub +func (p *BitcoinParser) DerivationBasePath(xpub string) (string, error) { + extKey, err := hdkeychain.NewKeyFromString(xpub) + if err != nil { + return "", err + } + var c, bip string + cn := extKey.ChildNum() + if cn >= 0x80000000 { + cn -= 0x80000000 + c = "'" + } + c = strconv.Itoa(int(cn)) + c + if extKey.Depth() != 3 { + return "unknown/" + c, nil + } + 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 +} diff --git a/bchain/coins/btc/bitcoinparser_test.go b/bchain/coins/btc/bitcoinparser_test.go index e3062215..deb15839 100644 --- a/bchain/coins/btc/bitcoinparser_test.go +++ b/bchain/coins/btc/bitcoinparser_test.go @@ -19,7 +19,7 @@ func TestMain(m *testing.M) { os.Exit(c) } -func Test_GetAddrDescFromAddress(t *testing.T) { +func TestGetAddrDescFromAddress(t *testing.T) { type args struct { address string } @@ -77,7 +77,7 @@ func Test_GetAddrDescFromAddress(t *testing.T) { } } -func Test_GetAddrDescFromVout(t *testing.T) { +func TestGetAddrDescFromVout(t *testing.T) { type args struct { vout bchain.Vout } @@ -141,7 +141,7 @@ func Test_GetAddrDescFromVout(t *testing.T) { } } -func Test_GetAddressesFromAddrDesc(t *testing.T) { +func TestGetAddressesFromAddrDesc(t *testing.T) { type args struct { script string } @@ -316,7 +316,7 @@ func init() { } } -func Test_PackTx(t *testing.T) { +func TestPackTx(t *testing.T) { type args struct { tx bchain.Tx height uint32 @@ -367,7 +367,7 @@ func Test_PackTx(t *testing.T) { } } -func Test_UnpackTx(t *testing.T) { +func TestUnpackTx(t *testing.T) { type args struct { packedTx string parser *BitcoinParser @@ -417,3 +417,245 @@ func Test_UnpackTx(t *testing.T) { }) } } + +func TestDeriveAddressDescriptors(t *testing.T) { + btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) + type args struct { + xpub string + change uint32 + indexes []uint32 + parser *BitcoinParser + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "m/44'/0'/0'", + args: args{ + xpub: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", + change: 0, + indexes: []uint32{0, 1234}, + parser: btcMainParser, + }, + want: []string{"1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA", "1P9w11dXAmG3QBjKLAvCsek8izs1iR2iFi"}, + }, + { + name: "m/49'/0'/0'", + args: args{ + xpub: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP", + change: 0, + indexes: []uint32{0, 1234}, + parser: btcMainParser, + }, + want: []string{"37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf", "367meFzJ9KqDLm9PX6U8Z8RdmkSNBuxX8T"}, + }, + { + name: "m/84'/0'/0'", + args: args{ + xpub: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", + change: 0, + indexes: []uint32{0, 1234}, + parser: btcMainParser, + }, + want: []string{"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", "bc1q4nm6g46ujzyjaeusralaz2nfv2rf04jjfyamkw"}, + }, + } + 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) + if (err != nil) != tt.wantErr { + t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr) + return + } + gotAddresses := make([]string, len(got)) + for i, ad := range got { + aa, _, err := tt.args.parser.GetAddressesFromAddrDesc(ad) + if err != nil || len(aa) != 1 { + t.Errorf("DeriveAddressDescriptorsFromTo() got incorrect address descriptor %v, error %v", ad, err) + return + } + gotAddresses[i] = aa[0] + } + if !reflect.DeepEqual(gotAddresses, tt.want) { + t.Errorf("DeriveAddressDescriptorsFromTo() = %v, want %v", gotAddresses, tt.want) + } + }) + } +} + +func TestDeriveAddressDescriptorsFromTo(t *testing.T) { + btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518}) + btcTestnetsParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198}) + type args struct { + xpub string + change uint32 + fromIndex uint32 + toIndex uint32 + parser *BitcoinParser + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "m/44'/0'/0'", + args: args{ + xpub: "xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: btcMainParser, + }, + want: []string{"1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA"}, + }, + { + name: "m/49'/0'/0'", + args: args{ + xpub: "ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: btcMainParser, + }, + want: []string{"37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf"}, + }, + { + name: "m/84'/0'/0'", + args: args{ + xpub: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", + change: 0, + fromIndex: 0, + toIndex: 1, + parser: btcMainParser, + }, + want: []string{"bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"}, + }, + { + name: "m/49'/1'/0'", + args: args{ + xpub: "upub5DR1Mg5nykixzYjFXWW5GghAU7dDqoPVJ2jrqFbL8sJ7Hs7jn69MP7KBnnmxn88GeZtnH8PRKV9w5MMSFX8AdEAoXY8Qd8BJPoXtpMeHMxJ", + change: 0, + fromIndex: 0, + toIndex: 10, + parser: btcTestnetsParser, + }, + want: []string{"2N4Q5FhU2497BryFfUgbqkAJE87aKHUhXMp", "2Mt7P2BAfE922zmfXrdcYTLyR7GUvbwSEns", "2N6aUMgQk8y1zvoq6FeWFyotyj75WY9BGsu", "2NA7tbZWM9BcRwBuebKSQe2xbhhF1paJwBM", "2N8RZMzvrUUnpLmvACX9ysmJ2MX3GK5jcQM", "2MvUUSiQZDSqyeSdofKX9KrSCio1nANPDTe", "2NBXaWu1HazjoUVgrXgcKNoBLhtkkD9Gmet", "2N791Ttf89tMVw2maj86E1Y3VgxD9Mc7PU7", "2NCJmwEq8GJm8t8GWWyBXAfpw7F2qZEVP5Y", "2NEgW71hWKer2XCSA8ZCC2VnWpB77L6bk68"}, + }, + } + 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) + if (err != nil) != tt.wantErr { + t.Errorf("DeriveAddressDescriptorsFromTo() error = %v, wantErr %v", err, tt.wantErr) + return + } + gotAddresses := make([]string, len(got)) + for i, ad := range got { + aa, _, err := tt.args.parser.GetAddressesFromAddrDesc(ad) + if err != nil || len(aa) != 1 { + t.Errorf("DeriveAddressDescriptorsFromTo() got incorrect address descriptor %v, error %v", ad, err) + return + } + gotAddresses[i] = aa[0] + } + if !reflect.DeepEqual(gotAddresses, tt.want) { + t.Errorf("DeriveAddressDescriptorsFromTo() = %v, want %v", gotAddresses, tt.want) + } + }) + } +} + +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) + } +} + +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) + } +} + +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) + } +} + +func TestBitcoinParser_DerivationBasePath(t *testing.T) { + btcMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, XPubMagicSegwitP2sh: 77429938, XPubMagicSegwitNative: 78792518, Slip44: 0}) + btcTestnetsParser := NewBitcoinParser(GetChainParams("test"), &Configuration{XPubMagic: 70617039, XPubMagicSegwitP2sh: 71979618, XPubMagicSegwitNative: 73342198, Slip44: 1}) + zecMainParser := NewBitcoinParser(GetChainParams("main"), &Configuration{XPubMagic: 76067358, Slip44: 133}) + type args struct { + xpub string + parser *BitcoinParser + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "m/84'/0'/0'", + args: args{ + xpub: "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", + parser: btcMainParser, + }, + want: "m/84'/0'/0'", + }, + { + name: "m/49'/0'/55 - not hardened account", + args: args{ + xpub: "ypub6XKbB5DJRAbW4TRJLp4uXQXG3ob5BtByXsNZFBjq9qcbzrczjVXfCz5cEo1SFDexmeWRnbCMDaRgaW4m9d2nBaa8FvUQCu3n9G1UBR8WhbT", + parser: btcMainParser, + }, + want: "m/49'/0'/55", + }, + { + name: "m/49'/0' - incomplete path, without account", + args: args{ + xpub: "ypub6UzM8PUqxcSoqC9gumfoiFhE8Qt84HbGpCD4eVJfJAojXTVtBxeddvTWJGJhGoaVBNJLmEgMdLXHgaLVJa4xEvk2tcokkdZhFdkxMLUE9sB", + parser: btcMainParser, + }, + want: "unknown/0'", + }, + { + name: "m/49'/1'/0'", + args: args{ + xpub: "upub5DR1Mg5nykixzYjFXWW5GghAU7dDqoPVJ2jrqFbL8sJ7Hs7jn69MP7KBnnmxn88GeZtnH8PRKV9w5MMSFX8AdEAoXY8Qd8BJPoXtpMeHMxJ", + parser: btcTestnetsParser, + }, + want: "m/49'/1'/0'", + }, + { + name: "m/44'/133'/12'", + args: args{ + xpub: "xpub6CQdEahwhKRTLYpP6cyb7ZaGb3r4tVdyPX6dC1PfrNuByrCkWDgUkmpD28UdV9QccKgY1ZiAbGv1Fakcg2LxdFVSTNKHcjdRjqhjPK8Trkb", + parser: zecMainParser, + }, + want: "m/44'/133'/12'", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.args.parser.DerivationBasePath(tt.args.xpub) + if (err != nil) != tt.wantErr { + t.Errorf("BitcoinParser.DerivationBasePath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("BitcoinParser.DerivationBasePath() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/bchain/coins/btc/bitcoinrpc.go b/bchain/coins/btc/bitcoinrpc.go index a0c5dced..ef7574cd 100644 --- a/bchain/coins/btc/bitcoinrpc.go +++ b/bchain/coins/btc/bitcoinrpc.go @@ -51,6 +51,10 @@ type Configuration struct { AddressFormat string `json:"address_format"` SupportsEstimateFee bool `json:"supports_estimate_fee"` SupportsEstimateSmartFee bool `json:"supports_estimate_smart_fee"` + XPubMagic uint32 `json:"xpub_magic,omitempty"` + XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh,omitempty"` + XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native,omitempty"` + Slip44 uint32 `json:"slip44,omitempty"` } // NewBitcoinRPC returns new BitcoinRPC instance. diff --git a/bchain/types.go b/bchain/types.go index a643b9dd..ead62535 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -263,6 +263,10 @@ type BlockChainParser interface { PackBlockHash(hash string) ([]byte, error) 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) // EthereumType specific EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error) } diff --git a/build/templates/blockbook/blockchaincfg.json b/build/templates/blockbook/blockchaincfg.json index 159eba57..525937c5 100644 --- a/build/templates/blockbook/blockchaincfg.json +++ b/build/templates/blockbook/blockchaincfg.json @@ -3,9 +3,8 @@ {{- if .Blockbook.BlockChain.AdditionalParams}} {{- range $name, $value := .Blockbook.BlockChain.AdditionalParams}} "{{$name}}": {{jsonToString $value}}, -{{- end}} +{{- end -}} {{end}} - "coin_name": "{{.Coin.Name}}", "coin_shortcut": "{{.Coin.Shortcut}}", "coin_label": "{{.Coin.Label}}", @@ -17,6 +16,11 @@ "message_queue_binding": "{{template "IPC.MessageQueueBindingTemplate" .}}", "subversion": "{{.Blockbook.BlockChain.Subversion}}", "address_format": "{{.Blockbook.BlockChain.AddressFormat}}", +{{if .Blockbook.BlockChain.XPubMagic}} "xpub_magic": {{.Blockbook.BlockChain.XPubMagic}}, +{{end}}{{if .Blockbook.BlockChain.XPubMagicSegwitP2sh}} "xpub_magic_segwit_p2sh": {{.Blockbook.BlockChain.XPubMagicSegwitP2sh}}, +{{end}}{{if .Blockbook.BlockChain.XPubMagicSegwitNative}} "xpub_magic_segwit_native": {{.Blockbook.BlockChain.XPubMagicSegwitNative}}, +{{end}}{{if .Blockbook.BlockChain.Slip44}} "slip44": {{.Blockbook.BlockChain.Slip44}}, +{{end}} "mempool_workers": {{.Blockbook.BlockChain.MempoolWorkers}}, "mempool_sub_workers": {{.Blockbook.BlockChain.MempoolSubWorkers}}, "block_addresses_to_keep": {{.Blockbook.BlockChain.BlockAddressesToKeep}} diff --git a/build/tools/templates.go b/build/tools/templates.go index ebd9d93a..b8101ada 100644 --- a/build/tools/templates.go +++ b/build/tools/templates.go @@ -13,18 +13,6 @@ import ( ) type Config struct { - Meta struct { - BuildDatetime string // generated field - PackageMaintainer string `json:"package_maintainer"` - PackageMaintainerEmail string `json:"package_maintainer_email"` - } - Env struct { - Version string `json:"version"` - BackendInstallPath string `json:"backend_install_path"` - BackendDataPath string `json:"backend_data_path"` - BlockbookInstallPath string `json:"blockbook_install_path"` - BlockbookDataPath string `json:"blockbook_data_path"` - } `json:"env"` Coin struct { Name string `json:"name"` Shortcut string `json:"shortcut"` @@ -63,7 +51,7 @@ type Config struct { Mainnet bool `json:"mainnet"` ServerConfigFile string `json:"server_config_file"` ClientConfigFile string `json:"client_config_file"` - AdditionalParams interface{} `json:"additional_params"` + AdditionalParams interface{} `json:"additional_params,omitempty"` } `json:"backend"` Blockbook struct { PackageName string `json:"package_name"` @@ -73,16 +61,32 @@ type Config struct { ExplorerURL string `json:"explorer_url"` AdditionalParams string `json:"additional_params"` BlockChain struct { - Parse bool `json:"parse"` - Subversion string `json:"subversion"` - AddressFormat string `json:"address_format"` - MempoolWorkers int `json:"mempool_workers"` - MempoolSubWorkers int `json:"mempool_sub_workers"` - BlockAddressesToKeep int `json:"block_addresses_to_keep"` - AdditionalParams map[string]json.RawMessage `json:"additional_params"` + Parse bool `json:"parse,omitempty"` + Subversion string `json:"subversion,omitempty"` + AddressFormat string `json:"address_format,omitempty"` + MempoolWorkers int `json:"mempool_workers"` + MempoolSubWorkers int `json:"mempool_sub_workers"` + BlockAddressesToKeep int `json:"block_addresses_to_keep"` + XPubMagic uint32 `json:"xpub_magic,omitempty"` + XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh,omitempty"` + XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native,omitempty"` + Slip44 uint32 `json:"slip44,omitempty"` + + AdditionalParams map[string]json.RawMessage `json:"additional_params"` } `json:"block_chain"` } `json:"blockbook"` - IntegrationTests map[string][]string `json:"integration_tests"` + Meta struct { + BuildDatetime string `json:"-"` // generated field + PackageMaintainer string `json:"package_maintainer"` + PackageMaintainerEmail string `json:"package_maintainer_email"` + } `json:"meta"` + Env struct { + Version string `json:"version"` + BackendInstallPath string `json:"backend_install_path"` + BackendDataPath string `json:"backend_data_path"` + BlockbookInstallPath string `json:"blockbook_install_path"` + BlockbookDataPath string `json:"blockbook_data_path"` + } `json:"-"` } func jsonToString(msg json.RawMessage) (string, error) { @@ -266,7 +270,7 @@ func makeOutputDir(path string) error { } func writeTemplate(path string, info os.FileInfo, templ *template.Template, config *Config) error { - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, info.Mode()) + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) if err != nil { return err } diff --git a/build/tools/trezor-common/sync-coins.go b/build/tools/trezor-common/sync-coins.go new file mode 100644 index 00000000..7a522058 --- /dev/null +++ b/build/tools/trezor-common/sync-coins.go @@ -0,0 +1,126 @@ +//usr/bin/go run $0 $@ ; exit +package main + +import ( + build "blockbook/build/tools" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" +) + +const ( + configsDir = "configs" + trezorCommonDefsURL = "https://raw.githubusercontent.com/trezor/trezor-common/master/defs/bitcoin/" +) + +type trezorCommonDef struct { + Name string `json:"coin_name"` + Shortcut string `json:"coin_shortcut"` + Label string `json:"coin_label"` + XPubMagic uint32 `json:"xpub_magic"` + XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh"` + XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native"` + Slip44 uint32 `json:"slip44,omitempty"` +} + +func getTrezorCommonDef(coin string) (*trezorCommonDef, error) { + req, err := http.NewRequest("GET", trezorCommonDefsURL+coin+".json", nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New("Github request status code " + strconv.Itoa(resp.StatusCode)) + } + bb, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var tcd trezorCommonDef + json.Unmarshal(bb, &tcd) + return &tcd, nil +} + +func writeConfig(coin string, config *build.Config) error { + path := filepath.Join(configsDir, "coins", coin+".json") + out, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer out.Close() + buf, err := json.MarshalIndent(config, "", " ") + if err != nil { + return err + } + n, err := out.Write(buf) + if err != nil { + return err + } + if n < len(buf) { + return io.ErrShortWrite + } + return nil +} + +func main() { + var coins []string + if len(os.Args) < 2 { + filepath.Walk(filepath.Join(configsDir, "coins"), func(path string, info os.FileInfo, err error) error { + n := strings.TrimSuffix(info.Name(), ".json") + if n != info.Name() { + coins = append(coins, n) + } + return nil + }) + } else { + coins = append(coins, os.Args[1]) + } + for _, coin := range coins { + config, err := build.LoadConfig(configsDir, coin) + if err == nil { + var tcd *trezorCommonDef + tcd, err = getTrezorCommonDef(coin) + if err == nil { + if tcd.Name != "" { + config.Coin.Name = tcd.Name + } + if tcd.Shortcut != "" { + config.Coin.Shortcut = tcd.Shortcut + } + if tcd.Label != "" { + config.Coin.Label = tcd.Label + } + if tcd.XPubMagic != 0 { + config.Blockbook.BlockChain.XPubMagic = tcd.XPubMagic + } + if tcd.XPubMagicSegwitP2sh != 0 { + config.Blockbook.BlockChain.XPubMagicSegwitP2sh = tcd.XPubMagicSegwitP2sh + } + if tcd.XPubMagicSegwitNative != 0 { + config.Blockbook.BlockChain.XPubMagicSegwitNative = tcd.XPubMagicSegwitNative + } + if tcd.Slip44 != 0 { + config.Blockbook.BlockChain.Slip44 = tcd.Slip44 + } + err = writeConfig(coin, config) + if err == nil { + fmt.Printf("%v updated\n", coin) + } + } + } + if err != nil { + fmt.Printf("%v update error %v\n", coin, err) + } + } +} diff --git a/configs/coins/bcash.json b/configs/coins/bcash.json index 82c2e9cd..569280e5 100644 --- a/configs/coins/bcash.json +++ b/configs/coins/bcash.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Bcash", - "shortcut": "BCH", - "label": "Bitcoin Cash", - "alias": "bcash" + "name": "Bcash", + "shortcut": "BCH", + "label": "Bitcoin Cash", + "alias": "bcash" }, "ports": { "backend_rpc": 8031, @@ -28,7 +28,7 @@ "verification_source": "788001fd5ce8ca0bd61e8a92f10d22b7a49695c3651c60fad11358ba29309a1b", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/bitcoin-qt" + "bin/bitcoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -54,6 +54,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 145, "additional_params": {} } }, @@ -61,4 +63,4 @@ "package_maintainer": "Petr Kracik", "package_maintainer_email": "petr.kracik@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/bcash_testnet.json b/configs/coins/bcash_testnet.json index d430a666..dfd8308a 100644 --- a/configs/coins/bcash_testnet.json +++ b/configs/coins/bcash_testnet.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Bcash Testnet", - "shortcut": "TBCH", - "label": "Bitcoin Cash Testnet", - "alias": "bcash_testnet" + "name": "Bcash Testnet", + "shortcut": "TBCH", + "label": "Bitcoin Cash Testnet", + "alias": "bcash_testnet" }, "ports": { "backend_rpc": 18031, @@ -28,7 +28,7 @@ "verification_source": "788001fd5ce8ca0bd61e8a92f10d22b7a49695c3651c60fad11358ba29309a1b", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/bitcoin-qt" + "bin/bitcoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/testnet3/*.log", @@ -54,6 +54,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "slip44": 1, "additional_params": {} } }, @@ -61,4 +63,4 @@ "package_maintainer": "Petr Kracik", "package_maintainer_email": "petr.kracik@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/bgold.json b/configs/coins/bgold.json index 01d225e4..02633bd7 100644 --- a/configs/coins/bgold.json +++ b/configs/coins/bgold.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Bgold", - "shortcut": "BTG", - "label": "Bitcoin Gold", - "alias": "bgold" + "name": "Bgold", + "shortcut": "BTG", + "label": "Bitcoin Gold", + "alias": "bgold" }, "ports": { "backend_rpc": 8035, @@ -28,7 +28,7 @@ "verification_source": "https://github.com/BTCGPU/BTCGPU/releases/download/v0.15.2/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/bitcoin-qt" + "bin/bitcoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bgoldd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,199 +40,199 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "mempoolexpiry": 72, - "timeout": 768, - "maxconnections": 250, - "addnode": [ - "188.126.0.134", - "45.56.84.44", - "109.201.133.93:8338", - "178.63.11.246:8338", - "188.120.223.153:8338", - "79.137.64.158:8338", - "78.193.221.106:8338", - "139.59.151.13:8338", - "76.16.12.81:8338", - "172.104.157.62:8338", - "43.207.67.209:8338", - "178.63.11.246:8338", - "79.137.64.158:8338", - "78.193.221.106:8338", - "139.59.151.13:8338", - "172.104.157.62:8338", - "178.158.247.119:8338", - "109.201.133.93:8338", - "178.63.11.246:8338", - "139.59.151.13:8338", - "172.104.157.62:8338", - "188.120.223.153:8338", - "178.158.247.119:8338", - "78.193.221.106:8338", - "79.137.64.158:8338", - "76.16.12.81:8338", - "176.12.32.153:8338", - "178.158.247.122:8338", - "81.37.147.185:8338", - "176.12.32.153:8338", - "79.137.64.158:8338", - "178.158.247.122:8338", - "66.70.247.151:8338", - "89.18.27.165:8338", - "178.63.11.246:8338", - "91.222.17.86:8338", - "37.59.50.143:8338", - "91.50.219.221:8338", - "154.16.63.17:8338", - "213.136.76.42:8338", - "176.99.4.140:8338", - "176.9.48.36:8338", - "78.193.221.106:8338", - "34.236.228.99:8338", - "213.154.230.107:8338", - "111.231.66.252:8338", - "188.120.223.153:8338", - "219.89.122.82:8338", - "109.192.23.101:8338", - "98.114.91.222:8338", - "217.66.156.41:8338", - "172.104.157.62:8338", - "114.44.222.73:8338", - "91.224.140.216:8338", - "149.154.71.96:8338", - "107.181.183.242:8338", - "36.78.96.92:8338", - "46.22.7.74:8338", - "89.110.53.186:8338", - "73.243.220.85:8338", - "109.86.137.8:8338", - "77.78.12.89:8338", - "87.92.116.26:8338", - "93.78.122.48:8338", - "35.195.83.0:8338", - "46.147.75.220:8338", - "212.47.236.104:8338", - "95.220.100.230:8338", - "178.70.142.247:8338", - "45.76.136.149:8338", - "94.155.74.206:8338", - "178.70.142.247:8338", - "128.199.228.97:8338", - "77.171.144.207:8338", - "159.89.192.119:8338", - "136.63.238.170:8338", - "31.27.193.105:8338", - "176.107.192.240:8338", - "94.140.241.96:8338", - "66.108.15.5:8338", - "81.177.127.204:8338", - "88.18.69.174:8338", - "178.70.130.94:8338", - "78.98.162.140:8338", - "95.133.156.224:8338", - "46.188.16.96:8338", - "94.247.16.21:8338", - "eunode.pool.gold:8338", - "asianode.pool.gold:8338", - "45.56.84.44:8338", - "176.9.48.36:8338", - "93.57.253.121:8338", - "172.104.157.62:8338", - "176.12.32.153:8338", - "pool.serverpower.net:8338", - "213.154.229.126:8338", - "213.154.230.106:8338", - "213.154.230.107:8338", - "213.154.229.50:8338", - "145.239.0.50:8338", - "107.181.183.242:8338", - "109.201.133.93:8338", - "120.41.190.109:8338", - "120.41.191.224:8338", - "138.68.249.79:8338", - "13.95.223.202:8338", - "145.239.0.50:8338", - "149.56.95.26:8338", - "158.69.103.228:8338", - "159.89.192.119:8338", - "164.132.207.143:8338", - "171.100.141.106:8338", - "172.104.157.62:8338", - "173.176.95.92:8338", - "176.12.32.153:8338", - "178.239.54.250:8338", - "178.63.11.246:8338", - "185.139.2.140:8338", - "188.120.223.153:8338", - "190.46.2.92:8338", - "192.99.194.113:8338", - "199.229.248.218:8338", - "213.154.229.126:8338", - "213.154.229.50:8338", - "213.154.230.106:8338", - "213.154.230.107:8338", - "217.182.199.21", - "35.189.127.200:8338", - "35.195.83.0:8338", - "35.197.197.166:8338", - "35.200.168.155:8338", - "35.203.167.11:8338", - "37.59.50.143:8338", - "45.27.161.195:8338", - "45.32.234.160:8338", - "45.56.84.44:8338", - "46.188.16.96:8338", - "46.251.19.171:8338", - "5.157.119.109:8338", - "52.28.162.48:8338", - "54.153.140.202:8338", - "54.68.81.2:83388338", - "62.195.190.190:8338", - "62.216.5.136:8338", - "65.110.125.175:8338", - "67.68.226.130:8338", - "73.243.220.85:8338", - "77.78.12.89:8338", - "78.193.221.106:8338", - "78.98.162.140:8338", - "79.137.64.158:8338", - "84.144.177.238:8338", - "87.92.116.26:8338", - "89.115.139.117:8338", - "89.18.27.165:8338", - "91.50.219.221:8338", - "93.88.74.26", - "93.88.74.26:8338", - "94.155.74.206:8338", - "95.154.201.132:8338", - "98.29.248.131:8338", - "u2.my.to:8338", - "[2001:470:b:ce:dc70:83ff:fe7a:1e74]:8338", - "2001:7b8:61d:1:250:56ff:fe90:c89f:8338", - "2001:7b8:63a:1002:213:154:230:106:8338", - "2001:7b8:63a:1002:213:154:230:107:8338", - "45.56.84.44", - "109.201.133.93:8338", - "120.41.191.224:30607", - "138.68.249.79:50992", - "138.68.249.79:51314", - "172.104.157.62", - "178.63.11.246:8338", - "185.139.2.140:8338", - "199.229.248.218:28830", - "35.189.127.200:41220", - "35.189.127.200:48244", - "35.195.83.0:35172", - "35.195.83.0:35576", - "35.195.83.0:35798", - "35.197.197.166:32794", - "35.197.197.166:33112", - "35.197.197.166:33332", - "35.203.167.11:52158", - "37.59.50.143:35254", - "45.27.161.195:33852", - "45.27.161.195:36738", - "45.27.161.195:58628" - ] + "addnode": [ + "188.126.0.134", + "45.56.84.44", + "109.201.133.93:8338", + "178.63.11.246:8338", + "188.120.223.153:8338", + "79.137.64.158:8338", + "78.193.221.106:8338", + "139.59.151.13:8338", + "76.16.12.81:8338", + "172.104.157.62:8338", + "43.207.67.209:8338", + "178.63.11.246:8338", + "79.137.64.158:8338", + "78.193.221.106:8338", + "139.59.151.13:8338", + "172.104.157.62:8338", + "178.158.247.119:8338", + "109.201.133.93:8338", + "178.63.11.246:8338", + "139.59.151.13:8338", + "172.104.157.62:8338", + "188.120.223.153:8338", + "178.158.247.119:8338", + "78.193.221.106:8338", + "79.137.64.158:8338", + "76.16.12.81:8338", + "176.12.32.153:8338", + "178.158.247.122:8338", + "81.37.147.185:8338", + "176.12.32.153:8338", + "79.137.64.158:8338", + "178.158.247.122:8338", + "66.70.247.151:8338", + "89.18.27.165:8338", + "178.63.11.246:8338", + "91.222.17.86:8338", + "37.59.50.143:8338", + "91.50.219.221:8338", + "154.16.63.17:8338", + "213.136.76.42:8338", + "176.99.4.140:8338", + "176.9.48.36:8338", + "78.193.221.106:8338", + "34.236.228.99:8338", + "213.154.230.107:8338", + "111.231.66.252:8338", + "188.120.223.153:8338", + "219.89.122.82:8338", + "109.192.23.101:8338", + "98.114.91.222:8338", + "217.66.156.41:8338", + "172.104.157.62:8338", + "114.44.222.73:8338", + "91.224.140.216:8338", + "149.154.71.96:8338", + "107.181.183.242:8338", + "36.78.96.92:8338", + "46.22.7.74:8338", + "89.110.53.186:8338", + "73.243.220.85:8338", + "109.86.137.8:8338", + "77.78.12.89:8338", + "87.92.116.26:8338", + "93.78.122.48:8338", + "35.195.83.0:8338", + "46.147.75.220:8338", + "212.47.236.104:8338", + "95.220.100.230:8338", + "178.70.142.247:8338", + "45.76.136.149:8338", + "94.155.74.206:8338", + "178.70.142.247:8338", + "128.199.228.97:8338", + "77.171.144.207:8338", + "159.89.192.119:8338", + "136.63.238.170:8338", + "31.27.193.105:8338", + "176.107.192.240:8338", + "94.140.241.96:8338", + "66.108.15.5:8338", + "81.177.127.204:8338", + "88.18.69.174:8338", + "178.70.130.94:8338", + "78.98.162.140:8338", + "95.133.156.224:8338", + "46.188.16.96:8338", + "94.247.16.21:8338", + "eunode.pool.gold:8338", + "asianode.pool.gold:8338", + "45.56.84.44:8338", + "176.9.48.36:8338", + "93.57.253.121:8338", + "172.104.157.62:8338", + "176.12.32.153:8338", + "pool.serverpower.net:8338", + "213.154.229.126:8338", + "213.154.230.106:8338", + "213.154.230.107:8338", + "213.154.229.50:8338", + "145.239.0.50:8338", + "107.181.183.242:8338", + "109.201.133.93:8338", + "120.41.190.109:8338", + "120.41.191.224:8338", + "138.68.249.79:8338", + "13.95.223.202:8338", + "145.239.0.50:8338", + "149.56.95.26:8338", + "158.69.103.228:8338", + "159.89.192.119:8338", + "164.132.207.143:8338", + "171.100.141.106:8338", + "172.104.157.62:8338", + "173.176.95.92:8338", + "176.12.32.153:8338", + "178.239.54.250:8338", + "178.63.11.246:8338", + "185.139.2.140:8338", + "188.120.223.153:8338", + "190.46.2.92:8338", + "192.99.194.113:8338", + "199.229.248.218:8338", + "213.154.229.126:8338", + "213.154.229.50:8338", + "213.154.230.106:8338", + "213.154.230.107:8338", + "217.182.199.21", + "35.189.127.200:8338", + "35.195.83.0:8338", + "35.197.197.166:8338", + "35.200.168.155:8338", + "35.203.167.11:8338", + "37.59.50.143:8338", + "45.27.161.195:8338", + "45.32.234.160:8338", + "45.56.84.44:8338", + "46.188.16.96:8338", + "46.251.19.171:8338", + "5.157.119.109:8338", + "52.28.162.48:8338", + "54.153.140.202:8338", + "54.68.81.2:83388338", + "62.195.190.190:8338", + "62.216.5.136:8338", + "65.110.125.175:8338", + "67.68.226.130:8338", + "73.243.220.85:8338", + "77.78.12.89:8338", + "78.193.221.106:8338", + "78.98.162.140:8338", + "79.137.64.158:8338", + "84.144.177.238:8338", + "87.92.116.26:8338", + "89.115.139.117:8338", + "89.18.27.165:8338", + "91.50.219.221:8338", + "93.88.74.26", + "93.88.74.26:8338", + "94.155.74.206:8338", + "95.154.201.132:8338", + "98.29.248.131:8338", + "u2.my.to:8338", + "[2001:470:b:ce:dc70:83ff:fe7a:1e74]:8338", + "2001:7b8:61d:1:250:56ff:fe90:c89f:8338", + "2001:7b8:63a:1002:213:154:230:106:8338", + "2001:7b8:63a:1002:213:154:230:107:8338", + "45.56.84.44", + "109.201.133.93:8338", + "120.41.191.224:30607", + "138.68.249.79:50992", + "138.68.249.79:51314", + "172.104.157.62", + "178.63.11.246:8338", + "185.139.2.140:8338", + "199.229.248.218:28830", + "35.189.127.200:41220", + "35.189.127.200:48244", + "35.195.83.0:35172", + "35.195.83.0:35576", + "35.195.83.0:35798", + "35.197.197.166:32794", + "35.197.197.166:33112", + "35.197.197.166:33332", + "35.203.167.11:52158", + "37.59.50.143:35254", + "45.27.161.195:33852", + "45.27.161.195:36738", + "45.27.161.195:58628" + ], + "maxconnections": 250, + "mempoolexpiry": 72, + "timeout": 768 } }, "blockbook": { @@ -248,6 +248,9 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "slip44": 156, "additional_params": {} } }, @@ -255,4 +258,4 @@ "package_maintainer": "Jakub Matys", "package_maintainer_email": "jakub.matys@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/bitcoin.json b/configs/coins/bitcoin.json index 1532fd53..c9dbf261 100644 --- a/configs/coins/bitcoin.json +++ b/configs/coins/bitcoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Bitcoin", - "shortcut": "BTC", - "label": "Bitcoin", - "alias": "bitcoin" + "name": "Bitcoin", + "shortcut": "BTC", + "label": "Bitcoin", + "alias": "bitcoin" }, "ports": { "backend_rpc": 8030, @@ -28,7 +28,7 @@ "verification_source": "https://bitcoin.org/bin/bitcoin-core-0.17.1/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/bitcoin-qt" + "bin/bitcoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin.conf", "client_config_file": "bitcoin_client.conf", "additional_params": { - "deprecatedrpc": "estimatefee" + "deprecatedrpc": "estimatefee" } }, "blockbook": { @@ -55,6 +55,9 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "xpub_magic_segwit_native": 78792518, "additional_params": {} } }, @@ -62,4 +65,4 @@ "package_maintainer": "Jakub Matys", "package_maintainer_email": "jakub.matys@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/bitcoin_testnet.json b/configs/coins/bitcoin_testnet.json index ea424cab..d137099c 100644 --- a/configs/coins/bitcoin_testnet.json +++ b/configs/coins/bitcoin_testnet.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Testnet", - "shortcut": "TEST", - "label": "Bitcoin Testnet", - "alias": "bitcoin_testnet" + "name": "Testnet", + "shortcut": "TEST", + "label": "Bitcoin Testnet", + "alias": "bitcoin_testnet" }, "ports": { "backend_rpc": 18030, @@ -28,7 +28,7 @@ "verification_source": "https://bitcoin.org/bin/bitcoin-core-0.17.1/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/bitcoin-qt" + "bin/bitcoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/bitcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/testnet3/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin.conf", "client_config_file": "bitcoin_client.conf", "additional_params": { - "deprecatedrpc": "estimatefee" + "deprecatedrpc": "estimatefee" } }, "blockbook": { @@ -55,6 +55,10 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "xpub_magic_segwit_p2sh": 71979618, + "xpub_magic_segwit_native": 73342198, + "slip44": 1, "additional_params": {} } }, @@ -62,4 +66,4 @@ "package_maintainer": "Jakub Matys", "package_maintainer_email": "jakub.matys@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/dash.json b/configs/coins/dash.json index 347202b0..011b5f07 100644 --- a/configs/coins/dash.json +++ b/configs/coins/dash.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Dash", - "shortcut": "DASH", - "label": "Dash", - "alias": "dash" + "name": "Dash", + "shortcut": "DASH", + "label": "Dash", + "alias": "dash" }, "ports": { "backend_rpc": 8033, @@ -28,7 +28,7 @@ "verification_source": "https://github.com/dashpay/dash/releases/download/v0.13.1.0/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/dash-qt" + "bin/dash-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dashd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "mempoolexpiry": 72 + "mempoolexpiry": 72 } }, "blockbook": { @@ -56,6 +56,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 50221772, + "slip44": 5, "additional_params": {} } }, @@ -63,4 +65,4 @@ "package_maintainer": "Petr Kracik", "package_maintainer_email": "petr.kracik@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/dash_testnet.json b/configs/coins/dash_testnet.json index ccf3acf2..ffbeb63c 100644 --- a/configs/coins/dash_testnet.json +++ b/configs/coins/dash_testnet.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Dash Testnet", - "shortcut": "tDASH", - "label": "Dash Testnet", - "alias": "dash_testnet" + "name": "Dash Testnet", + "shortcut": "tDASH", + "label": "Dash Testnet", + "alias": "dash_testnet" }, "ports": { "backend_rpc": 18033, @@ -28,7 +28,7 @@ "verification_source": "https://github.com/dashpay/dash/releases/download/v0.13.1.0/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/dash-qt" + "bin/dash-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dashd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/testnet3/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "mempoolexpiry": 72 + "mempoolexpiry": 72 } }, "blockbook": { @@ -56,6 +56,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "slip44": 1, "additional_params": {} } }, @@ -63,4 +65,4 @@ "package_maintainer": "Petr Kracik", "package_maintainer_email": "petr.kracik@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/digibyte.json b/configs/coins/digibyte.json index 3608a65d..5deb852d 100644 --- a/configs/coins/digibyte.json +++ b/configs/coins/digibyte.json @@ -1,9 +1,9 @@ { "coin": { - "name": "DigiByte", - "shortcut": "DGB", - "label": "DigiByte", - "alias": "digibyte" + "name": "DigiByte", + "shortcut": "DGB", + "label": "DigiByte", + "alias": "digibyte" }, "ports": { "backend_rpc": 8042, @@ -28,7 +28,7 @@ "verification_source": "dd6bed0228087fbb51f08be55cbc08a0e3251acfe1be3249b634447837ecd857", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/digibyte-qt" + "bin/digibyte-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/digibyted -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "whitelist": "127.0.0.1" + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -55,6 +55,9 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "slip44": 20, "additional_params": {} } }, @@ -62,4 +65,4 @@ "package_maintainer": "Martin Boehm", "package_maintainer_email": "martin.bohm@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/dogecoin.json b/configs/coins/dogecoin.json index d3c44e00..43ae23cc 100644 --- a/configs/coins/dogecoin.json +++ b/configs/coins/dogecoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Dogecoin", - "shortcut": "DOGE", - "label": "Dogecoin", - "alias": "dogecoin" + "name": "Dogecoin", + "shortcut": "DOGE", + "label": "Dogecoin", + "alias": "dogecoin" }, "ports": { "backend_rpc": 8038, @@ -28,7 +28,7 @@ "verification_source": "09871d8ff2ab5e0f05df2bdf5eba64c178229d030dd7c8473b08e6ed45d3327f", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/dogecoin-qt" + "bin/dogecoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/dogecoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,10 +40,10 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "whitelist": "127.0.0.1", - "rpcthreads": 16, - "upnp": 0, - "discover": 0 + "discover": 0, + "rpcthreads": 16, + "upnp": 0, + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -58,6 +58,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 49990397, + "slip44": 3, "additional_params": {} } }, @@ -65,4 +67,4 @@ "package_maintainer": "Jakub Matys", "package_maintainer_email": "jakub.matys@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/fujicoin.json b/configs/coins/fujicoin.json index b0f02543..11da3c99 100644 --- a/configs/coins/fujicoin.json +++ b/configs/coins/fujicoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Fujicoin", - "shortcut": "FJC", - "label": "Fujicoin", - "alias": "fujicoin" + "name": "Fujicoin", + "shortcut": "FJC", + "label": "Fujicoin", + "alias": "fujicoin" }, "ports": { "backend_rpc": 8048, @@ -27,8 +27,7 @@ "verification_type": "gpg-sha256", "verification_source": "https://www.fujicoin.org/fujicoin/3.0/SHA256SUMS.asc", "extract_command": "tar -C backend -xf", - "exclude_files": [ - ], + "exclude_files": [], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/fujicoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", "postinst_script_template": "", @@ -38,8 +37,7 @@ "mainnet": true, "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", - "additional_params": { - } + "additional_params": {} }, "blockbook": { "package_name": "blockbook-fujicoin", @@ -53,6 +51,10 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "xpub_magic_segwit_native": 78792518, + "slip44": 75, "additional_params": {} } }, @@ -60,4 +62,4 @@ "package_maintainer": "Motty", "package_maintainer_email": "fujicoin@gmail.com" } -} +} \ No newline at end of file diff --git a/configs/coins/gamecredits.json b/configs/coins/gamecredits.json index bfdb5744..dfb5efb5 100644 --- a/configs/coins/gamecredits.json +++ b/configs/coins/gamecredits.json @@ -1,9 +1,9 @@ { "coin": { - "name": "GameCredits", - "shortcut": "GAME", - "label": "GameCredits", - "alias": "gamecredits" + "name": "GameCredits", + "shortcut": "GAME", + "label": "GameCredits", + "alias": "gamecredits" }, "ports": { "backend_rpc": 8044, @@ -28,7 +28,7 @@ "verification_source": "38531ea877dfc1cedd3125bb79216a587f0974f20bee6243efcde61d05e07e5c", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/gamecredits-qt" + "bin/gamecredits-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/gamecreditsd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "whitelist": "127.0.0.1" + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -55,6 +55,9 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 27106558, + "xpub_magic_segwit_p2sh": 28471030, + "slip44": 101, "additional_params": {} } }, @@ -62,4 +65,4 @@ "package_maintainer": "Samad Sajanlal", "package_maintainer_email": "samad@gamecredits.org" } -} +} \ No newline at end of file diff --git a/configs/coins/groestlcoin.json b/configs/coins/groestlcoin.json index d7974b46..567650d7 100644 --- a/configs/coins/groestlcoin.json +++ b/configs/coins/groestlcoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Groestlcoin", - "shortcut": "GRS", - "label": "Groestlcoin", - "alias": "groestlcoin" + "name": "Groestlcoin", + "shortcut": "GRS", + "label": "Groestlcoin", + "alias": "groestlcoin" }, "ports": { "backend_rpc": 8045, @@ -28,7 +28,7 @@ "verification_source": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.17.2/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/groestlcoin-qt" + "bin/groestlcoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,8 +40,8 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "deprecatedrpc": "estimatefee", - "whitelist": "127.0.0.1" + "deprecatedrpc": "estimatefee", + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -56,6 +56,10 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "xpub_magic_segwit_native": 78792518, + "slip44": 17, "additional_params": {} } }, @@ -63,4 +67,4 @@ "package_maintainer": "Groestlcoin Development Team", "package_maintainer_email": "jackie@groestlcoin.org" } -} +} \ No newline at end of file diff --git a/configs/coins/groestlcoin_testnet.json b/configs/coins/groestlcoin_testnet.json index 9db3e0d6..bcc6ae27 100644 --- a/configs/coins/groestlcoin_testnet.json +++ b/configs/coins/groestlcoin_testnet.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Groestlcoin Testnet", - "shortcut": "tGRS", - "label": "Groestlcoin Testnet", - "alias": "groestlcoin_testnet" + "name": "Groestlcoin Testnet", + "shortcut": "tGRS", + "label": "Groestlcoin Testnet", + "alias": "groestlcoin_testnet" }, "ports": { "backend_rpc": 18045, @@ -28,7 +28,7 @@ "verification_source": "https://github.com/Groestlcoin/groestlcoin/releases/download/v2.17.2/SHA256SUMS.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/groestlcoin-qt" + "bin/groestlcoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/groestlcoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/testnet3/*.log", @@ -40,8 +40,8 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "deprecatedrpc": "estimatefee", - "whitelist": "127.0.0.1" + "deprecatedrpc": "estimatefee", + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -56,6 +56,10 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "xpub_magic_segwit_p2sh": 71979618, + "xpub_magic_segwit_native": 73342198, + "slip44": 1, "additional_params": {} } }, @@ -63,4 +67,4 @@ "package_maintainer": "Groestlcoin Development Team", "package_maintainer_email": "jackie@groestlcoin.org" } -} +} \ No newline at end of file diff --git a/configs/coins/koto.json b/configs/coins/koto.json index 4a928ae5..f392c14a 100644 --- a/configs/coins/koto.json +++ b/configs/coins/koto.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Koto", - "shortcut": "KOTO", - "label": "Koto", - "alias": "koto" + "name": "Koto", + "shortcut": "KOTO", + "label": "Koto", + "alias": "koto" }, "ports": { "backend_rpc": 8051, @@ -28,7 +28,7 @@ "verification_source": "https://github.com/KotoDevelopers/koto/releases/download/v2.0.3/koto-2.0.3-linux64.tar.gz.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/koto-qt" + "bin/koto-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/kotod -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,9 +40,9 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "addnode": [ - "dnsseed.ko-to.org" - ] + "addnode": [ + "dnsseed.ko-to.org" + ] } }, "blockbook": { @@ -57,6 +57,8 @@ "mempool_workers": 4, "mempool_sub_workers": 8, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 510, "additional_params": {} } }, @@ -64,4 +66,4 @@ "package_maintainer": "WO", "package_maintainer_email": "wo@kotocoin.info" } -} +} \ No newline at end of file diff --git a/configs/coins/litecoin.json b/configs/coins/litecoin.json index 7368dca1..600e8d34 100644 --- a/configs/coins/litecoin.json +++ b/configs/coins/litecoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Litecoin", - "shortcut": "LTC", - "label": "Litecoin", - "alias": "litecoin" + "name": "Litecoin", + "shortcut": "LTC", + "label": "Litecoin", + "alias": "litecoin" }, "ports": { "backend_rpc": 8034, @@ -28,7 +28,7 @@ "verification_source": "https://download.litecoin.org/litecoin-0.16.3/linux/litecoin-0.16.3-linux-signatures.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/litecoin-qt" + "bin/litecoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/litecoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "whitelist": "127.0.0.1" + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -55,6 +55,9 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 27108450, + "xpub_magic_segwit_p2sh": 28471030, + "slip44": 2, "additional_params": {} } }, @@ -62,4 +65,4 @@ "package_maintainer": "Jakub Matys", "package_maintainer_email": "jakub.matys@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/litecoin_testnet.json b/configs/coins/litecoin_testnet.json index 767f0a62..622bf912 100644 --- a/configs/coins/litecoin_testnet.json +++ b/configs/coins/litecoin_testnet.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Litecoin Testnet", - "shortcut": "TLTC", - "label": "Litecoin Testnet", - "alias": "litecoin_testnet" + "name": "Litecoin Testnet", + "shortcut": "TLTC", + "label": "Litecoin Testnet", + "alias": "litecoin_testnet" }, "ports": { "backend_rpc": 18034, @@ -28,7 +28,7 @@ "verification_source": "https://download.litecoin.org/litecoin-0.16.3/linux/litecoin-0.16.3-linux-signatures.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/litecoin-qt" + "bin/litecoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/litecoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/testnet4/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "whitelist": "127.0.0.1" + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -55,6 +55,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "slip44": 1, "additional_params": {} } }, @@ -62,4 +64,4 @@ "package_maintainer": "Jakub Matys", "package_maintainer_email": "jakub.matys@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/monacoin.json b/configs/coins/monacoin.json index d274e23f..db281c81 100644 --- a/configs/coins/monacoin.json +++ b/configs/coins/monacoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Monacoin", - "shortcut": "MONA", - "label": "Monacoin", - "alias": "monacoin" + "name": "Monacoin", + "shortcut": "MONA", + "label": "Monacoin", + "alias": "monacoin" }, "ports": { "backend_rpc": 8041, @@ -28,7 +28,7 @@ "verification_source": "https://github.com/monacoinproject/monacoin/releases/download/monacoin-0.16.3/monacoin-0.16.3-signatures.asc", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/monacoin-qt" + "bin/monacoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/monacoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,7 +40,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "whitelist": "127.0.0.1" + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -55,6 +55,9 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "slip44": 22, "additional_params": {} } }, @@ -62,4 +65,4 @@ "package_maintainer": "wakiyamap", "package_maintainer_email": "wakiyamap@gmail.com" } -} +} \ No newline at end of file diff --git a/configs/coins/myriad.json b/configs/coins/myriad.json index 6131ef20..35e9059e 100644 --- a/configs/coins/myriad.json +++ b/configs/coins/myriad.json @@ -53,6 +53,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 90, "additional_params": {} } }, @@ -60,4 +62,4 @@ "package_maintainer": "wlc-", "package_maintainer_email": "wwwwllllcccc@gmail.com" } -} +} \ No newline at end of file diff --git a/configs/coins/namecoin.json b/configs/coins/namecoin.json index 9d3f30e5..bf964613 100644 --- a/configs/coins/namecoin.json +++ b/configs/coins/namecoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Namecoin", - "shortcut": "NMC", - "label": "Namecoin", - "alias": "namecoin" + "name": "Namecoin", + "shortcut": "NMC", + "label": "Namecoin", + "alias": "namecoin" }, "ports": { "backend_rpc": 8039, @@ -28,7 +28,7 @@ "verification_source": "14ebaaf6f22f69b057a5bcb9b6959548f0a3f1b62cc113f19581d2297044827e", "extract_command": "tar -C backend --strip 1 -xf", "exclude_files": [ - "bin/namecoin-qt" + "bin/namecoin-qt" ], "exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/namecoind -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid", "logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/*.log", @@ -40,14 +40,14 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "whitelist": "127.0.0.1", - "upnp": 0, - "discover": 0, - "whitelistrelay": 1, - "listenonion": 0, - "addnode": [ - "45.24.110.177:8334" - ] + "addnode": [ + "45.24.110.177:8334" + ], + "discover": 0, + "listenonion": 0, + "upnp": 0, + "whitelist": "127.0.0.1", + "whitelistrelay": 1 } }, "blockbook": { @@ -62,6 +62,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 7, "additional_params": {} } }, @@ -69,4 +71,4 @@ "package_maintainer": "Jakub Matys", "package_maintainer_email": "jakub.matys@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/vertcoin.json b/configs/coins/vertcoin.json index 2f4c1b82..0dfa7545 100644 --- a/configs/coins/vertcoin.json +++ b/configs/coins/vertcoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Vertcoin", - "shortcut": "VTC", - "label": "Vertcoin", - "alias": "vertcoin" + "name": "Vertcoin", + "shortcut": "VTC", + "label": "Vertcoin", + "alias": "vertcoin" }, "ports": { "backend_rpc": 8040, @@ -38,7 +38,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "whitelist": "127.0.0.1" + "whitelist": "127.0.0.1" } }, "blockbook": { @@ -53,6 +53,9 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 1000, + "xpub_magic": 76067358, + "xpub_magic_segwit_p2sh": 77429938, + "slip44": 28, "additional_params": {} } }, @@ -60,4 +63,4 @@ "package_maintainer": "Petr Kracik", "package_maintainer_email": "petr.kracik@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/zcash.json b/configs/coins/zcash.json index 87cc4474..f4f3c802 100644 --- a/configs/coins/zcash.json +++ b/configs/coins/zcash.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Zcash", - "shortcut": "ZEC", - "label": "Zcash", - "alias": "zcash" + "name": "Zcash", + "shortcut": "ZEC", + "label": "Zcash", + "alias": "zcash" }, "ports": { "backend_rpc": 8032, @@ -38,9 +38,9 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "addnode": [ - "mainnet.z.cash" - ] + "addnode": [ + "mainnet.z.cash" + ] } }, "blockbook": { @@ -55,6 +55,8 @@ "mempool_workers": 4, "mempool_sub_workers": 8, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 133, "additional_params": {} } }, @@ -62,4 +64,4 @@ "package_maintainer": "Petr Kracik", "package_maintainer_email": "petr.kracik@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/zcash_testnet.json b/configs/coins/zcash_testnet.json index 08811e1d..8dd220a2 100644 --- a/configs/coins/zcash_testnet.json +++ b/configs/coins/zcash_testnet.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Zcash Testnet", - "shortcut": "TAZ", - "label": "Zcash Testnet", - "alias": "zcash_testnet" + "name": "Zcash Testnet", + "shortcut": "TAZ", + "label": "Zcash Testnet", + "alias": "zcash_testnet" }, "ports": { "backend_rpc": 18032, @@ -38,9 +38,9 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "addnode": [ - "testnet.z.cash" - ] + "addnode": [ + "testnet.z.cash" + ] } }, "blockbook": { @@ -55,6 +55,8 @@ "mempool_workers": 4, "mempool_sub_workers": 8, "block_addresses_to_keep": 300, + "xpub_magic": 70617039, + "slip44": 1, "additional_params": {} } }, @@ -62,4 +64,4 @@ "package_maintainer": "Petr Kracik", "package_maintainer_email": "petr.kracik@satoshilabs.com" } -} +} \ No newline at end of file diff --git a/configs/coins/zcoin.json b/configs/coins/zcoin.json index 92d67dec..082ad982 100644 --- a/configs/coins/zcoin.json +++ b/configs/coins/zcoin.json @@ -1,9 +1,9 @@ { "coin": { - "name": "Zcoin", - "shortcut": "XZC", - "label": "Zcoin", - "alias": "zcoin" + "name": "Zcoin", + "shortcut": "XZC", + "label": "Zcoin", + "alias": "zcoin" }, "ports": { "backend_rpc": 8050, @@ -52,7 +52,7 @@ "server_config_file": "bitcoin_like.conf", "client_config_file": "bitcoin_like_client.conf", "additional_params": { - "deprecatedrpc": "estimatefee" + "deprecatedrpc": "estimatefee" } }, "blockbook": { @@ -66,6 +66,8 @@ "mempool_workers": 8, "mempool_sub_workers": 2, "block_addresses_to_keep": 300, + "xpub_magic": 76067358, + "slip44": 136, "additional_params": {} } }, @@ -73,4 +75,4 @@ "package_maintainer": "Putta Khunchalee", "package_maintainer_email": "putta@zcoin.io" } -} +} \ No newline at end of file diff --git a/configs/environ.json b/configs/environ.json index d161716f..6171a41b 100644 --- a/configs/environ.json +++ b/configs/environ.json @@ -1,5 +1,5 @@ { - "version": "0.2.0", + "version": "0.2.1", "backend_install_path": "/opt/coins/nodes", "backend_data_path": "/opt/coins/data", "blockbook_install_path": "/opt/coins/blockbook", diff --git a/contrib/scripts/check-and-generate-port-registry.go b/contrib/scripts/check-and-generate-port-registry.go index 9e6039eb..9fcedcb1 100755 --- a/contrib/scripts/check-and-generate-port-registry.go +++ b/contrib/scripts/check-and-generate-port-registry.go @@ -237,7 +237,7 @@ func writeMarkdown(output string, slice PortInfoSlice) error { out := os.Stdout if output != "stdout" { - out, err = os.OpenFile(output, os.O_CREATE|os.O_WRONLY, 0644) + out, err = os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return err } diff --git a/docs/ports.md b/docs/ports.md index a5ddb131..7ed0863a 100644 --- a/docs/ports.md +++ b/docs/ports.md @@ -8,7 +8,7 @@ | Dash | 9033 | 9133 | 8033 | 38333 | | Litecoin | 9034 | 9134 | 8034 | 38334 | | Bitcoin Gold | 9035 | 9135 | 8035 | 38335 | -| Ethereum | 9036 | 9136 | 8036 | 38336 p2p, 8136 http | +| Ethereum | 9036 | 9136 | 8036 | 8136 http, 38336 p2p | | Ethereum Classic | 9037 | 9137 | 8037 | | | Dogecoin | 9038 | 9138 | 8038 | 38338 | | Namecoin | 9039 | 9139 | 8039 | 38339 | diff --git a/server/public.go b/server/public.go index a18b3efd..3b21a4ff 100644 --- a/server/public.go +++ b/server/public.go @@ -131,6 +131,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { // internal explorer handlers serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx)) serveMux.HandleFunc(path+"address/", s.htmlTemplateHandler(s.explorerAddress)) + serveMux.HandleFunc(path+"xpub/", s.htmlTemplateHandler(s.explorerXpub)) serveMux.HandleFunc(path+"search/", s.htmlTemplateHandler(s.explorerSearch)) serveMux.HandleFunc(path+"blocks", s.htmlTemplateHandler(s.explorerBlocks)) serveMux.HandleFunc(path+"block/", s.htmlTemplateHandler(s.explorerBlock)) @@ -156,7 +157,7 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/v1/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV1)) serveMux.HandleFunc(path+"api/v1/tx/", s.jsonHandler(s.apiTx, apiV1)) serveMux.HandleFunc(path+"api/v1/address/", s.jsonHandler(s.apiAddress, apiV1)) - serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiAddressUtxo, apiV1)) + serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiUtxo, apiV1)) serveMux.HandleFunc(path+"api/v1/block/", s.jsonHandler(s.apiBlock, apiV1)) serveMux.HandleFunc(path+"api/v1/sendtx/", s.jsonHandler(s.apiSendTx, apiV1)) serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1)) @@ -165,7 +166,8 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault)) serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault)) serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault)) - serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo, apiDefault)) + serveMux.HandleFunc(path+"api/xpub/", s.jsonHandler(s.apiXpub, apiDefault)) + serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiUtxo, apiDefault)) serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault)) serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault)) serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault)) @@ -174,7 +176,8 @@ func (s *PublicServer) ConnectFullPublicInterface() { serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2)) serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2)) serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2)) - serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiAddressUtxo, apiV2)) + serveMux.HandleFunc(path+"api/v2/xpub/", s.jsonHandler(s.apiXpub, apiV2)) + serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiUtxo, apiV2)) serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2)) serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2)) serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2)) @@ -372,6 +375,7 @@ const ( indexTpl txTpl addressTpl + xpubTpl blocksTpl blockTpl sendTransactionTpl @@ -381,26 +385,27 @@ const ( // TemplateData is used to transfer data to the templates type TemplateData struct { - CoinName string - CoinShortcut string - CoinLabel string - InternalExplorer bool - ChainType bchain.ChainType - Address *api.Address - AddrStr string - Tx *api.Tx - Error *api.APIError - Blocks *api.Blocks - Block *api.Block - Info *api.SystemInfo - Page int - PrevPage int - NextPage int - PagingRange []int - PageParams template.URL - TOSLink string - SendTxHex string - Status string + CoinName string + CoinShortcut string + CoinLabel string + InternalExplorer bool + ChainType bchain.ChainType + Address *api.Address + AddrStr string + Tx *api.Tx + Error *api.APIError + Blocks *api.Blocks + Block *api.Block + Info *api.SystemInfo + Page int + PrevPage int + NextPage int + PagingRange []int + PageParams template.URL + TOSLink string + SendTxHex string + Status string + NonZeroBalanceTokens bool } func (s *PublicServer) parseTemplates() []*template.Template { @@ -410,7 +415,8 @@ func (s *PublicServer) parseTemplates() []*template.Template { "formatAmount": s.formatAmount, "formatAmountWithDecimals": formatAmountWithDecimals, "setTxToTemplateData": setTxToTemplateData, - "stringInSlice": stringInSlice, + "isOwnAddress": isOwnAddress, + "isOwnAddresses": isOwnAddresses, } var createTemplate func(filenames ...string) *template.Template if s.debug { @@ -465,6 +471,7 @@ func (s *PublicServer) parseTemplates() []*template.Template { t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") } + t[xpubTpl] = createTemplate("./static/templates/xpub.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html") return t } @@ -498,6 +505,29 @@ func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData { return td } +// returns true if address is "own", +// i.e. either the address of the address detail or belonging to the xpub +func isOwnAddress(td *TemplateData, a string) bool { + if a == td.AddrStr { + return true + } + if td.Address != nil && td.Address.XPubAddresses != nil { + if _, found := td.Address.XPubAddresses[a]; found { + return true + } + } + return false +} + +// returns true if addresses are "own", +// i.e. either the address of the address detail or belonging to the xpub +func isOwnAddresses(td *TemplateData, addresses []string) bool { + if len(addresses) == 1 { + return isOwnAddress(td, addresses[0]) + } + return false +} + func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { var tx *api.Tx var err error @@ -560,7 +590,7 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) ( } } } - address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, api.TxHistoryLight, &api.AddressFilter{Vout: fn}) + address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, api.AccountDetailsTxHistoryLight, &api.AddressFilter{Vout: fn}) if err != nil { return errorTpl, nil, err } @@ -577,6 +607,68 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) ( return addressTpl, data, nil } +func (s *PublicServer) getXpubAddress(r *http.Request, xpub string, pageSize int, option api.AccountDetails) (*api.Address, api.TokenDetailLevel, error) { + var fn = api.AddressFilterVoutOff + page, ec := strconv.Atoi(r.URL.Query().Get("page")) + if ec != nil { + page = 0 + } + filter := r.URL.Query().Get("filter") + if len(filter) > 0 { + if filter == "inputs" { + fn = api.AddressFilterVoutInputs + } else if filter == "outputs" { + fn = api.AddressFilterVoutOutputs + } else { + fn, ec = strconv.Atoi(filter) + if ec != nil || fn < 0 { + filter = "" + fn = api.AddressFilterVoutOff + } + } + } + gap, ec := strconv.Atoi(r.URL.Query().Get("gap")) + if ec != nil { + gap = 0 + } + tokenLevel := api.TokenDetailNonzeroBalance + switch r.URL.Query().Get("tokenlevel") { + case "discovered": + tokenLevel = api.TokenDetailDiscovered + case "used": + tokenLevel = api.TokenDetailUsed + case "nonzero": + tokenLevel = api.TokenDetailNonzeroBalance + } + a, err := s.api.GetXpubAddress(xpub, page, pageSize, option, &api.AddressFilter{Vout: fn, TokenLevel: tokenLevel}, gap) + return a, tokenLevel, err +} + +func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { + var address *api.Address + var tokenLevel api.TokenDetailLevel + var err error + s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc() + if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { + address, tokenLevel, err = s.getXpubAddress(r, r.URL.Path[i+1:], txsOnPage, api.AccountDetailsTxHistoryLight) + if err != nil { + return errorTpl, nil, err + } + } + data := s.newTemplateData() + data.AddrStr = address.AddrStr + data.Address = address + data.Page = address.Page + data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages) + filter := r.URL.Query().Get("filter") + if filter != "" { + data.PageParams = template.URL("&filter=" + filter) + data.Address.Filter = filter + } + data.NonZeroBalanceTokens = tokenLevel == api.TokenDetailNonzeroBalance + return xpubTpl, data, nil +} + func (s *PublicServer) explorerBlocks(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { var blocks *api.Blocks var err error @@ -638,6 +730,11 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t var err error s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc() if len(q) > 0 { + address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0) + if err == nil { + http.Redirect(w, r, joinURL("/xpub/", address.AddrStr), 302) + return noTpl, nil, nil + } block, err = s.api.GetBlock(q, 0, 1) if err == nil { http.Redirect(w, r, joinURL("/block/", block.Hash), 302) @@ -648,7 +745,7 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302) return noTpl, nil, nil } - address, err = s.api.GetAddress(q, 0, 1, api.Basic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}) + address, err = s.api.GetAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}) if err == nil { http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302) return noTpl, nil, nil @@ -810,7 +907,7 @@ func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, if ec != nil { page = 0 } - address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, api.TxidHistory, &api.AddressFilter{Vout: api.AddressFilterVoutOff}) + address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, api.AccountDetailsTxidHistory, &api.AddressFilter{Vout: api.AddressFilterVoutOff}) if err == nil && apiVersion == apiV1 { return s.api.AddressToV1(address), nil } @@ -818,10 +915,22 @@ func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, return address, err } -func (s *PublicServer) apiAddressUtxo(r *http.Request, apiVersion int) (interface{}, error) { - var utxo []api.AddressUtxo +func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, error) { + var address *api.Address + var err error + s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc() + if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { + address, _, err = s.getXpubAddress(r, r.URL.Path[i+1:], txsInAPI, api.AccountDetailsTxidHistory) + if err == nil && apiVersion == apiV1 { + return s.api.AddressToV1(address), nil + } + } + return address, err +} + +func (s *PublicServer) apiUtxo(r *http.Request, apiVersion int) (interface{}, error) { + var utxo []api.Utxo var err error - s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc() if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 { onlyConfirmed := false c := r.URL.Query().Get("confirmed") @@ -831,7 +940,17 @@ func (s *PublicServer) apiAddressUtxo(r *http.Request, apiVersion int) (interfac return nil, api.NewAPIError("Parameter 'confirmed' cannot be converted to boolean", true) } } - utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed) + gap, ec := strconv.Atoi(r.URL.Query().Get("gap")) + if ec != nil { + gap = 0 + } + utxo, err = s.api.GetXpubUtxo(r.URL.Path[i+1:], onlyConfirmed, gap) + if err == nil { + s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub-utxo"}).Inc() + } else { + utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed) + s.metrics.ExplorerViews.With(common.Labels{"action": "api-address-utxo"}).Inc() + } if err == nil && apiVersion == apiV1 { return s.api.AddressUtxoToV1(utxo), nil } diff --git a/server/public_test.go b/server/public_test.go index 525a7248..6f2be7f1 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -60,7 +60,13 @@ func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *c func setupPublicHTTPServer(t *testing.T) (*PublicServer, string) { parser := btc.NewBitcoinParser( btc.GetChainParams("test"), - &btc.Configuration{BlockAddressesToKeep: 1}) + &btc.Configuration{ + BlockAddressesToKeep: 1, + XPubMagic: 70617039, + XPubMagicSegwitP2sh: 71979618, + XPubMagicSegwitNative: 73342198, + Slip44: 1, + }) d, is, path := setupRocksDB(t, parser) // setup internal state and match BestHeight to test data @@ -151,7 +157,7 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { `td class="data">0 FAKE`, `mzVznVsCHkVHX9UN8WPFASWUUHtxnNn4Jj`, `13.60030331 FAKE`, - `No Inputs (Newly Generated Coins)`, + `No Inputs (Newly Generated Coins)`, ``, }, }, @@ -168,8 +174,8 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { `0.00012345 FAKE`, `7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25`, `3172.83951061 FAKE `, - `mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL`, - `td>mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL`, + `mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL`, + `td>mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL`, `9172.83951061 FAKE ×`, `00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840`, ``, @@ -290,7 +296,7 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { `td class="data">0 FAKE`, `mzVznVsCHkVHX9UN8WPFASWUUHtxnNn4Jj`, `13.60030331 FAKE`, - `No Inputs (Newly Generated Coins)`, + `No Inputs (Newly Generated Coins)`, ``, }, }, @@ -307,13 +313,31 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { `0.00012345 FAKE`, `7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25`, `3172.83951061 FAKE `, - `mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL`, - `td>mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL`, + `mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL`, + `td>mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL`, `9172.83951061 FAKE ×`, `00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840`, ``, }, }, + { + name: "explorerSearch xpub", + r: newGetRequest(ts.URL + "/search?q=" + dbtestdata.Xpub), + status: http.StatusOK, + contentType: "text/html; charset=utf-8", + body: []string{ + `Fake Coin Explorer`, + `

XPUB 1186.419755 FAKE

upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q
`, + `Total Received1186.41975501 FAKE`, + `Total Sent0.00000001 FAKE`, + `Used XPUB Addresses2`, + `2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu1186.419755 FAKE1m/49'/1'/33'/1/3`, + ``, + ``, + `2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu10.00009876 FAKE `, + ``, + }, + }, { name: "explorerSearch not found", r: newGetRequest(ts.URL + "/search?q=1234"), @@ -437,7 +461,34 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { }, }, { - name: "apiAddressUtxo v1", + name: "apiXpub v2 tokenlevel=nonzero", + r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"totalTokens":2,"tokens":[{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, + }, + }, + { + name: "apiXpub v2 tokenlevel=used", + r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub + "?tokenlevel=used"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"totalTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"}]}`, + }, + }, + { + name: "apiXpub v2 tokenlevel=discovered", + r: newGetRequest(ts.URL + "/api/v2/xpub/" + dbtestdata.Xpub + "?tokenlevel=discovered"), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q","balance":"118641975500","totalReceived":"118641975501","totalSent":"1","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":2,"txids":["3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"],"totalTokens":2,"tokens":[{"type":"XPUBAddress","name":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","path":"m/49'/1'/33'/0/0","transfers":2,"decimals":8,"balance":"0","totalReceived":"1","totalSent":"1"},{"type":"XPUBAddress","name":"2MsYfbi6ZdVXLDNrYAQ11ja9Sd3otMk4Pmj","path":"m/49'/1'/33'/0/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuAZNAjLSo6RLFad2fvHSfgqBD7BoEVy4T","path":"m/49'/1'/33'/0/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEqKzw3BosGnBE9by5uaDy5QgwjHac4Zbg","path":"m/49'/1'/33'/0/3","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mw7vJNC8zUK6VNN4CEjtoTYmuNPLewxZzV","path":"m/49'/1'/33'/0/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1kvo97NFASPXiwephZUxE9PRXunjTxEc4","path":"m/49'/1'/33'/0/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuWrWMzoBt8VDFNvPmpJf42M1GTUs85fPx","path":"m/49'/1'/33'/0/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MuVZ2Ca6Da9zmYynt49Rx7uikAgubGcymF","path":"m/49'/1'/33'/0/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzRGWDUmrPP9HwYu4B43QGCTLwoop5cExa","path":"m/49'/1'/33'/0/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5C9EEWJzyBXhpyPHqa3UNed73Amsi5b3L","path":"m/49'/1'/33'/0/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzNawz2zjwq1L85GDE3YydEJGJYfXxaWkk","path":"m/49'/1'/33'/0/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7NdeuAMgL57WE7QCeV2gTWi2Um8iAu5dA","path":"m/49'/1'/33'/0/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8JQEP6DSHEZHNsSDPA1gHMUq9YFndhkfV","path":"m/49'/1'/33'/0/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mvbn3YXqKZVpQKugaoQrfjSYPvz76RwZkC","path":"m/49'/1'/33'/0/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8MRNxCfwUY9TSW27X9ooGYtqgrGCfLRHx","path":"m/49'/1'/33'/0/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6HvwrHC113KYZAmCtJ9XJNWgaTcnFunCM","path":"m/49'/1'/33'/0/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NEo3oNyHUoi7rmRWee7wki37jxPWsWCopJ","path":"m/49'/1'/33'/0/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mzm5KY8qdFbDHsQfy4akXbFvbR3FAwDuVo","path":"m/49'/1'/33'/0/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NGMwftmQCogp6XZNGvgiybz3WZysvsJzqC","path":"m/49'/1'/33'/0/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3fJrrefndYjLGycvFFfYgevpZtcRKCkRD","path":"m/49'/1'/33'/0/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N1T7TnHBwfdpBoyw53EGUL7vuJmb2mU6jF","path":"m/49'/1'/33'/0/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MzSBtRWHbBjeUcu3H5VRDqkvz5sfmDxJKo","path":"m/49'/1'/33'/1/0","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MtShtAJYb1afWduUTwF1SixJjan7urZKke","path":"m/49'/1'/33'/1/1","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N3cP668SeqyBEr9gnB4yQEmU3VyxeRYith","path":"m/49'/1'/33'/1/2","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3","transfers":1,"decimals":8,"balance":"118641975500","totalReceived":"118641975500","totalSent":"0"},{"type":"XPUBAddress","name":"2NEzatauNhf9kPTwwj6ZfYKjUdy52j4hVUL","path":"m/49'/1'/33'/1/4","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4RjsDp4LBpkNqyF91aNjgpF9CwDwBkJZq","path":"m/49'/1'/33'/1/5","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8XygTmQc4NoBBPEy3yybnfCYhsxFtzPDY","path":"m/49'/1'/33'/1/6","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5BjBomZvb48sccK2vwLMiQ5ETKp1fdPVn","path":"m/49'/1'/33'/1/7","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2MybMwbZRPCGU3SMWPwQCpDkbcQFw5Hbwen","path":"m/49'/1'/33'/1/8","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N7HexL4dyAQc7Th4iqcCW4hZuyiZsLWf74","path":"m/49'/1'/33'/1/9","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NF6X5FDGWrQj4nQrfP6hA77zB5WAc1DGup","path":"m/49'/1'/33'/1/10","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4ZRPdvc7BVioBTohy4F6QtxreqcjNj26b","path":"m/49'/1'/33'/1/11","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2Mtfho1rLmevh4qTnkYWxZEFCWteDMtTcUF","path":"m/49'/1'/33'/1/12","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFUCphKYvmMcNZRZrF261mRX6iADVB9Qms","path":"m/49'/1'/33'/1/13","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N5kBNMB8qgxE4Y4f8J19fScsE49J4aNvoJ","path":"m/49'/1'/33'/1/14","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NANWCaefhCKdXMcW8NbZnnrFRDvhJN2wPy","path":"m/49'/1'/33'/1/15","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NFHw7Yo2Bz8D2wGAYHW9qidbZFLpfJ72qB","path":"m/49'/1'/33'/1/16","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBDSsBgy5PpFniLCb1eAFHcSxgxwPSDsZa","path":"m/49'/1'/33'/1/17","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NDWCSQHogc7sCuc2WoYt9PX2i2i6a5k6dX","path":"m/49'/1'/33'/1/18","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N8vNyDP7iSDjm3BKpXrbDjAxyphqfvnJz8","path":"m/49'/1'/33'/1/19","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2N4tFKLurSbMusAyq1tv4tzymVjveAFV1Vb","path":"m/49'/1'/33'/1/20","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBx5WwjAr2cH6Yqrp3Vsf957HtRKwDUVdX","path":"m/49'/1'/33'/1/21","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NBu1seHTaFhQxbcW5L5BkZzqFLGmZqpxsa","path":"m/49'/1'/33'/1/22","transfers":0,"decimals":8},{"type":"XPUBAddress","name":"2NCDLoea22jGsXuarfT1n2QyCUh6RFhAPnT","path":"m/49'/1'/33'/1/23","transfers":0,"decimals":8}]}`, + }, + }, + { + name: "apiUtxo v1", r: newGetRequest(ts.URL + "/api/v1/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"), status: http.StatusOK, contentType: "application/json; charset=utf-8", @@ -446,7 +497,7 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { }, }, { - name: "apiAddressUtxo v2", + name: "apiUtxo v2", r: newGetRequest(ts.URL + "/api/v2/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"), status: http.StatusOK, contentType: "application/json; charset=utf-8", @@ -454,6 +505,15 @@ func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) { `[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":1,"value":"917283951061","height":225494,"confirmations":1}]`, }, }, + { + name: "apiUtxo v2 xpub", + r: newGetRequest(ts.URL + "/api/v2/utxo/" + dbtestdata.Xpub), + status: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: []string{ + `[{"txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","vout":0,"value":"118641975500","height":225494,"confirmations":1,"address":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu","path":"m/49'/1'/33'/1/3"}]`, + }, + }, { name: "apiSendTx", r: newGetRequest(ts.URL + "/api/sendtx/1234567890"), @@ -599,7 +659,7 @@ func socketioTests_BitcoinType(t *testing.T, ts *httptest.Server) { { name: "getDetailedTransaction", req: socketioReq{"getDetailedTransaction", []interface{}{"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71"}}, - want: `{"result":{"hex":"","height":225494,"blockTimestamp":22549400001,"version":0,"hash":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","inputs":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","outputIndex":0,"script":"","sequence":0,"address":"mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX","satoshis":317283951061},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","outputIndex":1,"script":"","sequence":0,"address":"2Mz1CYoppGGsLNUGF2YDhTif6J661JitALS","satoshis":1}],"inputSatoshis":317283951062,"outputs":[{"satoshis":118641975500,"script":"76a914b434eb0c1a3b7a02e8a29cc616e791ef1e0bf51f88ac","address":"mwwoKQE5Lb1G4picHSHDQKg8jw424PF9SC"},{"satoshis":198641975500,"script":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","address":"mmJx9Y8ayz9h14yd9fgCW1bUKoEpkBAquP"}],"outputSatoshis":317283951000,"feeSatoshis":62}}`, + want: `{"result":{"hex":"","height":225494,"blockTimestamp":22549400001,"version":0,"hash":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","inputs":[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","outputIndex":0,"script":"","sequence":0,"address":"mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX","satoshis":317283951061},{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","outputIndex":1,"script":"","sequence":0,"address":"2MzmAKayJmja784jyHvRUW1bXPget1csRRG","satoshis":1}],"inputSatoshis":317283951062,"outputs":[{"satoshis":118641975500,"script":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","address":"2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu"},{"satoshis":198641975500,"script":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","address":"mmJx9Y8ayz9h14yd9fgCW1bUKoEpkBAquP"}],"outputSatoshis":317283951000,"feeSatoshis":62}}`, }, { name: "sendTransaction", diff --git a/server/socketio.go b/server/socketio.go index e742f3cb..fc70510d 100644 --- a/server/socketio.go +++ b/server/socketio.go @@ -292,15 +292,6 @@ type resultGetAddressHistory struct { } `json:"result"` } -func stringInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false -} - func txToResTx(tx *api.Tx) resTx { inputs := make([]txInputs, len(tx.Vin)) for i := range tx.Vin { diff --git a/server/websocket.go b/server/websocket.go index 91bfbab8..e71b0c7e 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -317,10 +317,10 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { } if err == nil { glog.V(1).Info("Client ", c.id, " onRequest ", req.Method, " success") - s.metrics.SocketIORequests.With(common.Labels{"method": req.Method, "status": "success"}).Inc() + s.metrics.WebsocketRequests.With(common.Labels{"method": req.Method, "status": "success"}).Inc() } else { glog.Error("Client ", c.id, " onMessage ", req.Method, ": ", errors.ErrorStack(err)) - s.metrics.SocketIORequests.With(common.Labels{"method": req.Method, "status": err.Error()}).Inc() + s.metrics.WebsocketRequests.With(common.Labels{"method": req.Method, "status": err.Error()}).Inc() e := resultError{} e.Error.Message = err.Error() data = e @@ -347,27 +347,39 @@ func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) { } func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, err error) { - var opt api.GetAddressOption + var opt api.AccountDetails switch req.Details { - case "balance": - opt = api.Balance + case "tokens": + opt = api.AccountDetailsTokens + case "tokenBalances": + opt = api.AccountDetailsTokenBalances case "txids": - opt = api.TxidHistory + opt = api.AccountDetailsTxidHistory case "txs": - opt = api.TxHistory + opt = api.AccountDetailsTxHistory default: - opt = api.Basic + opt = api.AccountDetailsBasic } - return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &api.AddressFilter{ + filter := api.AddressFilter{ FromHeight: uint32(req.FromHeight), ToHeight: uint32(req.ToHeight), Contract: req.ContractFilter, Vout: api.AddressFilterVoutOff, - }) + TokenLevel: api.TokenDetailDiscovered, + } + a, err := s.api.GetXpubAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, 0) + if err != nil { + return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter) + } + return a, nil } func (s *WebsocketServer) getAccountUtxo(descriptor string) (interface{}, error) { - return s.api.GetAddressUtxo(descriptor, false) + utxo, err := s.api.GetXpubUtxo(descriptor, false, 0) + if err != nil { + return s.api.GetAddressUtxo(descriptor, false) + } + return utxo, nil } func (s *WebsocketServer) getTransaction(txid string) (interface{}, error) { diff --git a/static/css/main.css b/static/css/main.css index c4ef6a48..d54f9841 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -182,6 +182,26 @@ h3 { text-transform: uppercase; } +.tx-own { + background-color: #fbf8f0; +} + +.tx-amt { + float: right!important; +} + +.tx-in .tx-own .tx-amt { + color: #dc3545!important; +} + +.tx-out .tx-own .tx-amt { + color: #28a745!important; +} + +.tx-addr { + float: left!important; +} + .ellipsis { overflow: hidden; text-overflow: ellipsis; @@ -283,5 +303,5 @@ table.data-table table.data-table th { } .key { - color: #333 ; + color: #333; } \ No newline at end of file diff --git a/static/templates/base.html b/static/templates/base.html index bc1e9f02..cecc88d9 100644 --- a/static/templates/base.html +++ b/static/templates/base.html @@ -42,7 +42,7 @@ {{- end -}} diff --git a/static/templates/txdetail.html b/static/templates/txdetail.html index 97121b13..6b3a3f82 100644 --- a/static/templates/txdetail.html +++ b/static/templates/txdetail.html @@ -1,4 +1,4 @@ -{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}} +{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}{{$data := .}}
@@ -10,23 +10,23 @@
-
+
{{- range $vin := $tx.Vin -}} - + @@ -45,20 +45,20 @@
-
+
{{- if $vin.Txid -}} ➡  {{- end -}} {{- range $a := $vin.Addresses -}} - + {{if and (ne $a $addr) $vin.Searchable}}{{$a}}{{else}}{{$a}}{{end}} {{- else -}} - {{- if $vin.Hex -}}Unparsed address{{- else -}}No Inputs (Newly Generated Coins){{- end -}} + {{- if $vin.Hex -}}Unparsed address{{- else -}}No Inputs (Newly Generated Coins){{- end -}} {{- end -}}{{- if $vin.Addresses -}} - {{formatAmount $vin.ValueSat}} {{$cs}} + {{formatAmount $vin.ValueSat}} {{$cs}} {{- end -}}
{{- range $vout := $tx.Vout -}} - +
{{- range $a := $vout.Addresses -}} - + {{- if and (ne $a $addr) $vout.Searchable}}{{$a}}{{else}}{{$a}}{{- end -}} {{- else -}} - Unparsed address + Unparsed address {{- end -}} - + {{formatAmount $vout.ValueSat}} {{$cs}} {{if $vout.Spent}}{{else -}} × {{- end -}} diff --git a/static/templates/txdetail_ethereumtype.html b/static/templates/txdetail_ethereumtype.html index ec760955..bb71a8cc 100644 --- a/static/templates/txdetail_ethereumtype.html +++ b/static/templates/txdetail_ethereumtype.html @@ -1,4 +1,4 @@ -{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}} +{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}{{$data := .}}
@@ -11,18 +11,18 @@
-
+
{{- range $vin := $tx.Vin -}} - + @@ -41,18 +41,18 @@
-
+
{{- range $a := $vin.Addresses -}} - + {{if and (ne $a $addr) $vin.Searchable}}{{$a}}{{else}}{{$a}}{{end}} {{- else -}} - Unparsed address + Unparsed address {{- end -}}
{{- range $vout := $tx.Vout -}} - + @@ -76,12 +76,12 @@ {{- range $erc20 := $tx.TokenTransfers -}}
-
+
{{- range $a := $vout.Addresses -}} - + {{- if and (ne $a $addr) $vout.Searchable}}{{$a}}{{else}}{{$a}}{{- end -}} {{- else -}} - Unparsed address + Unparsed address {{- end -}}
- + @@ -94,12 +94,12 @@
-
+
- {{if ne $erc20.From $addr}}{{$erc20.From}}{{else}}{{$erc20.From}}{{end}} + {{if ne $erc20.From $addr}}{{$erc20.From}}{{else}}{{$erc20.From}}{{end}}
- + diff --git a/static/templates/xpub.html b/static/templates/xpub.html new file mode 100644 index 00000000..d67c76a3 --- /dev/null +++ b/static/templates/xpub.html @@ -0,0 +1,105 @@ +{{define "specific"}}{{$cs := .CoinShortcut}}{{$addr := .Address}}{{$data := .}} +

XPUB {{formatAmount $addr.BalanceSat}} {{$cs}} +

+
+ {{$addr.AddrStr}} +
+

Confirmed

+
+
+
- {{if ne $erc20.To $addr}}{{$erc20.To}}{{else}}{{$erc20.To}}{{end}} + {{if ne $erc20.To $addr}}{{$erc20.To}}{{else}}{{$erc20.To}}{{end}}
+ + + + + + + + + + + + + + + + + + + + + + {{- if or $addr.Tokens $addr.TotalTokens -}} + + + + + {{- end -}} + +
Total Received{{formatAmount $addr.TotalReceivedSat}} {{$cs}}
Total Sent{{formatAmount $addr.TotalSentSat}} {{$cs}}
Final Balance{{formatAmount $addr.BalanceSat}} {{$cs}}
No. Transactions{{$addr.Txs}}
Used XPUB Addresses{{$addr.TotalTokens}}
{{if $data.NonZeroBalanceTokens}}XPUB Addresses with Balance{{else}}XPUB Addresses{{end}} + + + + + + + + + {{- range $t := $addr.Tokens -}} + + + + + + + {{- end -}} + {{- if $data.NonZeroBalanceTokens -}} + + + + {{- end -}} + +
AddressBalanceTxsPath
{{$t.Name}}{{formatAmount $t.BalanceSat}} {{$cs}}{{$t.Transfers}}{{$t.Path}}
Show all XPUB addresses
+
+
+
+
+ + +
+
+{{- if $addr.UnconfirmedTxs -}} +

Unconfirmed

+
+ + + + + + + + + + + +
Unconfirmed Balance{{formatAmount $addr.UnconfirmedBalanceSat}} {{$cs}}
No. Transactions{{$addr.UnconfirmedTxs}}
+
+{{- end}}{{if or $addr.Transactions $addr.Filter -}} +
+

Transactions

+ +
+ +
+
+
+ {{- range $tx := $addr.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}} +
+ +{{end}}{{end}} \ No newline at end of file diff --git a/static/test-websocket.html b/static/test-websocket.html index c5cca40d..37274254 100644 --- a/static/test-websocket.html +++ b/static/test-websocket.html @@ -304,7 +304,8 @@ diff --git a/tests/dbtestdata/dbtestdata.go b/tests/dbtestdata/dbtestdata.go index 36a55c9b..e2f62f05 100644 --- a/tests/dbtestdata/dbtestdata.go +++ b/tests/dbtestdata/dbtestdata.go @@ -16,14 +16,16 @@ const ( TxidB2T3 = "05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07" TxidB2T4 = "fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db" + Xpub = "upub5E1xjDmZ7Hhej6LPpS8duATdKXnRYui7bDYj6ehfFGzWDZtmCmQkZhc3Zb7kgRLtHWd16QFxyP86JKL3ShZEBFX88aciJ3xyocuyhZZ8g6q" + Addr1 = "mfcWp7DB6NuaZsExybTTXpVgWz559Np4Ti" // 76a914010d39800f86122416e28f485029acf77507169288ac Addr2 = "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eEtz" // 76a9148bdf0aa3c567aa5975c2e61321b8bebbe7293df688ac Addr3 = "mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw" // 76a914a08eae93007f22668ab5e4a9c83c8cd1c325e3e088ac - Addr4 = "2Mz1CYoppGGsLNUGF2YDhTif6J661JitALS" // a9144a21db08fb6882cb152e1ff06780a430740f770487 + Addr4 = "2MzmAKayJmja784jyHvRUW1bXPget1csRRG" // a91452724c5178682f70e0ba31c6ec0633755a3b41d987, xpub m/49'/1'/33'/0/0 Addr5 = "2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1" // a914e921fc4912a315078f370d959f2c4f7b6d2a683c87 Addr6 = "mzB8cYrfRwFRFAGTDzV8LkUQy5BQicxGhX" // 76a914ccaaaf374e1b06cb83118453d102587b4273d09588ac Addr7 = "mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL" // 76a9148d802c045445df49613f6a70ddd2e48526f3701f88ac - Addr8 = "mwwoKQE5Lb1G4picHSHDQKg8jw424PF9SC" // 76a914b434eb0c1a3b7a02e8a29cc616e791ef1e0bf51f88ac + Addr8 = "2N6utyMZfPNUb1Bk8oz7p2JqJrXkq83gegu" // a91495e9fbe306449c991d314afe3c3567d5bf78efd287, xpub m/49'/1'/33'/1/3 Addr9 = "mmJx9Y8ayz9h14yd9fgCW1bUKoEpkBAquP" // 76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac AddrA = "mzVznVsCHkVHX9UN8WPFASWUUHtxnNn4Jj" // 76a914d03c0d863d189b23b061a95ad32940b65837609f88ac )