diff --git a/README.md b/README.md
index 2e3afd10..aa3c91b9 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,7 @@ General Methods:
* [Get XRP Distribution - `GET /v2/network/xrp_distribution`](#get-xrp-distribution)
* [Get Top Currencies - `GET /v2/network/top_currencies`](#get-top-currencies)
* [Get Top Markets - `GET /v2/network/top_markets`](#get-top-markets)
+* [Get Network Fees - `GET /v2/network/fees`](#get-network-fees)
* [Get Topology - `GET /v2/network/topology`](#get-topology)
* [Get Topology Nodes - `GET /v2/network/topology/nodes`](#get-topology-nodes)
* [Get Topology Node - `GET /v2/network/topology/nodes/{:pubkey}`](#get-topology-nodes)
@@ -2209,6 +2210,91 @@ Response:
+## Get Network Fees ##
+[[Source]
](https://github.com/ripple/rippled-historical-database/blob/develop/api/routesV2/network/getFees.js "Source")
+
+Returns network fee stats per ledger, hour, or day. The data shows the average, minimum, maximum, and total fees incurred for the given interval/ledger. _(New in [v2.2.0][])_
+
+#### Request Format ####
+
+
+
+```
+GET /v2/network/fees
+```
+
+
+
+Optionally, you can include the following query parameters:
+
+| Field | Value | Description |
+|--------|---------|-------------|
+| start | String - [Timestamp][] | Start time of query range. Defaults to the start of the most recent interval. |
+| end | String - [Timestamp][] | End time of query range. Defaults to the end of the most recent interval. |
+| interval | String | Aggregation interval - valid intervals are `ledger`, `hour`, or `day`. Defaults to `ledger`. |
+| descending | Boolean | Reverse chronological order |
+| limit | Integer | Maximum results per page. Defaults to 200. Cannot be more than 1000. |
+| marker | String | [Pagination](#pagination) key from previously returned response |
+| format | String | Format of returned results: `csv` or `json`. Defaults to `json`. |
+
+[Try it! >](https://ripple.com/build/data-api-tool/#get-network-fees)
+
+
+#### Response Format ####
+
+A successful response uses the HTTP code **200 OK** and has a JSON body with the following:
+
+| Field | Value | Description |
+|--------|-------|-------------|
+| result | String | The value `success` indicates that this is a successful response. |
+| marker | String | (May be omitted) [Pagination](#pagination) marker. |
+| count | Integer | Number of results in the `markets` field. |
+| rows | Array of Fee Summary Objects | Network fee statistics for each specific interval. |
+
+Each Fee Summary object has the following fields:
+
+| Field | Value | Description |
+|--------|-------|-------------|
+| avg | Number | Average network fee |
+| min | Number | Minimum network fee |
+| max | Number | Maximum network fee |
+| total | Number | Total XRP consumed as network fees |
+| tx_count | Number | Number of transactions in this interval |
+| date | String - [Timestamp][] | Interval start time or ledger close time |
+| ledger_index | Integer | Ledger index (present in `ledger` interval only) |
+
+#### Example ####
+
+Request:
+
+```
+GET /v2/network/fees?interval=hour
+```
+
+Response:
+
+```
+{
+ result: "success",
+ marker: "hour|20130124080000",
+ count: 200,
+ rows: [
+ {
+ avg: 0.00001,
+ max: 0.00001,
+ min: 0.00001,
+ total: 0.00001,
+ tx_count: 1,
+ date: "2013-01-02T06:00:00Z"
+ },
+ ...
+ ]
+}
+```
+
+
+
+
## Get Topology ##
[[Source]
](https://github.com/ripple/rippled-historical-database/blob/develop/api/routesV2/network/getTopology.js "Source")
diff --git a/api/routesV2/accountExchanges.js b/api/routesV2/accountExchanges.js
index 4767e6f7..4ddeef17 100644
--- a/api/routesV2/accountExchanges.js
+++ b/api/routesV2/accountExchanges.js
@@ -17,7 +17,7 @@ AccountExchanges = function (req, res, next) {
} else if (!options.end) {
errorResponse({
- error: 'invalid start date format',
+ error: 'invalid end date format',
code: 400
});
return;
diff --git a/api/routesV2/network/getFees.js b/api/routesV2/network/getFees.js
new file mode 100644
index 00000000..4f2b9aed
--- /dev/null
+++ b/api/routesV2/network/getFees.js
@@ -0,0 +1,113 @@
+'use strict';
+
+var Logger = require('../../../lib/logger');
+var log = new Logger({scope: 'network fees'});
+var smoment = require('../../../lib/smoment');
+var utils = require('../../../lib/utils');
+
+var hbase;
+var intervals = [
+ 'ledger',
+ 'hour',
+ 'day'
+];
+
+var getFees = function(req, res) {
+ var options = {
+ interval: req.query.interval || 'ledger',
+ start: smoment(req.query.start || '2013-01-01'),
+ end: smoment(req.query.end),
+ limit: req.query.limit,
+ marker: req.query.marker,
+ descending: (/true/i).test(req.query.descending) ? true : false,
+ format: (req.query.format || 'json').toLowerCase()
+ };
+
+ if (!options.start) {
+ errorResponse({
+ error: 'invalid start date format',
+ code: 400
+ });
+ return;
+
+ } else if (!options.end) {
+ errorResponse({
+ error: 'invalid end date format',
+ code: 400
+ });
+ return;
+
+ } else if (intervals.indexOf(options.interval) === -1) {
+ errorResponse({
+ error: 'invalid interval',
+ code: 400
+ });
+ return;
+ }
+
+ if (isNaN(options.limit)) {
+ options.limit = 200;
+ } else if (options.limit > 1000) {
+ options.limit = 1000;
+ }
+
+ log.info('interval:', options.interval);
+
+ hbase.getNetworkFees(options)
+ .then(successResponse)
+ .catch(errorResponse);
+
+ /**
+ * errorResponse
+ * return an error response
+ * @param {Object} err
+ */
+
+ function errorResponse(err) {
+ log.error(err.error || err);
+ if (err.code && err.code.toString()[0] === '4') {
+ res.status(err.code).json({
+ result: 'error',
+ message: err.error
+ });
+ } else {
+ res.status(500).json({
+ result: 'error',
+ message: 'unable to retrieve fee summary(s)'
+ });
+ }
+ }
+
+ /**
+ * successResponse
+ * return a successful response
+ * @param {Object} markets
+ * @param {Object} options
+ */
+
+ function successResponse(data) {
+ var filename;
+
+ if (data.marker) {
+ utils.addLinkHeader(req, res, data.marker);
+ }
+
+ if (options.format === 'csv') {
+ filename = 'network fees.csv';
+ res.csv(data.rows, filename);
+
+ } else {
+ res.json({
+ result: 'success',
+ marker: data.marker,
+ count: data.rows.length,
+ rows: data.rows
+ });
+ }
+ }
+};
+
+module.exports = function(db) {
+ hbase = db;
+ return getFees;
+};
diff --git a/api/routesV2/network/index.js b/api/routesV2/network/index.js
index eb789cef..eb300d49 100644
--- a/api/routesV2/network/index.js
+++ b/api/routesV2/network/index.js
@@ -7,6 +7,7 @@ module.exports = function(db) {
xrpDistribution: require('./xrpDistribution')(db),
topMarkets: require('./topMarkets')(db),
topCurrencies: require('./topCurrencies')(db),
+ getFees: require('./getFees')(db),
getNodes: require('./getNodes')(db),
getLinks: require('./getLinks')(db),
getTopology: require('./getTopology')(db),
diff --git a/api/server.js b/api/server.js
index d8beb879..1589b78f 100644
--- a/api/server.js
+++ b/api/server.js
@@ -68,6 +68,7 @@ var Server = function (options) {
app.get('/v2/network/xrp_distribution', routesV2.network.xrpDistribution);
app.get('/v2/network/top_markets/:date?', routesV2.network.topMarkets);
app.get('/v2/network/top_currencies/:date?', routesV2.network.topCurrencies);
+ app.get('/v2/network/fees', routesV2.network.getFees);
app.get('/v2/network/topology', routesV2.network.getTopology);
app.get('/v2/network/topology/nodes', routesV2.network.getNodes);
app.get('/v2/network/topology/nodes/:pubkey', routesV2.network.getNodes);
diff --git a/lib/hbase/hbase-thrift/data.js b/lib/hbase/hbase-thrift/data.js
index 7977de71..8d0943b2 100644
--- a/lib/hbase/hbase-thrift/data.js
+++ b/lib/hbase/hbase-thrift/data.js
@@ -1860,6 +1860,55 @@ HbaseClient.getTransactions = function(options, callback) {
}
};
+/**
+ * getNetworkFees
+ */
+
+HbaseClient.getNetworkFees = function(options) {
+ var self = this;
+ var startRow = [
+ options.interval,
+ options.start.hbaseFormatStartRow()
+ ].join('|');
+
+ var stopRow = [
+ options.interval,
+ options.end.hbaseFormatStopRow()
+ ].join('|');
+
+ return new Promise(function(resolve, reject) {
+ self.getScanWithMarker(self, {
+ table: 'network_fees',
+ startRow: startRow,
+ stopRow: stopRow,
+ limit: options.limit,
+ marker: options.marker,
+ descending: options.descending
+ }, function(err, resp) {
+ if (err) {
+ reject(err);
+ } else {
+ resp.rows.forEach(function(r) {
+ if (r.ledger_index) {
+ r.ledger_index = Number(r.ledger_index);
+ }
+
+ r.avg = Number(r.avg);
+ r.max = Number(r.max);
+ r.min = Number(r.min);
+ r.total = Number(r.total);
+ r.tx_count = Number(r.tx_count);
+
+ delete r.interval;
+ delete r.rowkey;
+ });
+
+ resolve(resp);
+ }
+ });
+ });
+}
+
/**
* getAccounts
*/
diff --git a/test/setup.importLedgers.js b/test/setup.importLedgers.js
index a56c7d6e..84a953ef 100644
--- a/test/setup.importLedgers.js
+++ b/test/setup.importLedgers.js
@@ -8,6 +8,7 @@ var moment = require('moment');
var exAggregation = require('../lib/aggregation/exchanges');
var statsAggregation = require('../lib/aggregation/stats');
var paymentsAggregation = require('../lib/aggregation/accountPayments');
+var feesAggregation = require('../lib/aggregation/fees');
var fs = require('fs');
var path = __dirname + '/mock/ledgers/';
@@ -17,10 +18,12 @@ var statsConfig;
var updates = [];
var exchanges = [];
var payments = [];
+var fees = [];
var pairs = { };
var hbase;
var stats;
var aggPayments;
+var aggFees;
hbaseConfig.prefix = config.get('prefix');
@@ -29,6 +32,7 @@ hbaseConfig.max_sockets = 100;
hbaseConfig.timeout = 60000;
aggPayments = new paymentsAggregation(hbaseConfig);
+aggFees = new feesAggregation(hbaseConfig);
stats = new statsAggregation(hbaseConfig);
hbase = new HBase(hbaseConfig);
@@ -46,6 +50,9 @@ describe('import ledgers', function(done) {
//save payments
payments.push.apply(payments, parsed.payments);
+ //save fees
+ fees.push(parsed.feeSummary);
+
//save stats
addStats(parsed);
updates.push({
@@ -76,6 +83,19 @@ describe('import ledgers', function(done) {
});
});
+ it('should aggregate network fees', function(done) {
+ this.timeout(7000);
+ Promise.map(fees, function(feeSummary) {
+ return aggFees.handleFeeSummary(feeSummary);
+ })
+ .then(function() {
+ done();
+ })
+ .catch(function(e) {
+ assert.ifError(e);
+ });
+ });
+
it('should save exchanges into hbase', function(done) {
this.timeout(15000);
exchanges.forEach(function(ex, i) {
diff --git a/test/test.network.js b/test/test.network.js
index 9f169f5c..3837f9ad 100644
--- a/test/test.network.js
+++ b/test/test.network.js
@@ -147,6 +147,239 @@ describe('setup mock data', function() {
});
});
+/**
+ * network fees
+ */
+
+describe('network fees', function() {
+ it('should get ledger fee summaries', function(done) {
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees';
+
+ request({
+ url: url,
+ json: true,
+ },
+ function (err, res, body) {
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 200);
+ assert.strictEqual(typeof body, 'object');
+ assert.strictEqual(body.result, 'success');
+ assert.strictEqual(body.count, 52);
+ body.rows.forEach(function(r) {
+ assert.strictEqual(typeof r.avg, 'number');
+ assert.strictEqual(typeof r.min, 'number');
+ assert.strictEqual(typeof r.max, 'number');
+ assert.strictEqual(typeof r.total, 'number');
+ assert.strictEqual(typeof r.tx_count, 'number');
+ assert.strictEqual(typeof r.ledger_index, 'number');
+ assert(moment(r.date).isValid());
+ });
+ done();
+ });
+ });
+
+ it('should restrict by start and end dates', function(done) {
+ var start = '2015-01-14T18:00:00';
+ var end = '2015-02-01';
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees?' +
+ 'start=' + start +
+ '&end=' + end;
+
+ request({
+ url: url,
+ json: true,
+ },
+ function (err, res, body) {
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 200);
+ assert.strictEqual(typeof body, 'object');
+ assert.strictEqual(body.result, 'success');
+ assert.strictEqual(body.count, 38);
+ body.rows.forEach(function(r) {
+ var date = moment(r.date);
+ assert.strictEqual(typeof r.avg, 'number');
+ assert.strictEqual(typeof r.min, 'number');
+ assert.strictEqual(typeof r.max, 'number');
+ assert.strictEqual(typeof r.total, 'number');
+ assert.strictEqual(typeof r.tx_count, 'number');
+ assert(date.isValid());
+ assert(date.diff(start) >= 0);
+ assert(date.diff(end) <= 0);
+ });
+ done();
+ });
+ });
+
+ it('should get fee summaries in decending order', function(done) {
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees?descending=true';
+
+ request({
+ url: url,
+ json: true,
+ },
+ function (err, res, body) {
+ var date;
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 200);
+ assert.strictEqual(typeof body, 'object');
+ assert.strictEqual(body.result, 'success');
+ assert.strictEqual(body.count, 52);
+ body.rows.forEach(function(r) {
+ if (date) {
+ assert(date.diff(r.date) >= 0)
+ }
+
+ date = moment(r.date);
+ });
+ done();
+ });
+ });
+
+ it('should get hourly fee summaries', function(done) {
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees?interval=hour';
+
+ request({
+ url: url,
+ json: true,
+ },
+ function (err, res, body) {
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 200);
+ assert.strictEqual(typeof body, 'object');
+ assert.strictEqual(body.result, 'success');
+ assert.strictEqual(body.count, 8);
+ body.rows.forEach(function(r) {
+ assert.strictEqual(typeof r.avg, 'number');
+ assert.strictEqual(typeof r.min, 'number');
+ assert.strictEqual(typeof r.max, 'number');
+ assert.strictEqual(typeof r.total, 'number');
+ assert.strictEqual(typeof r.tx_count, 'number');
+ assert(moment(r.date).isValid());
+ });
+ done();
+ });
+ });
+
+ it('should get daily fee summaries', function(done) {
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees?interval=day';
+
+ request({
+ url: url,
+ json: true,
+ },
+ function (err, res, body) {
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 200);
+ assert.strictEqual(typeof body, 'object');
+ assert.strictEqual(body.result, 'success');
+ assert.strictEqual(body.count, 5);
+ body.rows.forEach(function(r) {
+ assert.strictEqual(typeof r.avg, 'number');
+ assert.strictEqual(typeof r.min, 'number');
+ assert.strictEqual(typeof r.max, 'number');
+ assert.strictEqual(typeof r.total, 'number');
+ assert.strictEqual(typeof r.tx_count, 'number');
+ assert(moment(r.date).isValid());
+ });
+ done();
+ });
+ });
+
+ it('should handle pagination correctly', function(done) {
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees?interval=hour';
+
+ utils.checkPagination(url, undefined, function(ref, i, body) {
+ assert.strictEqual(body.rows.length, 1);
+ assert.deepEqual(body.rows[0], ref.rows[i]);
+ }, done);
+ });
+
+ it('should include a link header when marker is present', function(done) {
+ var url = 'http://localhost:' + port + '/v2/network/fees?limit=1';
+ var linkHeader = '<' + url +
+ '&marker=ledger|20131025102710|000002964124>; rel="next"';
+
+ request({
+ url: url,
+ json: true
+ },
+ function (err, res, body) {
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 200);
+ assert.strictEqual(res.headers.link, linkHeader);
+ done();
+ });
+ });
+
+ it('should error on invalid start date', function(done) {
+ var start = 'x2015-01-14T00:00';
+ var end = '2015-01-14T00:00';
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees' +
+ '?start=' + start +
+ '&end=' + end;
+
+ request({
+ url: url,
+ json: true,
+ },
+ function (err, res, body) {
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 400);
+ assert.strictEqual(typeof body, 'object');
+ assert.strictEqual(body.result, 'error');
+ assert.strictEqual(body.message, 'invalid start date format');
+ done();
+ });
+ });
+
+ it('should error on invalid end date', function(done) {
+ var start = '2015-01-14T00:00';
+ var end = 'x2015-01-14T00:00';
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees' +
+ '?start=' + start +
+ '&end=' + end;
+
+ request({
+ url: url,
+ json: true,
+ },
+ function (err, res, body) {
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 400);
+ assert.strictEqual(typeof body, 'object');
+ assert.strictEqual(body.result, 'error');
+ assert.strictEqual(body.message, 'invalid end date format');
+ done();
+ });
+ });
+
+ it('should error on invalid interval', function(done) {
+ var url = 'http://localhost:' + port +
+ '/v2/network/fees?interval=zzz';
+
+ request({
+ url: url,
+ json: true,
+ },
+ function (err, res, body) {
+ assert.ifError(err);
+ assert.strictEqual(res.statusCode, 400);
+ assert.strictEqual(typeof body, 'object');
+ assert.strictEqual(body.result, 'error');
+ assert.strictEqual(body.message, 'invalid interval');
+ done();
+ });
+ });
+});
+
/**
* exchange volume
*/