Implement the "feestats" endpoint (#250)

pull/252/head
Vladyslav Burzakovskyy 2019-08-07 13:13:45 +02:00 committed by Martin
parent 298ec5ea35
commit 4224aab5f2
5 changed files with 168 additions and 9 deletions

View File

@ -198,6 +198,14 @@ type Tx struct {
EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"`
}
// FeeStats contains detailed block fee statistics
type FeeStats struct {
TxCount int `json:"txCount"`
TotalFeesSat *Amount `json:"totalFeesSat"`
AverageFeePerKb int64 `json:"averageFeePerKb"`
DecilesFeePerKb [11]int64 `json:"decilesFeePerKb"`
}
// Paging contains information about paging for address, blocks and block
type Paging struct {
Page int `json:"page,omitempty"`

View File

@ -972,13 +972,7 @@ func (w *Worker) GetBlocks(page int, blocksOnPage int) (*Blocks, error) {
return r, nil
}
// GetBlock returns paged data about block
func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) {
start := time.Now()
page--
if page < 0 {
page = 0
}
func (w *Worker) getBlockInfoFromBlockID(bid string) (*bchain.BlockInfo, error) {
// try to decide if passed string (bid) is block height or block hash
// if it's a number, must be less than int32
var hash string
@ -995,6 +989,123 @@ func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) {
return nil, NewAPIError("Block not found", true)
}
bi, err := w.chain.GetBlockInfo(hash)
return bi, err
}
// GetFeeStats returns statistics about block fees
func (w *Worker) GetFeeStats(bid string) (*FeeStats, error) {
// txSpecific extends Tx with an additional Size and Vsize info
type txSpecific struct {
*bchain.Tx
Vsize int `json:"vsize,omitempty"`
Size int `json:"size,omitempty"`
}
start := time.Now()
bi, err := w.getBlockInfoFromBlockID(bid)
if err != nil {
if err == bchain.ErrBlockNotFound {
return nil, NewAPIError("Block not found", true)
}
return nil, NewAPIError(fmt.Sprintf("Block not found, %v", err), true)
}
feesPerKb := make([]int64, 0, len(bi.Txids))
totalFeesSat := big.NewInt(0)
averageFeePerKb := int64(0)
for _, txid := range bi.Txids {
// Get a raw JSON with transaction details, including size, vsize, hex
txSpecificJSON, err := w.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid})
if err != nil {
return nil, errors.Annotatef(err, "GetTransactionSpecific")
}
// Serialize the raw JSON into TxSpecific struct
var txSpec txSpecific
err = json.Unmarshal(txSpecificJSON, &txSpec)
if err != nil {
return nil, errors.Annotatef(err, "Unmarshal")
}
// Calculate the TX size in bytes
txSize := 0
if txSpec.Vsize > 0 {
txSize = txSpec.Vsize
} else if txSpec.Size > 0 {
txSize = txSpec.Size
} else if txSpec.Hex != "" {
txSize = len(txSpec.Hex) / 2
} else {
errMsg := "Cannot determine the transaction size from neither Vsize, Size nor Hex! Txid: " + txid
return nil, NewAPIError(errMsg, true)
}
// Get values of TX inputs and outputs
txAddresses, err := w.db.GetTxAddresses(txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses")
}
// Caclulate total fees in Satoshis
feeSat := big.NewInt(0)
for _, input := range txAddresses.Inputs {
feeSat = feeSat.Add(&input.ValueSat, feeSat)
}
// Zero inputs means it's a Coinbase TX - skip it
if feeSat.Cmp(big.NewInt(0)) == 0 {
continue
}
for _, output := range txAddresses.Outputs {
feeSat = feeSat.Sub(feeSat, &output.ValueSat)
}
totalFeesSat.Add(totalFeesSat, feeSat)
// Convert feeSat to fee per kilobyte and add to an array for decile calculation
feePerKb := int64(float64(feeSat.Int64()) / float64(txSize) * 1000)
averageFeePerKb += feePerKb
feesPerKb = append(feesPerKb, feePerKb)
}
var deciles [11]int64
n := len(feesPerKb)
if n > 0 {
averageFeePerKb /= int64(n)
// Sort fees and calculate the deciles
sort.Slice(feesPerKb, func(i, j int) bool { return feesPerKb[i] < feesPerKb[j] })
for k := 0; k <= 10; k++ {
index := int(math.Floor(0.5+float64(k)*float64(n+1)/10)) - 1
if index < 0 {
index = 0
} else if index >= n {
index = n - 1
}
deciles[k] = feesPerKb[index]
}
}
glog.Info("GetFeeStats ", bid, " (", len(feesPerKb), " txs) finished in ", time.Since(start))
return &FeeStats{
TxCount: len(feesPerKb),
AverageFeePerKb: averageFeePerKb,
TotalFeesSat: (*Amount)(totalFeesSat),
DecilesFeePerKb: deciles,
}, nil
}
// GetBlock returns paged data about block
func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) {
start := time.Now()
page--
if page < 0 {
page = 0
}
bi, err := w.getBlockInfoFromBlockID(bid)
if err != nil {
if err == bchain.ErrBlockNotFound {
return nil, NewAPIError("Block not found", true)

View File

@ -185,6 +185,7 @@ func (s *PublicServer) ConnectFullPublicInterface() {
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))
serveMux.HandleFunc(path+"api/v2/feestats/", s.jsonHandler(s.apiFeeStats, apiV2))
// socket.io interface
serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
// websocket interface
@ -1041,6 +1042,16 @@ func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, e
return block, err
}
func (s *PublicServer) apiFeeStats(r *http.Request, apiVersion int) (interface{}, error) {
var feeStats *api.FeeStats
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-feestats"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
feeStats, err = s.api.GetFeeStats(r.URL.Path[i+1:])
}
return feeStats, err
}
type resultSendTransaction struct {
Result string `json:"result"`
}

