diff --git a/packages/api/README.md b/packages/api/README.md index f7b839ec2..a78c93abe 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -48,6 +48,8 @@ Reference: * [Blocks](#blocks) * [Validators](#validators) * [Validator by ProTxHash](#validator-by-protxhash) +* [Validator Blocks Statistic](#validator-stats-by-protxhash) +* [Validator Rewards Statistic](#validator-rewards-stats-by-protxhash) * [Transaction by hash](#transaction-by-hash) * [Transactions](#transactions) * [Data Contract By Identifier](#data-contract-by-identifier) @@ -375,6 +377,25 @@ GET /validator/F60A6BF9EC0794BB0CFD1E0F2217933F4B33EDE6FE810692BC275CA18148AEF0 } ``` --- +### Validator rewards stats by ProTxHash +Return a series data for the reward from proposed blocks by validator chart with + +* `start` lower interval threshold in ISO string ( _optional_ ) +* `end` upper interval threshold in ISO string ( _optional_ ) + + +``` +GET /validator/F60A6BF9EC0794BB0CFD1E0F2217933F4B33EDE6FE810692BC275CA18148AEF0/reward/stats?start=2024-01-01T00:00:00&end=2025-01-01T00:00:00 +[ + { + timestamp: "2024-06-23T13:51:44.154Z", + data: { + reward: 34000000 + } + },... +] +``` +--- ### Validator stats by ProTxHash Return a series data for the amount of proposed blocks by validator chart with diff --git a/packages/api/src/controllers/ValidatorsController.js b/packages/api/src/controllers/ValidatorsController.js index 88ae90ae7..3c2aefc4f 100644 --- a/packages/api/src/controllers/ValidatorsController.js +++ b/packages/api/src/controllers/ValidatorsController.js @@ -175,6 +175,54 @@ class ValidatorsController { response.send(stats) } + + getValidatorRewardStatsByProTxHash = async (request, response) => { + const { hash } = request.params + const { + start = new Date().getTime() - 3600000, + end = new Date().getTime(), + timespan = null + } = request.query + + if (timespan) { + const possibleValues = ['1h', '24h', '3d', '1w'] + + if (possibleValues.indexOf(timespan) === -1) { + return response.status(400) + .send({ message: `invalid timespan value ${timespan}. only one of '${possibleValues}' is valid` }) + } + } + + let timespanStart = null + let timespanEnd = null + + const timespanInterval = { + '1h': { offset: 3600000, step: 'PT5M' }, + '24h': { offset: 86400000, step: 'PT2H' }, + '3d': { offset: 259200000, step: 'PT6H' }, + '1w': { offset: 604800000, step: 'PT14H' } + }[timespan] + + if (start > end) { + return response.status(400).send({ message: 'start timestamp cannot be more than end timestamp' }) + } + + if (timespanInterval) { + timespanStart = new Date().getTime() - timespanInterval.offset + timespanEnd = new Date().getTime() + } + + const interval = timespanInterval?.step ?? calculateInterval(new Date(start), new Date(end)) + + const stats = await this.validatorsDAO.getValidatorRewardStatsByProTxHash( + hash, + new Date(timespanStart ?? start), + new Date(timespanEnd ?? end), + interval + ) + + response.send(stats) + } } module.exports = ValidatorsController diff --git a/packages/api/src/dao/ValidatorsDAO.js b/packages/api/src/dao/ValidatorsDAO.js index 6fbdde8fd..3eb1d1dab 100644 --- a/packages/api/src/dao/ValidatorsDAO.js +++ b/packages/api/src/dao/ValidatorsDAO.js @@ -231,4 +231,36 @@ module.exports = class ValidatorsDAO { })) .map(({ timestamp, data }) => new SeriesData(timestamp, data)) } + + getValidatorRewardStatsByProTxHash = async (proTxHash, start, end, interval) => { + const startSql = `'${start.toISOString()}'::timestamptz` + + const endSql = `'${end.toISOString()}'::timestamptz` + + const ranges = this.knex + .from(this.knex.raw(`generate_series(${startSql}, ${endSql}, '${interval}'::interval) date_to`)) + .select('date_to', this.knex.raw('LAG(date_to, 1) over (order by date_to asc) date_from')) + + const rows = await this.knex.with('ranges', ranges) + .select(this.knex.raw(`COALESCE(date_from, date_to - interval '${interval}'::interval) date_from`), 'date_to') + .select( + this.knex('blocks') + .whereRaw('blocks.timestamp > date_from and blocks.timestamp <= date_to') + .whereILike('validator', proTxHash) + .sum('gas_used') + .leftJoin('state_transitions', 'state_transitions.block_hash', 'blocks.hash') + .as('gas_used') + ) + .from('ranges') + + return rows + .slice(1) + .map(row => ({ + timestamp: row.date_from, + data: { + reward: parseInt(row.gas_used ?? 0) + } + })) + .map(({ timestamp, data }) => new SeriesData(timestamp, data)) + } } diff --git a/packages/api/src/routes.js b/packages/api/src/routes.js index ba1b24b64..edb948ae0 100644 --- a/packages/api/src/routes.js +++ b/packages/api/src/routes.js @@ -309,6 +309,20 @@ module.exports = ({ querystring: { $ref: 'timeInterval#' } } }, + { + path: '/validator/:hash/rewards/stats', + method: 'GET', + handler: validatorsController.getValidatorRewardStatsByProTxHash, + schema: { + params: { + type: 'object', + properties: { + hash: { $ref: 'hash#' } + } + }, + querystring: { $ref: 'timeInterval#' } + } + }, { path: '/validator/:hash', method: 'GET', diff --git a/packages/api/test/integration/validators.spec.js b/packages/api/test/integration/validators.spec.js index 94598a4c0..b9c08d468 100644 --- a/packages/api/test/integration/validators.spec.js +++ b/packages/api/test/integration/validators.spec.js @@ -1717,6 +1717,70 @@ describe('Validators routes', () => { assert.deepEqual(expectedStats.reverse(), body) }) + it('should return reward stats by proTxHash with custom timespan', async () => { + validators = [ + await fixtures.validator(knex), + await fixtures.validator(knex) + ] + blocks = [] + transactions = [] + + const [validator] = validators + + for (let i = 0; i <= 20; i++) { + const block = await fixtures.block(knex, { + validator: validators[i % 2].pro_tx_hash, + timestamp: new Date(new Date().getTime() + (3600000 * (i % 3)) + 30000000) + }) + + blocks.push(block) + } + + for (let i = 0; i <= 35; i++) { + const transaction = await fixtures.transaction(knex, { + block_hash: blocks[i % 20].hash, + gas_used: 1000, + type: 0, + owner: identities[0].identifier + }) + transactions.push(transaction) + } + + const { body } = await client.get(`/validator/${validator.pro_tx_hash}/rewards/stats?start=${new Date(new Date().getTime() + 30000000).toISOString()}&end=${new Date(new Date().getTime() + 42000000).toISOString()}`) + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8') + + const [firstPeriod] = body.toReversed() + const firstTimestamp = new Date(firstPeriod.timestamp).getTime() + + const expectedStats = [] + + for (let i = 0; i < body.length; i++) { + const nextPeriod = firstTimestamp - 3600000 * i + const prevPeriod = firstTimestamp - 3600000 * (i - 1) + + const rewardedBlocksHash = blocks.filter(block => + (block.validator === validator.pro_tx_hash && + block.timestamp.getTime() <= prevPeriod && + block.timestamp.getTime() >= nextPeriod + )) + .map(block => block.hash) + + const txs = transactions.filter(transaction => rewardedBlocksHash.includes(transaction.block_hash)) + + expectedStats.push( + { + timestamp: new Date(nextPeriod).toISOString(), + data: { + reward: txs.length > 0 ? txs.reduce((total, next) => total + next.gas_used, 0) : 0 + } + } + ) + } + + assert.deepEqual(expectedStats.reverse(), body) + }) + it('should return error on wrong bounds', async () => { await client.get(`/validator/${validators[0].pro_tx_hash}/stats?start=2025-01-02T00:00:00&end=2024-01-08T00:00:00`) .expect(400) diff --git a/packages/frontend/src/app/api/content.md b/packages/frontend/src/app/api/content.md index a770c6996..1c546c38a 100644 --- a/packages/frontend/src/app/api/content.md +++ b/packages/frontend/src/app/api/content.md @@ -15,6 +15,8 @@ Reference: * [Blocks](#blocks) * [Validators](#validators) * [Validator by ProTxHash](#validator-by-protxhash) +* [Validator Blocks Statistic](#validator-stats-by-protxhash) +* [Validator Rewards Statistic](#validator-rewards-stats-by-protxhash) * [Transaction by hash](#transaction-by-hash) * [Transactions](#transactions) * [Data Contract By Identifier](#data-contract-by-identifier) @@ -342,10 +344,34 @@ GET /validator/F60A6BF9EC0794BB0CFD1E0F2217933F4B33EDE6FE810692BC275CA18148AEF0 } ``` --- +### Validator rewards stats by ProTxHash +Return a series data for the reward from proposed blocks by validator chart with + +* `start` lower interval threshold in ISO string ( _optional_ ) +* `end` upper interval threshold in ISO string ( _optional_ ) + + +``` +GET /validator/F60A6BF9EC0794BB0CFD1E0F2217933F4B33EDE6FE810692BC275CA18148AEF0/reward/stats?start=2024-01-01T00:00:00&end=2025-01-01T00:00:00 +[ + { + timestamp: "2024-06-23T13:51:44.154Z", + data: { + reward: 34000000 + } + },... +] +``` +--- ### Validator stats by ProTxHash -Return a series data for the amount of proposed blocks by validator chart with variable timespan (1h, 24h, 3d, 1w) +Return a series data for the amount of proposed blocks by validator chart with + +* `start` lower interval threshold in ISO string ( _optional_ ) +* `end` upper interval threshold in ISO string ( _optional_ ) + + ``` -GET /validator/F60A6BF9EC0794BB0CFD1E0F2217933F4B33EDE6FE810692BC275CA18148AEF0/stats?timespan=24h +GET /validator/F60A6BF9EC0794BB0CFD1E0F2217933F4B33EDE6FE810692BC275CA18148AEF0/stats?start=2024-01-01T00:00:00&end=2025-01-01T00:00:00 [ { timestamp: "2024-06-23T13:51:44.154Z", @@ -772,9 +798,13 @@ Response codes: 500: Internal Server Error ``` ### Transactions history -Return a series data for the amount of transactions chart with variable timespan (1h, 24h, 3d, 1w) +Return a series data for the amount of transactions chart + +* `start` lower interval threshold in ISO string ( _optional_ ) +* `end` upper interval threshold in ISO string ( _optional_ ) + ``` -GET /transactions/history?timespan=1h +GET /transactions/history?start=2024-01-01T00:00:00&end=2025-01-01T00:00:00 [ { timestamp: "2024-04-22T08:45:20.911Z", @@ -797,7 +827,7 @@ GET /transactions/history?timespan=1h Response codes: ``` 200: OK -400: Invalid input, check timespan value +400: Invalid input, check start/end values 500: Internal Server Error ``` ### Rate