Skip to content

Commit aae897a

Browse files
authored
feat: show asset_identifier in ft list response, add filter by valid metadata (#298)
* feat: show asset_identifier in ft list response * feat: valid metadata filter
1 parent 50c75e6 commit aae897a

File tree

6 files changed

+73
-4
lines changed

6 files changed

+73
-4
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';
3+
4+
export const shorthands: ColumnDefinitions | undefined = undefined;
5+
6+
export function up(pgm: MigrationBuilder): void {
7+
pgm.dropIndex('tokens', ['type', 'name']);
8+
pgm.createIndex('tokens', ['type', 'LOWER(name)'], { where: "type = 'ft'" });
9+
10+
pgm.dropIndex('tokens', ['type', 'symbol']);
11+
pgm.createIndex('tokens', ['type', 'LOWER(symbol)'], { where: "type = 'ft'" });
12+
}
13+
14+
export function down(pgm: MigrationBuilder): void {
15+
pgm.dropIndex('tokens', ['type', 'LOWER(name)']);
16+
pgm.createIndex('tokens', ['type', 'name'], { where: "type = 'ft'" });
17+
18+
pgm.dropIndex('tokens', ['type', 'LOWER(symbol)']);
19+
pgm.createIndex('tokens', ['type', 'symbol'], { where: "type = 'ft'" });
20+
}

src/api/routes/ft.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTy
3838
name: Type.Optional(Type.String()),
3939
symbol: Type.Optional(Type.String()),
4040
address: Type.Optional(StacksAddressParam),
41+
valid_metadata_only: Type.Optional(
42+
Type.Boolean({
43+
description: 'If enabled, only tokens with valid SIP-016 metadata will be returned',
44+
})
45+
),
4146
// Pagination
4247
offset: Type.Optional(OffsetParam),
4348
limit: Type.Optional(LimitParam),
@@ -59,6 +64,7 @@ const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTy
5964
name: request.query.name,
6065
symbol: request.query.symbol,
6166
address: request.query.address,
67+
valid_metadata_only: request.query.valid_metadata_only,
6268
},
6369
order: {
6470
order_by: request.query.order_by ?? FtOrderBy.name,
@@ -78,6 +84,7 @@ const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTy
7884
description: t.description,
7985
tx_id: t.tx_id,
8086
sender_address: t.principal?.split('.')[0],
87+
asset_identifier: `${t.principal}::${t.fungible_token_name}`,
8188
image_uri: t.cached_image,
8289
image_canonical_uri: t.image,
8390
image_thumbnail_uri: t.cached_thumbnail_image,

src/api/schemas.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ export const FtBasicMetadataResponse = Type.Object(
301301
examples: ['0xef2ac1126e16f46843228b1dk4830e19eb7599129e4jf392cab9e65ae83a45c0'],
302302
}),
303303
sender_address: Type.String({ examples: ['ST399W7Z9WS0GMSNQGJGME5JAENKN56D65VGMGKGA'] }),
304+
asset_identifier: Type.String({
305+
examples: ['SPZA22A4D15RKH5G8XDGQ7BPC20Q5JNMH0VQKSR6.token-ststx-earn-v1::stSTXearn'],
306+
description: 'Clarity asset identifier',
307+
}),
304308
contract_principal: Type.String({
305309
examples: ['SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2'],
306310
}),

