Skip to content

Commit 0fe8fd5

Browse files
authored
feat: new account balances endpoints, optimized for high tx volume (#2229)
* feat: new `/extended/v2/address/:addr/balances` endpoint, optimized for high account tx volume * chore: fix merge conflict * chore: move new sql into pg-store-v2 class * chore: remove nft counts from balances response * docs: mark the old balance endpoint as deprecated * test: add tests * feat: separate /balances/ft endpoint * fix: /balance/ft doesn't need mempool cache invalidation * fix: typo in sql index ORDER clause resulting in indexes not being used in several queries * fix: explicit indexes on ft_events for each "entry point" condition (sender, recipient) * fix: use faster cache preHandlers for now until per-principal caching is optimized * chore: condense new migrations
1 parent cb83f40 commit 0fe8fd5

File tree

11 files changed

+569
-16
lines changed

11 files changed

+569
-16
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// @ts-check
2+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
3+
exports.up = pgm => {
4+
/**
5+
* A previous migration used `order` instead of `sort` in the index definition which caused it to be ignored and default to ASC.
6+
* The `@ts-check` directive at the top of the file will catch these errors in the future.
7+
*/
8+
9+
pgm.dropIndex('principal_stx_txs', [], { name: 'idx_principal_stx_txs_optimized' });
10+
pgm.dropIndex('ft_events', [], { name: 'idx_ft_events_optimized' });
11+
pgm.dropIndex('nft_events', [], { name: 'idx_nft_events_optimized' });
12+
pgm.dropIndex('mempool_txs', [], { name: 'idx_mempool_txs_optimized' });
13+
14+
pgm.createIndex(
15+
'principal_stx_txs',
16+
[
17+
'principal',
18+
{ name: 'block_height', sort: 'DESC' },
19+
{ name: 'microblock_sequence', sort: 'DESC' },
20+
{ name: 'tx_index', sort: 'DESC' }],
21+
{
22+
name: 'idx_principal_stx_txs_optimized',
23+
where: 'canonical = TRUE AND microblock_canonical = TRUE',
24+
}
25+
);
26+
27+
pgm.createIndex(
28+
'nft_events',
29+
[
30+
'sender',
31+
'recipient',
32+
{ name: 'block_height', sort: 'DESC' },
33+
{ name: 'microblock_sequence', sort: 'DESC' },
34+
{ name: 'tx_index', sort: 'DESC' },
35+
{ name: 'event_index', sort: 'DESC' }
36+
],
37+
{
38+
name: 'idx_nft_events_optimized',
39+
where: 'canonical = TRUE AND microblock_canonical = TRUE',
40+
}
41+
);
42+
43+
pgm.createIndex(
44+
'ft_events',
45+
[
46+
'sender',
47+
{ name: 'block_height', sort: 'DESC' },
48+
{ name: 'microblock_sequence', sort: 'DESC' },
49+
{ name: 'tx_index', sort: 'DESC' },
50+
{ name: 'event_index', sort: 'DESC' }
51+
],
52+
{
53+
name: 'idx_ft_events_optimized_sender',
54+
where: 'canonical = TRUE AND microblock_canonical = TRUE',
55+
}
56+
);
57+
58+
pgm.createIndex(
59+
'ft_events',
60+
[
61+
'recipient',
62+
{ name: 'block_height', sort: 'DESC' },
63+
{ name: 'microblock_sequence', sort: 'DESC' },
64+
{ name: 'tx_index', sort: 'DESC' },
65+
{ name: 'event_index', sort: 'DESC' }
66+
],
67+
{
68+
name: 'idx_ft_events_optimized_recipient',
69+
where: 'canonical = TRUE AND microblock_canonical = TRUE',
70+
}
71+
);
72+
73+
pgm.createIndex(
74+
'mempool_txs',
75+
[
76+
{ name: 'receipt_time', sort: 'DESC' }
77+
],
78+
{
79+
name: 'idx_mempool_txs_optimized',
80+
where: 'pruned = FALSE',
81+
}
82+
);
83+
84+
};
85+
86+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
87+
exports.down = pgm => {
88+
pgm.dropIndex('principal_stx_txs', [], { name: 'idx_principal_stx_txs_optimized' });
89+
pgm.dropIndex('ft_events', [], { name: 'idx_ft_events_optimized_sender' });
90+
pgm.dropIndex('ft_events', [], { name: 'idx_ft_events_optimized_recipient' });
91+
pgm.dropIndex('nft_events', [], { name: 'idx_nft_events_optimized' });
92+
pgm.dropIndex('mempool_txs', [], { name: 'idx_mempool_txs_optimized' });
93+
94+
pgm.createIndex(
95+
'principal_stx_txs',
96+
[
97+
'principal',
98+
// @ts-ignore
99+
{ name: 'block_height', order: 'DESC' },
100+
// @ts-ignore
101+
{ name: 'microblock_sequence', order: 'DESC' },
102+
// @ts-ignore
103+
{ name: 'tx_index', order: 'DESC' }],
104+
{
105+
name: 'idx_principal_stx_txs_optimized',
106+
where: 'canonical = TRUE AND microblock_canonical = TRUE',
107+
}
108+
);
109+
110+
pgm.createIndex(
111+
'ft_events',
112+
[
113+
'sender',
114+
'recipient',
115+
// @ts-ignore
116+
{ name: 'block_height', order: 'DESC' },
117+
// @ts-ignore
118+
{ name: 'microblock_sequence', order: 'DESC' },
119+
// @ts-ignore
120+
{ name: 'tx_index', order: 'DESC' },
121+
// @ts-ignore
122+
{ name: 'event_index', order: 'DESC' }
123+
],
124+
{
125+
name: 'idx_ft_events_optimized',
126+
where: 'canonical = TRUE AND microblock_canonical = TRUE',
127+
}
128+
);
129+
130+
pgm.createIndex(
131+
'nft_events',
132+
[
133+
'sender',
134+
'recipient',
135+
// @ts-ignore
136+
{ name: 'block_height', order: 'DESC' },
137+
// @ts-ignore
138+
{ name: 'microblock_sequence', order: 'DESC' },
139+
// @ts-ignore
140+
{ name: 'tx_index', order: 'DESC' },
141+
// @ts-ignore
142+
{ name: 'event_index', order: 'DESC' }
143+
],
144+
{
145+
name: 'idx_nft_events_optimized',
146+
where: 'canonical = TRUE AND microblock_canonical = TRUE',
147+
}
148+
);
149+
150+
pgm.createIndex(
151+
'mempool_txs',
152+
[
153+
'sender_address',
154+
'sponsor_address',
155+
'token_transfer_recipient_address',
156+
// @ts-ignore
157+
{ name: 'receipt_time', order: 'DESC' }
158+
],
159+
{
160+
name: 'idx_mempool_txs_optimized',
161+
where: 'pruned = FALSE',
162+
}
163+
);
164+
};

src/api/controllers/cache-controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { BlockParams } from '../routes/v2/schemas';
1515
* state of the chain depending on the type of information being requested by the endpoint.
1616
* This entry will have an `ETag` string as the value.
1717
*/
18-
enum ETagType {
18+
export enum ETagType {
1919
/** ETag based on the latest `index_block_hash` or `microblock_hash`. */
2020
chainTip = 'chain_tip',
2121
/** ETag based on a digest of all pending mempool `tx_id`s. */
@@ -149,7 +149,7 @@ async function calculateETag(
149149
}
150150
}
151151

152-
async function handleCache(type: ETagType, request: FastifyRequest, reply: FastifyReply) {
152+
export async function handleCache(type: ETagType, request: FastifyRequest, reply: FastifyReply) {
153153
const metrics = getETagMetrics();
154154
const ifNoneMatch = parseIfNoneMatchHeader(request.headers['if-none-match']);
155155
const etag = await calculateETag(request.server.db, type, request);

src/api/pagination.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export enum ResourceType {
4040
PoxCycle,
4141
TokenHolders,
4242
BlockSignerSignature,
43+
FtBalance,
4344
}
4445

4546
export const pagingQueryLimits: Record<ResourceType, { defaultLimit: number; maxLimit: number }> = {
@@ -99,6 +100,10 @@ export const pagingQueryLimits: Record<ResourceType, { defaultLimit: number; max
99100
defaultLimit: 500,
100101
maxLimit: 1000,
101102
},
103+
[ResourceType.FtBalance]: {
104+
defaultLimit: 100,
105+
maxLimit: 200,
106+
},
102107
};
103108

104109
export function getPagingQueryLimit(

src/api/routes/address.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ export const AddressRoutes: FastifyPluginAsync<
9191
{
9292
preHandler: handlePrincipalMempoolCache,
9393
schema: {
94+
deprecated: true,
9495
operationId: 'get_account_stx_balance',
9596
summary: 'Get account STX balance',
96-
description: `Retrieves STX token balance for a given Address or Contract Identifier.`,
97+
description: `**NOTE:** This endpoint is deprecated in favor of [Get address STX balance](/api/get-principal-stx-balance).
98+
99+
Retrieves STX token balance for a given Address or Contract Identifier.`,
97100
tags: ['Accounts'],
98101
params: Type.Object({
99102
principal: PrincipalSchema,
@@ -159,9 +162,12 @@ export const AddressRoutes: FastifyPluginAsync<
159162
{
160163
preHandler: handlePrincipalMempoolCache,
161164
schema: {
165+
deprecated: true,
162166
operationId: 'get_account_balance',
163167
summary: 'Get account balances',
164-
description: `Retrieves total account balance information for a given Address or Contract Identifier. This includes the balances of STX Tokens, Fungible Tokens and Non-Fungible Tokens for the account.`,
168+
description: `**NOTE:** This endpoint is deprecated in favor of [Get address FT balances](/api/get-principal-ft-balances).
169+
170+
Retrieves total account balance information for a given Address or Contract Identifier. This includes the balances of STX Tokens, Fungible Tokens and Non-Fungible Tokens for the account.`,
165171
tags: ['Accounts'],
166172
params: Type.Object({
167173
principal: PrincipalSchema,

0 commit comments

Comments
 (0)