diff --git a/api/types.go b/api/types.go index 3941dcdc..10d595f4 100644 --- a/api/types.go +++ b/api/types.go @@ -245,26 +245,28 @@ type AddressAliasesMap map[string]AddressAlias // Tx holds information about a transaction type Tx struct { - Txid string `json:"txid"` - Version int32 `json:"version,omitempty"` - Locktime uint32 `json:"lockTime,omitempty"` - Vin []Vin `json:"vin"` - Vout []Vout `json:"vout"` - Blockhash string `json:"blockHash,omitempty"` - Blockheight int `json:"blockHeight"` - Confirmations uint32 `json:"confirmations"` - Blocktime int64 `json:"blockTime"` - Size int `json:"size,omitempty"` - VSize int `json:"vsize,omitempty"` - ValueOutSat *Amount `json:"value"` - ValueInSat *Amount `json:"valueIn,omitempty"` - FeesSat *Amount `json:"fees,omitempty"` - Hex string `json:"hex,omitempty"` - Rbf bool `json:"rbf,omitempty"` - CoinSpecificData json.RawMessage `json:"coinSpecificData,omitempty"` - TokenTransfers []TokenTransfer `json:"tokenTransfers,omitempty"` - EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"` - AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"` + Txid string `json:"txid"` + Version int32 `json:"version,omitempty"` + Locktime uint32 `json:"lockTime,omitempty"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` + Blockhash string `json:"blockHash,omitempty"` + Blockheight int `json:"blockHeight"` + Confirmations uint32 `json:"confirmations"` + ConfirmationETABlocks uint32 `json:"confirmationETABlocks,omitempty"` + ConfirmationETASeconds int64 `json:"confirmationETASeconds,omitempty"` + Blocktime int64 `json:"blockTime"` + Size int `json:"size,omitempty"` + VSize int `json:"vsize,omitempty"` + ValueOutSat *Amount `json:"value"` + ValueInSat *Amount `json:"valueIn,omitempty"` + FeesSat *Amount `json:"fees,omitempty"` + Hex string `json:"hex,omitempty"` + Rbf bool `json:"rbf,omitempty"` + CoinSpecificData json.RawMessage `json:"coinSpecificData,omitempty"` + TokenTransfers []TokenTransfer `json:"tokenTransfers,omitempty"` + EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"` + AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"` } // FeeStats contains detailed block fee statistics diff --git a/api/worker.go b/api/worker.go index 255e99df..150bbe5b 100644 --- a/api/worker.go +++ b/api/worker.go @@ -208,6 +208,49 @@ func (w *Worker) getParsedEthereumInputData(data string) *bchain.EthereumParsedI return eth.ParseInputData(signatures, data) } +// getConfirmationETA returns confirmation ETA in seconds and blocks +func (w *Worker) getConfirmationETA(tx *Tx) (int64, uint32) { + var etaBlocks uint32 + var etaSeconds int64 + if w.chainType == bchain.ChainBitcoinType && tx.FeesSat != nil { + _, _, mempoolSize := w.is.GetMempoolSyncState() + // if there are a few transactions in the mempool, the estimate fee does not work well + // and the tx is most probably going to be confirmed in the first block + if mempoolSize < 32 { + etaBlocks = 1 + } else { + var txFeePerKB int64 + if tx.VSize > 0 { + txFeePerKB = 1000 * tx.FeesSat.AsInt64() / int64(tx.VSize) + } else if tx.Size > 0 { + txFeePerKB = 1000 * tx.FeesSat.AsInt64() / int64(tx.Size) + } + if txFeePerKB > 0 { + // binary search the estimate, split it to more common first 7 blocks and the rest up to 70 blocks + var b int + fee, _ := w.cachedEstimateFee(7, true) + if fee.Int64() <= txFeePerKB { + b = sort.Search(7, func(i int) bool { + // fee is in sats/kB + fee, _ := w.cachedEstimateFee(i+1, true) + return fee.Int64() <= txFeePerKB + }) + b += 1 + } else { + b = sort.Search(63, func(i int) bool { + fee, _ := w.cachedEstimateFee(i+7, true) + return fee.Int64() <= txFeePerKB + }) + b += 7 + } + etaBlocks = uint32(b) + } + } + etaSeconds = int64(etaBlocks * w.is.AvgBlockPeriod) + } + return etaSeconds, etaBlocks +} + // getTransactionFromBchainTx reads transaction data from txid func (w *Worker) getTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spendingTxs bool, specificJSON bool, addresses map[string]struct{}) (*Tx, error) { var err error @@ -392,8 +435,6 @@ func (w *Worker) getTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe } } - // for now do not return size, we would have to compute vsize of segwit transactions - // size:=len(bchainTx.Hex) / 2 var sj json.RawMessage // return CoinSpecificData for all mempool transactions or if requested if specificJSON || bchainTx.Confirmations == 0 { @@ -402,10 +443,6 @@ func (w *Worker) getTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe return nil, err } } - // for mempool transaction get first seen time - if bchainTx.Confirmations == 0 { - bchainTx.Blocktime = int64(w.mempool.GetTransactionTime(bchainTx.Txid)) - } r := &Tx{ Blockhash: blockhash, Blockheight: height, @@ -427,6 +464,10 @@ func (w *Worker) getTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe TokenTransfers: tokens, EthereumSpecific: ethSpecific, } + if bchainTx.Confirmations == 0 { + r.Blocktime = int64(w.mempool.GetTransactionTime(bchainTx.Txid)) + r.ConfirmationETASeconds, r.ConfirmationETABlocks = w.getConfirmationETA(r) + } return r, nil } @@ -521,6 +562,8 @@ func (w *Worker) GetTransactionFromMempoolTx(mempoolTx *bchain.MempoolTx) (*Tx, ValueInSat: (*Amount)(pValInSat), ValueOutSat: (*Amount)(&valOutSat), Version: mempoolTx.Version, + Size: len(mempoolTx.Hex) >> 1, + VSize: int(mempoolTx.VSize), Hex: mempoolTx.Hex, Rbf: rbf, Vin: vins, @@ -529,6 +572,7 @@ func (w *Worker) GetTransactionFromMempoolTx(mempoolTx *bchain.MempoolTx) (*Tx, EthereumSpecific: ethSpecific, AddressAliases: w.getAddressAliases(addresses), } + r.ConfirmationETASeconds, r.ConfirmationETABlocks = w.getConfirmationETA(r) return r, nil } @@ -2249,7 +2293,13 @@ const estimatedFeeCacheSize = 300 var estimatedFeeCache [estimatedFeeCacheSize]bitcoinTypeEstimatedFee var estimatedFeeConservativeCache [estimatedFeeCacheSize]bitcoinTypeEstimatedFee -func (w *Worker) cachedBitcoinTypeEstimateFee(blocks int, conservative bool, s *bitcoinTypeEstimatedFee) (big.Int, error) { +func (w *Worker) cachedEstimateFee(blocks int, conservative bool) (big.Int, error) { + var s *bitcoinTypeEstimatedFee + if conservative { + s = &estimatedFeeConservativeCache[blocks] + } else { + s = &estimatedFeeCache[blocks] + } s.lock.Lock() defer s.lock.Unlock() // 10 seconds cache @@ -2261,10 +2311,13 @@ func (w *Worker) cachedBitcoinTypeEstimateFee(blocks int, conservative bool, s * if err == nil { s.timestamp = time.Now().Unix() s.fee = fee - w.metrics.EstimatedFee.With(common.Labels{ - "blocks": strconv.Itoa(blocks), - "conservative": strconv.FormatBool(conservative), - }).Set(float64(fee.Int64())) + // store metrics for the first 32 block estimates + if blocks < 33 { + w.metrics.EstimatedFee.With(common.Labels{ + "blocks": strconv.Itoa(blocks), + "conservative": strconv.FormatBool(conservative), + }).Set(float64(fee.Int64())) + } } return fee, err } @@ -2275,8 +2328,5 @@ func (w *Worker) EstimateFee(blocks int, conservative bool) (big.Int, error) { if blocks >= estimatedFeeCacheSize { return w.chain.EstimateSmartFee(blocks, conservative) } - if conservative { - return w.cachedBitcoinTypeEstimateFee(blocks, conservative, &estimatedFeeConservativeCache[blocks]) - } - return w.cachedBitcoinTypeEstimateFee(blocks, conservative, &estimatedFeeCache[blocks]) + return w.cachedEstimateFee(blocks, conservative) } diff --git a/bchain/basemempool.go b/bchain/basemempool.go index 79fe3b18..d22c9495 100644 --- a/bchain/basemempool.go +++ b/bchain/basemempool.go @@ -122,6 +122,7 @@ func (m *BaseMempool) txToMempoolTx(tx *Tx) *MempoolTx { Blocktime: time.Now().Unix(), LockTime: tx.LockTime, Txid: tx.Txid, + VSize: tx.VSize, Version: tx.Version, Vout: tx.Vout, CoinSpecificData: tx.CoinSpecificData, diff --git a/bchain/types.go b/bchain/types.go index 8a69fbee..cdabf935 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -107,6 +107,7 @@ type MempoolTx struct { Txid string `json:"txid"` Version int32 `json:"version"` LockTime uint32 `json:"locktime"` + VSize int64 `json:"vsize,omitempty"` Vin []MempoolVin `json:"vin"` Vout []Vout `json:"vout"` Blocktime int64 `json:"blocktime,omitempty"` diff --git a/common/internalstate.go b/common/internalstate.go index 7651cf47..3e65d9a3 100644 --- a/common/internalstate.go +++ b/common/internalstate.go @@ -210,6 +210,16 @@ func (is *InternalState) GetBlockTime(height uint32) uint32 { return 0 } +// GetLastBlockTime returns time of the last block +func (is *InternalState) GetLastBlockTime() uint32 { + is.mux.Lock() + defer is.mux.Unlock() + if len(is.BlockTimes) > 0 { + return is.BlockTimes[len(is.BlockTimes)-1] + } + return 0 +} + // SetBlockTimes initializes BlockTimes array, returns AvgBlockPeriod func (is *InternalState) SetBlockTimes(blockTimes []uint32) uint32 { is.mux.Lock() diff --git a/db/rocksdb.go b/db/rocksdb.go index 181ce35e..783cdd12 100644 --- a/db/rocksdb.go +++ b/db/rocksdb.go @@ -1648,7 +1648,7 @@ func (d *RocksDB) checkColumns(is *common.InternalState) ([]common.InternalState if sc[j].Name == nc[i].Name { // check the version of the column, if it does not match, the db is not compatible if sc[j].Version != dbVersion { - // upgrade of DB 5 to 6 for BitecoinType coins is possible + // upgrade of DB 5 to 6 for BitcoinType coins is possible // columns transactions and fiatRates must be cleared as they are not compatible if sc[j].Version == 5 && dbVersion == 6 && d.chainParser.GetChainType() == bchain.ChainBitcoinType { if nc[i].Name == "transactions" { @@ -2098,7 +2098,7 @@ const ( // big int is packed in BigEndian order without memory allocation as 1 byte length followed by bytes of big int // number of written bytes is returned -// limitation: bigints longer than 248 bytes are truncated to 248 bytes +// limitation: big ints longer than 248 bytes are truncated to 248 bytes // caution: buffer must be big enough to hold the packed big int, buffer 249 bytes big is always safe func packBigint(bi *big.Int, buf []byte) int { w := bi.Bits() diff --git a/server/public.go b/server/public.go index 28481087..9b704f03 100644 --- a/server/public.go +++ b/server/public.go @@ -516,6 +516,7 @@ type TemplateData struct { func (s *PublicServer) parseTemplates() []*template.Template { templateFuncMap := template.FuncMap{ "timeSpan": timeSpan, + "relativeTime": relativeTime, "unixTimeSpan": unixTimeSpan, "amountSpan": s.amountSpan, "tokenAmountSpan": s.tokenAmountSpan, @@ -598,7 +599,7 @@ func (s *PublicServer) parseTemplates() []*template.Template { return t } -func relativeTime(d int64) string { +func relativeTimeUnit(d int64) string { var u string if d < 60 { if d == 1 { @@ -631,6 +632,22 @@ func relativeTime(d int64) string { return strconv.FormatInt(d, 10) + u } +func relativeTime(d int64) string { + r := relativeTimeUnit(d) + if d > 3600*24 { + d = d % (3600 * 24) + if d >= 3600 { + r += " " + relativeTimeUnit(d) + } + } else if d > 3600 { + d = d % 3600 + if d >= 60 { + r += " " + relativeTimeUnit(d) + } + } + return r +} + func unixTimeSpan(ut int64) template.HTML { t := time.Unix(ut, 0) return timeSpan(&t) @@ -652,17 +669,6 @@ func timeSpan(t *time.Time) template.HTML { return template.HTML(f) } r := relativeTime(d) - if d > 3600*24 { - d = d % (3600 * 24) - if d >= 3600 { - r += " " + relativeTime(d) - } - } else if d > 3600 { - d = d % 3600 - if d >= 60 { - r += " " + relativeTime(d) - } - } return template.HTML(`` + r + " ago") } diff --git a/static/templates/tx.html b/static/templates/tx.html index 6eab41cd..396d6c14 100644 --- a/static/templates/tx.html +++ b/static/templates/tx.html @@ -80,6 +80,14 @@ {{amountSpan $tx.FeesSat $data "copyable"}}{{if $tx.Size}} ({{feePerByte $tx}}){{end}} {{end}} {{if not $tx.Confirmations}} + {{if $tx.ConfirmationETABlocks}} + + Confirmation ETA + + in approx. {{relativeTime $tx.ConfirmationETASeconds}} ({{$tx.ConfirmationETABlocks}} blocks) + + + {{end}} RBF diff --git a/static/templates/txdetail.html b/static/templates/txdetail.html index 7f02fa19..a5f53758 100644 --- a/static/templates/txdetail.html +++ b/static/templates/txdetail.html @@ -6,6 +6,10 @@ {{if $tx.Rbf}} RBF{{end}} {{if $tx.Blocktime}}
{{if $tx.Confirmations}}mined{{else}}first seen{{end}} {{unixTimeSpan $tx.Blocktime}}
{{end}} + {{if $tx.ConfirmationETABlocks}}
+ confirmation estimated in {{relativeTime $tx.ConfirmationETASeconds}} ({{$tx.ConfirmationETABlocks}} blocks) +
+ {{end}}