From 36eab05c690ff1230ea21c269b63037fecd2585c Mon Sep 17 00:00:00 2001 From: Nhan Phan Date: Wed, 19 Jun 2024 13:13:35 -0700 Subject: [PATCH] add helpers, handle unknwon external plugins --- README.md | 16 +++++++++ package.json | 4 +-- pnpm-lock.yaml | 8 ++--- src/das.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++---- src/helpers.ts | 83 +++++++++++++++++++++++++++++++++----------- src/types.ts | 16 ++++++++- test/das.test.ts | 24 +++++++++++-- 7 files changed, 206 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 8015a6a..d66ff03 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,22 @@ const assetsByCollection = await das.getAssetsByCollection(umi, { const derivedAssets = assetsByCollection.map((asset) => deriveAssetPlugins(asset, collection)) ``` +## Using DAS-to-Core type conversions +If you are working with not only Core assets, it might be useful to directly access the conversion helpers along side the other DAS asset types when fetching using [@metaplex-foundation/digital-asset-standard-api](https://github.com/metaplex-foundation/digital-asset-standard-api). + + +```js +// ... standard setup for @metaplex-foundation/digital-asset-standard-api + +const dasAssets = await umi.rpc.getAssetsByOwner({ owner: publicKey('') }); + +// filter out only core assets +const dasCoreAssets = assets.items.filter((a) => a.interface === 'MplCoreAsset') + +// convert them to AssetV1 type (actually AssetResult type which will also have the content field populated from DAS) +const coreAssets = await das.dasAssetsToCoreAssets(umi, dasCoreAssets) + +``` ## Contributing diff --git a/package.json b/package.json index cdbcddb..7a00678 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,14 @@ "registry": "https://registry.npmjs.org" }, "peerDependencies": { - "@metaplex-foundation/digital-asset-standard-api": ">=1.0.4-alpha.0", + "@metaplex-foundation/digital-asset-standard-api": ">=1.0.4-alpha.2", "@metaplex-foundation/mpl-core": ">=1.0.1", "@metaplex-foundation/umi": ">=0.8.2 < 1" }, "devDependencies": { "@ava/typescript": "^4.1.0", "@metaplex-foundation/mpl-core": "1.0.1", - "@metaplex-foundation/digital-asset-standard-api": "^1.0.4-alpha.0", + "@metaplex-foundation/digital-asset-standard-api": "^1.0.4-alpha.2", "@metaplex-foundation/umi": "^0.9.1", "@metaplex-foundation/umi-bundle-tests": "^0.9.1", "@typescript-eslint/eslint-plugin": "^7.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9acff75..ffb2a0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,8 +5,8 @@ devDependencies: specifier: ^4.1.0 version: 4.1.0 '@metaplex-foundation/digital-asset-standard-api': - specifier: ^1.0.4-alpha.0 - version: 1.0.4-alpha.0(@metaplex-foundation/umi@0.9.1) + specifier: ^1.0.4-alpha.2 + version: 1.0.4-alpha.2(@metaplex-foundation/umi@0.9.1) '@metaplex-foundation/mpl-core': specifier: 1.0.1 version: 1.0.1(@metaplex-foundation/umi@0.9.1)(@noble/hashes@1.4.0) @@ -162,8 +162,8 @@ packages: - supports-color dev: true - /@metaplex-foundation/digital-asset-standard-api@1.0.4-alpha.0(@metaplex-foundation/umi@0.9.1): - resolution: {integrity: sha512-xVhvDhpF/5jBxJ+rlMJh70C129G64pQ6YyylFnm1iHgMmsjmun0PlX87Jp8DQLpac8fdveQ862SVotELkyHV1g==} + /@metaplex-foundation/digital-asset-standard-api@1.0.4-alpha.2(@metaplex-foundation/umi@0.9.1): + resolution: {integrity: sha512-IUkziTpulrZKAYDnMZp1PYXoPgynNHp3LMFOh5yeFdy6sE8XYvEcOf9S2oyo4wVtuXBnVqee/cIj/eGR0XNoow==} peerDependencies: '@metaplex-foundation/umi': '>= 0.8.2 < 1' dependencies: diff --git a/src/das.ts b/src/das.ts index c4c3458..f20aad1 100644 --- a/src/das.ts +++ b/src/das.ts @@ -1,15 +1,19 @@ import { PublicKey, Umi } from '@metaplex-foundation/umi'; import { - DasApiAssetInterface, + DasApiAsset, SearchAssetsRpcInput, } from '@metaplex-foundation/digital-asset-standard-api'; import { AssetV1, - CollectionV1, deriveAssetPluginsWithFetch, } from '@metaplex-foundation/mpl-core'; import { MPL_CORE_ASSET, MPL_CORE_COLLECTION } from './constants'; -import { AssetOptions, Pagination } from './types'; +import { + AssetOptions, + AssetResult, + CollectionResult, + Pagination, +} from './types'; import { dasAssetToCoreAssetOrCollection } from './helpers'; async function searchAssets( @@ -17,13 +21,13 @@ async function searchAssets( input: Omit & { interface?: typeof MPL_CORE_ASSET; } & AssetOptions -): Promise; +): Promise; async function searchAssets( context: Umi, input: Omit & { interface?: typeof MPL_CORE_COLLECTION; } & AssetOptions -): Promise; +): Promise; async function searchAssets( context: Umi, input: Omit & { @@ -32,7 +36,7 @@ async function searchAssets( ) { const dasAssets = await context.rpc.searchAssets({ ...input, - interface: (input.interface ?? MPL_CORE_ASSET) as DasApiAssetInterface, + interface: input.interface ?? MPL_CORE_ASSET, burnt: false, }); @@ -93,6 +97,40 @@ function getAssetsByCollection( }); } +/** + * Convenience function to fetch a single asset by pubkey + * @param context Umi + * @param asset pubkey of the asset + * @param options + * @returns + */ +async function getAsset( + context: Umi, + asset: PublicKey, + options: AssetOptions = {} +): Promise { + const dasAsset = await context.rpc.getAsset(asset); + + return ( + await dasAssetsToCoreAssets(context, [dasAsset], options) + )[0] as AssetResult; +} + +/** + * Convenience function to fetch a single collection by pubkey + * @param context + * @param collection + * @returns + */ +async function getCollection( + context: Umi, + collection: PublicKey +): Promise { + const dasCollection = await context.rpc.getAsset(collection); + + return dasAssetToCoreCollection(context, dasCollection); +} + function getCollectionsByUpdateAuthority( context: Umi, input: { @@ -106,6 +144,41 @@ function getCollectionsByUpdateAuthority( }); } +async function dasAssetsToCoreAssets( + context: Umi, + assets: DasApiAsset[], + options: AssetOptions +): Promise { + const coreAssets = assets.map((asset) => { + if (asset.interface !== MPL_CORE_ASSET) { + throw new Error( + `Invalid interface, expecting interface to be ${MPL_CORE_ASSET} but got ${asset.interface}` + ); + } + return dasAssetToCoreAssetOrCollection(asset); + }) as AssetResult[]; + + if (options.skipDerivePlugins) { + return coreAssets; + } + + return deriveAssetPluginsWithFetch(context, coreAssets) as Promise< + AssetResult[] + >; +} + +async function dasAssetToCoreCollection( + context: Umi, + asset: DasApiAsset & AssetOptions +): Promise { + if (asset.interface !== MPL_CORE_COLLECTION) { + throw new Error( + `Invalid interface, expecting interface to be ${MPL_CORE_COLLECTION} but got ${asset.interface}` + ); + } + return dasAssetToCoreAssetOrCollection(asset) as CollectionResult; +} + export const das = { searchAssets, searchCollections, @@ -113,4 +186,8 @@ export const das = { getAssetsByAuthority, getAssetsByCollection, getCollectionsByUpdateAuthority, + getAsset, + getCollection, + dasAssetsToCoreAssets, + dasAssetToCoreCollection, } as const; diff --git a/src/helpers.ts b/src/helpers.ts index 70c1702..4e7aae9 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -6,11 +6,9 @@ import { } from '@metaplex-foundation/digital-asset-standard-api'; import { AddBlocker, - AssetV1, Attributes, UpdateAuthority, BurnDelegate, - CollectionV1, Edition, FreezeDelegate, getPluginSerializer, @@ -35,6 +33,10 @@ import { CheckResult, Autograph, VerifiedCreators, + ExternalPluginAdapterType, + getExternalPluginAdapterSerializer, + BaseExternalPluginAdapter, + oracleFromBase, } from '@metaplex-foundation/mpl-core'; import { AccountHeader, @@ -46,6 +48,7 @@ import { some, } from '@metaplex-foundation/umi'; import { MPL_CORE_COLLECTION } from './constants'; +import { AssetResult, CollectionResult } from './types'; function convertSnakeCase(str: string, toCase: 'pascal' | 'camel' = 'camel') { return str @@ -379,18 +382,65 @@ function handleUnknownPlugins(unknownDasPlugins?: Record[]) { }, {}); } +function handleUnknownExternalPlugins( + unknownDasPlugins?: Record[] +) { + if (!unknownDasPlugins) return {}; + + return unknownDasPlugins.reduce( + (acc: ExternalPluginAdaptersList, unknownPlugin) => { + if (!ExternalPluginAdapterType[unknownPlugin.type]) return acc; + + const deserializedPlugin = + getExternalPluginAdapterSerializer().deserialize( + base64ToUInt8Array(unknownPlugin.data) + )[0]; + + const { + authority, + offset, + lifecycle_checks: lifecycleChecks, + } = unknownPlugin; + + const mappedPlugin: BaseExternalPluginAdapter = { + lifecycleChecks: lifecycleChecks + ? parseLifecycleChecks(lifecycleChecks) + : undefined, + authority, + offset: BigInt(offset), + }; + + if (deserializedPlugin.__kind === 'Oracle') { + if (!acc.oracles) { + acc.oracles = []; + } + + acc.oracles.push({ + type: 'Oracle', + ...mappedPlugin, + // Oracle conversion does not use the record or account data so we pass in dummies + ...oracleFromBase( + deserializedPlugin.fields[0], + {} as any, + new Uint8Array(0) + ), + }); + } + + return acc; + }, + {} + ); +} + export function dasAssetToCoreAssetOrCollection( dasAsset: DasApiAsset -): AssetV1 | CollectionV1 { - // TODO: Define types in Umi DAS client. +): AssetResult | CollectionResult { const { interface: assetInterface, id, ownership: { owner }, - content: { - metadata: { name }, - json_uri: uri, - }, + content, compression: { seq }, grouping, authorities, @@ -401,32 +451,27 @@ export function dasAssetToCoreAssetOrCollection( rent_epoch: rentEpoch, mpl_core_info: mplCoreInfo, external_plugins: externalPlugins, + unknown_external_plugins: unknownExternalPlugins, } = dasAsset as DasApiAsset & { - plugins: Record; - unknown_plugins?: Array>; executable?: boolean; lamports?: number; rent_epoch?: number; - mpl_core_info?: { - num_minted?: number; - current_size?: number; - plugins_json_version: number; - }; - external_plugins: Record[]; }; const { num_minted: numMinted = 0, current_size: currentSize = 0 } = mplCoreInfo ?? {}; const commonFields = { publicKey: id, - uri, - name, + uri: content.json_uri, + name: content.metadata.name, + content, ...getAccountHeader(executable, lamps, rentEpoch), - ...dasPluginsToCorePlugins(plugins), + ...(plugins ? dasPluginsToCorePlugins(plugins) : {}), ...(externalPlugins !== undefined ? dasExternalPluginsToCoreExternalPlugins(externalPlugins) : {}), ...handleUnknownPlugins(unknownPlugins), + ...handleUnknownExternalPlugins(unknownExternalPlugins), // pluginHeader: // TODO: Reconstruct }; diff --git a/src/types.ts b/src/types.ts index ba23b1c..b745348 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,8 @@ -import { SearchAssetsRpcInput } from '@metaplex-foundation/digital-asset-standard-api'; +import { + DasApiAssetContent, + SearchAssetsRpcInput, +} from '@metaplex-foundation/digital-asset-standard-api'; +import { AssetV1, CollectionV1 } from '@metaplex-foundation/mpl-core'; export type Pagination = Pick< SearchAssetsRpcInput, @@ -8,3 +12,13 @@ export type Pagination = Pick< export type AssetOptions = { skipDerivePlugins?: boolean; }; + +/** + * Extra fields that are not on AssetV1 or CollectionV1 but returned by DAS + */ +export type DasExtra = { + content: DasApiAssetContent; +}; + +export type AssetResult = AssetV1 & DasExtra; +export type CollectionResult = CollectionV1 & DasExtra; diff --git a/test/das.test.ts b/test/das.test.ts index 8c73d8c..c16ed46 100644 --- a/test/das.test.ts +++ b/test/das.test.ts @@ -203,8 +203,7 @@ test.serial( } ); -// TODO renenable after more das providers support this -test.skip('das: it can fetch asset with oracle', async (t) => { +test.serial('das: it can fetch asset with oracle', async (t) => { const umi = createUmiWithDas(DAS_API_ENDPOINT); const assets = await das.searchAssets(umi, { owner: publicKey('APrZTeVysBJqAznfLXS71NAzjr2fCVTSF1A66MeErzM7'), @@ -239,6 +238,27 @@ test.serial('das: it can fetch derived asset', async (t) => { t.like(asset, mplCoreAsset); }); +test.serial('das: it can getAsset', async (t) => { + const umi = createUmiWithDas(DAS_API_ENDPOINT); + const asset = await das.getAsset( + umi, + publicKey('9KvAqZVYJbXZzNvaV1HhxvybD6xfguztQwnqhkmzxWV3') + ); + prepareAssetForComparison(asset, false); + + const mplCoreAsset = await fetchAsset(umi, asset.publicKey); + prepareAssetForComparison(mplCoreAsset); + + t.like(asset, mplCoreAsset); + t.like(asset.content, { + $schema: 'https://schema.metaplex.com/nft1.0.json', + json_uri: 'https://example.com/asset', + files: [], + metadata: { name: 'new name 2', symbol: '' }, + links: {}, + }); +}); + // TODO test.skip('das: lifecycle hooks', async (t) => {}); test.skip('das: data store', async (t) => {});