Skip to content

Commit cb38b68

Browse files
authored
feat: GET /extended/v1/burn_block (#1766)
* feat: new endpoint to get burn blocks `/extended/v1/burn_block` * feat: return stacks block hashes in burn block results * test: add tests for `/extended/v1/burn_block` * ci: fix openapi lint
1 parent 7c45f53 commit cb38b68

File tree

14 files changed

+500
-1
lines changed

14 files changed

+500
-1
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"limit": 1,
3+
"offset": 0,
4+
"total": 21707,
5+
"results": [
6+
{
7+
"burn_block_time": 1626281749,
8+
"burn_block_time_iso": "2021-07-14T16:55:49.000Z",
9+
"burn_block_hash": "0x0000000000000000000ea16f8e906e85ee1cb4dff1e5424e93843b3cec8b0bcb",
10+
"burn_block_height": 691014,
11+
"stacks_blocks": [
12+
"0x54647c277eefe60519b407f2c897749005fdb7f831034135063b2ee43fdacb04",
13+
"0xdaf61d2b355f35c94cf019af99aeb73d8e7db7301c7cd693a464ebd1cfc2228c",
14+
"0xb9e9b308cf9621ecbf66ca7b4689fe384b9b67c4588ec827d8163ab602fb935e",
15+
"0x754562cba6ec243f90485e97778ab472f462fd123ef5b83cc79d8759ca8875f5"
16+
]
17+
}
18+
]
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"description": "GET request that returns burn blocks",
3+
"additionalProperties": false,
4+
"title": "BurnBlockListResponse",
5+
"type": "object",
6+
"required": ["results", "limit", "offset", "total"],
7+
"properties": {
8+
"limit": {
9+
"type": "integer",
10+
"maximum": 30,
11+
"description": "The number of burn blocks to return"
12+
},
13+
"offset": {
14+
"type": "integer",
15+
"description": "The number to burn blocks to skip (starting at `0`)",
16+
"default": 0
17+
},
18+
"total": {
19+
"type": "integer",
20+
"description": "The number of burn blocks available (regardless of filter parameters)"
21+
},
22+
"results": {
23+
"type": "array",
24+
"items": {
25+
"$ref": "../../entities/blocks/burn-block.schema.json"
26+
}
27+
}
28+
}
29+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"burn_block_time": 1594233639,
3+
"burn_block_time_iso": "2020-08-27T16:41:26.000Z",
4+
"burn_block_hash": "0xb154c008df2101023a6d0d54986b3964cee58119eed14f5bed98e15678e18fe2",
5+
"burn_block_height": 654439,
6+
"stacks_blocks": [
7+
"0x54647c277eefe60519b407f2c897749005fdb7f831034135063b2ee43fdacb04",
8+
"0xdaf61d2b355f35c94cf019af99aeb73d8e7db7301c7cd693a464ebd1cfc2228c",
9+
"0xb9e9b308cf9621ecbf66ca7b4689fe384b9b67c4588ec827d8163ab602fb935e",
10+
"0x754562cba6ec243f90485e97778ab472f462fd123ef5b83cc79d8759ca8875f5"
11+
]
12+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"title": "BurnBlock",
3+
"description": "A burn block",
4+
"type": "object",
5+
"additionalProperties": false,
6+
"required": [
7+
"burn_block_time",
8+
"burn_block_time_iso",
9+
"burn_block_hash",
10+
"burn_block_height",
11+
"stacks_blocks"
12+
],
13+
"properties": {
14+
"burn_block_time": {
15+
"type": "number",
16+
"description": "Unix timestamp (in seconds) indicating when this block was mined."
17+
},
18+
"burn_block_time_iso": {
19+
"type": "string",
20+
"description": "An ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ) indicating when this block was mined."
21+
},
22+
"burn_block_hash": {
23+
"type": "string",
24+
"description": "Hash of the anchor chain block"
25+
},
26+
"burn_block_height": {
27+
"type": "integer",
28+
"description": "Height of the anchor chain block"
29+
},
30+
"stacks_blocks": {
31+
"type": "array",
32+
"items": {
33+
"type": "string"
34+
},
35+
"description": "Hashes of the Stacks blocks included in the burn block"
36+
}
37+
}
38+
}

