From 4224aab5f23bf4f569f61c3108166b37480f45c2 Mon Sep 17 00:00:00 2001 From: Vladyslav Burzakovskyy Date: Wed, 7 Aug 2019 13:13:45 +0200 Subject: [PATCH] Implement the "feestats" endpoint (#250) --- api/types.go | 8 +++ api/worker.go | 125 ++++++++++++++++++++++++++++++++-- server/public.go | 11 +++ server/public_test.go | 11 ++- tests/dbtestdata/fakechain.go | 22 +++++- 5 files changed, 168 insertions(+), 9 deletions(-) diff --git a/api/types.go b/api/types.go index 279c073c..bb2988e3 100644 --- a/api/types.go +++ b/api/types.go @@ -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"` diff --git a/api/worker.go b/api/worker.go index b5302f67..3b55bfb5 100644 --- a/api/worker.go +++ b/api/worker.go @@ -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) diff --git a/server/public.go b/server/public.go index 60ca2e53..a966347b 100644 --- a/server/public.go +++ b/server/public.go @@ -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"` } diff --git a/server/public_test.go b/server/public_test.go index 96a54939..60afb45c 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -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", diff --git a/tests/dbtestdata/fakechain.go b/tests/dbtestdata/fakechain.go index 01545256..b5e548bb 100644 --- a/tests/dbtestdata/fakechain.go +++ b/tests/dbtestdata/fakechain.go @@ -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 }