-
Notifications
You must be signed in to change notification settings - Fork 3
Feat: add GET /auth/pools endpoint #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| 'use strict' | ||
|
|
||
| const test = require('brittle') | ||
| const { | ||
| getPools, | ||
| flattenPoolStats, | ||
| calculatePoolsSummary | ||
| } = require('../../../workers/lib/server/handlers/pools.handlers') | ||
|
|
||
| test('getPools - happy path', async (t) => { | ||
| const mockCtx = { | ||
| conf: { | ||
| orks: [{ rpcPublicKey: 'key1' }] | ||
| }, | ||
| net_r0: { | ||
| jRequest: async (key, method, payload) => { | ||
| if (method === 'getWrkExtData') { | ||
| return [{ | ||
| ts: '1770000000000', | ||
| stats: [ | ||
| { poolType: 'f2pool', username: 'user1', hashrate: 100000, worker_count: 5, balance: 0.5, timestamp: 1770000000000 }, | ||
| { poolType: 'ocean', username: 'user2', hashrate: 200000, worker_count: 10, balance: 1.2, timestamp: 1770000000000 } | ||
| ] | ||
| }] | ||
| } | ||
| return [] | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const mockReq = { query: {} } | ||
| const result = await getPools(mockCtx, mockReq, {}) | ||
| t.ok(result.pools, 'should return pools array') | ||
| t.ok(result.summary, 'should return summary') | ||
| t.ok(Array.isArray(result.pools), 'pools should be array') | ||
| t.is(result.pools.length, 2, 'should have 2 pools') | ||
| t.is(result.summary.poolCount, 2, 'summary should count 2 pools') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('getPools - with filter', async (t) => { | ||
| const mockCtx = { | ||
| conf: { orks: [{ rpcPublicKey: 'key1' }] }, | ||
| net_r0: { | ||
| jRequest: async () => [{ | ||
| ts: '1770000000000', | ||
| stats: [ | ||
| { poolType: 'f2pool', username: 'user1', hashrate: 100000, worker_count: 5, balance: 0.5 }, | ||
| { poolType: 'ocean', username: 'user2', hashrate: 200000, worker_count: 10, balance: 0 } | ||
| ] | ||
| }] | ||
| } | ||
| } | ||
|
|
||
| const mockReq = { query: { query: '{"pool":"f2pool"}' } } | ||
| const result = await getPools(mockCtx, mockReq, {}) | ||
| t.ok(result.pools, 'should return filtered pools') | ||
| t.is(result.pools.length, 1, 'should have 1 pool after filter') | ||
| t.is(result.pools[0].pool, 'f2pool', 'should match filter') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('getPools - empty ork results', async (t) => { | ||
| const mockCtx = { | ||
| conf: { orks: [{ rpcPublicKey: 'key1' }] }, | ||
| net_r0: { jRequest: async () => ([]) } | ||
| } | ||
|
|
||
| const result = await getPools(mockCtx, { query: {} }, {}) | ||
| t.ok(result.pools, 'should return pools array') | ||
| t.is(result.pools.length, 0, 'pools should be empty') | ||
| t.is(result.summary.poolCount, 0, 'pool count should be 0') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('flattenPoolStats - extracts pools from ext-data stats array', (t) => { | ||
| const results = [ | ||
| [{ | ||
| ts: '1770000000000', | ||
| stats: [ | ||
| { poolType: 'f2pool', username: 'user1', hashrate: 100000, worker_count: 5, balance: 0.5 }, | ||
| { poolType: 'ocean', username: 'user2', hashrate: 200000, worker_count: 10, balance: 1.2 } | ||
| ] | ||
| }] | ||
| ] | ||
| const pools = flattenPoolStats(results) | ||
| t.is(pools.length, 2, 'should extract 2 pools') | ||
| t.is(pools[0].pool, 'f2pool', 'should have correct pool type') | ||
| t.is(pools[0].account, 'user1', 'should have correct account') | ||
| t.is(pools[0].hashrate, 100000, 'should have correct hashrate') | ||
| t.is(pools[1].pool, 'ocean', 'should have correct pool type') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('flattenPoolStats - deduplicates pools across orks', (t) => { | ||
| const results = [ | ||
| [{ ts: '1770000000000', stats: [{ poolType: 'f2pool', username: 'user1', hashrate: 100 }] }], | ||
| [{ ts: '1770000000000', stats: [{ poolType: 'f2pool', username: 'user1', hashrate: 200 }] }] | ||
| ] | ||
| const pools = flattenPoolStats(results) | ||
| t.is(pools.length, 1, 'should deduplicate by poolType:username') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('flattenPoolStats - handles error results', (t) => { | ||
| const results = [{ error: 'timeout' }] | ||
| const pools = flattenPoolStats(results) | ||
| t.is(pools.length, 0, 'should be empty') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('flattenPoolStats - handles non-array input', (t) => { | ||
| const pools = flattenPoolStats(null) | ||
| t.is(pools.length, 0, 'should return empty array') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('calculatePoolsSummary - calculates totals', (t) => { | ||
| const pools = [ | ||
| { hashrate: 100, workerCount: 5, balance: 50000 }, | ||
| { hashrate: 200, workerCount: 10, balance: 30000 } | ||
| ] | ||
|
|
||
| const summary = calculatePoolsSummary(pools) | ||
| t.is(summary.poolCount, 2, 'should count pools') | ||
| t.is(summary.totalHashrate, 300, 'should sum hashrate') | ||
| t.is(summary.totalWorkers, 15, 'should sum workers') | ||
| t.is(summary.totalBalance, 80000, 'should sum balance') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('calculatePoolsSummary - handles empty pools', (t) => { | ||
| const summary = calculatePoolsSummary([]) | ||
| t.is(summary.poolCount, 0, 'should be zero') | ||
| t.is(summary.totalHashrate, 0, 'should be zero') | ||
| t.pass() | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| 'use strict' | ||
|
|
||
| const test = require('brittle') | ||
| const { testModuleStructure, testHandlerFunctions, testOnRequestFunctions } = require('../helpers/routeTestHelpers') | ||
| const { createRoutesForTest } = require('../helpers/mockHelpers') | ||
|
|
||
| const ROUTES_PATH = '../../../workers/lib/server/routes/pools.routes.js' | ||
|
|
||
| test('pools routes - module structure', (t) => { | ||
| testModuleStructure(t, ROUTES_PATH, 'pools') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('pools routes - route definitions', (t) => { | ||
| const routes = createRoutesForTest(ROUTES_PATH) | ||
| const routeUrls = routes.map(route => route.url) | ||
| t.ok(routeUrls.includes('/auth/pools'), 'should have pools route') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('pools routes - HTTP methods', (t) => { | ||
| const routes = createRoutesForTest(ROUTES_PATH) | ||
| routes.forEach(route => { | ||
| t.is(route.method, 'GET', `route ${route.url} should be GET`) | ||
| }) | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('pools routes - handler functions', (t) => { | ||
| const routes = createRoutesForTest(ROUTES_PATH) | ||
| testHandlerFunctions(t, routes, 'pools') | ||
| t.pass() | ||
| }) | ||
|
|
||
| test('pools routes - onRequest functions', (t) => { | ||
| const routes = createRoutesForTest(ROUTES_PATH) | ||
| testOnRequestFunctions(t, routes, 'pools') | ||
| t.pass() | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| 'use strict' | ||
|
|
||
| const mingo = require('mingo') | ||
| const { | ||
| RPC_METHODS, | ||
| MINERPOOL_EXT_DATA_KEYS | ||
| } = require('../../constants') | ||
| const { | ||
| requestRpcMapLimit, | ||
| parseJsonQueryParam | ||
| } = require('../../utils') | ||
|
|
||
| async function getPools (ctx, req) { | ||
| const filter = req.query.query ? parseJsonQueryParam(req.query.query, 'ERR_QUERY_INVALID_JSON') : null | ||
| const sort = req.query.sort ? parseJsonQueryParam(req.query.sort, 'ERR_SORT_INVALID_JSON') : null | ||
| const fields = req.query.fields ? parseJsonQueryParam(req.query.fields, 'ERR_FIELDS_INVALID_JSON') : null | ||
|
|
||
| const statsResults = await requestRpcMapLimit(ctx, RPC_METHODS.GET_WRK_EXT_DATA, { | ||
| type: 'minerpool', | ||
| query: { key: MINERPOOL_EXT_DATA_KEYS.STATS } | ||
| }) | ||
|
|
||
| const pools = flattenPoolStats(statsResults) | ||
|
|
||
| const query = new mingo.Query(filter || {}) | ||
| let cursor = query.find(pools, fields || {}) | ||
| if (sort) cursor = cursor.sort(sort) | ||
| const result = cursor.all() | ||
|
|
||
| const summary = calculatePoolsSummary(pools) | ||
|
|
||
| return { pools: result, summary } | ||
| } | ||
|
|
||
| function flattenPoolStats (results) { | ||
| const pools = [] | ||
| const seen = new Set() | ||
| if (!Array.isArray(results)) return pools | ||
|
|
||
| for (const orkResult of results) { | ||
| if (!orkResult || orkResult.error) continue | ||
| const items = Array.isArray(orkResult) ? orkResult : (orkResult.data || orkResult.result || []) | ||
| if (!Array.isArray(items)) continue | ||
|
|
||
| for (const item of items) { | ||
| if (!item) continue | ||
| const stats = item.stats || item.data || [] | ||
| if (!Array.isArray(stats)) continue | ||
|
|
||
| for (const stat of stats) { | ||
| if (!stat) continue | ||
| const poolKey = `${stat.poolType}:${stat.username}` | ||
| if (seen.has(poolKey)) continue | ||
| seen.add(poolKey) | ||
|
|
||
| pools.push({ | ||
| name: stat.username || stat.poolType, | ||
| pool: stat.poolType, | ||
| account: stat.username, | ||
| status: 'active', | ||
| hashrate: stat.hashrate || 0, | ||
| hashrate1h: stat.hashrate_1h || 0, | ||
| hashrate24h: stat.hashrate_24h || 0, | ||
| workerCount: stat.worker_count || 0, | ||
| activeWorkerCount: stat.active_workers_count || 0, | ||
| balance: stat.balance || 0, | ||
| unsettled: stat.unsettled || 0, | ||
| revenue24h: stat.revenue_24h || stat.estimated_today_income || 0, | ||
| yearlyBalances: stat.yearlyBalances || [], | ||
| lastUpdated: stat.timestamp || null | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return pools | ||
| } | ||
|
|
||
| function calculatePoolsSummary (pools) { | ||
| const totals = pools.reduce((acc, pool) => { | ||
| acc.totalHashrate += pool.hashrate || 0 | ||
| acc.totalWorkers += pool.workerCount || pool.worker_count || 0 | ||
| acc.totalBalance += pool.balance || 0 | ||
| return acc | ||
| }, { totalHashrate: 0, totalWorkers: 0, totalBalance: 0 }) | ||
|
|
||
| return { | ||
| poolCount: pools.length, | ||
| ...totals | ||
| } | ||
| } | ||
|
|
||
| module.exports = { | ||
| getPools, | ||
| flattenPoolStats, | ||
| calculatePoolsSummary | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| 'use strict' | ||
|
|
||
| const { | ||
| ENDPOINTS, | ||
| HTTP_METHODS | ||
| } = require('../../constants') | ||
| const { | ||
| getPools | ||
| } = require('../handlers/pools.handlers') | ||
| const { createCachedAuthRoute } = require('../lib/routeHelpers') | ||
|
|
||
| module.exports = (ctx) => { | ||
| const schemas = require('../schemas/pools.schemas.js') | ||
|
|
||
| return [ | ||
| { | ||
| method: HTTP_METHODS.GET, | ||
| url: ENDPOINTS.POOLS, | ||
| schema: { | ||
| querystring: schemas.query.pools | ||
| }, | ||
| ...createCachedAuthRoute( | ||
| ctx, | ||
| (req) => [ | ||
| 'pools', | ||
| req.query.query, | ||
| req.query.sort, | ||
| req.query.fields | ||
| ], | ||
| ENDPOINTS.POOLS, | ||
| getPools | ||
| ) | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| 'use strict' | ||
|
|
||
| const schemas = { | ||
| query: { | ||
| pools: { | ||
| type: 'object', | ||
| properties: { | ||
| query: { type: 'string' }, | ||
| sort: { type: 'string' }, | ||
| fields: { type: 'string' }, | ||
| overwriteCache: { type: 'boolean' } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| module.exports = schemas | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be similar to list-things schema
schema: {
querystring: {
type: 'object',
properties: {
query: { type: 'string' },
sort: { type: 'string' },
fields: { type: 'string' },
overwriteCache: { type: 'boolean' }
}
}