Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 77 additions & 5 deletions api/docs/RPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,55 @@ Returns a time series of the average transaction count per block over a configur

---

### GET /block/:hash
### GET /blocks/difficulty/chart

Returns a single block by its hash.
Returns a time series of the average mining difficulty over a configurable time range.

**Query Parameters**

| Parameter | Type | Default | Constraints | Description |
|-------------------|--------|-----------------------|----------------------|----------------------------------------------------------|
| `timestamp_start` | string | 1 hour ago (ISO 8601) | | Start of the time range |
| `timestamp_end` | string | now (ISO 8601) | | End of the time range |
| `intervals_count` | number | auto | minimum: 2, max: 100 | Number of buckets. When omitted, chosen automatically via `calculateInterval` |

**Response `200`**

```json
[
{
"timestamp": "2026-07-12T06:00:00.000Z",
"data": { "avg": 103483713.33 }
},
{
"timestamp": "2026-07-12T10:00:00.000Z",
"data": { "avg": 97596905.48 }
}
]
```

| Field | Type | Description |
|------------|--------|-------------------------------------------------------------------------|
| `timestamp`| string | ISO 8601 start of the bucket |
| `data.avg` | number | Average `difficulty` across all blocks in the bucket. `0` if no blocks fell in the bucket |

**Response `400`**

```json
{ "message": "start timestamp cannot be more than end timestamp" }
```

---

### GET /block/:identifier

Returns a single block by its hash or height.

**Path Parameters**

| Parameter | Type | Constraints | Description |
|-----------|--------|--------------------------------|------------------|
| `hash` | string | 64-char alphanumeric | Block hash |
| Parameter | Type | Constraints | Description |
|--------------|--------|------------------------------------------------------|----------------------|
| `identifier` | string | 64-char alphanumeric (hash) or 1–9 digits (height) | Block hash or height |

