Skip to content

Commit d002dad

Browse files
friedgerzone117x
authored andcommitted
feat: add get block by burn block height and by burn block hash (#675)
fix: formatting fix: extract BlockIdentifier type fix: test fix: test fix: test fix: test fix: query
1 parent d3f23d3 commit d002dad

File tree

7 files changed

+207
-10
lines changed

7 files changed

+207
-10
lines changed

docs/openapi.yaml

+59
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,65 @@ paths:
477477
application/json:
478478
example:
479479
$ref: ./api/errors/block-not-found.json
480+
/extended/v1/block/by_burn_block_hash/{burn_block_hash}:
481+
parameters:
482+
- name: burn_block_hash
483+
in: path
484+
description: Hash of the burnchain block
485+
required: true
486+
schema:
487+
type: string
488+
get:
489+
summary: Get block
490+
description: Get a specific block by burnchain block hash
491+
tags:
492+
- Blocks
493+
operationId: get_block_by_burn_block_hash
494+
responses:
495+
200:
496+
description: Block
497+
content:
498+
application/json:
499+
schema:
500+
$ref: ./entities/blocks/block.schema.json
501+
example:
502+
$ref: ./entities/blocks/block.example.json
503+
404:
504+
description: Cannot find block with given height
505+
content:
506+
application/json:
507+
example:
508+
$ref: ./api/errors/block-not-found.json
509+
510+
/extended/v1/block/by_burn_block_height/{burn_block_height}:
511+
parameters:
512+
- name: burn_block_height
513+
in: path
514+
description: Height of the burn chain block
515+
required: true
516+
schema:
517+
type: number
518+
get:
519+
summary: Get block
520+
description: Get a specific block by burn chain height
521+
tags:
522+
- Blocks
523+
operationId: get_block_by_burn_block_height
524+
responses:
525+
200:
526+
description: Block
527+
content:
528+
application/json:
529+
schema:
530+
$ref: ./entities/blocks/block.schema.json
531+
example:
532+
$ref: ./entities/blocks/block.example.json
533+
404:
534+
description: Cannot find block with given height
535+
content:
536+
application/json:
537+
example:
538+
$ref: ./api/errors/block-not-found.json
480539

481540
/extended/v1/burnchain/reward_slot_holders:
482541
get:

src/api/controllers/db-controller.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
} from '@stacks/stacks-blockchain-api-types';
4545