docs/generated.d.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type SchemaMergeRootStub =
1313
| AddressTransactionsWithTransfersListResponse
1414
| AddressTransactionsListResponse
1515
| BlockListResponse
16+
| BurnBlockListResponse
1617
| BnsError
1718
| BnsFetchFileZoneResponse
1819
| BnsGetAllNamesResponse
@@ -114,6 +115,7 @@ export type SchemaMergeRootStub =
114115
| NftBalance
115116
| StxBalance
116117
| Block
118+
| BurnBlock
117119
| BurnchainRewardSlotHolder
118120
| BurnchainReward
119121
| BurnchainRewardsTotal
@@ -1274,6 +1276,49 @@ export interface Block {
12741276
[k: string]: number | undefined;
12751277
};
12761278
}
1279+
/**
1280+
* GET request that returns burn blocks
1281+
*/
1282+
export interface BurnBlockListResponse {
1283+
/**
1284+
* The number of burn blocks to return
1285+
*/
1286+
limit: number;
1287+
/**
1288+
* The number to burn blocks to skip (starting at `0`)
1289+
*/
1290+
offset: number;
1291+
/**
1292+
* The number of burn blocks available (regardless of filter parameters)
1293+
*/
1294+
total: number;
1295+
results: BurnBlock[];
1296+
}
1297+
/**
1298+
* A burn block
1299+
*/
1300+
export interface BurnBlock {
1301+
/**
1302+
* Unix timestamp (in seconds) indicating when this block was mined.
1303+
*/
1304+
burn_block_time: number;
1305+
/**
1306+
* An ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ) indicating when this block was mined.
1307+
*/
1308+
burn_block_time_iso: string;
1309+
/**
1310+
* Hash of the anchor chain block
1311+
*/
1312+
burn_block_hash: string;
1313+
/**
1314+
* Height of the anchor chain block
1315+
*/
1316+
burn_block_height: number;
1317+
/**
1318+
* Hashes of the Stacks blocks included in the burn block
1319+
*/
1320+
stacks_blocks: string[];
1321+
}
12771322
/**
12781323
* Error
12791324
*/

docs/openapi.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,54 @@ paths:
612612
schema:
613613
$ref: ./api/microblocks/get-unanchored-txs.schema.json
614614

615+
/extended/v1/burn_block:
616+
get:
617+
summary: Get recent burn blocks
618+
description: |
619+
Retrieves a list of recent burn blocks
620+
tags:
621+
- Blocks
622+
operationId: get_burn_block_list
623+
parameters:
624+
- name: limit
625+
in: query
626+
description: max number of burn blocks to fetch
627+
required: false
628+
schema:
629+
type: integer
630+
default: 20
631+
maximum: 30
632+
- name: offset
633+
in: query
634+
description: index of first burn block to fetch
635+
required: false
636+
schema:
637+
type: integer
638+
example: 42000
639+
- name: height
640+
in: query
641+
description: filter by burn block height
642+
required: false
643+
schema:
644+
type: integer
645+
example: 42000
646+
- name: hash
647+
in: query
648+
description: filter by burn block hash or the constant 'latest' to filter for the most recent burn block
649+
required: false
650+
schema:
651+
type: string
652+
example: "0x4839a8b01cfb39ffcc0d07d3db31e848d5adf5279d529ed5062300b9f353ff79"
653+
responses:
654+
200:
655+
description: List of burn blocks
656+
content:
657+
application/json:
658+
schema:
659+
$ref: ./api/blocks/get-burn-blocks.schema.json
660+
example:
661+
$ref: ./api/blocks/get-burn-blocks.example.json
662+
615663
/extended/v1/block:
616664
get:
617665
summary: Get recent blocks

src/api/controllers/db-controller.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
AbstractTransaction,
1717
BaseTransaction,
1818
Block,
19+
BurnBlock,
1920
CoinbaseTransactionMetadata,
2021
ContractCallTransactionMetadata,
2122
MempoolTransaction,
@@ -530,6 +531,35 @@ export async function getMicroblockFromDataStore({
530531
};
531532
}
532533

