Skip to content

Commit

Permalink
refactor!: change MetadatumMap type to allow any metadatum as key
Browse files Browse the repository at this point in the history
  • Loading branch information
mkazlauskas committed Feb 17, 2022
1 parent af620d4 commit 48c33e5
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 198 deletions.
8 changes: 4 additions & 4 deletions packages/blockfrost/src/blockfrostWalletProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js';
import { BlockfrostToCore, BlockfrostTransactionContent, BlockfrostUtxo } from './BlockfrostToCore';
import { Options, PaginationOptions } from '@blockfrost/blockfrost-js/lib/types';
import { dummyLogger } from 'ts-log';
import { fetchSequentially, formatBlockfrostError, replaceNumbersWithBigints, toProviderError } from './util';
import { fetchSequentially, formatBlockfrostError, jsonToMetadatum, toProviderError } from './util';
import { flatten, groupBy } from 'lodash-es';

const fetchByAddressSequentially = async <Item, Response>(props: {
Expand Down Expand Up @@ -252,17 +252,17 @@ export const blockfrostWalletProvider = (options: Options, logger = dummyLogger)
];
};

const fetchJsonMetadata = async (txHash: Cardano.TransactionId): Promise<Cardano.MetadatumMap | null> => {
const fetchJsonMetadata = async (txHash: Cardano.TransactionId): Promise<Cardano.TxMetadata | null> => {
try {
const response = await blockfrost.txsMetadata(txHash.toString());
return response.reduce((map, metadatum) => {
// Not sure if types are correct, missing 'label', but it's present in docs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { json_metadata, label } = metadatum as any;
if (!json_metadata || !label) return map;
map[label] = replaceNumbersWithBigints(json_metadata) as Cardano.MetadatumMap;
map.set(BigInt(label), jsonToMetadatum(json_metadata));
return map;
}, {} as Cardano.MetadatumMap);
}, new Map<bigint, Cardano.Metadatum>());
} catch (error) {
if (formatBlockfrostError(error).status_code === 404) {
return null;
Expand Down
39 changes: 27 additions & 12 deletions packages/blockfrost/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Error as BlockfrostError } from '@blockfrost/blockfrost-js';
import { InvalidStringError, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { Cardano, InvalidStringError, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { PaginationOptions } from '@blockfrost/blockfrost-js/lib/types';

export const formatBlockfrostError = (error: unknown) => {
Expand Down Expand Up @@ -75,18 +75,33 @@ export const fetchSequentially = async <Item, Arg, Response>(
}
};

const tryParseBigIntKey = (key: string) => {
try {
return BigInt(key);
} catch {
return key;
}
};

/**
* Recursively replaces all numbers with bigints.
* Recursively maps blockfrost JSON metadata to core metadata.
* As JSON doesn't support numeric keys,
* this function assumes that all metadata (and metadatum map) keys parseable by BigInt.parse are numeric.
*/
export const replaceNumbersWithBigints = (obj: unknown): unknown => {
if (typeof obj === 'number') return BigInt(obj);
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(replaceNumbersWithBigints);
}
const newObj: any = {};
for (const k in obj) {
newObj[k] = replaceNumbersWithBigints((obj as any)[k]);
export const jsonToMetadatum = (obj: unknown): Cardano.Metadatum => {
switch (typeof obj) {
case 'number':
return BigInt(obj);
case 'string':
case 'bigint':
return obj;
case 'object': {
if (obj === null) break;
if (Array.isArray(obj)) {
return obj.map(jsonToMetadatum);
}
return new Map(Object.keys(obj).map((key) => [tryParseBigIntKey(key), jsonToMetadatum((obj as any)[key])]));
}
}
return newObj;
throw new ProviderError(ProviderFailure.NotImplemented, null, `Unsupported metadatum type: ${typeof obj}`);
};
37 changes: 23 additions & 14 deletions packages/blockfrost/test/blockfrostWalletProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,20 +366,29 @@ describe('blockfrostWalletProvider', () => {
expect(response[0]).toMatchObject({
auxiliaryData: {
body: {
blob: {
'1967': {
hash: '6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af',
metadata: 'https://nut.link/metadata.json'
},
'1968': {
ADAUSD: [
{
source: 'ergoOracles',
value: 3n
}
]
}
} as Cardano.MetadatumMap
blob: new Map<bigint, Cardano.Metadatum>([
[
1967n,
new Map([
['hash', '6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af'],
['metadata', 'https://nut.link/metadata.json']
])
],
[
1968n,
new Map([
[
'ADAUSD',
[
new Map<Cardano.Metadatum, Cardano.Metadatum>([
['source', 'ergoOracles'],
['value', 3n]
])
]
]
])
]
])
}
},
blockHeader: {
Expand Down
2 changes: 1 addition & 1 deletion packages/blockfrost/test/e2e/queryTransactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('blockfrostWalletProvider', () => {
const [tx] = await walletProvider.queryTransactionsByHashes([
Cardano.TransactionId('84801fb64a9c5078c406ead24017ba0b069ef6ac6446fef8bdb8f97bade3cfa5')
]);
expect(tx.auxiliaryData!.body.blob!['9223372036854775707']).toEqual(
expect(tx.auxiliaryData!.body.blob!.get(9_223_372_036_854_775_707n)).toEqual(
'9223372036854775707922337203685477570792233720368547757079223372'
);
});
Expand Down
25 changes: 16 additions & 9 deletions packages/blockfrost/test/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { InvalidStringError, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { fetchSequentially, formatBlockfrostError, replaceNumbersWithBigints } from '../src/util';
import { Cardano, InvalidStringError, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { fetchSequentially, formatBlockfrostError, jsonToMetadatum } from '../src/util';

describe('util', () => {
describe('formatBlockfrostError', () => {
Expand All @@ -11,14 +11,21 @@ describe('util', () => {
});
});

test('replaceNumbersWithBigints', () => {
expect(replaceNumbersWithBigints(1)).toBe(1n);
expect(replaceNumbersWithBigints('a')).toBe('a');
expect(replaceNumbersWithBigints(null)).toBe(null);
test('jsonToMetadatum', () => {
expect(jsonToMetadatum(1)).toBe(1n);
expect(jsonToMetadatum('a')).toBe('a');
expect(() => jsonToMetadatum(null)).toThrowError(ProviderError);
// eslint-disable-next-line unicorn/no-useless-undefined
expect(replaceNumbersWithBigints(undefined)).toBe(undefined);
expect(replaceNumbersWithBigints(['a', 1, [2]])).toEqual(['a', 1n, [2n]]);
expect(replaceNumbersWithBigints({ a: 'a', b: 1, c: { d: 2 } })).toEqual({ a: 'a', b: 1n, c: { d: 2n } });
expect(() => jsonToMetadatum(undefined)).toThrowError(ProviderError);
expect(jsonToMetadatum(['a', 1, [2]])).toEqual(['a', 1n, [2n]]);
expect(jsonToMetadatum({ 123: '1234', a: 'a', b: 1, c: { d: 2 } })).toEqual(
new Map<Cardano.Metadatum, Cardano.Metadatum>([
[123n, '1234'],
['a', 'a'],
['b', 1n],
['c', new Map([['d', 2n]])]
])
);
});

test('fetchSequentially', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ const witnessScriptsToCore = (scripts: GraphqlTransaction['witness']['scripts'])
);

type GraphqlAuxiliaryDataBody = NonNullable<GraphqlTransaction['auxiliaryData']>['body'];
const auxiliaryScriptsToCore = (scripts: GraphqlAuxiliaryDataBody['scripts']): Cardano.Script[] | undefined =>
scripts?.map(({ script }) => scriptToCore(script));
// const auxiliaryScriptsToCore = (scripts: GraphqlAuxiliaryDataBody['scripts']): Cardano.Script[] | undefined =>
// scripts?.map(({ script }) => scriptToCore(script));

const inputsToCore = (inputs: GraphqlTransaction['inputs'], txId: Cardano.TransactionId) =>
inputs.map(
Expand Down Expand Up @@ -99,6 +99,7 @@ const metadatumToCore = (metadatum: GraphqlMetadatum): Cardano.Metadatum => {
return (metadatum.array || []).map((md) => metadatumToCore(md as GraphqlMetadatum));
case 'MetadatumMap':
return (metadatum.map || {}).reduce(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(map, { label, metadatum: md }) => ({ ...map, [label]: metadatumToCore(md as GraphqlMetadatum) }),
{} as Cardano.MetadatumMap
);
Expand All @@ -109,19 +110,21 @@ const metadatumToCore = (metadatum: GraphqlMetadatum): Cardano.Metadatum => {
}
};

const auxiliaryDataToCore = (auxiliaryData: GraphqlTransaction['auxiliaryData']): Cardano.AuxiliaryData | undefined =>
auxiliaryData
? {
body: {
blob: auxiliaryData.body.blob?.reduce(
(blob, { label, metadatum }) => ({ ...blob, [label]: metadatumToCore(metadatum) }),
{} as Cardano.MetadatumMap
),
scripts: auxiliaryScriptsToCore(auxiliaryData.body.scripts)
},
hash: Cardano.Hash32ByteBase16(auxiliaryData.hash)
}
: undefined;
const auxiliaryDataToCore = (_auxiliaryData: GraphqlTransaction['auxiliaryData']): Cardano.AuxiliaryData | undefined =>
undefined;
// TODO: map to updated Cardano.Metadata
// auxiliaryData
// ? {
// body: {
// blob: auxiliaryData.body.blob?.reduce(
// (blob, { label, metadatum }) => ({ ...blob, [label]: metadatumToCore(metadatum) }),
// {} as Cardano.MetadatumMap
// ),
// scripts: auxiliaryScriptsToCore(auxiliaryData.body.scripts)
// },
// hash: Cardano.Hash32ByteBase16(auxiliaryData.hash)
// }
// : undefined;

const datumsToCore = (datums: GraphqlTransaction['witness']['datums']) =>
datums?.reduce(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,27 +328,26 @@ describe('WalletProvider/queryTransactions/graphqlTransactionsToCore', () => {
});
});

describe('auxiliaryData', () => {
// TODO
describe.skip('auxiliaryData', () => {
const hash = Cardano.Hash32ByteBase16('3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d');

describe('blob', () => {
const label = 'label';
const label = 123n;
const testMetadatumConversion = (metadatum: GraphqlMetadatum, coreMetadatum: Cardano.Metadatum) =>
testTxPropertiesConversion(
{
auxiliaryData: {
body: {
blob: [{ label, metadatum }]
blob: [{ label: label.toString(), metadatum }]
},
hash: hash.toString()
}
},
{
auxiliaryData: {
body: {
blob: {
[label]: coreMetadatum
},
blob: new Map([[label, coreMetadatum]]),
scripts: undefined
},
hash
Expand Down Expand Up @@ -376,9 +375,10 @@ describe('WalletProvider/queryTransactions/graphqlTransactionsToCore', () => {
__typename: 'MetadatumMap' as const,
map: [{ label: 'nested', metadatum: { __typename: 'StringMetadatum' as const, string: 'value' } }]
};
testMetadatumConversion(metadatum, {
[metadatum.map[0].label]: metadatum.map[0].metadatum.string
});
testMetadatumConversion(
metadatum,
new Map([[BigInt(metadatum.map[0].label), metadatum.map[0].metadatum.string]])
);
});

it('maps a metadatum array to core type', () => {
Expand Down
8 changes: 2 additions & 6 deletions packages/core/src/Asset/types/NftMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ export interface NftMetadataFile {
name: string;
mediaType: MediaType;
src: Uri[];
otherProperties?: {
[key: string]: Metadatum | undefined;
};
otherProperties?: Map<string, Metadatum>;
}

/**
Expand All @@ -50,7 +48,5 @@ export interface NftMetadata {
mediaType?: ImageMediaType;
files?: NftMetadataFile[];
description?: string[];
otherProperties?: {
[key: string]: Metadatum | undefined;
};
otherProperties?: Map<string, Metadatum>;
}
42 changes: 25 additions & 17 deletions packages/core/src/Asset/util/metadatumToCip25.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { AssetInfo, ImageMediaType, MediaType, NftMetadata, NftMetadataFile, Uri } from '../types';
import { CustomError } from 'ts-custom-error';
import { Metadatum, MetadatumMap, util } from '../../Cardano';
import { difference } from 'lodash-es';
import { dummyLogger } from 'ts-log';
import { omit } from 'lodash-es';

class InvalidFileError extends CustomError {}

const isString = (obj: unknown): obj is string => typeof obj === 'string';

const asString = (obj: unknown): string | undefined => {
if (typeof obj === 'string') {
return obj;
}
};

const asStringArray = (metadatum: Metadatum): string[] | undefined => {
const asStringArray = (metadatum: Metadatum | undefined): string[] | undefined => {
if (Array.isArray(metadatum)) {
const result = metadatum.map(asString);
if (result.some((str) => typeof str === 'undefined')) {
Expand All @@ -27,21 +29,27 @@ const asStringArray = (metadatum: Metadatum): string[] | undefined => {
};

const mapOtherProperties = (metadata: MetadatumMap, primaryProperties: string[]) => {
const extraProperties = omit(metadata, primaryProperties);
return Object.keys(extraProperties).length > 0 ? extraProperties : undefined;
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>());
};

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

const mapFile = (metadatum: Metadatum): NftMetadataFile => {
const file = util.metadatum.asMetadatumMap(metadatum);
if (!file) throw new InvalidFileError();
const mediaType = asString(file.mediaType);
const name = asString(file.name);
const srcAsString = asString(file.src);
const mediaType = asString(file.get('mediaType'));
const name = asString(file.get('name'));
const unknownTypeSrc = file.get('src');
if (!unknownTypeSrc) throw new InvalidFileError();
const srcAsString = asString(unknownTypeSrc);
const src = srcAsString
? Uri(srcAsString)
: util.metadatum.asMetadatumArray(file.src)?.map((fileSrc) => {
: util.metadatum.asMetadatumArray(unknownTypeSrc)?.map((fileSrc) => {
const fileSrcAsString = asString(fileSrc);
if (!fileSrcAsString) throw new InvalidFileError();
return Uri(fileSrcAsString);
Expand All @@ -60,7 +68,7 @@ const mapFile = (metadatum: Metadatum): NftMetadataFile => {
*/
const getAssetMetadata = (policy: MetadatumMap, asset: Pick<AssetInfo, 'name'>) =>
util.metadatum.asMetadatumMap(
policy[asset.name.toString()] || policy[Buffer.from(asset.name, 'hex').toString('utf8')]
policy.get(asset.name.toString()) || policy.get(Buffer.from(asset.name, 'hex').toString('utf8'))
);

// TODO: consider hoisting this function together with cip25 types to core or a new cip25 package
Expand All @@ -72,31 +80,31 @@ export const metadatumToCip25 = (
metadatumMap: MetadatumMap | undefined,
logger = dummyLogger
): NftMetadata | undefined => {
const cip25Metadata = metadatumMap?.['721'];
const cip25Metadata = metadatumMap?.get(721n);
if (!cip25Metadata) return;
const cip25MetadatumMap = util.metadatum.asMetadatumMap(cip25Metadata);
if (!cip25MetadatumMap) return;
const policy = util.metadatum.asMetadatumMap(cip25MetadatumMap[asset.policyId.toString()]);
const policy = util.metadatum.asMetadatumMap(cip25MetadatumMap.get(asset.policyId.toString())!);
if (!policy) return;
const assetMetadata = getAssetMetadata(policy, asset);
if (!assetMetadata) return;
const name = asString(assetMetadata.name);
const image = asStringArray(assetMetadata.image);
const name = asString(assetMetadata.get('name'));
const image = asStringArray(assetMetadata.get('image'));
if (!name || !image) {
logger.warn('Invalid CIP-25 metadata', assetMetadata);
return;
}
const mediaType = asString(assetMetadata.mediaType);
const files = util.metadatum.asMetadatumArray(assetMetadata.files);
const mediaType = asString(assetMetadata.get('mediaType'));
const files = util.metadatum.asMetadatumArray(assetMetadata.get('files'));
try {
return {
description: asStringArray(assetMetadata.description),
description: asStringArray(assetMetadata.get('description')),
files: files ? files.map(mapFile) : undefined,
image: image.map((img) => Uri(img)),
mediaType: mediaType ? ImageMediaType(mediaType) : undefined,
name,
otherProperties: mapOtherProperties(assetMetadata, ['name', 'image', 'mediaType', 'description', 'files']),
version: asString(policy.version) || '1.0'
version: asString(policy.get('version')) || '1.0'
};
} catch (error: unknown) {
// Any error here means metadata was invalid
Expand Down
Loading

0 comments on commit 48c33e5

Please sign in to comment.