Merge branch 'xpub'

This commit is contained in:
Martin Boehm 2019-03-01 11:12:40 +01:00
commit dda96b4a8f
51 changed files with 2305 additions and 778 deletions

8
.gitignore vendored
View File

@ -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

4
Gopkg.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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 {

View File

@ -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

574
api/xpub.go Normal file
View File

@ -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
}

View File

@ -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")

View File

@ -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 <witness version: OP_0><len pubKeyHash: 20><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
}

View File

@ -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)
}
})
}
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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}}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -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
}

View File

@ -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 |

View File

@ -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
}

File diff suppressed because one or more lines are too long

View File

@ -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 {

View File

@ -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) {

View File

@ -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;
}

View File

@ -42,7 +42,7 @@
</ul>
<span class="d-none ml-md-auto d-md-flex navbar-form navbar-left">
<form id="search" action="/search" method="get">
<input name="q" type="text" class="form-control" placeholder="Search for block, transaction or address" focus="true">
<input name="q" type="text" class="form-control" placeholder="Search for block, transaction, address or xpub" focus="true">
</form>
</span>
{{- end -}}

View File

@ -1,4 +1,4 @@
{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}
{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}{{$data := .}}
<div class="alert alert-data">
<div class="row line-bot">
<div class="col-xs-7 col-md-8 ellipsis">
@ -10,23 +10,23 @@
</div>
<div class="row line-mid">
<div class="col-md-5">
<div class="row">
<div class="row tx-in">
<table class="table data-table">
<tbody>
{{- range $vin := $tx.Vin -}}
<tr>
<tr{{if isOwnAddresses $data $vin.Addresses}} class="tx-own"{{end}}>
<td>
{{- if $vin.Txid -}}
<a class="float-left text-muted" href="/tx/{{$vin.Txid}}" title="Outpoint {{$vin.Txid}},{{$vin.Vout}}">&nbsp;</a>
{{- end -}}
{{- range $a := $vin.Addresses -}}
<span class="ellipsis float-left">
<span class="ellipsis tx-addr">
{{if and (ne $a $addr) $vin.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{end}}
</span>
{{- else -}}
<span class="float-left">{{- if $vin.Hex -}}Unparsed address{{- else -}}No Inputs (Newly Generated Coins){{- end -}}</span>
<span class="tx-addr">{{- if $vin.Hex -}}Unparsed address{{- else -}}No Inputs (Newly Generated Coins){{- end -}}</span>
{{- end -}}{{- if $vin.Addresses -}}
<span class="float-right{{if stringInSlice $addr $vin.Addresses}} text-danger{{end}}">{{formatAmount $vin.ValueSat}} {{$cs}}</span>
<span class="tx-amt">{{formatAmount $vin.ValueSat}} {{$cs}}</span>
{{- end -}}
</td>
</tr>
@ -45,20 +45,20 @@
</svg>
</div>
<div class="col-md-6">
<div class="row">
<div class="row tx-out">
<table class="table data-table">
<tbody>
{{- range $vout := $tx.Vout -}}
<tr>
<tr{{if isOwnAddresses $data $vout.Addresses}} class="tx-own"{{end}}>
<td>
{{- range $a := $vout.Addresses -}}
<span class="ellipsis float-left">
<span class="ellipsis tx-addr">
{{- if and (ne $a $addr) $vout.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{- end -}}
</span>
{{- else -}}
<span class="float-left">Unparsed address</span>
<span class="tx-addr">Unparsed address</span>
{{- end -}}
<span class="float-right{{if stringInSlice $addr $vout.Addresses}} text-success{{end}}">
<span class="tx-amt">
{{formatAmount $vout.ValueSat}} {{$cs}} {{if $vout.Spent}}<a class="text-danger" href="{{if $vout.SpentTxID}}/tx/{{$vout.SpentTxID}}{{else}}/spending/{{$tx.Txid}}/{{$vout.N}}{{end}}" title="Spent"></a>{{else -}}
<span class="text-success" title="Unspent"> <b>×</b></span>
{{- end -}}

View File

@ -1,4 +1,4 @@
{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}
{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}{{$data := .}}
<div class="alert alert-data"{{if eq $tx.EthereumSpecific.Status 0}} style="background-color: #faf2ee;"{{end}}>
<div class="row line-bot">
<div class="col-xs-7 col-md-8 ellipsis">
@ -11,18 +11,18 @@
</div>
<div class="row line-mid">
<div class="col-md-4">
<div class="row">
<div class="row tx-in">
<table class="table data-table">
<tbody>
{{- range $vin := $tx.Vin -}}
<tr>
<tr{{if isOwnAddresses $data $vin.Addresses}} class="tx-own"{{end}}>
<td>
{{- range $a := $vin.Addresses -}}
<span class="ellipsis float-left">
<span class="ellipsis tx-addr">
{{if and (ne $a $addr) $vin.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{end}}
</span>
{{- else -}}
<span class="float-left">Unparsed address</span>
<span class="tx-addr">Unparsed address</span>
{{- end -}}
</td>
</tr>
@ -41,18 +41,18 @@
</svg>
</div>
<div class="col-md-4">
<div class="row">
<div class="row tx-out">
<table class="table data-table">
<tbody>
{{- range $vout := $tx.Vout -}}
<tr>
<tr{{if isOwnAddresses $data $vout.Addresses}} class="tx-own"{{end}}>
<td>
{{- range $a := $vout.Addresses -}}
<span class="ellipsis float-left">
<span class="ellipsis tx-addr">
{{- if and (ne $a $addr) $vout.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{- end -}}
</span>
{{- else -}}
<span class="float-left">Unparsed address</span>
<span class="tx-addr">Unparsed address</span>
{{- end -}}
</td>
</tr>
@ -76,12 +76,12 @@
{{- range $erc20 := $tx.TokenTransfers -}}
<div class="row" style="padding: 2px 15px;">
<div class="col-md-4">
<div class="row">
<div class="row tx-in">
<table class="table data-table">
<tbody>
<tr>
<tr{{if isOwnAddress $data $erc20.From}} class="tx-own"{{end}}>
<td>
<span class="ellipsis float-left">{{if ne $erc20.From $addr}}<a href="/address/{{$erc20.From}}">{{$erc20.From}}</a>{{else}}{{$erc20.From}}{{end}}</span>
<span class="ellipsis tx-addr">{{if ne $erc20.From $addr}}<a href="/address/{{$erc20.From}}">{{$erc20.From}}</a>{{else}}{{$erc20.From}}{{end}}</span>
</td>
</tr>
</tbody>
@ -94,12 +94,12 @@
</svg>
</div>
<div class="col-md-4">
<div class="row">
<div class="row tx-out">
<table class="table data-table">
<tbody>
<tr>
<tr{{if isOwnAddress $data $erc20.To}} class="tx-own"{{end}}>
<td>
<span class="ellipsis float-left">{{if ne $erc20.To $addr}}<a href="/address/{{$erc20.To}}">{{$erc20.To}}</a>{{else}}{{$erc20.To}}{{end}}</span>
<span class="ellipsis tx-addr">{{if ne $erc20.To $addr}}<a href="/address/{{$erc20.To}}">{{$erc20.To}}</a>{{else}}{{$erc20.To}}{{end}}</span>
</td>
</tr>
</tbody>

105
static/templates/xpub.html Normal file
View File

@ -0,0 +1,105 @@
{{define "specific"}}{{$cs := .CoinShortcut}}{{$addr := .Address}}{{$data := .}}
<h1>XPUB <small class="text-muted">{{formatAmount $addr.BalanceSat}} {{$cs}}</small>
</h1>
<div class="alert alert-data ellipsis">
<span class="data">{{$addr.AddrStr}}</span>
</div>
<h3>Confirmed</h3>
<div class="data-div row">
<div class="col-md-10">
<table class="table data-table">
<tbody>
<tr>
<td style="width: 25%;">Total Received</td>
<td class="data">{{formatAmount $addr.TotalReceivedSat}} {{$cs}}</td>
</tr>
<tr>
<td>Total Sent</td>
<td class="data">{{formatAmount $addr.TotalSentSat}} {{$cs}}</td>
</tr>
<tr>
<td>Final Balance</td>
<td class="data">{{formatAmount $addr.BalanceSat}} {{$cs}}</td>
</tr>
<tr>
<td>No. Transactions</td>
<td class="data">{{$addr.Txs}}</td>
</tr>
<tr>
<td>Used XPUB Addresses</td>
<td class="data">{{$addr.TotalTokens}}</td>
</tr>
{{- if or $addr.Tokens $addr.TotalTokens -}}
<tr>
<td>{{if $data.NonZeroBalanceTokens}}XPUB Addresses with Balance{{else}}XPUB Addresses{{end}}</td>
<td style="padding: 0;">
<table class="table data-table">
<tbody>
<tr>
<th style="width: 50%;">Address</th>
<th>Balance</th>
<th style="width: 8%;">Txs</th>
<th style="width: 18%;">Path</th>
</tr>
{{- range $t := $addr.Tokens -}}
<tr>
<td class="data ellipsis"><a href="/address/{{$t.Name}}">{{$t.Name}}</a></td>
<td class="data">{{formatAmount $t.BalanceSat}} {{$cs}}</td>
<td class="data">{{$t.Transfers}}</td>
<td>{{$t.Path}}</td>
</tr>
{{- end -}}
{{- if $data.NonZeroBalanceTokens -}}
<tr>
<td colspan="4"><a href="?tokenlevel=used">Show all XPUB addresses</a></td>
</tr>
{{- end -}}
</tbody>
</table>
</td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
<div class="col-md-2">
<div id="qrcode" style="width: 160px; height: 160px;"></div>
<script type="text/javascript" src="/static/js/qrcode.min.js"></script>
<script type="text/javascript">
new QRCode(document.getElementById("qrcode"), { text: "{{$addr.AddrStr}}", width: 160, height: 160 });
</script>
</div>
</div>
{{- if $addr.UnconfirmedTxs -}}
<h3>Unconfirmed</h3>
<div class="data-div">
<table class="table data-table">
<tbody>
<tr>
<td style="width: 25%;">Unconfirmed Balance</td>
<td class="data">{{formatAmount $addr.UnconfirmedBalanceSat}} {{$cs}}</td>
</tr>
<tr>
<td>No. Transactions</td>
<td class="data">{{$addr.UnconfirmedTxs}}</td>
</tr>
</tbody>
</table>
</div>
{{- end}}{{if or $addr.Transactions $addr.Filter -}}
<div class="row h-container">
<h3 class="col-md-3">Transactions</h3>
<select class="col-md-2" style="background-color: #eaeaea;" onchange="self.location='?filter='+options[selectedIndex].value">
<option>All</option>
<option {{if eq $addr.Filter "inputs" -}} selected{{end}} value="inputs">Inputs</option>
<option {{if eq $addr.Filter "outputs" -}} selected{{end}} value="outputs">Outputs</option>
</select>
<div class="col-md-7">
<nav>{{template "paging" $data}}</nav>
</div>
</div>
<div class="data-div">
{{- range $tx := $addr.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}}
</div>
<nav>{{template "paging" $data }}</nav>
{{end}}{{end}}

View File

@ -304,7 +304,8 @@
<input type="text" placeholder="descriptor" style="width: 79%" class="form-control" id="getAccountInfoDescriptor" value="0xba98d6a5ac827632e3457de7512d211e4ff7e8bd">
<select id="getAccountInfoDetails" style="width: 20%; margin-left: 5px;">
<option value="basic">Basic</option>
<option value="balance">Balance</option>
<option value="tokens">Tokens</option>
<option value="tokenBalances">TokenBalances</option>
<option value="txids">Txids</option>
<option value="txs">Transactions</option>
</select>

View File

@ -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
)