From be21a75898e15bea252489f50e82286434f73929 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 7 Feb 2023 17:57:05 +0100 Subject: [PATCH] feat(core): adds support for cip-0025 version 2 --- packages/core/package.json | 1 + .../core/src/Asset/util/metadatumToCip25.ts | 60 +++++++++++++++---- .../test/Asset/util/metadatumToCip25.test.ts | 37 +++++++----- yarn.lock | 1 + 4 files changed, 73 insertions(+), 26 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 08680cdcf17..f4c81ce8376 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -52,6 +52,7 @@ "prepack": "yarn build" }, "devDependencies": { + "@cardano-sdk/util-dev": "^0.6.0", "@types/lodash": "^4.14.182", "eslint": "^7.32.0", "jest": "^28.1.3", diff --git a/packages/core/src/Asset/util/metadatumToCip25.ts b/packages/core/src/Asset/util/metadatumToCip25.ts index 2db7ce47901..58462486fd5 100644 --- a/packages/core/src/Asset/util/metadatumToCip25.ts +++ b/packages/core/src/Asset/util/metadatumToCip25.ts @@ -2,7 +2,6 @@ import { AssetInfo, ImageMediaType, MediaType, NftMetadata, NftMetadataFile, Uri import { Cardano } from '../..'; import { CustomError } from 'ts-custom-error'; import { Logger } from 'ts-log'; -import { Metadatum, MetadatumMap } from '../../Cardano/types/AuxiliaryData'; import { asMetadatumArray, asMetadatumMap } from '../../util/metadatum'; import { assetIdFromPolicyAndName } from './assetId'; import { isNotNil } from '@cardano-sdk/util'; @@ -18,7 +17,7 @@ const asString = (obj: unknown): string | undefined => { } }; -const asStringArray = (metadatum: Metadatum | undefined): string[] | undefined => { +const asStringArray = (metadatum: Cardano.Metadatum | undefined): string[] | undefined => { if (Array.isArray(metadatum)) { const result = metadatum.map(asString); if (result.some((str) => typeof str === 'undefined')) { @@ -32,13 +31,13 @@ const asStringArray = (metadatum: Metadatum | undefined): string[] | undefined = } }; -const mapOtherProperties = (metadata: MetadatumMap, primaryProperties: string[]) => { +const mapOtherProperties = (metadata: Cardano.MetadatumMap, primaryProperties: string[]) => { const extraProperties = difference([...metadata.keys()].filter(isString), primaryProperties); if (extraProperties.length === 0) return; return extraProperties.reduce((result, key) => { result.set(key, metadata.get(key)!); return result; - }, new Map()); + }, new Map()); }; const toArray = (value: T | T[]): T[] => (Array.isArray(value) ? value : [value]); @@ -46,7 +45,7 @@ const toArray = (value: T | T[]): T[] => (Array.isArray(value) ? value : [val const missingFileFieldLogMessage = (fieldType: string, assetId: Cardano.AssetId) => `Omitting cip25 metadata file: missing "${fieldType}". AssetId: ${assetId}`; -const mapFile = (metadatum: Metadatum, assetId: Cardano.AssetId, logger: Logger): NftMetadataFile | null => { +const mapFile = (metadatum: Cardano.Metadatum, assetId: Cardano.AssetId, logger: Logger): NftMetadataFile | null => { const file = asMetadatumMap(metadatum); if (!file) throw new InvalidFileError(); @@ -85,10 +84,48 @@ const mapFile = (metadatum: Metadatum, assetId: Cardano.AssetId, logger: Logger) }; /** - * Also considers asset name encoded in utf8 within metadata valid + * Gets the `Map` relative to the given policyId from the given + * `Map>`. + * + * The policyId in the `policy` Map can be encoded as per CIP-0025 v1 or v2 specifications. + * + * @param policy The `MetadatumMap` containing the NFT metadata for all the NFT assets + * @returns The `MetadatumMap` containing the NFT metadata for all the NFT assets with the given policyId */ -const getAssetMetadata = (policy: MetadatumMap, asset: Pick) => - asMetadatumMap(policy.get(asset.name.toString()) || policy.get(Buffer.from(asset.name, 'hex').toString('utf8'))); +const getPolicyMetadata = (policy: Cardano.MetadatumMap, policyId: Cardano.PolicyId) => { + const policyIdString = policyId.toString(); + + return asMetadatumMap( + policy.get(policyIdString) || + (() => { + for (const [key, value] of policy.entries()) { + if (ArrayBuffer.isView(key) && Buffer.from(key).toString('hex') === policyIdString) return value; + } + })() + ); +}; + +/** + * Gets the `NFTMetadata` relative to the given assetName from the given `Map`. + * + * The assetName in the `policy` Map can be encoded as per CIP-0025 v1 (hex or utf8) or v2 specifications. + * + * @param policy The `MetadatumMap` containing the NFT metadata for all the NFT assets with a specific policyId. + * @returns The NFT metadata for the requested asset + */ +const getAssetMetadata = (policy: Cardano.MetadatumMap, assetName: Cardano.AssetName) => { + const assetNameString = assetName.toString(); + + return asMetadatumMap( + policy.get(assetNameString) || + policy.get(Buffer.from(assetNameString, 'hex').toString('utf8')) || + (() => { + for (const [key, value] of policy.entries()) { + if (ArrayBuffer.isView(key) && Buffer.from(key).toString('hex') === assetNameString) return value; + } + })() + ); +}; // TODO: consider hoisting this function together with cip25 types to core or a new cip25 package /** @@ -96,17 +133,16 @@ const getAssetMetadata = (policy: MetadatumMap, asset: Pick) */ export const metadatumToCip25 = ( asset: Pick, - metadatumMap: MetadatumMap | undefined, + metadatumMap: Cardano.MetadatumMap | undefined, logger: Logger ): NftMetadata | null => { const cip25Metadata = metadatumMap?.get(721n); if (!cip25Metadata) return null; const cip25MetadatumMap = asMetadatumMap(cip25Metadata); if (!cip25MetadatumMap) return null; - const policy = asMetadatumMap(cip25MetadatumMap.get(asset.policyId.toString())!); + const policy = getPolicyMetadata(cip25MetadatumMap, asset.policyId); if (!policy) return null; - const assetMetadata = getAssetMetadata(policy, asset); - + const assetMetadata = getAssetMetadata(policy, asset.name); if (!assetMetadata) return null; const name = asString(assetMetadata.get('name')); const image = asStringArray(assetMetadata.get('image')); diff --git a/packages/core/test/Asset/util/metadatumToCip25.test.ts b/packages/core/test/Asset/util/metadatumToCip25.test.ts index f0c09a12797..1a6e52b032b 100644 --- a/packages/core/test/Asset/util/metadatumToCip25.test.ts +++ b/packages/core/test/Asset/util/metadatumToCip25.test.ts @@ -2,13 +2,17 @@ import { AssetInfo } from '../../../src/Asset'; import { AssetName, Metadatum, PolicyId, TxMetadata } from '../../../src/Cardano'; import { Cardano } from '../../../src'; import { fromSerializableObject } from '@cardano-sdk/util'; -import { dummyLogger as logger } from 'ts-log'; +import { logger } from '@cardano-sdk/util-dev'; import { metadatumToCip25 } from '../../../src/Asset/util'; describe('NftMetadata/metadatumToCip25', () => { + const assetNameStringUtf8 = 'CIP0025-v2'; + const assetNameString = Buffer.from(assetNameStringUtf8).toString('hex'); + const policyIdString = 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7'; + const asset = { - name: AssetName('abc123'), - policyId: PolicyId('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7') + name: AssetName(assetNameString), + policyId: PolicyId(policyIdString) } as AssetInfo; const minimalMetadata = new Map([ @@ -174,25 +178,30 @@ describe('NftMetadata/metadatumToCip25', () => { it('returns null for cip25 metadatum with no metadata for given assetId', () => { const metadatum: TxMetadata = new Map([ - [721n, new Map([[asset.policyId.toString(), new Map([['other_asset_id', minimalMetadata]])]])] + [721n, new Map([[policyIdString, new Map([['other_asset_id', minimalMetadata]])]])] ]); expect(metadatumToCip25(asset, metadatum, logger)).toBeNull(); }); it('converts minimal metadata', () => { const metadatum: TxMetadata = new Map([ - [721n, new Map([[asset.policyId.toString(), new Map([[asset.name.toString(), minimalMetadata]])]])] + [721n, new Map([[policyIdString, new Map([[assetNameString, minimalMetadata]])]])] ]); expect(metadatumToCip25(asset, metadatum, logger)).toEqual(minimalConvertedMetadata); }); it('supports asset name as utf8 string', () => { + const metadatum: TxMetadata = new Map([ + [721n, new Map([[policyIdString, new Map([[assetNameStringUtf8, minimalMetadata]])]])] + ]); + expect(metadatumToCip25(asset, metadatum, logger)).toEqual(minimalConvertedMetadata); + }); + + it('supports CIP-0025 v2', () => { const metadatum: TxMetadata = new Map([ [ 721n, - new Map([ - [asset.policyId.toString(), new Map([[Buffer.from(asset.name.toString()).toString('utf8'), minimalMetadata]])] - ]) + new Map([[Buffer.from(policyIdString, 'hex'), new Map([[Buffer.from(assetNameStringUtf8), minimalMetadata]])]]) ] ]); expect(metadatumToCip25(asset, metadatum, logger)).toEqual(minimalConvertedMetadata); @@ -204,9 +213,9 @@ describe('NftMetadata/metadatumToCip25', () => { 721n, new Map([ [ - asset.policyId.toString(), + policyIdString, new Map([ - [asset.name.toString(), minimalMetadata], + [assetNameString, minimalMetadata], ['version', '2.0'] ]) ] @@ -225,10 +234,10 @@ describe('NftMetadata/metadatumToCip25', () => { 721n, new Map([ [ - asset.policyId.toString(), + policyIdString, new Map([ [ - asset.name.toString(), + assetNameString, new Map([ ...minimalMetadata.entries(), ['description', 'description'], @@ -266,10 +275,10 @@ describe('NftMetadata/metadatumToCip25', () => { 721n, new Map([ [ - asset.policyId.toString(), + policyIdString, new Map([ [ - asset.name.toString(), + assetNameString, new Map([...minimalMetadata.entries(), ['files', [file1, file2]]]) ] ]) diff --git a/yarn.lock b/yarn.lock index 410c3278809..72502e526f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2423,6 +2423,7 @@ __metadata: "@cardano-ogmios/schema": 5.5.7 "@cardano-sdk/crypto": ^0.1.0 "@cardano-sdk/util": ^0.7.0 + "@cardano-sdk/util-dev": ^0.6.0 "@dcspark/cardano-multiplatform-lib-nodejs": ^3.1.1 "@emurgo/cip14-js": ^3.0.1 "@types/lodash": ^4.14.182