Skip to content

Commit 7e59b1d

Browse files
committed
feat: separate /balances/ft endpoint
1 parent 00435cf commit 7e59b1d

File tree

9 files changed

+237
-141
lines changed

9 files changed

+237
-141
lines changed

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: 5 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,
@@ -162,7 +165,7 @@ export const AddressRoutes: FastifyPluginAsync<
162165
deprecated: true,
163166
operationId: 'get_account_balance',
164167
summary: 'Get account balances',
165-
description: `**NOTE:** This endpoint is deprecated in favor of [Get address transactions](/api/get-principal-balances).
168+
description: `**NOTE:** This endpoint is deprecated in favor of [Get address FT balances](/api/get-principal-ft-balances).
166169
167170
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.`,
168171
tags: ['Accounts'],

src/api/routes/v2/addresses.ts

Lines changed: 96 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {
2+
ETagType,
3+
handleCache,
24
handlePrincipalCache,
35
handlePrincipalMempoolCache,
46
handleTransactionCache,
@@ -13,17 +15,15 @@ import { LimitParam, OffsetParam, PrincipalSchema } from '../../schemas/params';
1315
import { getPagingQueryLimit, ResourceType } from '../../pagination';
1416
import { PaginatedResponse } from '../../schemas/util';
1517
import {
16-
AddressBalance,
17-
AddressBalanceSchema,
18-
AddressBalanceV2,
19-
AddressBalanceV2Schema,
2018
AddressTransaction,
2119
AddressTransactionEvent,
2220
AddressTransactionEventSchema,
2321
AddressTransactionSchema,
22+
PrincipalFtBalance,
23+
PrincipalFtBalanceSchema,
2424
} from '../../schemas/entities/addresses';
25-
import { formatMapToObject } from '../../../helpers';
2625
import { validatePrincipal } from '../../query-helpers';
26+
import { StxBalance, StxBalanceSchema } from '../../schemas/entities/balances';
2727

2828
export const AddressRoutesV2: FastifyPluginAsync<
2929
Record<never, never>,
@@ -125,21 +125,31 @@ export const AddressRoutesV2: FastifyPluginAsync<
125125
}
126126
);
127127

128-
// get balances for STX, FTs, and counts for NFTs
129128
fastify.get(
130-
'/:principal/balances',
129+
'/:principal/balances/stx',
131130
{
132-
preHandler: handlePrincipalMempoolCache,
131+
preHandler: (req, reply) => {
132+
const etagType = req.query.include_mempool ? ETagType.principalMempool : ETagType.principal;
133+
return handleCache(etagType, req, reply);
134+
},
133135
schema: {
134-
operationId: 'get_principal_balances',
135-
summary: 'Get principal balances',
136-
description: `Retrieves total account balance information for a given Address or Contract Identifier. This includes the balances of STX Tokens and Fungible Tokens for the account.`,
136+
operationId: 'get_principal_stx_balance',
137+
summary: 'Get principal STX balance',
138+
description: `Retrieves STX account balance information for a given Address or Contract Identifier.`,
137139
tags: ['Accounts'],
138140
params: Type.Object({
139141
principal: PrincipalSchema,
140142
}),
143+
querystring: Type.Object({
144+
include_mempool: Type.Optional(
145+
Type.Boolean({
146+
description: 'Include pending mempool transactions in the balance calculation',
147+
default: false,
148+
})
149+
),
150+
}),
141151
response: {
142-
200: AddressBalanceV2Schema,
152+
200: StxBalanceSchema,
143153
},
144154
},
145155
},
@@ -150,21 +160,12 @@ export const AddressRoutesV2: FastifyPluginAsync<
150160
const result = await fastify.db.sqlTransaction(async sql => {
151161
const chainTip = await fastify.db.getChainTip(sql);
152162

153-
// Get balances for fungible tokens
154-
const ftBalancesResult = await fastify.db.v2.getFungibleTokenHolderBalances({
163+
// Get stx balance (sum of credits, debits, and fees) for address
164+
const stxBalancesResult = await fastify.db.v2.getStxHolderBalance({
155165
sql,
156166
stxAddress,
157167
});
158-
const ftBalances: Record<string, string> = {};
159-
for (const { token, balance } of ftBalancesResult) {
160-
if (token !== 'stx') {
161-
ftBalances[token] = balance;
162-
}
163-
}
164-
165-
// Get stx balance (sum of credits, debits, and fees) for address
166-
const stxBalanceResult = ftBalancesResult.find(entry => entry.token === 'stx');
167-
let stxBalance = BigInt(stxBalanceResult?.balance ?? '0');
168+
let stxBalance = stxBalancesResult.found ? stxBalancesResult.result.balance : 0n;
168169

169170
// Get pox-locked info for STX token
170171
const stxPoxLockedResult = await fastify.db.v2.getStxPoxLockedAtBlock({
@@ -182,23 +183,78 @@ export const AddressRoutesV2: FastifyPluginAsync<
182183
});
183184
stxBalance += totalMinerRewardsReceived;
184185

185-
const mempoolResult = await fastify.db.getPrincipalMempoolStxBalanceDelta(sql, stxAddress);
186-
const mempoolBalance: bigint = stxBalance + mempoolResult.delta;
186+
const result: StxBalance = {
187+
balance: stxBalance.toString(),
188+
total_miner_rewards_received: totalMinerRewardsReceived.toString(),
189+
lock_tx_id: stxPoxLockedResult.lockTxId,
190+
locked: stxPoxLockedResult.locked.toString(),
191+
lock_height: stxPoxLockedResult.lockHeight,
192+
burnchain_lock_height: stxPoxLockedResult.burnchainLockHeight,
193+
burnchain_unlock_height: stxPoxLockedResult.burnchainUnlockHeight,
194+
};
195+
196+
if (req.query.include_mempool) {
197+
const mempoolResult = await fastify.db.getPrincipalMempoolStxBalanceDelta(
198+
sql,
199+
stxAddress
200+
);
201+
const mempoolBalance = stxBalance + mempoolResult.delta;
202+
result.estimated_balance = mempoolBalance.toString();
203+
result.pending_balance_inbound = mempoolResult.inbound.toString();
204+
result.pending_balance_outbound = mempoolResult.outbound.toString();
205+
}
206+
207+
return result;
208+
});
209+
await reply.send(result);
210+
}
211+
);
187212

188-
const result: AddressBalanceV2 = {
189-
stx: {
190-
balance: stxBalance.toString(),
191-
estimated_balance: mempoolBalance.toString(),
192-
pending_balance_inbound: mempoolResult.inbound.toString(),
193-
pending_balance_outbound: mempoolResult.outbound.toString(),
194-
total_miner_rewards_received: totalMinerRewardsReceived.toString(),
195-
lock_tx_id: stxPoxLockedResult.lockTxId,
196-
locked: stxPoxLockedResult.locked.toString(),
197-
lock_height: stxPoxLockedResult.lockHeight,
198-
burnchain_lock_height: stxPoxLockedResult.burnchainLockHeight,
199-
burnchain_unlock_height: stxPoxLockedResult.burnchainUnlockHeight,
200-
},
201-
fungible_tokens: ftBalances,
213+
fastify.get(
214+
'/:principal/balances/ft',
215+
{
216+
preHandler: handlePrincipalMempoolCache,
217+
schema: {
218+
operationId: 'get_principal_ft_balances',
219+
summary: 'Get principal FT balances',
220+
description: `Retrieves Fungible-token account balance information for a given Address or Contract Identifier.`,
221+
tags: ['Accounts'],
222+
params: Type.Object({
223+
principal: PrincipalSchema,
224+
}),
225+
querystring: Type.Object({
226+
limit: LimitParam(ResourceType.FtBalance),
227+
offset: OffsetParam(),
228+
}),
229+
response: {
230+
200: PaginatedResponse(PrincipalFtBalanceSchema),
231+
},
232+
},
233+
},
234+
async (req, reply) => {
235+
const stxAddress = req.params.principal;
236+
validatePrincipal(stxAddress);
237+
const limit = getPagingQueryLimit(ResourceType.FtBalance, req.query.limit);
238+
const offset = req.query.offset ?? 0;
239+
const result = await fastify.db.sqlTransaction(async sql => {
240+
// Get balances for fungible tokens
241+
const ftBalancesResult = await fastify.db.v2.getFungibleTokenHolderBalances({
242+
sql,
243+
stxAddress,
244+
limit,
245+
offset,
246+
});
247+
const ftBalances: PrincipalFtBalance[] = ftBalancesResult.results.map(
248+
({ token, balance }) => ({
249+
token,
250+
balance,
251+
})
252+
);
253+
const result = {
254+
limit,
255+
offset,
256+
total: ftBalancesResult.total,
257+
results: ftBalances,
202258
};
203259
return result;
204260
});

src/api/schemas/entities/addresses.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@ import { Static, Type } from '@sinclair/typebox';
22
import { TransactionSchema } from './transactions';
33
import { TransactionEventAssetTypeSchema } from './transaction-events';
44
import { Nullable } from '../util';
5-
import {
6-
FtBalanceSchema,
7-
NftBalanceSchema,
8-
StxBalanceSchema,
9-
StxBalanceV2Schema,
10-
} from './balances';
5+
import { FtBalanceSchema, NftBalanceSchema, StxBalanceSchema } from './balances';
116

127
export const AddressNoncesSchema = Type.Object(
138
{
@@ -272,17 +267,11 @@ export const AddressBalanceSchema = Type.Object(
272267
);
273268
export type AddressBalance = Static<typeof AddressBalanceSchema>;
274269

275-
export const AddressBalanceV2Schema = Type.Object(
276-
{
277-
stx: StxBalanceV2Schema,
278-
fungible_tokens: Type.Record(
279-
Type.String({ description: 'token' }),
280-
Type.String({ description: 'balance' })
281-
),
282-
},
283-
{ title: 'AddressBalanceResponseV2', description: 'GET request that returns address balances' }
284-
);
285-
export type AddressBalanceV2 = Static<typeof AddressBalanceV2Schema>;
270+
export const PrincipalFtBalanceSchema = Type.Object({
271+
token: Type.String(),
272+
balance: Type.String(),
273+
});
274+
export type PrincipalFtBalance = Static<typeof PrincipalFtBalanceSchema>;
286275

287276
enum InboundStxTransferType {
288277
bulkSend = 'bulk-send',

src/api/schemas/entities/balances.ts

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ export const StxBalanceSchema = Type.Object(
3636
description: 'Outbound STX balance from pending mempool transactions',
3737
})
3838
),
39-
total_sent: Type.String(),
40-
total_received: Type.String(),
41-
total_fees_sent: Type.String(),
39+
total_sent: Type.Optional(Type.String()),
40+
total_received: Type.Optional(Type.String()),
41+
total_fees_sent: Type.Optional(Type.String()),
4242
total_miner_rewards_received: Type.String(),
4343
lock_tx_id: Type.String({
4444
description: 'The transaction where the lock event occurred. Empty if no tokens are locked.',
@@ -62,39 +62,4 @@ export const StxBalanceSchema = Type.Object(
6262
},
6363
{ title: 'StxBalance' }
6464
);
65-
66-
export const StxBalanceV2Schema = Type.Object(
67-
{
68-
balance: Type.String(),
69-
estimated_balance: Type.String({
70-
description: 'Total STX balance considering pending mempool transactions',
71-
}),
72-
pending_balance_inbound: Type.String({
73-
description: 'Inbound STX balance from pending mempool transactions',
74-
}),
75-
pending_balance_outbound: Type.String({
76-
description: 'Outbound STX balance from pending mempool transactions',
77-
}),
78-
total_miner_rewards_received: Type.String(),
79-
lock_tx_id: Type.String({
80-
description: 'The transaction where the lock event occurred. Empty if no tokens are locked.',
81-
}),
82-
locked: Type.String({
83-
description:
84-
'The amount of locked STX, as string quoted micro-STX. Zero if no tokens are locked.',
85-
}),
86-
lock_height: Type.Integer({
87-
description:
88-
'The STX chain block height of when the lock event occurred. Zero if no tokens are locked.',
89-
}),
90-
burnchain_lock_height: Type.Integer({
91-
description:
92-
'The burnchain block height of when the lock event occurred. Zero if no tokens are locked.',
93-
}),
94-
burnchain_unlock_height: Type.Integer({
95-
description:
96-
'The burnchain block height of when the tokens unlock. Zero if no tokens are locked.',
97-
}),
98-
},
99-
{ title: 'StxBalanceV2' }
100-
);
65+
export type StxBalance = Static<typeof StxBalanceSchema>;

0 commit comments

Comments
 (0)