4646
import {
47+
BlockIdentifier,
4748
DataStore,
4849
DbAssetEventTypeId,
4950
DbBlock,
@@ -415,7 +416,7 @@ export async function getBlockFromDataStore({
415416
blockIdentifer,
416417
db,
417418
}: {
418-
blockIdentifer: { hash: string } | { height: number };
419+
blockIdentifer: BlockIdentifier;
419420
db: DataStore;
420421
}): Promise<FoundOrNot<Block>> {
421422
const blockQuery = await db.getBlockWithMetadata(blockIdentifer, {

src/api/routes/block.ts

+37
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,27 @@ export function createBlockRouter(db: DataStore): RouterWithAsync {
5252
res.json(block.result);
5353
});
5454

55+
router.getAsync('/by_burn_block_height/:burnBlockHeight', async (req, res) => {
56+
const burnBlockHeight = parseInt(req.params['burnBlockHeight'], 10);
57+
if (!Number.isInteger(burnBlockHeight)) {
58+
return res.status(400).json({
59+
error: `burnchain height is not a valid integer: ${req.params['burnBlockHeight']}`,
60+
});
61+
}
62+
if (burnBlockHeight < 1) {
63+
return res
64+
.status(400)
65+
.json({ error: `burnchain height is not a positive integer: ${burnBlockHeight}` });
66+
}
67+
const block = await getBlockFromDataStore({ blockIdentifer: { burnBlockHeight }, db });
68+
if (!block.found) {
69+
res.status(404).json({ error: `cannot find block by height ${burnBlockHeight}` });
70+
return;
71+
}
72+
// TODO: block schema validation
73+
res.json(block.result);
74+
});
75+
5576
router.getAsync('/:hash', async (req, res) => {
5677
const { hash } = req.params;
5778

@@ -68,5 +89,21 @@ export function createBlockRouter(db: DataStore): RouterWithAsync {
6889
res.json(block.result);
6990
});
7091

92+
router.getAsync('/by_burn_block_hash/:burnBlockHash', async (req, res) => {
93+
const { burnBlockHash } = req.params;
94+
95+
if (!has0xPrefix(burnBlockHash)) {
96+
return res.redirect('/extended/v1/block/by_burn_block_hash/0x' + burnBlockHash);
97+
}
98+
99+
const block = await getBlockFromDataStore({ blockIdentifer: { burnBlockHash }, db });
100+
if (!block.found) {
101+
res.status(404).json({ error: `cannot find block by burn block hash ${burnBlockHash}` });
102+
return;
103+
}
104+
// TODO: block schema validation
105+
res.json(block.result);
106+
});
107+
71108
return router;
72109
}

src/datastore/common.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -492,13 +492,19 @@ export interface DbRawEventRequest {
492492
payload: string;
493493
}
494494

495+
export type BlockIdentifier =
496+
| { hash: string }
497+
| { height: number }
498+
| { burnBlockHash: string }
499+
| { burnBlockHeight: number };
500+
495501
export interface DataStore extends DataStoreEventEmitter {
496502
storeRawEventRequest(eventPath: string, payload: string): Promise<void>;
497503
getSubdomainResolver(name: { name: string }): Promise<FoundOrNot<string>>;
498504
getNameCanonical(txId: string, indexBlockHash: string): Promise<FoundOrNot<boolean>>;
499-
getBlock(blockIdentifer: { hash: string } | { height: number }): Promise<FoundOrNot<DbBlock>>;
505+
getBlock(blockIdentifer: BlockIdentifier): Promise<FoundOrNot<DbBlock>>;
500506
getBlockWithMetadata<TWithTxs extends boolean = false, TWithMicroblocks extends boolean = false>(
501-
blockIdentifer: { hash: string } | { height: number },
507+
blockIdentifer: BlockIdentifier,
502508
metadata?: DbGetBlockWithMetadataOpts<TWithTxs, TWithMicroblocks>
503509
): Promise<FoundOrNot<DbGetBlockWithMetadataResponse<TWithTxs, TWithMicroblocks>>>;
504510

src/datastore/memory-store.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
DbMicroblock,
3434
DbGetBlockWithMetadataOpts,
3535
DbGetBlockWithMetadataResponse,
36+
BlockIdentifier,
3637
} from './common';
3738
import { logger, FoundOrNot } from '../helpers';
3839
import { AddressTokenOfferingLocked, TransactionType } from '@stacks/stacks-blockchain-api-types';
@@ -173,27 +174,43 @@ export class MemoryDataStore
173174
}
174175

175176
getBlockWithMetadata<TWithTxs extends boolean, TWithMicroblocks extends boolean>(
176-
blockIdentifer: { hash: string } | { height: number },
177+
blockIdentifer: BlockIdentifier,
177178
metadata?: DbGetBlockWithMetadataOpts<TWithTxs, TWithMicroblocks>
178179
): Promise<FoundOrNot<DbGetBlockWithMetadataResponse<TWithTxs, TWithMicroblocks>>> {
179180
throw new Error('Method not implemented.');
180181
}
181182

182-
getBlock(blockIdentifer: { hash: string } | { height: number }): Promise<FoundOrNot<DbBlock>> {
183+
getBlock(blockIdentifer: BlockIdentifier): Promise<FoundOrNot<DbBlock>> {
183184
if ('hash' in blockIdentifer) {
184185
const block = this.blocks.get(blockIdentifer.hash);
185186
if (!block) {
186187
return Promise.resolve({ found: false });
187188
}
188189
return Promise.resolve({ found: true, result: block.entry });
189-
} else {
190+
} else if ('height' in blockIdentifer) {
190191
const block = [...this.blocks.values()].find(
191192
b => b.entry.block_height === blockIdentifer.height
192193
);
193194
if (!block) {
194195
return Promise.resolve({ found: false });
195196
}
196197
return Promise.resolve({ found: true, result: block.entry });
198+
} else if ('burnBlockHash' in blockIdentifer) {
199+
const block = [...this.blocks.values()].find(
200+
b => b.entry.burn_block_hash === blockIdentifer.burnBlockHash
201+
);
202+
if (!block) {
203+
return Promise.resolve({ found: false });
204+
}
205+
return Promise.resolve({ found: true, result: block.entry });
206+
} else {
207+
const block = [...this.blocks.values()].find(
208+
b => b.entry.burn_block_height === blockIdentifer.burnBlockHeight
209+
);
210+
if (!block) {
211+
return Promise.resolve({ found: false });
212+
}
213+
return Promise.resolve({ found: true, result: block.entry });
197214
}
198215
}
199216

src/datastore/postgres-store.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
DbMicroblockPartial,
7171
DataStoreTxEventData,
7272
DbRawEventRequest,
73+
BlockIdentifier,
7374
} from './common';
7475
import {
7576
AddressTokenOfferingLocked,
@@ -2202,7 +2203,7 @@ export class PgDataStore
22022203
}
22032204

22042205
async getBlockWithMetadata<TWithTxs extends boolean, TWithMicroblocks extends boolean>(
2205-
blockIdentifer: { hash: string } | { height: number },
2206+
blockIdentifer: BlockIdentifier,
22062207
metadata?: DbGetBlockWithMetadataOpts<TWithTxs, TWithMicroblocks>
22072208
): Promise<FoundOrNot<DbGetBlockWithMetadataResponse<TWithTxs, TWithMicroblocks>>> {
22082209
return await this.queryTx(async client => {
@@ -2265,13 +2266,13 @@ export class PgDataStore
22652266
});
22662267
}
22672268

2268-
getBlock(blockIdentifer: { hash: string } | { height: number }): Promise<FoundOrNot<DbBlock>> {
2269+
getBlock(blockIdentifer: BlockIdentifier): Promise<FoundOrNot<DbBlock>> {
22692270
return this.query(client => this.getBlockInternal(client, blockIdentifer));
22702271
}
22712272

22722273
async getBlockInternal(
22732274
client: ClientBase,
2274-
blockIdentifer: { hash: string } | { height: number }
2275+
blockIdentifer: BlockIdentifier
22752276
): Promise<FoundOrNot<DbBlock>> {
22762277
let result: QueryResult<BlockQueryResult>;
22772278
if ('hash' in blockIdentifer) {
@@ -2285,7 +2286,7 @@ export class PgDataStore
22852286
`,
22862287
[hexToBuffer(blockIdentifer.hash)]
22872288
);
2288-
} else {
2289+
} else if ('height' in blockIdentifer) {
22892290
result = await client.query<BlockQueryResult>(
22902291
`
22912292
SELECT ${BLOCK_COLUMNS}
@@ -2296,6 +2297,28 @@ export class PgDataStore
22962297
`,
22972298
[blockIdentifer.height]
22982299
);
2300+
} else if ('burnBlockHash' in blockIdentifer) {
2301+
result = await client.query<BlockQueryResult>(
2302+
`
2303+
SELECT ${BLOCK_COLUMNS}
2304+
FROM blocks
2305+
WHERE burn_block_hash = $1
2306+
ORDER BY canonical DESC, block_height DESC
2307+
LIMIT 1
2308+
`,
2309+
[hexToBuffer(blockIdentifer.burnBlockHash)]
2310+
);
2311+
} else {
2312+
result = await client.query<BlockQueryResult>(
2313+
`
2314+
SELECT ${BLOCK_COLUMNS}
2315+
FROM blocks
2316+
WHERE burn_block_height = $1
2317+
ORDER BY canonical DESC, block_height DESC
2318+
LIMIT 1
2319+
`,
2320+
[blockIdentifer.burnBlockHeight]
2321+
);
22992322
}
23002323

23012324
if (result.rowCount === 0) {

src/tests/api-tests.ts

+54
Original file line numberDiff line numberDiff line change
@@ -3120,6 +3120,60 @@ describe('api tests', () => {
31203120
expect(fetchBlockByHeight.status).toBe(200);
31213121
expect(fetchBlockByHeight.type).toBe('application/json');
31223122
expect(JSON.parse(fetchBlockByHeight.text)).toEqual(expectedResp);
3123+
3124+
const fetchBlockByBurnBlockHeight = await supertest(api.server).get(
3125+
`/extended/v1/block/by_burn_block_height/${block.burn_block_height}`
3126+
);
3127+
expect(fetchBlockByBurnBlockHeight.status).toBe(200);
3128+
expect(fetchBlockByBurnBlockHeight.type).toBe('application/json');
3129+
expect(JSON.parse(fetchBlockByBurnBlockHeight.text)).toEqual(expectedResp);
3130+
3131+
const fetchBlockByInvalidBurnBlockHeight1 = await supertest(api.server).get(
3132+
`/extended/v1/block/by_burn_block_height/999`
3133+
);
3134+
expect(fetchBlockByInvalidBurnBlockHeight1.status).toBe(404);
3135+
expect(fetchBlockByInvalidBurnBlockHeight1.type).toBe('application/json');
3136+
const expectedResp1 = {
3137+
error: 'cannot find block by height 999',
3138+
};
3139+
expect(JSON.parse(fetchBlockByInvalidBurnBlockHeight1.text)).toEqual(expectedResp1);
3140+
3141+
const fetchBlockByInvalidBurnBlockHeight2 = await supertest(api.server).get(
3142+
`/extended/v1/block/by_burn_block_height/abc`
3143+
);
3144+
expect(fetchBlockByInvalidBurnBlockHeight2.status).toBe(400);
3145+
expect(fetchBlockByInvalidBurnBlockHeight2.type).toBe('application/json');
3146+
const expectedResp2 = {
3147+
error: 'burnchain height is not a valid integer: abc',
3148+
};
3149+
expect(JSON.parse(fetchBlockByInvalidBurnBlockHeight2.text)).toEqual(expectedResp2);
3150+
3151+
const fetchBlockByInvalidBurnBlockHeight3 = await supertest(api.server).get(
3152+
`/extended/v1/block/by_burn_block_height/0`
3153+
);
3154+
expect(fetchBlockByInvalidBurnBlockHeight3.status).toBe(400);
3155+
expect(fetchBlockByInvalidBurnBlockHeight3.type).toBe('application/json');
3156+
const expectedResp3 = {
3157+
error: 'burnchain height is not a positive integer: 0',
3158+
};
3159+
expect(JSON.parse(fetchBlockByInvalidBurnBlockHeight3.text)).toEqual(expectedResp3);
3160+
3161+
const fetchBlockByBurnBlockHash = await supertest(api.server).get(
3162+
`/extended/v1/block/by_burn_block_hash/${block.burn_block_hash}`
3163+
);
3164+
expect(fetchBlockByBurnBlockHash.status).toBe(200);
3165+
expect(fetchBlockByBurnBlockHash.type).toBe('application/json');
3166+
expect(JSON.parse(fetchBlockByBurnBlockHash.text)).toEqual(expectedResp);
3167+
3168+
const fetchBlockByInvalidBurnBlockHash = await supertest(api.server).get(
3169+
`/extended/v1/block/by_burn_block_hash/0x000000`
3170+
);
3171+
expect(fetchBlockByInvalidBurnBlockHash.status).toBe(404);
3172+
expect(fetchBlockByInvalidBurnBlockHash.type).toBe('application/json');
3173+
const expectedResp4 = {
3174+
error: 'cannot find block by burn block hash 0x000000',
3175+
};
3176+
expect(JSON.parse(fetchBlockByInvalidBurnBlockHash.text)).toEqual(expectedResp4);
31233177
});
31243178

31253179
test('tx - sponsored', async () => {

0 commit comments

Comments
 (0)