Skip to content

Commit

Permalink
feat(core): adds support for cip-0025 version 2
Browse files Browse the repository at this point in the history
  • Loading branch information
iccicci committed Feb 9, 2023
1 parent ca9a110 commit be21a75
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 26 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
60 changes: 48 additions & 12 deletions packages/core/src/Asset/util/metadatumToCip25.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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')) {
Expand All @@ -32,21 +31,21 @@ 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<string, Metadatum>());
}, new Map<string, Cardano.Metadatum>());
};

const toArray = <T>(value: T | T[]): T[] => (Array.isArray(value) ? value : [value]);

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();

Expand Down Expand Up @@ -85,28 +84,65 @@ const mapFile = (metadatum: Metadatum, assetId: Cardano.AssetId, logger: Logger)
};

/**
* Also considers asset name encoded in utf8 within metadata valid
* Gets the `Map<AssetName, NFTMetadata>` relative to the given policyId from the given
* `Map<PolicyId, Map<AssetName, NFTMetadata>>`.
*
* 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<AssetInfo, 'name'>) =>
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<AssetName, NFTMetadata>`.
*
* 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
/**
* @returns {NftMetadata | null} CIP-0025 NFT metadata
*/
export const metadatumToCip25 = (
asset: Pick<AssetInfo, 'policyId' | 'name'>,
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'));
Expand Down
37 changes: 23 additions & 14 deletions packages/core/test/Asset/util/metadatumToCip25.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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);
Expand All @@ -204,9 +213,9 @@ describe('NftMetadata/metadatumToCip25', () => {
721n,
new Map([
[
asset.policyId.toString(),
policyIdString,
new Map<Metadatum, Metadatum>([
[asset.name.toString(), minimalMetadata],
[assetNameString, minimalMetadata],
['version', '2.0']
])
]
Expand All @@ -225,10 +234,10 @@ describe('NftMetadata/metadatumToCip25', () => {
721n,
new Map([
[
asset.policyId.toString(),
policyIdString,
new Map<Metadatum, Metadatum>([
[
asset.name.toString(),
assetNameString,
new Map([
...minimalMetadata.entries(),
['description', 'description'],
Expand Down Expand Up @@ -266,10 +275,10 @@ describe('NftMetadata/metadatumToCip25', () => {
721n,
new Map([
[
asset.policyId.toString(),
policyIdString,
new Map<Metadatum, Metadatum>([
[
asset.name.toString(),
assetNameString,
new Map<Metadatum, Metadatum>([...minimalMetadata.entries(), ['files', [file1, file2]]])
]
])
Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit be21a75

Please sign in to comment.