Skip to content

Commit 3d73f3f

Browse files
authored
Merge pull request #301 from hirosystems/develop
release to master
2 parents 52f24f3 + f77a91f commit 3d73f3f

File tree

8 files changed

+103
-26
lines changed

8 files changed

+103
-26
lines changed

.github/workflows/vercel.yml

+3-24
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@ jobs:
1515
vercel:
1616
runs-on: ubuntu-latest
1717

18-
environment:
19-
name: ${{ github.ref_name == 'master' && 'Production' || 'Preview' }}
20-
url: ${{ github.ref_name == 'master' && 'https://token-metadata-api.vercel.app/' || 'https://token-metadata-api-pbcblockstack-blockstack.vercel.app/' }}
21-
22-
env:
23-
PROD: ${{ github.ref_name == 'master' }}
24-
2518
steps:
2619
- uses: actions/checkout@v2
2720
with:
@@ -32,35 +25,21 @@ jobs:
3225
with:
3326
node-version-file: '.nvmrc'
3427

35-
- name: Cache node modules
36-
uses: actions/cache@v2
37-
env:
38-
cache-name: cache-node-modules
39-
with:
40-
path: |
41-
~/.npm
42-
**/node_modules
43-
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
44-
restore-keys: |
45-
${{ runner.os }}-build-${{ env.cache-name }}-
46-
${{ runner.os }}-build-
47-
${{ runner.os }}-
48-
4928
- name: Install deps
5029
run: npm ci --audit=false
5130

5231
- name: Install Vercel CLI
5332
run: npm install --global vercel@latest
5433

5534
- name: Pull Vercel environment information
56-
run: vercel pull --yes --environment=${{ env.PROD && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }}
35+
run: vercel pull --yes --environment=${{ github.ref_name == 'master' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }}
5736

5837
- name: Build project artifacts
59-
run: vercel build ${{ env.PROD && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }}
38+
run: vercel build ${{ github.ref_name == 'master' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }}
6039

6140
- name: Deploy project artifacts to Vercel
6241
id: deploy
63-
run: vercel ${{ env.PROD && '--prod' || 'deploy' }} --prebuilt --token=${{ secrets.VERCEL_TOKEN }} | awk '{print "deployment_url="$1}' >> $GITHUB_OUTPUT
42+
run: vercel ${{ github.ref_name == 'master' && '--prod' || 'deploy' }} --prebuilt --token=${{ secrets.VERCEL_TOKEN }} | awk '{print "deployment_url="$1}' >> $GITHUB_OUTPUT
6443

6544
- name: Trigger docs.hiro.so deployment
6645
if: github.ref_name == 'master'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.createIndex('jobs', ['token_id']);
8+
pgm.createIndex('jobs', ['smart_contract_id']);
9+
pgm.createIndex('jobs', ['status'], { name: 'jobs_status_all_index' });
10+
pgm.createIndex('jobs', ['status', { name: 'updated_at', sort: 'ASC' }], {
11+
where: "status = 'queued'",
12+
});
13+
14+
pgm.createIndex('tokens', ['type', 'name'], { where: "type = 'ft'" });
15+
pgm.createIndex('tokens', ['type', 'symbol'], { where: "type = 'ft'" });
16+
pgm.createIndex('tokens', ['type']);
17+
18+
pgm.createIndex(
19+
'update_notifications',
20+
[
21+
'update_mode',
22+
'token_id',
23+
{ name: 'block_height', sort: 'DESC' },
24+
{ name: 'tx_index', sort: 'DESC' },
25+
{ name: 'event_index', sort: 'DESC' },
26+
],
27+
{ where: "update_mode = 'dynamic'" }
28+
);
29+
}
+20
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

+7
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

+4
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

+3-1
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ 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) {
@@ -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

+2
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

+35-1
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)