diff --git a/libs/api/core/feature/src/lib/api-core-feature.module.ts b/libs/api/core/feature/src/lib/api-core-feature.module.ts index ef01eb5..83a1571 100644 --- a/libs/api/core/feature/src/lib/api-core-feature.module.ts +++ b/libs/api/core/feature/src/lib/api-core-feature.module.ts @@ -9,6 +9,7 @@ import { ApiMintFeatureModule } from '@tokengator-mint/api-mint-feature' import { ApiCommunityFeatureModule } from '@tokengator-mint/api-community-feature' import { ApiCommunityMemberFeatureModule } from '@tokengator-mint/api-community-member-feature' import { ApiSolanaFeatureModule } from '@tokengator-mint/api-solana-feature' +import { ApiMetadataFeatureModule } from '@tokengator-mint/api-metadata-feature' const imports = [ // The api-feature generator will add the imports here @@ -20,6 +21,7 @@ const imports = [ ApiCommunityFeatureModule, ApiCommunityMemberFeatureModule, ApiSolanaFeatureModule, + ApiMetadataFeatureModule, ] @Module({ diff --git a/libs/api/metadata/data-access/.eslintrc.json b/libs/api/metadata/data-access/.eslintrc.json new file mode 100644 index 0000000..632e9b0 --- /dev/null +++ b/libs/api/metadata/data-access/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/api/metadata/data-access/README.md b/libs/api/metadata/data-access/README.md new file mode 100644 index 0000000..9f4c9b8 --- /dev/null +++ b/libs/api/metadata/data-access/README.md @@ -0,0 +1,7 @@ +# api-metadata-data-access + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test api-metadata-data-access` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/api/metadata/data-access/jest.config.ts b/libs/api/metadata/data-access/jest.config.ts new file mode 100644 index 0000000..fec5c98 --- /dev/null +++ b/libs/api/metadata/data-access/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'api-metadata-data-access', + preset: '../../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../../coverage/libs/api/metadata/data-access', +} diff --git a/libs/api/metadata/data-access/project.json b/libs/api/metadata/data-access/project.json new file mode 100644 index 0000000..210fe6d --- /dev/null +++ b/libs/api/metadata/data-access/project.json @@ -0,0 +1,20 @@ +{ + "name": "api-metadata-data-access", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/api/metadata/data-access/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/api/metadata/data-access/jest.config.ts" + } + } + }, + "tags": ["app:api", "type:data-access"] +} diff --git a/libs/api/metadata/data-access/src/index.ts b/libs/api/metadata/data-access/src/index.ts new file mode 100644 index 0000000..4a7abd3 --- /dev/null +++ b/libs/api/metadata/data-access/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/api-metadata.data-access.module' +export * from './lib/api-metadata.service' diff --git a/libs/api/metadata/data-access/src/lib/api-metadata.data-access.module.ts b/libs/api/metadata/data-access/src/lib/api-metadata.data-access.module.ts new file mode 100644 index 0000000..5c94153 --- /dev/null +++ b/libs/api/metadata/data-access/src/lib/api-metadata.data-access.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { ApiCoreDataAccessModule } from '@tokengator-mint/api-core-data-access' +import { ApiSolanaDataAccessModule } from '@tokengator-mint/api-solana-data-access' +import { ApiMetadataService } from './api-metadata.service' + +@Module({ + imports: [ApiCoreDataAccessModule, ApiSolanaDataAccessModule], + providers: [ApiMetadataService], + exports: [ApiMetadataService], +}) +export class ApiMetadataDataAccessModule {} diff --git a/libs/api/metadata/data-access/src/lib/api-metadata.service.ts b/libs/api/metadata/data-access/src/lib/api-metadata.service.ts new file mode 100644 index 0000000..3c46863 --- /dev/null +++ b/libs/api/metadata/data-access/src/lib/api-metadata.service.ts @@ -0,0 +1,154 @@ +import { Injectable, Logger } from '@nestjs/common' +import { PublicKey } from '@solana/web3.js' +import { ApiCoreService } from '@tokengator-mint/api-core-data-access' +import { ApiSolanaService, SolanaAccountInfo } from '@tokengator-mint/api-solana-data-access' +import { LRUCache } from 'lru-cache' + +export interface CantFindTheRightTypeScrewItHackathonMode { + extension: 'tokenMetadata' + state: { + additionalMetadata: [string, string][] + mint: PublicKey + name: string + symbol: string + updateAuthority?: PublicKey + uri: string + } +} + +export interface ExternalMetadata { + image: string + [key: string]: string +} + +export interface LocalMetadata { + name: string + symbol: string + description: string + external_url: string + image: string +} + +const ONE_HOUR = 1000 * 60 * 60 +const TEN_MINUTES = 1000 * 60 * 10 + +@Injectable() +export class ApiMetadataService { + private readonly logger = new Logger(ApiMetadataService.name) + + private readonly externalMetadataCache = new LRUCache({ + max: 1000, + ttl: ONE_HOUR, + fetchMethod: async (url) => { + const fetched = await fetch(url).then((r) => r.json()) + if (fetched.image) { + this.logger.verbose(`Caching external metadata for ${url}`) + return fetched + } + throw new Error(`No image found in external metadata for ${url}`) + }, + }) + + private readonly accountCache = new LRUCache({ + max: 1000, + ttl: ONE_HOUR, + fetchMethod: async (account: string) => { + const found = await this.solana.getAccount(account) + if (found) { + this.logger.verbose(`Caching account info for ${account}`) + return found + } + throw new Error(`Failed to fetch account info for ${account}`) + }, + }) + + private readonly accountMetadataCache = new LRUCache({ + max: 1000, + ttl: TEN_MINUTES, + fetchMethod: async (account: string) => { + const found = await this.accountCache.fetch(account) + const extensions = found?.data?.parsed?.info?.extensions as CantFindTheRightTypeScrewItHackathonMode[] + const metadata = extensions.find((e) => e.extension === 'tokenMetadata') + + if (!metadata) { + throw new Error(`Failed to fetch metadata for ${account}`) + } + return metadata + }, + }) + + private readonly jsonCache = new LRUCache({ + max: 1000, + ttl: ONE_HOUR, + fetchMethod: async (account: string) => { + const image = `${this.core.config.apiUrl}/metadata/image/${account}` + const metadata = await this.accountMetadataCache.fetch(account) + + const { name, symbol, description, external_url } = defaults() + + if (metadata) { + if (!metadata.state.uri.startsWith(this.core.config.apiUrl)) { + // We have external metadata + const externalMetadata = await this.externalMetadataCache.fetch(metadata.state.uri) + + if (!externalMetadata) { + throw new Error(`Failed to fetch external metadata for ${metadata.state.uri}`) + } + if (externalMetadata.image) { + return { + name: metadata.state.name ?? name, + symbol: metadata.state.symbol ?? symbol, + description: metadata.state.uri ?? description, + external_url: metadata.state.uri ?? external_url, + image: externalMetadata.image, + } + } + } + + return { + name: metadata.state.name ?? name, + symbol: metadata.state.symbol ?? symbol, + description: metadata.state.uri ?? description, + external_url: metadata.state.uri ?? external_url, + image, + } + } + + return { + name, + symbol, + description, + external_url, + image, + } + }, + }) + + constructor(private readonly core: ApiCoreService, private readonly solana: ApiSolanaService) {} + + async getImage(account: string): Promise { + const accountMetadata = await this.accountMetadataCache.fetch(account) + if (!accountMetadata) { + throw new Error(`Failed to fetch metadata for ${account}`) + } + const externalMetadata = await this.externalMetadataCache.fetch(accountMetadata.state.uri) + if (!externalMetadata) { + throw new Error(`Failed to fetch external metadata for ${accountMetadata.state.uri}`) + } + + return externalMetadata.image + } + + async getJson(account: string) { + return this.jsonCache.fetch(account) + } +} + +function defaults() { + const name = `Unknown Name` + const symbol = `Unknown Symbol` + const description = `Unknown Description` + const external_url = `https://example.com` + + return { name, symbol, description, external_url } +} diff --git a/libs/api/metadata/data-access/tsconfig.json b/libs/api/metadata/data-access/tsconfig.json new file mode 100644 index 0000000..4022fd4 --- /dev/null +++ b/libs/api/metadata/data-access/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/api/metadata/data-access/tsconfig.lib.json b/libs/api/metadata/data-access/tsconfig.lib.json new file mode 100644 index 0000000..c6b908a --- /dev/null +++ b/libs/api/metadata/data-access/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es6", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/api/metadata/data-access/tsconfig.spec.json b/libs/api/metadata/data-access/tsconfig.spec.json new file mode 100644 index 0000000..56497b8 --- /dev/null +++ b/libs/api/metadata/data-access/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/libs/api/metadata/feature/.eslintrc.json b/libs/api/metadata/feature/.eslintrc.json new file mode 100644 index 0000000..632e9b0 --- /dev/null +++ b/libs/api/metadata/feature/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/api/metadata/feature/README.md b/libs/api/metadata/feature/README.md new file mode 100644 index 0000000..b58d48e --- /dev/null +++ b/libs/api/metadata/feature/README.md @@ -0,0 +1,7 @@ +# api-metadata-feature + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test api-metadata-feature` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/api/metadata/feature/jest.config.ts b/libs/api/metadata/feature/jest.config.ts new file mode 100644 index 0000000..3a413d1 --- /dev/null +++ b/libs/api/metadata/feature/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'api-metadata-feature', + preset: '../../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../../coverage/libs/api/metadata/feature', +} diff --git a/libs/api/metadata/feature/project.json b/libs/api/metadata/feature/project.json new file mode 100644 index 0000000..19bd1b2 --- /dev/null +++ b/libs/api/metadata/feature/project.json @@ -0,0 +1,20 @@ +{ + "name": "api-metadata-feature", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/api/metadata/feature/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/api/metadata/feature/jest.config.ts" + } + } + }, + "tags": ["app:api", "type:feature"] +} diff --git a/libs/api/metadata/feature/src/index.ts b/libs/api/metadata/feature/src/index.ts new file mode 100644 index 0000000..23c672a --- /dev/null +++ b/libs/api/metadata/feature/src/index.ts @@ -0,0 +1 @@ +export * from './lib/api-metadata.feature.module' diff --git a/libs/api/metadata/feature/src/lib/api-metadata.controller.ts b/libs/api/metadata/feature/src/lib/api-metadata.controller.ts new file mode 100644 index 0000000..0b3819f --- /dev/null +++ b/libs/api/metadata/feature/src/lib/api-metadata.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Param, Res } from '@nestjs/common' +import { ApiMetadataService } from '@tokengator-mint/api-metadata-data-access' +import { Response } from 'express-serve-static-core' + +@Controller('metadata') +export class ApiMetadataController { + constructor(private readonly service: ApiMetadataService) {} + + @Get('image/:account') + async image(@Param('account') account: string, @Res() res: Response) { + const imageUrl = await this.service.getImage(account) + + return res.redirect(imageUrl) + } + + @Get('json/:account') + async json(@Param('account') account: string) { + return this.service.getJson(account) + } +} diff --git a/libs/api/metadata/feature/src/lib/api-metadata.feature.module.ts b/libs/api/metadata/feature/src/lib/api-metadata.feature.module.ts new file mode 100644 index 0000000..58f247f --- /dev/null +++ b/libs/api/metadata/feature/src/lib/api-metadata.feature.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { ApiMetadataDataAccessModule } from '@tokengator-mint/api-metadata-data-access' +import { ApiMetadataController } from './api-metadata.controller' + +@Module({ + controllers: [ApiMetadataController], + imports: [ApiMetadataDataAccessModule], +}) +export class ApiMetadataFeatureModule {} diff --git a/libs/api/metadata/feature/tsconfig.json b/libs/api/metadata/feature/tsconfig.json new file mode 100644 index 0000000..4022fd4 --- /dev/null +++ b/libs/api/metadata/feature/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/api/metadata/feature/tsconfig.lib.json b/libs/api/metadata/feature/tsconfig.lib.json new file mode 100644 index 0000000..c6b908a --- /dev/null +++ b/libs/api/metadata/feature/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es6", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/api/metadata/feature/tsconfig.spec.json b/libs/api/metadata/feature/tsconfig.spec.json new file mode 100644 index 0000000..56497b8 --- /dev/null +++ b/libs/api/metadata/feature/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/libs/api/metadata/util/.eslintrc.json b/libs/api/metadata/util/.eslintrc.json new file mode 100644 index 0000000..632e9b0 --- /dev/null +++ b/libs/api/metadata/util/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/api/metadata/util/README.md b/libs/api/metadata/util/README.md new file mode 100644 index 0000000..a63b48e --- /dev/null +++ b/libs/api/metadata/util/README.md @@ -0,0 +1,7 @@ +# api-metadata-util + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test api-metadata-util` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/api/metadata/util/jest.config.ts b/libs/api/metadata/util/jest.config.ts new file mode 100644 index 0000000..9e88f38 --- /dev/null +++ b/libs/api/metadata/util/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'api-metadata-util', + preset: '../../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../../coverage/libs/api/metadata/util', +} diff --git a/libs/api/metadata/util/project.json b/libs/api/metadata/util/project.json new file mode 100644 index 0000000..b7ecc11 --- /dev/null +++ b/libs/api/metadata/util/project.json @@ -0,0 +1,20 @@ +{ + "name": "api-metadata-util", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/api/metadata/util/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/api/metadata/util/jest.config.ts" + } + } + }, + "tags": ["app:api", "type:util"] +} diff --git a/libs/api/metadata/util/src/index.ts b/libs/api/metadata/util/src/index.ts new file mode 100644 index 0000000..12e6598 --- /dev/null +++ b/libs/api/metadata/util/src/index.ts @@ -0,0 +1 @@ +export * from './lib/api-metadata-util.module' diff --git a/libs/api/metadata/util/src/lib/api-metadata-util.module.ts b/libs/api/metadata/util/src/lib/api-metadata-util.module.ts new file mode 100644 index 0000000..4404ba4 --- /dev/null +++ b/libs/api/metadata/util/src/lib/api-metadata-util.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common' + +@Module({ + controllers: [], + providers: [], + exports: [], +}) +export class ApiMetadataUtilModule {} diff --git a/libs/api/metadata/util/tsconfig.json b/libs/api/metadata/util/tsconfig.json new file mode 100644 index 0000000..4022fd4 --- /dev/null +++ b/libs/api/metadata/util/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/api/metadata/util/tsconfig.lib.json b/libs/api/metadata/util/tsconfig.lib.json new file mode 100644 index 0000000..c6b908a --- /dev/null +++ b/libs/api/metadata/util/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es6", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/api/metadata/util/tsconfig.spec.json b/libs/api/metadata/util/tsconfig.spec.json new file mode 100644 index 0000000..56497b8 --- /dev/null +++ b/libs/api/metadata/util/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/libs/api/solana/data-access/src/lib/api-solana.service.ts b/libs/api/solana/data-access/src/lib/api-solana.service.ts index 5366a4b..f2719b1 100644 --- a/libs/api/solana/data-access/src/lib/api-solana.service.ts +++ b/libs/api/solana/data-access/src/lib/api-solana.service.ts @@ -6,6 +6,8 @@ import { TokenMetadata } from '@solana/spl-token-metadata' import { AccountInfo, Connection, LAMPORTS_PER_SOL, ParsedAccountData, PublicKey } from '@solana/web3.js' import { ApiCoreService, CORE_APP_STARTED } from '@tokengator-mint/api-core-data-access' +export type SolanaAccountInfo = AccountInfo + @Injectable() export class ApiSolanaService { private readonly logger = new Logger(ApiSolanaService.name) @@ -32,10 +34,10 @@ export class ApiSolanaService { return balance } - async getAccount(publicKey: PublicKey | string): Promise | null> { + async getAccount(publicKey: PublicKey | string): Promise { return this.connection .getParsedAccountInfo(new PublicKey(publicKey)) - .then((res) => (res.value ? (res.value as AccountInfo) : null)) + .then((res) => (res.value ? (res.value as SolanaAccountInfo) : null)) } async getMint(publicKey: PublicKey | string, programId: PublicKey = TOKEN_2022_PROGRAM_ID) { return getMint(this.connection, new PublicKey(publicKey), 'confirmed', programId) diff --git a/package.json b/package.json index b1dce7a..ac297d9 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@pubkeyapp/solana-verify-wallet": "^1.3.3", "@pubkeyapp/wallet-adapter-mantine-ui": "^2.3.0", "@solana-developers/helpers": "^2.0.0", - "@solana/spl-token": "^0.3.11", + "@solana/spl-token": "^0.4.3", "@solana/spl-token-metadata": "^0.1.2", "@solana/wallet-adapter-base": "^0.9.23", "@solana/wallet-adapter-react": "^0.15.35", @@ -83,6 +83,7 @@ "jotai": "^2.6.1", "linkify-react": "^4.1.3", "linkifyjs": "^4.1.3", + "lru-cache": "^10.2.0", "mantine-datatable": "^7.4.1", "passport": "^0.7.0", "passport-discord": "^0.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 412bdb8..3d7fbb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,8 +90,8 @@ dependencies: specifier: ^2.0.0 version: 2.0.0 '@solana/spl-token': - specifier: ^0.3.11 - version: 0.3.11(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22) + specifier: ^0.4.3 + version: 0.4.3(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22) '@solana/spl-token-metadata': specifier: ^0.1.2 version: 0.1.2(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22) @@ -167,6 +167,9 @@ dependencies: linkifyjs: specifier: ^4.1.3 version: 4.1.3 + lru-cache: + specifier: ^10.2.0 + version: 10.2.0 mantine-datatable: specifier: ^7.4.1 version: 7.4.1(@mantine/core@7.4.1)(@mantine/hooks@7.4.1)(clsx@2.1.0)(react@18.2.0) @@ -6439,6 +6442,12 @@ packages: resolution: {integrity: sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA==} dev: false + /@solana/codecs-core@2.0.0-preview.2: + resolution: {integrity: sha512-gLhCJXieSCrAU7acUJjbXl+IbGnqovvxQLlimztPoGgfLQ1wFYu+XJswrEVQqknZYK1pgxpxH3rZ+OKFs0ndQg==} + dependencies: + '@solana/errors': 2.0.0-preview.2 + dev: false + /@solana/codecs-data-structures@2.0.0-experimental.8618508: resolution: {integrity: sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw==} dependencies: @@ -6446,12 +6455,27 @@ packages: '@solana/codecs-numbers': 2.0.0-experimental.8618508 dev: false + /@solana/codecs-data-structures@2.0.0-preview.2: + resolution: {integrity: sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg==} + dependencies: + '@solana/codecs-core': 2.0.0-preview.2 + '@solana/codecs-numbers': 2.0.0-preview.2 + '@solana/errors': 2.0.0-preview.2 + dev: false + /@solana/codecs-numbers@2.0.0-experimental.8618508: resolution: {integrity: sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q==} dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 dev: false + /@solana/codecs-numbers@2.0.0-preview.2: + resolution: {integrity: sha512-aLZnDTf43z4qOnpTcDsUVy1Ci9im1Md8thWipSWbE+WM9ojZAx528oAql+Cv8M8N+6ALKwgVRhPZkto6E59ARw==} + dependencies: + '@solana/codecs-core': 2.0.0-preview.2 + '@solana/errors': 2.0.0-preview.2 + dev: false + /@solana/codecs-strings@2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA==} peerDependencies: @@ -6462,6 +6486,37 @@ packages: fastestsmallesttextencoderdecoder: 1.0.22 dev: false + /@solana/codecs-strings@2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-EgBwY+lIaHHgMJIqVOGHfIfpdmmUDNoNO/GAUGeFPf+q0dF+DtwhJPEMShhzh64X2MeCZcmSO6Kinx0Bvmmz2g==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + dependencies: + '@solana/codecs-core': 2.0.0-preview.2 + '@solana/codecs-numbers': 2.0.0-preview.2 + '@solana/errors': 2.0.0-preview.2 + fastestsmallesttextencoderdecoder: 1.0.22 + dev: false + + /@solana/codecs@2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-4HHzCD5+pOSmSB71X6w9ptweV48Zj1Vqhe732+pcAQ2cMNnN0gMPMdDq7j3YwaZDZ7yrILVV/3+HTnfT77t2yA==} + dependencies: + '@solana/codecs-core': 2.0.0-preview.2 + '@solana/codecs-data-structures': 2.0.0-preview.2 + '@solana/codecs-numbers': 2.0.0-preview.2 + '@solana/codecs-strings': 2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/options': 2.0.0-preview.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + dev: false + + /@solana/errors@2.0.0-preview.2: + resolution: {integrity: sha512-H2DZ1l3iYF5Rp5pPbJpmmtCauWeQXRJapkDg8epQ8BJ7cA2Ut/QEtC3CMmw/iMTcuS6uemFNLcWvlOfoQhvQuA==} + hasBin: true + dependencies: + chalk: 5.3.0 + commander: 12.0.0 + dev: false + /@solana/options@2.0.0-experimental.8618508: resolution: {integrity: sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==} dependencies: @@ -6469,6 +6524,26 @@ packages: '@solana/codecs-numbers': 2.0.0-experimental.8618508 dev: false + /@solana/options@2.0.0-preview.2: + resolution: {integrity: sha512-FAHqEeH0cVsUOTzjl5OfUBw2cyT8d5Oekx4xcn5hn+NyPAfQJgM3CEThzgRD6Q/4mM5pVUnND3oK/Mt1RzSE/w==} + dependencies: + '@solana/codecs-core': 2.0.0-preview.2 + '@solana/codecs-numbers': 2.0.0-preview.2 + dev: false + + /@solana/spl-token-group@0.0.2(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-vLePrFvT9+PfK2KZaddPebTWtRykXUR+060gqomFUcBk/2UPpZtsJGW+xshI9z9Ryrx7FieprZEUCApw34BwrQ==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.91.1 + dependencies: + '@solana/codecs': 2.0.0-preview.2(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/spl-type-length-value': 0.1.0 + '@solana/web3.js': 1.88.0 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + dev: false + /@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw==} engines: {node: '>=16'} @@ -6486,14 +6561,15 @@ packages: - fastestsmallesttextencoderdecoder dev: false - /@solana/spl-token@0.3.11(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22): - resolution: {integrity: sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==} + /@solana/spl-token@0.4.3(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-mRjJJE9CIBejsg9WAmDp369pWeObm42K2fwsZ4dkJAMCt1KBPb5Eb1vzM5+AYfV/BUTy3QP2oFx8kV+8Doa1xQ==} engines: {node: '>=16'} peerDependencies: - '@solana/web3.js': ^1.88.0 + '@solana/web3.js': ^1.91.1 dependencies: '@solana/buffer-layout': 4.0.1 '@solana/buffer-layout-utils': 0.2.0 + '@solana/spl-token-group': 0.0.2(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22) '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.88.0)(fastestsmallesttextencoderdecoder@1.0.22) '@solana/web3.js': 1.88.0 buffer: 6.0.3 @@ -9031,7 +9107,6 @@ packages: /chalk@5.3.0: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true /change-case-all@1.0.14: resolution: {integrity: sha512-CWVm2uT7dmSHdO/z1CXT/n47mWonyypzBbuCy5tN7uMg22BsfkhwT6oHmFCAk+gL1LOOxhdbB9SZz3J1KTY3gA==} @@ -9315,6 +9390,11 @@ packages: engines: {node: '>=16'} dev: true + /commander@12.0.0: + resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==} + engines: {node: '>=18'} + dev: false + /commander@2.13.0: resolution: {integrity: sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==} dev: false @@ -11939,7 +12019,7 @@ packages: resolution: {integrity: sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==} engines: {node: ^16.14.0 || >=18.0.0} dependencies: - lru-cache: 10.1.0 + lru-cache: 10.2.0 /hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} @@ -13892,8 +13972,8 @@ packages: engines: {node: '>=8'} dev: true - /lru-cache@10.1.0: - resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14} /lru-cache@4.1.5: diff --git a/tsconfig.base.json b/tsconfig.base.json index 285ea8f..b4a3445 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -26,6 +26,9 @@ "@tokengator-mint/api-core-feature": ["libs/api/core/feature/src/index.ts"], "@tokengator-mint/api-identity-data-access": ["libs/api/identity/data-access/src/index.ts"], "@tokengator-mint/api-identity-feature": ["libs/api/identity/feature/src/index.ts"], + "@tokengator-mint/api-metadata-data-access": ["libs/api/metadata/data-access/src/index.ts"], + "@tokengator-mint/api-metadata-feature": ["libs/api/metadata/feature/src/index.ts"], + "@tokengator-mint/api-metadata-util": ["libs/api/metadata/util/src/index.ts"], "@tokengator-mint/api-mint-data-access": ["libs/api/mint/data-access/src/index.ts"], "@tokengator-mint/api-mint-feature": ["libs/api/mint/feature/src/index.ts"], "@tokengator-mint/api-solana-data-access": ["libs/api/solana/data-access/src/index.ts"],