Skip to content

Commit d77cf05

Browse files
committed
Provide TypeScript helper function to convert metadatum into JS objects.
Throws whenever the conversion isn't possible. This should work for CIP-0025's media NFT and probably few other metadata format. But it is unsound in the general cases.
1 parent 8e9c921 commit d77cf05

File tree

3 files changed

+223
-2
lines changed

3 files changed

+223
-2
lines changed

clients/TypeScript/packages/client/src/util.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
BlockMary,
1010
BlockShelley,
1111
EpochBoundaryBlock,
12+
Metadatum,
13+
MetadatumMap,
1214
Point,
1315
ProtocolParametersAlonzo,
1416
ProtocolParametersBabbage,
@@ -176,6 +178,56 @@ export function eventEmitterToGenerator <T> (eventEmitter: EventEmitter, eventNa
176178
}
177179
}
178180

181+
/** Convert a CBOR-description as raw JSON object, or throw if given an invalid
182+
* representation. This function is meant to use for converting transaction's
183+
* metadata into plain JSON in context where that conversion is expected to work.
184+
*
185+
* It isn't generally possible to do so because not every CBOR object have a 1:1
186+
* mapping to a JSON object. This function should therefore work for metadata
187+
* coming from CIP-0025, and likely a few other standards but is unsound in the
188+
* general case and isn't expected to work on *any* metadata that can be found on
189+
* chain.
190+
*
191+
* @category Helper */
192+
export function unsafeMetadatumAsJSON (metadatum: Metadatum): any {
193+
function fromMetadatum (o: Metadatum): any {
194+
if (Object.keys(o).length > 1) {
195+
throw new Error('Malformed metadatum object. A JSON object that describes CBOR encoded datum is expected.')
196+
}
197+
198+
if ('int' in o) {
199+
return o.int
200+
} else if ('string' in o) {
201+
return o.string
202+
} else if ('bytes' in o) {
203+
return Buffer.from(o.bytes, 'hex')
204+
} else if ('list' in o) {
205+
return o.list.map(fromMetadatum)
206+
} else if ('map' in o) {
207+
return o.map.reduce(fromMetadatumMap, {})
208+
} else {
209+
const type = Object.keys(o)[0]
210+
const msg = `Unexpected metadatum type '${type}'.`
211+
let hint = ''
212+
if (Number.isInteger(Number.parseInt(type, 10))) {
213+
hint = ' Hint: this function expects metadatum objects without metadatum label.'
214+
}
215+
throw new Error(`${msg}${hint}`)
216+
}
217+
}
218+
219+
function fromMetadatumMap (acc: { [k: string]: any }, { k, v }: MetadatumMap) {
220+
const kStr = fromMetadatum(k)
221+
if (typeof kStr !== 'string') {
222+
throw new Error(`Invalid non-string key: ${k}.`)
223+
}
224+
acc[kStr] = fromMetadatum(v)
225+
return acc
226+
}
227+
228+
return fromMetadatum(metadatum)
229+
}
230+
179231
/** @category Helper */
180232
export const isAllegraBlock = (block: Block): block is { allegra: BlockAllegra } =>
181233
(block as { allegra: BlockAllegra }).allegra !== undefined

clients/TypeScript/packages/client/test/util.test.ts

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
TxOut
1111
} from '@cardano-ogmios/schema'
1212
import { EventEmitter } from 'events'
13-
import { safeJSON, eventEmitterToGenerator } from '../src'
13+
import { safeJSON, eventEmitterToGenerator, unsafeMetadatumAsJSON } from '../src'
1414

