Estimate mining time of mempool tx
This commit is contained in:
parent
76a19b7f10
commit
55f3ad3caa
42
api/types.go
42
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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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(`<span tt="` + f + `">` + r + " ago</span>")
|
||||
}
|
||||
|
||||
|
||||
@ -80,6 +80,14 @@
|
||||
<td>{{amountSpan $tx.FeesSat $data "copyable"}}{{if $tx.Size}} ({{feePerByte $tx}}){{end}}</td>
|
||||
</tr>{{end}}
|
||||
{{if not $tx.Confirmations}}
|
||||
{{if $tx.ConfirmationETABlocks}}
|
||||
<tr>
|
||||
<td>Confirmation ETA</td>
|
||||
<td>
|
||||
<span tt="Estimated first potential confirmation of this transaction.">in approx. {{relativeTime $tx.ConfirmationETASeconds}} <span class="fw-normal ps-1">({{$tx.ConfirmationETABlocks}} blocks)</span></span>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr>
|
||||
<td><span tt="Replace by fee">RBF</span></td>
|
||||
<td>
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
{{if $tx.Rbf}}<span class="ps-1" tt="Replace-by-Fee (RBF) transaction, could be overridden"> RBF</span>{{end}}
|
||||
</div>
|
||||
{{if $tx.Blocktime}}<div class="col-xs-5 col-md-4 text-end">{{if $tx.Confirmations}}mined{{else}}first seen{{end}} <span class="txvalue ms-1">{{unixTimeSpan $tx.Blocktime}}</span></div>{{end}}
|
||||
{{if $tx.ConfirmationETABlocks}}<div class="col-12 text-end">
|
||||
<span class="badge bg-info fw-bold" style="text-transform: none;" tt="Estimated first potential confirmation of this transaction.">confirmation estimated in {{relativeTime $tx.ConfirmationETASeconds}} <span class="fw-normal ps-1">({{$tx.ConfirmationETABlocks}} blocks)</span></span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="row body">
|
||||
<div class="col-md-5">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user