diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fd31692..8be3d68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.1.1](https://github.com/hirosystems/token-metadata-api/compare/v0.1.0...v0.1.1) (2023-03-14) + + +### Bug Fixes + +* return FT metadata in a backwards compatible way ([#130](https://github.com/hirosystems/token-metadata-api/issues/130)) ([3ca57a0](https://github.com/hirosystems/token-metadata-api/commit/3ca57a06bb14423a374cd7ceac368c1a6fbf0f57)) + ## [0.1.0](https://github.com/hirosystems/token-metadata-service/compare/v0.0.1...v0.1.0) (2023-02-22) diff --git a/docs/feature-guides/token-meta-data-service.md b/docs/feature-guides/token-metadata-api.md similarity index 68% rename from docs/feature-guides/token-meta-data-service.md rename to docs/feature-guides/token-metadata-api.md index 95abe859..ebb97754 100644 --- a/docs/feature-guides/token-meta-data-service.md +++ b/docs/feature-guides/token-metadata-api.md @@ -1,8 +1,10 @@ --- -title: Token Metadata Service + +title: Token Metadata API + --- -# Stacks Token Metadata Service +# Token Metadata API A microservice that indexes metadata for all Fungible, Non-Fungible, and Semi-Fungible Tokens in the Stacks blockchain and exposes it via JSON REST API endpoints. @@ -12,12 +14,12 @@ This section gives you an overview of external and internal architectural diagra ### External architecture -The external architectural diagram shows how the Token metadata service is connected to three different systems, Stacks node, Stacks blockchain API database, and Postgres database. -![Architecture](../../architecture.png) +The external architectural diagram shows how the Token metadata API is connected to three different systems, Stacks node, Stacks blockchain API database, and Postgres database. +![Architecture](../../architecture.png) -1. Token metadata service interacts with Stacks Blockchain API database( referred to as Local Metadata DB in the diagram) to import all historical smart contracts when booting up and to listen for new contracts that may be deployed. Read-only access is recommended as this service will never need to write anything to this database(DB). +1. Token metadata API interacts with Stacks Blockchain API database( referred to as Local Metadata DB in the diagram) to import all historical smart contracts when booting up and to listen for new contracts that may be deployed. Read-only access is recommended as this service will never need to write anything to this database(DB). 2. A Stacks node to respond to all read-only contract calls required when fetching token metadata (calls to get token count, token metadata URIs, etc.). 3. A local Postgres DB to store all processed metadata info. @@ -25,21 +27,23 @@ The service needs to fetch external metadata files (JSONs, images) from the inte ### Internal architecture -The following is the internal architectural diagram of the Token Metadata Service. + +The following is the internal architectural diagram of the Token metadata API. ![Flowchart](../../flowchart.png) #### Blockchain importer -The [`BlockchainImporter`](https://github.com/hirosystems/token-metadata-api/blob/develop/src/token-processor/blockchain-api/blockchain-importer.ts) is a component in the Token metadata service that takes token contracts from the API database. This component is only used on service boot. -It connects to the Stacks Blockchain API database and scans the entire `smart_contracts` table looking for any contract that conforms to [SIP-009](https://github.com/stacksgov/sips/blob/main/sips/sip-009/sip-009-nft-standard.md), SIP-010 or SIP-013. When it finds a token contract, it creates a [`ProcessSmartContractJob`](https://github.com/hirosystems/token-metadata-api/blob/develop/src/token-processor/process-smart-contract-job.ts) and adds it to the [Job queue](#job-queue), ßso its tokens can be read and processed thereafter. +The [`BlockchainImporter`](https://github.com/hirosystems/token-metadata-api/tree/master/src/token-processor/blockchain-api/blockchain-importer.ts) is a component in the Token metadata API that takes token contracts from the API database. This component is only used on service boot. -This process runs only once. If the Token Metadata Service is ever restarted, though, this component re-scans the API `smart_contracts` table from the last processed block height. So, it can pick up any newer contracts it might have missed while the service was unavailable. +It connects to the Stacks Blockchain API database and scans the entire `smart_contracts` table looking for any contract that conforms to [SIP-009](https://github.com/stacksgov/sips/blob/main/sips/sip-009/sip-009-nft-standard.md), SIP-010 or SIP-013. When it finds a token contract, it creates a [`ProcessSmartContractJob`](https://github.com/hirosystems/token-metadata-api/tree/master/src/token-processor/queue/job/process-smart-contract-job.ts) and adds it to the [Job queue](#job-queue), ßso its tokens can be read and processed thereafter. + +This process runs only once. If the Token metadata API is ever restarted, though, this component re-scans the API `smart_contracts` table from the last processed block height. So, it can pick up any newer contracts it might have missed while the service was unavailable. #### Smart contract monitor -The [`BlockchainSmartContractMonitor`](https://github.com/hirosystems/token-metadata-api/blob/develop/src/token-processor/blockchain-api/blockchain-smart-contract-monitor.ts) component constantly listens to the following Stacks Blockchain API events: +The [`BlockchainSmartContractMonitor`](https://github.com/hirosystems/token-metadata-api/tree/master/src/token-processor/blockchain-api/blockchain-smart-contract-monitor.ts) component constantly listens to the following Stacks Blockchain API events: * **Smart contract log events** @@ -53,7 +57,8 @@ This process is kept alive throughout the entire service lifetime. #### Job queue -The role of the [`JobQueue`](https://github.com/hirosystems/token-metadata-api/blob/develop/src/token-processor/queue/job-queue.ts) is to perform all the smart contract and token processing in the service. + +The role of the [`JobQueue`](https://github.com/hirosystems/token-metadata-api/tree/master/src/token-processor/queue/job-queue.ts) is to perform all the smart contract and token processing in the service. It is a priority queue that organizes all necessary work for contract ingestion and token metadata processing. Every job this queue processes corresponds to one row in the `jobs` DB table, which marks its processing status and related objects to be worked on (smart contract or token). @@ -99,13 +104,13 @@ This job fetches the metadata JSON object for a single token and other relevant ## API reference -See the [Token Metadata Service API Reference](https://token-metadata-api-dlkidjgff-blockstack.vercel.app/) for more information. +See the [Token metadata API Reference](https://docs.hiro.so/metadata/) for more information. ## Quick start ### System requirements -The Token Metadata Service is a microservice with hard dependencies on other Stacks blockchain components. Before you start, you'll need to have access to the following: +The Token metadata API is a microservice with hard dependencies on other Stacks blockchain components. Before you start, you'll need to have access to the following: 1. A fully synchronized [Stacks node](https://github.com/stacks-network/stacks-blockchain) 1. A fully synchronized instance of the [Stacks Blockchain API](https://github.com/hirosystems/stacks-blockchain-api) running in `default` or `write-only` mode, with its Postgres database exposed for new connections. A read-only DB replica is also acceptable. @@ -117,18 +122,19 @@ This section helps you to initiate the service by following the steps below. 1. Clone the repository by using the following command: -`git clone https://github.com/hirosystems/token-metadata-api` +`git clone https://github.com/hirosystems/token-metadata-api.git` -2. Create a `.env` file and specify the appropriate values to configure access to the Stacks API database, the Token Metadata Service local database, and the Stacks node RPC interface. See [`env.ts`](https://github.com/hirosystems/token-metadata-api/blob/develop/src/env.ts) for all -available configuration options. +1. Create a `.env` file and specify the appropriate values to configure access to the Stacks API database, the Token metadata API local database, and the Stacks node RPC interface. See [`env.ts`](https://github.com/hirosystems/token-metadata-api/tree/master/src/env.ts) for all available configuration options. + +2. Build the app (NodeJS v18+ is required) -3. Build the app (NodeJS v18+ is required) ``` npm install npm run build ``` -4. Start the service +1. Start the service + ``` npm run start ``` @@ -139,12 +145,12 @@ When shutting down, you should always prefer to send the `SIGINT` signal instead ### Using image cache service -The Token Metadata Service allows you to specify the path to a custom script that can pre-process every image URL detected by the service before it's inserted into the DB. This will enable you to serve CDN image URLs in your metadata responses instead of raw URLs, providing key advantages such as: +The Token metadata API allows you to specify the path to a custom script that can pre-process every image URL detected by the service before it's inserted into the DB. This will enable you to serve CDN image URLs in your metadata responses instead of raw URLs, providing key advantages such as: * Improves image load speed * Increases reliability in case the original image becomes unavailable * Protects original image hosts from [DDoS attacks](https://wikipedia.org/wiki/Denial-of-service_attack) * Increases user privacy -An example IMGIX processor script is included in [`config/image-cache.js`](https://github.com/hirosystems/token-metadata-api/blob/develop/config/image-cache.js). -You can customize the script path by altering the `METADATA_IMAGE_CACHE_PROCESSOR` environment variable. \ No newline at end of file +An example IMGIX processor script is included in [`config/image-cache.js`](https://github.com/hirosystems/token-metadata-api/blob/master/config/image-cache.js). +You can customize the script path by altering the `METADATA_IMAGE_CACHE_PROCESSOR` environment variable. diff --git a/src/api/routes/ft.ts b/src/api/routes/ft.ts index 4cd493da..e82f24a2 100644 --- a/src/api/routes/ft.ts +++ b/src/api/routes/ft.ts @@ -44,6 +44,11 @@ export const FtRoutes: FastifyPluginCallback, Server, TypeB decimals: metadataBundle?.token?.decimals ?? undefined, total_supply: metadataBundle?.token?.total_supply?.toString() ?? undefined, token_uri: metadataBundle?.token?.uri ?? undefined, + description: metadataBundle?.metadataLocale?.metadata?.description ?? undefined, + tx_id: metadataBundle?.smartContract.tx_id, + sender_address: metadataBundle?.smartContract.principal.split('.')[0], + image_uri: metadataBundle?.metadataLocale?.metadata?.cached_image ?? undefined, + image_canonical_uri: metadataBundle?.metadataLocale?.metadata?.image ?? undefined, metadata: parseMetadataLocaleBundle(metadataBundle?.metadataLocale), }); } catch (error) { diff --git a/src/api/schemas.ts b/src/api/schemas.ts index 8b0044cf..6e642fdc 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -118,28 +118,28 @@ export const MetadataLocalization = Type.Object({ locales: Type.Array(Type.String(), { examples: [['en', 'jp']] }), }); +const TokenDescription = Type.String({ + examples: [ + 'Heavy hitters, all-stars and legends of the game join forces to create a collection of unique varsity jackets', + ], +}); + +const TokenImage = Type.String({ + format: 'uri', + examples: ['ipfs://ipfs/QmZMqhh2ztwuZ3Y8PyEp2z5auyH3TCm3nnr5ZfjjgDjd5q/12199.png'], +}); + +const TokenCachedImage = Type.String({ + format: 'uri', + examples: ['https://ipfs.io/ipfs/QmZMqhh2ztwuZ3Y8PyEp2z5auyH3TCm3nnr5ZfjjgDjd5q/12199.png'], +}); + export const Metadata = Type.Object({ sip: Type.Integer({ examples: [16] }), name: Type.Optional(Type.String({ examples: ["Satoshi's Team #12200"] })), - description: Type.Optional( - Type.String({ - examples: [ - 'Heavy hitters, all-stars and legends of the game join forces to create a collection of unique varsity jackets', - ], - }) - ), - image: Type.Optional( - Type.String({ - format: 'uri', - examples: ['ipfs://ipfs/QmZMqhh2ztwuZ3Y8PyEp2z5auyH3TCm3nnr5ZfjjgDjd5q/12199.png'], - }) - ), - cached_image: Type.Optional( - Type.String({ - format: 'uri', - examples: ['https://ipfs.io/ipfs/QmZMqhh2ztwuZ3Y8PyEp2z5auyH3TCm3nnr5ZfjjgDjd5q/12199.png'], - }) - ), + description: Type.Optional(TokenDescription), + image: Type.Optional(TokenImage), + cached_image: Type.Optional(TokenCachedImage), attributes: Type.Optional(Type.Array(MetadataAttribute)), properties: Type.Optional(MetadataProperties), localization: Type.Optional(MetadataLocalization), @@ -167,6 +167,13 @@ export const FtMetadataResponse = Type.Object({ decimals: Type.Optional(Type.Integer({ examples: [8] })), total_supply: Type.Optional(Type.String({ examples: ['9999980000000'] })), token_uri: Type.Optional(TokenUri), + description: Type.Optional(TokenDescription), + image_uri: Type.Optional(TokenCachedImage), + image_canonical_uri: Type.Optional(TokenImage), + tx_id: Type.String({ + examples: ['0xef2ac1126e16f46843228b1dk4830e19eb7599129e4jf392cab9e65ae83a45c0'], + }), + sender_address: Type.String({ examples: ['ST399W7Z9WS0GMSNQGJGME5JAENKN56D65VGMGKGA'] }), metadata: Type.Optional(Metadata), }); diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 469a0ae7..a8561077 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -151,7 +151,11 @@ export class PgStore extends BasePgStore { if (args.locale && !(await this.isTokenLocaleAvailable(tokenIdRes[0].id, args.locale))) { throw new TokenLocaleNotFoundError(); } - return await this.getTokenMetadataBundleInternal(tokenIdRes[0].id, args.locale); + return await this.getTokenMetadataBundleInternal( + tokenIdRes[0].id, + args.contractPrincipal, + args.locale + ); }); } @@ -442,6 +446,7 @@ export class PgStore extends BasePgStore { private async getTokenMetadataBundleInternal( tokenId: number, + smartContractPrincipal: string, locale?: string ): Promise { const tokenRes = await this.sql` @@ -477,8 +482,13 @@ export class PgStore extends BasePgStore { properties: properties, }; } + const smartContract = await this.getSmartContract({ principal: smartContractPrincipal }); + if (!smartContract) { + throw new TokenNotFoundError(); + } return { - token: token, + token, + smartContract, metadataLocale: localeBundle, }; } diff --git a/src/pg/types.ts b/src/pg/types.ts index e2003f40..e7e6901b 100644 --- a/src/pg/types.ts +++ b/src/pg/types.ts @@ -40,7 +40,6 @@ export type DbSmartContract = { id: number; principal: string; sip: DbSipNumber; - abi: string; tx_id: string; block_height: number; token_count?: bigint; @@ -188,6 +187,7 @@ export type DbMetadataLocaleBundle = { export type DbTokenMetadataLocaleBundle = { token: DbToken; + smartContract: DbSmartContract; metadataLocale?: DbMetadataLocaleBundle; }; @@ -195,7 +195,6 @@ export const SMART_CONTRACTS_COLUMNS = [ 'id', 'principal', 'sip', - 'abi', 'tx_id', 'block_height', 'token_count', diff --git a/tests/ft.test.ts b/tests/ft.test.ts index 4acb1965..d5907058 100644 --- a/tests/ft.test.ts +++ b/tests/ft.test.ts @@ -117,6 +117,8 @@ describe('FT routes', () => { symbol: 'HELLO', token_uri: 'http://test.com/uri.json', total_supply: '1', + sender_address: 'SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS', + tx_id: '0x123456', }); }); @@ -182,6 +184,11 @@ describe('FT routes', () => { token_uri: 'http://test.com/uri.json', total_supply: '1', decimals: 6, + sender_address: 'SP2SYHR84SDJJDK8M09HFS4KBFXPPCX9H7RZ9YVTS', + tx_id: '0x123456', + description: 'test', + image_canonical_uri: 'http://test.com/image.png', + image_uri: 'http://test.com/image.png?processed=true', metadata: { sip: 16, description: 'test',