src/pg/pg-store.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,14 +370,15 @@ export class PgStore extends BasePgStore {
370370
order?: DbFungibleTokenOrder;
371371
}): Promise<DbPaginatedResult<DbFungibleTokenMetadataItem>> {
372372
return await this.sqlTransaction(async sql => {
373+
const validMetadataOnly = args.filters?.valid_metadata_only ?? false;
373374
// `ORDER BY` statement
374375
let orderBy: PgSqlQuery;
375376
switch (args.order?.order_by) {
376377
case FtOrderBy.symbol:
377-
orderBy = sql`t.symbol`;
378+
orderBy = sql`LOWER(t.symbol)`;
378379
break;
379380
default:
380-
orderBy = sql`t.name`;
381+
orderBy = sql`LOWER(t.name)`;
381382
break;
382383
}
383384
// `ORDER` statement
@@ -392,11 +393,12 @@ export class PgStore extends BasePgStore {
392393
m.description,
393394
s.principal,
394395
s.tx_id,
396+
s.fungible_token_name,
395397
m.image,
396398
m.cached_image,
397399
COUNT(*) OVER() as total
398400
FROM tokens AS t
399-
LEFT JOIN metadata AS m ON t.id = m.token_id
401+
${validMetadataOnly ? sql`INNER` : sql`LEFT`} JOIN metadata AS m ON t.id = m.token_id
400402
INNER JOIN smart_contracts AS s ON t.smart_contract_id = s.id
401403
WHERE t.type = 'ft'
402404
${

src/pg/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ export type DbFungibleTokenFilters = {
228228
name?: string;
229229
symbol?: string;
230230
address?: string;
231+
valid_metadata_only?: boolean;
231232
};
232233

233234
export type DbFungibleTokenOrder = {
@@ -250,6 +251,7 @@ export type DbFungibleTokenMetadataItem = {
250251
tx_id: string;
251252
principal: string;
252253
image?: string;
254+
fungible_token_name?: string;
253255
cached_image?: string;
254256
cached_thumbnail_image?: string;
255257
};

tests/api/ft.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ describe('FT routes', () => {
378378
image_uri: 'http://img.com/meme.jpg',
379379
name: 'Meme token',
380380
sender_address: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1',
381+
asset_identifier: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1.meme-token::ft-token',
381382
symbol: 'MEME',
382383
token_uri: 'https://ipfs.io/abcd.json',
383384
total_supply: '200000',
@@ -391,6 +392,7 @@ describe('FT routes', () => {
391392
image_uri: 'https://cdn.citycoins.co/logos/miamicoin.png',
392393
name: 'miamicoin',
393394
sender_address: 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R',
395+
asset_identifier: 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2::ft-token',
394396
symbol: 'MIA',
395397
token_uri: 'https://cdn.citycoins.co/metadata/miamicoin.json',
396398
total_supply: '5586789829000000',
@@ -404,6 +406,7 @@ describe('FT routes', () => {
404406
image_uri: 'https://app.stackswap.org/icon/stsw.svg',
405407
name: 'STACKSWAP',
406408
sender_address: 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275',
409+
asset_identifier: 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stsw-token-v4a::ft-token',
407410
symbol: 'STSW',
408411
token_uri: 'https://app.stackswap.org/token/stsw.json',
409412
total_supply: '1000000000000000',
@@ -447,7 +450,7 @@ describe('FT routes', () => {
447450
symbol: 'rstSTX',
448451
decimals: 5,
449452
tx_id: '0xbdc41843d5e0cd4a70611f6badeb5c87b07b12309e77c4fbaf2334c7b4cee89b',
450-
principal: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1.meme-token',
453+
principal: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1.scam-token',
451454
total_supply: '200000',
452455
},
453456
true
@@ -462,6 +465,37 @@ describe('FT routes', () => {
462465
expect(json4.results[0].symbol).toBe('rstSTX');
463466
});
464467

468+
test('filters by valid metadata', async () => {
469+
await insertFtList();
470+
await insertFt(
471+
{
472+
name: 'Scam token',
473+
symbol: 'rstSTX',
474+
decimals: 5,
475+
tx_id: '0xbdc41843d5e0cd4a70611f6badeb5c87b07b12309e77c4fbaf2334c7b4cee89b',
476+
principal: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1.scam-token',
477+
total_supply: '200000',
478+
},
479+
true
480+
);
481+
482+
const response = await fastify.inject({
483+
method: 'GET',
484+
url: '/metadata/ft',
485+
});
486+
expect(response.statusCode).toBe(200);
487+
const json = response.json();
488+
expect(json.total).toBe(4);
489+
490+
const response2 = await fastify.inject({
491+
method: 'GET',
492+
url: '/metadata/ft?valid_metadata_only=true',
493+
});
494+
expect(response2.statusCode).toBe(200);
495+
const json2 = response2.json();
496+
expect(json2.total).toBe(3);
497+
});
498+
465499
test('filters by symbol', async () => {
466500
await insertFtList();
467501
const response = await fastify.inject({

0 commit comments

Comments
 (0)