**Response `200`** — [Block Object](#block-object)

Expand Down Expand Up @@ -335,6 +375,38 @@ Returns a single block by its hash.

---

### GET /block/:hash/transactions

Returns a paginated list of transaction hashes for the block with the given hash. Coinbase transaction first, then by insertion order. For full transaction objects use [`GET /transactions/height/:height`](#get-transactionsheightheight).

**Path Parameters**

| Parameter | Type | Constraints | Description |
|-----------|--------|----------------------|-------------|
| `hash` | string | 64-char alphanumeric | Block hash |

**Query Parameters:** [Pagination](#pagination-query-parameters)

**Response `200`**

```json
{
"resultSet": [
"9d778417705481977761c27e835666d02da820e51505add96e64ebfc36f3e7d2",
"0eb957d2edee426a1848c6f9b9a2bedcc0db2df1c82ae9af54e435c129f50d8a"
],
"pagination": {
"page": 1,
"limit": 10,
"total": 39
}
}
```

> `total` comes from the block's `tx_count`. An unknown block hash yields an empty `resultSet` with `total: -1`.

---

### GET /transactions

Returns a paginated list of transactions. Include pending transactions
Expand Down
43 changes: 40 additions & 3 deletions api/src/controllers/BlocksController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,47 @@ export default class BlocksController {
response.send(series);
}

getBlockByHash = async (request: FastifyRequest<{ Params: { hash: string } }>, response: FastifyReply): Promise<void> => {
const { hash } = request.params;
getDifficultyStats = async (
request: FastifyRequest<{
Querystring: { timestamp_start: string; timestamp_end: string; intervals_count: number }
}>,
response: FastifyReply
): Promise<void> => {
const {
timestamp_start: start = new Date(new Date().getTime() - 3600000).toISOString(),
timestamp_end: end = new Date().toISOString(),
intervals_count: intervalsCount,
} = request.query;

if (new Date(start).getTime() > new Date(end).getTime()) {
return response.status(400).send({ message: 'start timestamp cannot be more than end timestamp' });
}

const intervalInMs =
Math.ceil(
(new Date(end).getTime() - new Date(start).getTime()) / Number(intervalsCount ?? NaN) / 1000
) * 1000;

const interval = intervalsCount
? iso8601duration(intervalInMs)
: calculateInterval(new Date(start), new Date(end));

const series = await this.blocksDAO.getDifficultySeries(
new Date(start),
new Date(end),
interval,
isNaN(intervalInMs) ? Intervals[interval] : intervalInMs,
);

response.send(series);
}

getBlockByHashOrHeight = async (request: FastifyRequest<{ Params: { identifier: string } }>, response: FastifyReply): Promise<void> => {
const { identifier } = request.params;

const block = await this.blocksDAO.getBlockByHash(hash);
const block = identifier.length === 64
? await this.blocksDAO.getBlockByHash(identifier)
: await this.blocksDAO.getBlockByHeight(Number(identifier));

if (!block) {
return response.status(404).send({ error: 'Block not found' });
Expand Down
9 changes: 9 additions & 0 deletions api/src/controllers/TransactionsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ export default class TransactionsController {
response.send(transactions);
};

getBlockTransactionHashes = async (request: FastifyRequest<{ Querystring: PaginatedQuery; Params: { hash: string } }>, response: FastifyReply): Promise<void> => {
const { page = 1, limit = 10, order = 'asc' } = request.query;
const { hash } = request.params;

const hashes = await this.transactionsDAO.getTransactionHashesByBlockHash(hash, page, limit, order);

response.send(hashes);
};

getPendingTransactions = async (request: FastifyRequest<{ Querystring: PaginatedQuery }>, response: FastifyReply): Promise<void> => {
const { page = 1, limit = 10, order = 'asc' } = request.query;

Expand Down
47 changes: 46 additions & 1 deletion api/src/dao/BlocksDAO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,58 @@ export default class BlocksDAO {
});
};

getDifficultySeries = async (start: Date, end: Date, interval: string, intervalInMs: number): Promise<SeriesData[]> => {
const startSql = `'${new Date(start.getTime() + intervalInMs).toISOString()}'::timestamptz`;
const endSql = `'${new Date(end.getTime()).toISOString()}'::timestamptz`;

const ranges = this.knex
.from(this.knex.raw(`generate_series(${startSql}, ${endSql}, '${interval}'::interval) date_to`))
.select('date_to')
.select(
this.knex.raw(
'LAG(date_to, 1, ?::timestamptz) OVER (ORDER BY date_to ASC) AS date_from',
[start.toISOString()]
)
);

const bucketsCTE = this.knex('ranges')
.select('date_from')
.select(this.knex.raw('AVG(blocks.difficulty) AS avg_difficulty'))
.leftJoin('blocks', function () {
this.on('blocks.timestamp', '>', 'ranges.date_from')
.andOn('blocks.timestamp', '<=', 'ranges.date_to');
})
.groupBy('date_from');

const rows = await this.knex
.with('ranges', ranges)
.with('buckets', bucketsCTE)
.select('date_from')
.select(this.knex.raw('COALESCE(avg_difficulty, 0) AS avg_difficulty'))
.from('buckets')
.orderBy('date_from', 'asc');

return rows.map((row: any) => new SeriesData(
new Date(row.date_from),
{ avg: row.avg_difficulty !== null ? parseFloat(parseFloat(row.avg_difficulty).toFixed(2)) : null },
));
}

getBlockByHash = async (hash: string): Promise<Block | null> => {
return this.getBlock({ 'blocks.hash': hash });
};

getBlockByHeight = async (height: number): Promise<Block | null> => {
return this.getBlock({ 'blocks.height': height });
};

private getBlock = async (where: Record<string, string | number>): Promise<Block | null> => {
const rows = await this.knex('blocks')
.select('blocks.height', 'blocks.hash', 'blocks.difficulty', 'blocks.superblock',
'blocks.version', 'blocks.timestamp', 'blocks.tx_count', 'blocks.size', 'blocks.nonce',
'blocks.previous_block_hash', 'blocks.merkle_root', 'blocks.credit_pool_balance')
.select(this.knex.raw('(SELECT MAX(height) FROM blocks) - blocks.height + 1 AS confirmations'))
.where('blocks.hash', hash)
.where(where)
.limit(1)

const [row] = rows;
Expand Down
18 changes: 18 additions & 0 deletions api/src/dao/TransactionsDAO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,24 @@ export default class TransactionsDAO {
return new PaginatedResultSet(rows.map(Transaction.fromRow), page, limit, row?.total_count);
};

getTransactionHashesByBlockHash = async (blockHash: string, page: number, limit: number, order: string): Promise<PaginatedResultSet<string>> => {
const fromRank = (page - 1) * limit;

const rows = await this.knex('transactions')
.select('transactions.hash')
.select('blocks.tx_count as total_count')
.join('blocks', 'blocks.height', 'transactions.block_height')
.where('blocks.hash', blockHash)
.orderBy('transactions.is_coinbase', 'desc')
.orderBy('transactions.id', order)
.limit(limit)
.offset(fromRank)

const [row] = rows;

return new PaginatedResultSet(rows.map((row) => row.hash), page, limit, row?.total_count);
};

getPendingTransactions = async (page: number, limit: number, order: string): Promise<PaginatedResultSet<Transaction>> => {
const fromRank = (page - 1) * limit;

Expand Down
33 changes: 31 additions & 2 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,44 @@ export default function Routes({ fastify, mainController, blocksController, tran
},
},
{
path: '/block/:hash',
path: '/blocks/difficulty/chart',
method: 'get',
handler: blocksController.getBlockByHash,
handler: blocksController.getDifficultyStats,
schema: {
querystring: { $ref: 'timeInterval#' },
},
},
{
path: '/block/:identifier',
method: 'get',
handler: blocksController.getBlockByHashOrHeight,
schema: {
params: {
type: 'object',
properties: {
identifier: {
oneOf: [
{ $ref: 'hash#' },
{ type: 'string', pattern: '^[0-9]{1,9}$' },
],
},
},
required: ['identifier'],
},
},
},
{
path: '/block/:hash/transactions',
method: 'get',
handler: transactionsController.getBlockTransactionHashes,
schema: {
querystring: { $ref: 'paginationOptions#' },
params: {
type: 'object',
properties: {
hash: { $ref: 'hash#' },
},
required: ['hash'],
},
},
},
Expand Down
Loading