From 23f4faa55684f8f614c582bf274a35827d794ff7 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Wed, 1 Sep 2021 16:29:57 -0600 Subject: [PATCH] feat: make token metadata processing opt-in --- .env | 4 +++ src/api/routes/tokens/tokens.ts | 28 +++++++++++++++++++++ src/datastore/postgres-store.ts | 4 +-- src/event-stream/tokens-contract-handler.ts | 20 +++++++++++++-- src/index.ts | 24 +++++++++++------- src/tests-tokens/tokens-metadata-tests.ts | 19 ++++++++++++++ 6 files changed, 86 insertions(+), 13 deletions(-) diff --git a/.env b/.env index 568c0fa1bc..bb823d93d1 100644 --- a/.env +++ b/.env @@ -52,6 +52,10 @@ MAINNET_SEND_MANY_CONTRACT_ID=SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.send-man # Override the default file path for the proxy cache control file # STACKS_API_PROXY_CACHE_CONTROL_FILE=/path/to/.proxy-cache-control.json +# Enable token metadata processing. Disabled by default. +# STACKS_API_ENABLE_FT_METADATA=1 +# STACKS_API_ENABLE_NFT_METADATA=1 + # Configure a script to handle image URLs during token metadata processing. # This example script uses the `imgix.net` service to create CDN URLs. # Must be an executable script that accepts the URL as the first program argument diff --git a/src/api/routes/tokens/tokens.ts b/src/api/routes/tokens/tokens.ts index 076e4bd4c2..cb564c1e76 100644 --- a/src/api/routes/tokens/tokens.ts +++ b/src/api/routes/tokens/tokens.ts @@ -8,6 +8,10 @@ import { NonFungibleTokensMetadataList, } from '@stacks/stacks-blockchain-api-types'; import { parseLimitQuery, parsePagingQueryInput } from './../../pagination'; +import { + isFtMetadataEnabled, + isNftMetadataEnabled, +} from '../../../event-stream/tokens-contract-handler'; const MAX_TOKENS_PER_REQUEST = 200; const parseTokenQueryLimit = parseLimitQuery({ @@ -20,6 +24,12 @@ export function createTokenRouter(db: DataStore): RouterWithAsync { router.use(express.json()); router.getAsync('/ft/metadata', async (req, res) => { + if (!isFtMetadataEnabled()) { + return res.status(500).json({ + error: 'FT metadata processing is not enabled on this server', + }); + } + const limit = parseTokenQueryLimit(req.query.limit ?? 96); const offset = parsePagingQueryInput(req.query.offset ?? 0); @@ -36,6 +46,12 @@ export function createTokenRouter(db: DataStore): RouterWithAsync { }); router.getAsync('/nft/metadata', async (req, res) => { + if (!isNftMetadataEnabled()) { + return res.status(500).json({ + error: 'NFT metadata processing is not enabled on this server', + }); + } + const limit = parseTokenQueryLimit(req.query.limit ?? 96); const offset = parsePagingQueryInput(req.query.offset ?? 0); @@ -53,6 +69,12 @@ export function createTokenRouter(db: DataStore): RouterWithAsync { //router for fungible tokens router.getAsync('/:contractId/ft/metadata', async (req, res) => { + if (!isFtMetadataEnabled()) { + return res.status(500).json({ + error: 'FT metadata processing is not enabled on this server', + }); + } + const { contractId } = req.params; const metadata = await db.getFtMetadata(contractId); @@ -89,6 +111,12 @@ export function createTokenRouter(db: DataStore): RouterWithAsync { //router for non-fungible tokens router.getAsync('/:contractId/nft/metadata', async (req, res) => { + if (!isNftMetadataEnabled()) { + return res.status(500).json({ + error: 'NFT metadata processing is not enabled on this server', + }); + } + const { contractId } = req.params; const metadata = await db.getNftMetadata(contractId); diff --git a/src/datastore/postgres-store.ts b/src/datastore/postgres-store.ts index afa80b716d..13a9ca1835 100644 --- a/src/datastore/postgres-store.ts +++ b/src/datastore/postgres-store.ts @@ -82,7 +82,7 @@ import { AddressUnlockSchedule, } from '@stacks/stacks-blockchain-api-types'; import { getTxTypeId } from '../api/controllers/db-controller'; -import { isCompliantToken } from '../event-stream/tokens-contract-handler'; +import { isProcessableTokenMetadata } from '../event-stream/tokens-contract-handler'; import { ClarityAbi } from '@stacks/transactions'; const MIGRATIONS_TABLE = 'pgmigrations'; @@ -1069,7 +1069,7 @@ export class PgDataStore }; return queueEntry; }) - .filter(entry => isCompliantToken(entry.contractAbi)); + .filter(entry => isProcessableTokenMetadata(entry.contractAbi)); for (const pendingQueueEntry of tokenContractDeployments) { const queueEntry = await this.updateTokenMetadataQueue(client, pendingQueueEntry); tokenMetadataQueueEntries.push(queueEntry); diff --git a/src/event-stream/tokens-contract-handler.ts b/src/event-stream/tokens-contract-handler.ts index 228e04e8c9..b34d3b3d68 100644 --- a/src/event-stream/tokens-contract-handler.ts +++ b/src/event-stream/tokens-contract-handler.ts @@ -47,6 +47,16 @@ const METADATA_MAX_PAYLOAD_BYTE_SIZE = 1_000_000; // 1 megabyte const PUBLIC_IPFS = 'https://ipfs.io'; +export function isFtMetadataEnabled() { + const opt = process.env['STACKS_API_ENABLE_FT_METADATA']?.toLowerCase().trim(); + return opt === '1' || opt === 'true'; +} + +export function isNftMetadataEnabled() { + const opt = process.env['STACKS_API_ENABLE_NFT_METADATA']?.toLowerCase().trim(); + return opt === '1' || opt === 'true'; +} + const FT_FUNCTIONS: ClarityAbiFunction[] = [ { access: 'public', @@ -195,8 +205,14 @@ export interface TokenHandlerArgs { dbQueueId: number; } -export function isCompliantToken(abi: ClarityAbi): boolean { - return isCompliantFt(abi) || isCompliantNft(abi); +/** + * Checks if the given ABI contains functions from FT or NFT metadata standards (e.g. sip-09, sip-10) which can be resolved. + * The function also checks if the server has FT and/or NFT metadata processing enabled. + */ +export function isProcessableTokenMetadata(abi: ClarityAbi): boolean { + return ( + (isFtMetadataEnabled() && isCompliantFt(abi)) || (isNftMetadataEnabled() && isCompliantNft(abi)) + ); } function isCompliantNft(abi: ClarityAbi): boolean { diff --git a/src/index.ts b/src/index.ts index 99db783dfd..3daca145e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,11 @@ import { cycleMigrations, dangerousDropAllTables, PgDataStore } from './datastor import { MemoryDataStore } from './datastore/memory-store'; import { startApiServer } from './api/init'; import { startEventServer } from './event-stream/event-server'; -import { TokensProcessorQueue } from './event-stream/tokens-contract-handler'; +import { + isFtMetadataEnabled, + isNftMetadataEnabled, + TokensProcessorQueue, +} from './event-stream/tokens-contract-handler'; import { StacksCoreRpcClient } from './core-rpc/client'; import { createServer as createPrometheusServer } from '@promster/server'; import { ChainID } from '@stacks/transactions'; @@ -138,14 +142,16 @@ async function init(): Promise { logger.error(`Error monitoring RPC connection: ${error}`, error); }); - const tokenMetadataProcessor = new TokensProcessorQueue(db, configuredChainID); - registerShutdownConfig({ - name: 'Token Metadata Processor', - handler: () => tokenMetadataProcessor.close(), - forceKillable: true, - }); - // check if db has any non-processed token queues and await them all here - await tokenMetadataProcessor.drainDbQueue(); + if (isFtMetadataEnabled() || isNftMetadataEnabled()) { + const tokenMetadataProcessor = new TokensProcessorQueue(db, configuredChainID); + registerShutdownConfig({ + name: 'Token Metadata Processor', + handler: () => tokenMetadataProcessor.close(), + forceKillable: true, + }); + // check if db has any non-processed token queues and await them all here + await tokenMetadataProcessor.drainDbQueue(); + } } const apiServer = await startApiServer({ datastore: db, chainId: getConfiguredChainID() }); diff --git a/src/tests-tokens/tokens-metadata-tests.ts b/src/tests-tokens/tokens-metadata-tests.ts index 51c9e5fdb7..c72c223fec 100644 --- a/src/tests-tokens/tokens-metadata-tests.ts +++ b/src/tests-tokens/tokens-metadata-tests.ts @@ -116,9 +116,28 @@ describe('api tests', () => { }); beforeEach(() => { + process.env['STACKS_API_ENABLE_FT_METADATA'] = '1'; + process.env['STACKS_API_ENABLE_NFT_METADATA'] = '1'; nock.cleanAll(); }); + test('metadata disabled', async () => { + process.env['STACKS_API_ENABLE_FT_METADATA'] = '0'; + process.env['STACKS_API_ENABLE_NFT_METADATA'] = '0'; + const query1 = await supertest(api.server).get(`/extended/v1/tokens/nft/metadata`); + expect(query1.status).toBe(500); + expect(query1.body.error).toMatch(/not enabled/); + const query2 = await supertest(api.server).get(`/extended/v1/tokens/ft/metadata`); + expect(query2.status).toBe(500); + expect(query2.body.error).toMatch(/not enabled/); + const query3 = await supertest(api.server).get(`/extended/v1/tokens/example/nft/metadata`); + expect(query3.status).toBe(500); + expect(query3.body.error).toMatch(/not enabled/); + const query4 = await supertest(api.server).get(`/extended/v1/tokens/example/ft/metadata`); + expect(query4.status).toBe(500); + expect(query4.body.error).toMatch(/not enabled/); + }); + test('token nft-metadata data URL plain percent-encoded', async () => { const contract1 = await deployContract( 'beeple-a',