1515
describe('util', () => {
1616
describe('eventToGenerator', () => {
@@ -253,4 +253,173 @@ describe('util', () => {
253253
})
254254
})
255255
})
256+
257+
describe('unsafeMetadatumAsJSON', () => {
258+
it('primitives :: int', () => {
259+
const json = unsafeMetadatumAsJSON({
260+
int: 42n
261+
})
262+
expect(json as number).toEqual(42n)
263+
})
264+
265+
it('primitives :: string', () => {
266+
const json = unsafeMetadatumAsJSON({
267+
string: 'foo'
268+
})
269+
expect(json as string).toEqual('foo')
270+
})
271+
272+
it('primitives :: bytes', () => {
273+
const json = unsafeMetadatumAsJSON({
274+
bytes: '626172'
275+
})
276+
expect(json as Buffer).toEqual(Buffer.from('bar'))
277+
})
278+
279+
it('primitives :: list', () => {
280+
const json = unsafeMetadatumAsJSON({
281+
list: [{ int: 42n }]
282+
})
283+
expect(json as Array<any>).toEqual([42n])
284+
})
285+
286+
it('primitives :: map', () => {
287+
const json = unsafeMetadatumAsJSON({
288+
map: [{ k: { string: 'foo' }, v: { int: 42n } }]
289+
})
290+
expect(json as Object).toEqual({ foo: 42n })
291+
})
292+
293+
it('compound', () => {
294+
const json = unsafeMetadatumAsJSON({
295+
map: [
296+
{
297+
k: {
298+
string: 'f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a'
299+
},
300+
v: {
301+
map: [
302+
{
303+
k: {
304+
string: 'reactant'
305+
},
306+
v: {
307+
map: [
308+
{
309+
k: {
310+
string: 'name'
311+
},
312+
v: {
313+
string: '$reactant'
314+
}
315+
},
316+
{
317+
k: {
318+
string: 'description'
319+
},
320+
v: {
321+
string: 'The Handle Standard'
322+
}
323+
},
324+
{
325+
k: {
326+
string: 'website'
327+
},
328+
v: {
329+
string: 'https://adahandle.com'
330+
}
331+
},
332+
{
333+
k: {
334+
string: 'image'
335+
},
336+
v: {
337+
string: 'ipfs://QmWLeos8yGBE7o69wUxGYcXmS8qgc4TiyZ4VHcGmfkBiY3'
338+
}
339+
},
340+
{
341+
k: {
342+
string: 'core'
343+
},
344+
v: {
345+
map: [
346+
{
347+
k: {
348+
string: 'og'
349+
},
350+
v: {
351+
int: 0n
352+
}
353+
},
354+
{
355+
k: {
356+
string: 'termsofuse'
357+
},
358+
v: {
359+
string: 'https://adahandle.com/tou'
360+
}
361+
},
362+
{
363+
k: {
364+
string: 'handleEncoding'
365+
},
366+
v: {
367+
string: 'utf-8'
368+
}
369+
},
370+
{
371+
k: {
372+
string: 'prefix'
373+
},
374+
v: {
375+
string: '$'
376+
}
377+
},
378+
{
379+
k: {
380+
string: 'version'
381+
},
382+
v: {
383+
int: 0n
384+
}
385+
}
386+
]
387+
}
388+
},
389+
{
390+
k: {
391+
string: 'augmentations'
392+
},
393+
v: {
394+
list: []
395+
}
396+
}
397+
]
398+
}
399+
}
400+
]
401+
}
402+
}
403+
]
404+
})
405+
expect(json as Object).toEqual({
406+
f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a: {
407+
reactant: {
408+
augmentations: [],
409+
core: {
410+
handleEncoding: 'utf-8',
411+
og: 0n,
412+
prefix: '$',
413+
termsofuse: 'https://adahandle.com/tou',
414+
version: 0n
415+
},
416+
description: 'The Handle Standard',
417+
image: 'ipfs://QmWLeos8yGBE7o69wUxGYcXmS8qgc4TiyZ4VHcGmfkBiY3',
418+
name: '$reactant',
419+
website: 'https://adahandle.com'
420+
}
421+
}
422+
})
423+
})
424+
})
256425
})

clients/TypeScript/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"composite": true,
44
"module": "commonjs",
5-
"target": "es2017",
5+
"target": "es2020",
66
"esModuleInterop": true,
77
"declaration": true,
88
"declarationMap": true,

0 commit comments

Comments
 (0)