From 05e01bc403416b37a14464c9811c42531731d6e0 Mon Sep 17 00:00:00 2001 From: deantchi <21262275+deantchi@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:05:25 -0700 Subject: [PATCH 1/4] ci: semantic-release use bot (#292) --- .github/workflows/ci.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 117b428..5fde86f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,25 +120,45 @@ jobs: if: always() build-publish: + permissions: + contents: write + issues: write + pull-requests: write runs-on: ubuntu-latest needs: - lint - test steps: + - name: Generate release bot app token + id: generate_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.HIROSYSTEMS_RELEASE_BOT_ID }} + private-key: ${{ secrets.HIROSYSTEMS_RELEASE_BOT_PEM }} + - uses: actions/checkout@v4 with: token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} fetch-depth: 0 persist-credentials: false + - name: Get bot user ID + id: bot-user-id + run: | + echo "user-id=$(gh api "/users/${{ steps.generate_token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + - name: Semantic Release uses: cycjimmy/semantic-release-action@v4 id: semantic # Only run on non-PR events or only PRs that aren't from forks if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} SEMANTIC_RELEASE_PACKAGE: ${{ github.event.repository.name }} + GIT_AUTHOR_EMAIL: "${{ steps.bot-user-id.outputs.user-id }}+${{ steps.generate_token.outputs.app-slug }}[bot]@users.noreply.github.com" + GIT_COMMITTER_EMAIL: "${{ steps.bot-user-id.outputs.user-id }}+${{ steps.generate_token.outputs.app-slug }}[bot]@users.noreply.github.com" with: semantic_version: 19 extra_plugins: | From 50c75e639c5527d6127692f21d3e72f1c56461c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Wed, 9 Apr 2025 12:08:43 -0600 Subject: [PATCH 2/4] fix: add db indexes to optimize endpoint queries (#296) * fix: add db indexes to optimize endpoint queries * fix: index name --- migrations/1744169665745_optimize-indexes.ts | 29 ++++++++++++++++++++ src/pg/pg-store.ts | 4 +-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 migrations/1744169665745_optimize-indexes.ts diff --git a/migrations/1744169665745_optimize-indexes.ts b/migrations/1744169665745_optimize-indexes.ts new file mode 100644 index 0000000..e930294 --- /dev/null +++ b/migrations/1744169665745_optimize-indexes.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export function up(pgm: MigrationBuilder): void { + pgm.createIndex('jobs', ['token_id']); + pgm.createIndex('jobs', ['smart_contract_id']); + pgm.createIndex('jobs', ['status'], { name: 'jobs_status_all_index' }); + pgm.createIndex('jobs', ['status', { name: 'updated_at', sort: 'ASC' }], { + where: "status = 'queued'", + }); + + pgm.createIndex('tokens', ['type', 'name'], { where: "type = 'ft'" }); + pgm.createIndex('tokens', ['type', 'symbol'], { where: "type = 'ft'" }); + pgm.createIndex('tokens', ['type']); + + pgm.createIndex( + 'update_notifications', + [ + 'update_mode', + 'token_id', + { name: 'block_height', sort: 'DESC' }, + { name: 'tx_index', sort: 'DESC' }, + { name: 'event_index', sort: 'DESC' }, + ], + { where: "update_mode = 'dynamic'" } + ); +} diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 96f0c8b..b950f99 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -374,10 +374,10 @@ export class PgStore extends BasePgStore { let orderBy: PgSqlQuery; switch (args.order?.order_by) { case FtOrderBy.symbol: - orderBy = sql`LOWER(t.symbol)`; + orderBy = sql`t.symbol`; break; default: - orderBy = sql`LOWER(t.name)`; + orderBy = sql`t.name`; break; } // `ORDER` statement From aae897ad862ed8489f7f36c8083240c1e1677214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Wed, 9 Apr 2025 16:58:19 -0600 Subject: [PATCH 3/4] 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 --- migrations/1744225682001_ft-lower-index.ts | 20 ++++++++++++ src/api/routes/ft.ts | 7 +++++ src/api/schemas.ts | 4 +++ src/pg/pg-store.ts | 8 +++-- src/pg/types.ts | 2 ++ tests/api/ft.test.ts | 36 +++++++++++++++++++++- 6 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 migrations/1744225682001_ft-lower-index.ts diff --git a/migrations/1744225682001_ft-lower-index.ts b/migrations/1744225682001_ft-lower-index.ts new file mode 100644 index 0000000..a0740b2 --- /dev/null +++ b/migrations/1744225682001_ft-lower-index.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export function up(pgm: MigrationBuilder): void { + pgm.dropIndex('tokens', ['type', 'name']); + pgm.createIndex('tokens', ['type', 'LOWER(name)'], { where: "type = 'ft'" }); + + pgm.dropIndex('tokens', ['type', 'symbol']); + pgm.createIndex('tokens', ['type', 'LOWER(symbol)'], { where: "type = 'ft'" }); +} + +export function down(pgm: MigrationBuilder): void { + pgm.dropIndex('tokens', ['type', 'LOWER(name)']); + pgm.createIndex('tokens', ['type', 'name'], { where: "type = 'ft'" }); + + pgm.dropIndex('tokens', ['type', 'LOWER(symbol)']); + pgm.createIndex('tokens', ['type', 'symbol'], { where: "type = 'ft'" }); +} diff --git a/src/api/routes/ft.ts b/src/api/routes/ft.ts index 2c54e08..d11bbfe 100644 --- a/src/api/routes/ft.ts +++ b/src/api/routes/ft.ts @@ -38,6 +38,11 @@ const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTy name: Type.Optional(Type.String()), symbol: Type.Optional(Type.String()), address: Type.Optional(StacksAddressParam), + valid_metadata_only: Type.Optional( + Type.Boolean({ + description: 'If enabled, only tokens with valid SIP-016 metadata will be returned', + }) + ), // Pagination offset: Type.Optional(OffsetParam), limit: Type.Optional(LimitParam), @@ -59,6 +64,7 @@ const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTy name: request.query.name, symbol: request.query.symbol, address: request.query.address, + valid_metadata_only: request.query.valid_metadata_only, }, order: { order_by: request.query.order_by ?? FtOrderBy.name, @@ -78,6 +84,7 @@ const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTy description: t.description, tx_id: t.tx_id, sender_address: t.principal?.split('.')[0], + asset_identifier: `${t.principal}::${t.fungible_token_name}`, image_uri: t.cached_image, image_canonical_uri: t.image, image_thumbnail_uri: t.cached_thumbnail_image, diff --git a/src/api/schemas.ts b/src/api/schemas.ts index ce5444a..6b005c7 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -301,6 +301,10 @@ export const FtBasicMetadataResponse = Type.Object( examples: ['0xef2ac1126e16f46843228b1dk4830e19eb7599129e4jf392cab9e65ae83a45c0'], }), sender_address: Type.String({ examples: ['ST399W7Z9WS0GMSNQGJGME5JAENKN56D65VGMGKGA'] }), + asset_identifier: Type.String({ + examples: ['SPZA22A4D15RKH5G8XDGQ7BPC20Q5JNMH0VQKSR6.token-ststx-earn-v1::stSTXearn'], + description: 'Clarity asset identifier', + }), contract_principal: Type.String({ examples: ['SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2'], }), diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index b950f99..427e96f 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -370,14 +370,15 @@ export class PgStore extends BasePgStore { order?: DbFungibleTokenOrder; }): Promise> { return await this.sqlTransaction(async sql => { + const validMetadataOnly = args.filters?.valid_metadata_only ?? false; // `ORDER BY` statement let orderBy: PgSqlQuery; switch (args.order?.order_by) { case FtOrderBy.symbol: - orderBy = sql`t.symbol`; + orderBy = sql`LOWER(t.symbol)`; break; default: - orderBy = sql`t.name`; + orderBy = sql`LOWER(t.name)`; break; } // `ORDER` statement @@ -392,11 +393,12 @@ export class PgStore extends BasePgStore { m.description, s.principal, s.tx_id, + s.fungible_token_name, m.image, m.cached_image, COUNT(*) OVER() as total FROM tokens AS t - LEFT JOIN metadata AS m ON t.id = m.token_id + ${validMetadataOnly ? sql`INNER` : sql`LEFT`} JOIN metadata AS m ON t.id = m.token_id INNER JOIN smart_contracts AS s ON t.smart_contract_id = s.id WHERE t.type = 'ft' ${ diff --git a/src/pg/types.ts b/src/pg/types.ts index b84f07d..06eb70f 100644 --- a/src/pg/types.ts +++ b/src/pg/types.ts @@ -228,6 +228,7 @@ export type DbFungibleTokenFilters = { name?: string; symbol?: string; address?: string; + valid_metadata_only?: boolean; }; export type DbFungibleTokenOrder = { @@ -250,6 +251,7 @@ export type DbFungibleTokenMetadataItem = { tx_id: string; principal: string; image?: string; + fungible_token_name?: string; cached_image?: string; cached_thumbnail_image?: string; }; diff --git a/tests/api/ft.test.ts b/tests/api/ft.test.ts index 3f9ffc2..2cb1c7a 100644 --- a/tests/api/ft.test.ts +++ b/tests/api/ft.test.ts @@ -378,6 +378,7 @@ describe('FT routes', () => { image_uri: 'http://img.com/meme.jpg', name: 'Meme token', sender_address: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1', + asset_identifier: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1.meme-token::ft-token', symbol: 'MEME', token_uri: 'https://ipfs.io/abcd.json', total_supply: '200000', @@ -391,6 +392,7 @@ describe('FT routes', () => { image_uri: 'https://cdn.citycoins.co/logos/miamicoin.png', name: 'miamicoin', sender_address: 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R', + asset_identifier: 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2::ft-token', symbol: 'MIA', token_uri: 'https://cdn.citycoins.co/metadata/miamicoin.json', total_supply: '5586789829000000', @@ -404,6 +406,7 @@ describe('FT routes', () => { image_uri: 'https://app.stackswap.org/icon/stsw.svg', name: 'STACKSWAP', sender_address: 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275', + asset_identifier: 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stsw-token-v4a::ft-token', symbol: 'STSW', token_uri: 'https://app.stackswap.org/token/stsw.json', total_supply: '1000000000000000', @@ -447,7 +450,7 @@ describe('FT routes', () => { symbol: 'rstSTX', decimals: 5, tx_id: '0xbdc41843d5e0cd4a70611f6badeb5c87b07b12309e77c4fbaf2334c7b4cee89b', - principal: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1.meme-token', + principal: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1.scam-token', total_supply: '200000', }, true @@ -462,6 +465,37 @@ describe('FT routes', () => { expect(json4.results[0].symbol).toBe('rstSTX'); }); + test('filters by valid metadata', async () => { + await insertFtList(); + await insertFt( + { + name: 'Scam token', + symbol: 'rstSTX', + decimals: 5, + tx_id: '0xbdc41843d5e0cd4a70611f6badeb5c87b07b12309e77c4fbaf2334c7b4cee89b', + principal: 'SP22PCWZ9EJMHV4PHVS0C8H3B3E4Q079ZHY6CXDS1.scam-token', + total_supply: '200000', + }, + true + ); + + const response = await fastify.inject({ + method: 'GET', + url: '/metadata/ft', + }); + expect(response.statusCode).toBe(200); + const json = response.json(); + expect(json.total).toBe(4); + + const response2 = await fastify.inject({ + method: 'GET', + url: '/metadata/ft?valid_metadata_only=true', + }); + expect(response2.statusCode).toBe(200); + const json2 = response2.json(); + expect(json2.total).toBe(3); + }); + test('filters by symbol', async () => { await insertFtList(); const response = await fastify.inject({ From f77a91fef0cd4e69922ce4d0ae89a0d4b1d65ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Fri, 18 Apr 2025 08:41:57 -0600 Subject: [PATCH 4/4] ci: update vercel ci (#294) Update vercel.yml --- .github/workflows/vercel.yml | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml index bde001d..5bce175 100644 --- a/.github/workflows/vercel.yml +++ b/.github/workflows/vercel.yml @@ -15,13 +15,6 @@ jobs: vercel: runs-on: ubuntu-latest - environment: - name: ${{ github.ref_name == 'master' && 'Production' || 'Preview' }} - url: ${{ github.ref_name == 'master' && 'https://token-metadata-api.vercel.app/' || 'https://token-metadata-api-pbcblockstack-blockstack.vercel.app/' }} - - env: - PROD: ${{ github.ref_name == 'master' }} - steps: - uses: actions/checkout@v2 with: @@ -32,20 +25,6 @@ jobs: with: node-version-file: '.nvmrc' - - name: Cache node modules - uses: actions/cache@v2 - env: - cache-name: cache-node-modules - with: - path: | - ~/.npm - **/node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - name: Install deps run: npm ci --audit=false @@ -53,14 +32,14 @@ jobs: run: npm install --global vercel@latest - name: Pull Vercel environment information - run: vercel pull --yes --environment=${{ env.PROD && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }} + run: vercel pull --yes --environment=${{ github.ref_name == 'master' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }} - name: Build project artifacts - run: vercel build ${{ env.PROD && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }} + run: vercel build ${{ github.ref_name == 'master' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy project artifacts to Vercel id: deploy - run: vercel ${{ env.PROD && '--prod' || 'deploy' }} --prebuilt --token=${{ secrets.VERCEL_TOKEN }} | awk '{print "deployment_url="$1}' >> $GITHUB_OUTPUT + run: vercel ${{ github.ref_name == 'master' && '--prod' || 'deploy' }} --prebuilt --token=${{ secrets.VERCEL_TOKEN }} | awk '{print "deployment_url="$1}' >> $GITHUB_OUTPUT - name: Trigger docs.hiro.so deployment if: github.ref_name == 'master'