534+
export async function getBurnBlocksFromDataStore(args: {
535+
db: PgStore;
536+
limit: number;
537+
offset: number;
538+
height: number | null;
539+
hash: 'latest' | string | null;
540+
}): Promise<{ total: number; results: BurnBlock[] }> {
541+
const query = await args.db.getBurnBlocks({
542+
limit: args.limit,
543+
offset: args.offset,
544+
height: args.height,
545+
hash: args.hash,
546+
});
547+
const results = query.results.map(r => {
548+
const burnBlock: BurnBlock = {
549+
burn_block_time: r.burn_block_time,
550+
burn_block_time_iso: unixEpochToIso(r.burn_block_time),
551+
burn_block_hash: r.burn_block_hash,
552+
burn_block_height: r.burn_block_height,
553+
stacks_blocks: r.stacks_blocks,
554+
};
555+
return burnBlock;
556+
});
557+
return {
558+
total: query.total,
559+
results,
560+
};
561+
}
562+
533563
export async function getMicroblocksFromDataStore(args: {
534564
db: PgStore;
535565
limit: number;

src/api/init.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { createPox3EventsRouter } from './routes/pox3';
4545
import { createStackingRouter } from './routes/stacking';
4646
import { logger, loggerMiddleware } from '../logger';
4747
import { SERVER_VERSION, isPgConnectionError, isProdEnv, waiter } from '@hirosystems/api-toolkit';
48+
import { createBurnBlockRouter } from './routes/burn-block';
4849

4950
export interface ApiServer {
5051
expressApp: express.Express;
@@ -184,6 +185,7 @@ export async function startApiServer(opts: {
184185
router.use('/tx', createTxRouter(datastore));
185186
router.use('/block', createBlockRouter(datastore));
186187
router.use('/microblock', createMicroblockRouter(datastore));
188+
router.use('/burn_block', createBurnBlockRouter(datastore));
187189
router.use('/burnchain', createBurnchainRouter(datastore));
188190
router.use('/contract', createContractRouter(datastore));
189191
// same here, exclude account nonce route

src/api/pagination.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@ export enum ResourceType {
3535
Token,
3636
Pox2Event,
3737
Stacker,
38+
BurnBlock,
3839
}
3940

4041
const pagingQueryLimits: Record<ResourceType, { defaultLimit: number; maxLimit: number }> = {
4142
[ResourceType.Block]: {
4243
defaultLimit: 20,
4344
maxLimit: 30,
4445
},
46+
[ResourceType.BurnBlock]: {
47+
defaultLimit: 20,
48+
maxLimit: 30,
49+
},
4550
[ResourceType.Tx]: {
4651
defaultLimit: 20,
4752
maxLimit: 50,

src/api/query-helpers.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,53 @@ export function getBlockParams(
111111
}
112112
}
113113

114+
/**
115+
* Parses a block hash value from a given request query param.
116+
* If an error is encountered while parsing the param then a 400 response with an error message is sent and the function throws.
117+
* @param queryParamName - name of the query param
118+
* @param paramRequired - if true then the function will throw and return a 400 if the param is missing, if false then the function will return null if the param is missing
119+
*/
120+
export function getBlockHashQueryParam<TRequired extends boolean>(
121+
queryParamName: string,
122+
paramRequired: TRequired,
123+
req: Request,
124+
res: Response,
125+
next: NextFunction
126+
): TRequired extends true ? string | never : string | null {
127+
if (!(queryParamName in req.query)) {
128+
if (paramRequired) {
129+
handleBadRequest(
130+
res,
131+
next,
132+
`Request is missing required "${queryParamName}" query parameter`
133+
);
134+
} else {
135+
return null as TRequired extends true ? string : string | null;
136+
}
137+
}
138+
const hashParamVal = req.query[queryParamName];
139+
if (typeof hashParamVal !== 'string') {
140+
handleBadRequest(
141+
res,
142+
next,
143+
`Unexpected type for block hash query parameter: ${JSON.stringify(hashParamVal)}`
144+
);
145+
}
146+
147+
// Extract the hash part, ignoring '0x' if present
148+
const match = hashParamVal.match(/^(0x)?([a-fA-F0-9]{64})$/i);
149+
if (!match) {
150+
handleBadRequest(
151+
res,
152+
next,
153+
"Invalid hash string. Ensure it is 64 hexadecimal characters long, with an optional '0x' prefix"
154+
);
155+
}
156+
157+
// Normalize the string
158+
return '0x' + match[2].toLowerCase();
159+
}
160+
114161
/**
115162
* Parses a block height value from a given request query param.
116163
* If an error is encountered while parsing the param then a 400 response with an error message is sent and the function throws.

0 commit comments

Comments
 (0)