View File

@ -451,6 +451,15 @@ func httpTestsBitcoinType(t *testing.T, ts *httptest.Server) {
`{"hex":"","txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","version":0,"locktime":0,"vin":[],"vout":[{"ValueSat":100000000,"value":0,"n":0,"scriptPubKey":{"hex":"76a914010d39800f86122416e28f485029acf77507169288ac","addresses":null}},{"ValueSat":12345,"value":0,"n":1,"scriptPubKey":{"hex":"76a9148bdf0aa3c567aa5975c2e61321b8bebbe7293df688ac","addresses":null}}],"confirmations":2,"time":22549300000,"blocktime":22549300000}`,
},
},
{
name: "apiFeeStats",
r: newGetRequest(ts.URL + "/api/v2/feestats/225494"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"txCount":3,"totalFeesSat":"1284","averageFeePerKb":1398,"decilesFeePerKb":[155,155,155,155,1679,1679,1679,2361,2361,2361,2361]}`,
},
},
{
name: "apiAddress v1",
r: newGetRequest(ts.URL + "/api/v1/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"),
@ -892,7 +901,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) {
"txid": dbtestdata.TxidB2T2,
},
},
want: `{"id":"9","data":{"hex":"","txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","version":0,"locktime":0,"vin":[{"coinbase":"","txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":0,"scriptSig":{"hex":""},"sequence":0,"addresses":null},{"coinbase":"","txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"scriptSig":{"hex":""},"sequence":0,"addresses":null}],"vout":[{"ValueSat":118641975500,"value":0,"n":0,"scriptPubKey":{"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":null}},{"ValueSat":198641975500,"value":0,"n":1,"scriptPubKey":{"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":null}}],"confirmations":1,"time":22549400001,"blocktime":22549400001}}`,
want: `{"id":"9","data":{"hex":"","txid":"3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71","version":0,"locktime":0,"vin":[{"coinbase":"","txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":0,"scriptSig":{"hex":""},"sequence":0,"addresses":null},{"coinbase":"","txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":1,"scriptSig":{"hex":""},"sequence":0,"addresses":null}],"vout":[{"ValueSat":118641975500,"value":0,"n":0,"scriptPubKey":{"hex":"a91495e9fbe306449c991d314afe3c3567d5bf78efd287","addresses":null}},{"ValueSat":198641975500,"value":0,"n":1,"scriptPubKey":{"hex":"76a9143f8ba3fda3ba7b69f5818086e12223c6dd25e3c888ac","addresses":null}}],"confirmations":1,"time":22549400001,"blocktime":22549400001,"vsize":400}}`,
},
{
name: "websocket estimateFee",

View File

@ -147,11 +147,31 @@ func (c *fakeBlockChain) GetTransaction(txid string) (v *bchain.Tx, err error) {
}
func (c *fakeBlockChain) GetTransactionSpecific(tx *bchain.Tx) (v json.RawMessage, err error) {
// txSpecific extends Tx with an additional Size and Vsize info
type txSpecific struct {
*bchain.Tx
Vsize int `json:"vsize,omitempty"`
Size int `json:"size,omitempty"`
}
tx, err = c.GetTransaction(tx.Txid)
if err != nil {
return nil, err
}
rm, err := json.Marshal(tx)
txS := txSpecific{Tx: tx}
if tx.Txid == "7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25" {
txS.Vsize = 206
txS.Size = 376
} else if tx.Txid == "fdd824a780cbb718eeb766eb05d83fdefc793a27082cd5e67f856d69798cf7db" {
txS.Size = 300
} else if tx.Txid == "05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07" {
txS.Hex = "010000000001012720b597ef06045c935960342b0bbc45aab5fd5642017282f5110216caaa2364010000002322002069dae530beb09a05d46d0b2aee98645b15bb5d1e808a386b5ef0c48aed5531cbffffffff021ec403000000000017a914203c9dbd3ffbd1a790fc1609fb430efa5cbe516d87061523000000000017a91465dfc5c16e80b86b589df3f85dacd43f5c5b4a8f8704004730440220783e9349fc48f22aa0064acf32bc255eafa761eb9fa8f90a504986713c52dc3702206fc6a1a42f74ea0b416b35671770c0d26fc453668e6107edc271f11e629cda1001483045022100b82ef510c7eec61f39bee3e73a19df451fb8cca842b66bc94696d6a095dd8e96022071767bf8e4859de06cd5caf75e833e284328570ea1caa88bc93478a8d0fa9ac90147522103958c08660082c9ce90399ded0da7c3b39ed20a7767160f12428191e005aa42572102b1e6d8187f54d83d1ffd70508e24c5bd3603bccb2346d8c6677434169de8bc2652ae00000000"
} else if tx.Txid == "3d90d15ed026dc45e19ffb52875ed18fa9e8012ad123d7f7212176e2b0ebdb71" {
txS.Vsize = 400
}
rm, err := json.Marshal(txS)
if err != nil {
return nil, err
}