Skip to content

Commit 164032d

Browse files
committed
fix: project cip68 metadata when datum resides in tx witness
Some CIP-68 NFTs are created by using transaction witness datum instead of inline datum
1 parent 2e0e2c7 commit 164032d

File tree

6 files changed

+13770
-11
lines changed

6 files changed

+13770
-11
lines changed

Diff for: packages/projection-typeorm/src/operators/storeUtxo.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { Cardano, Serialization } from '@cardano-sdk/core';
22
import { ChainSyncEventType, Mappers } from '@cardano-sdk/projection';
3+
import { Hash32ByteBase16 } from '@cardano-sdk/crypto';
34
import { ObjectLiteral } from 'typeorm';
45
import { OutputEntity, TokensEntity } from '../entity';
56
import { typeormOperator } from './util';
67

7-
const serializeDatumIfExists = (datum: Cardano.PlutusData | undefined) =>
8-
datum ? Serialization.PlutusData.fromCore(datum).toCbor() : undefined;
8+
const serializeInlineDatumIfExists = (
9+
datum: Cardano.PlutusData | undefined,
10+
datumHash: Hash32ByteBase16 | undefined
11+
) => {
12+
// withUtxo mapper hydrates utxo with datum from witness
13+
// we probably don't need to store it in the db
14+
if (datumHash) return;
15+
return datum ? Serialization.PlutusData.fromCore(datum).toCbor() : undefined;
16+
};
917

1018
export interface WithStoredProducedUtxo {
1119
storedProducedUtxo: Map<Mappers.ProducedUtxo, ObjectLiteral>;
@@ -27,7 +35,7 @@ export const storeUtxo = typeormOperator<Mappers.WithUtxo, WithStoredProducedUtx
2735
address,
2836
block: { slot: header.slot },
2937
coins: value.coins,
30-
datum: serializeDatumIfExists(datum),
38+
datum: serializeInlineDatumIfExists(datum, datumHash),
3139
datumHash,
3240
outputIndex: index,
3341
scriptReference,

Diff for: packages/projection-typeorm/test/operators/storeNftMetadata.test.ts

+19
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,25 @@ describe('storeNftMetadata', () => {
494494
const metadata = await nftMetadataRepo.findOneBy({ userTokenAssetId });
495495
expect(metadata).toBeTruthy();
496496
});
497+
498+
it('stores metadata from witness datum', async () => {
499+
const USER_TOKEN_ASSET_ID = Cardano.AssetId(
500+
'ecd0970cb2d599a8bdb61f6eb597e25eaf34d76bdf8ade17b6a8fa59000de140534832303037'
501+
);
502+
const REFERENCE_TOKEN_ASSET_ID = Cardano.AssetId(
503+
'ecd0970cb2d599a8bdb61f6eb597e25eaf34d76bdf8ade17b6a8fa59000643b0534832303037'
504+
);
505+
const eventsWithCip68Handle = filterAssets(chainSyncData(ChainSyncDataSet.Cip68WitnessDatumProblem), [
506+
REFERENCE_TOKEN_ASSET_ID,
507+
USER_TOKEN_ASSET_ID
508+
]);
509+
const evt = await firstValueFrom(project$(eventsWithCip68Handle));
510+
const nftMetadata = evt.nftMetadata.find(({ userTokenAssetId }) => userTokenAssetId === USER_TOKEN_ASSET_ID);
511+
expect(nftMetadata).toBeTruthy();
512+
513+
const storedMetadata = await nftMetadataRepo.findOneBy({ userTokenAssetId: USER_TOKEN_ASSET_ID });
514+
expect(typeof storedMetadata?.image).toBe('string');
515+
});
497516
});
498517

499518
describe('willStoreNftMetadata', () => {

Diff for: packages/projection/src/operators/Mappers/withUtxo.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Cardano } from '@cardano-sdk/core';
1+
import { Cardano, Serialization } from '@cardano-sdk/core';
22
import { FilterByPolicyIds } from './types';
33
import { ProjectionOperator } from '../../types';
44
import { map } from 'rxjs';
55
import { unifiedProjectorOperator } from '../utils';
66

7+
/** Output datum is hydrated with the datum from witness if present */
78
export type ProducedUtxo = [Cardano.TxIn, Cardano.TxOut];
89

910
export interface WithUtxo {
@@ -14,15 +15,27 @@ export interface WithUtxo {
1415
};
1516
}
1617

18+
const attemptHydrateDatum = (txOut: Cardano.TxOut, witness: Cardano.Witness): Cardano.TxOut => {
19+
if (!txOut.datumHash) return txOut;
20+
const witnessDatum = witness.datums?.find(
21+
(datum) => Serialization.PlutusData.fromCore(datum).hash() === txOut.datumHash
22+
);
23+
if (!witnessDatum) return txOut;
24+
return {
25+
...txOut,
26+
datum: witnessDatum
27+
};
28+
};
29+
1730
export const withUtxo = unifiedProjectorOperator<{}, WithUtxo>((evt) => {
18-
const produced = evt.block.body.flatMap(({ body: { outputs, collateralReturn }, inputSource, id }) =>
31+
const produced = evt.block.body.flatMap(({ body: { outputs, collateralReturn }, inputSource, id, witness }) =>
1932
(inputSource === Cardano.InputSource.inputs ? outputs : collateralReturn ? [collateralReturn] : []).map(
2033
(txOut, outputIndex): [Cardano.TxIn, Cardano.TxOut] => [
2134
{
2235
index: outputIndex,
2336
txId: id
2437
},
25-
txOut
38+
attemptHydrateDatum(txOut, witness)
2639
]
2740
)
2841
);

Diff for: packages/projection/test/operators/Mappers/withUtxo.test.ts

+171-5
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export const validTxSource$ = of({
4343
}
4444
]
4545
},
46-
inputSource: Cardano.InputSource.inputs
46+
inputSource: Cardano.InputSource.inputs,
47+
witness: {}
4748
},
4849
{
4950
body: {
@@ -71,7 +72,8 @@ export const validTxSource$ = of({
7172
}
7273
]
7374
},
74-
inputSource: Cardano.InputSource.inputs
75+
inputSource: Cardano.InputSource.inputs,
76+
witness: {}
7577
},
7678
{
7779
body: {
@@ -108,7 +110,8 @@ export const validTxSource$ = of({
108110
}
109111
]
110112
},
111-
inputSource: Cardano.InputSource.inputs
113+
inputSource: Cardano.InputSource.inputs,
114+
witness: {}
112115
}
113116
]
114117
}
@@ -151,7 +154,8 @@ describe('withUtxo', () => {
151154
}
152155
]
153156
},
154-
inputSource: Cardano.InputSource.collaterals
157+
inputSource: Cardano.InputSource.collaterals,
158+
witness: {}
155159
},
156160
{
157161
body: {
@@ -200,7 +204,8 @@ describe('withUtxo', () => {
200204
}
201205
]
202206
},
203-
inputSource: Cardano.InputSource.collaterals
207+
inputSource: Cardano.InputSource.collaterals,
208+
witness: {}
204209
}
205210
]
206211
}
@@ -214,6 +219,167 @@ describe('withUtxo', () => {
214219
expect(produced).toHaveLength(5);
215220
});
216221

222+
it('hydrates produced output datum from witness', async () => {
223+
const {
224+
utxo: { produced }
225+
} = await firstValueFrom(
226+
of({
227+
block: {
228+
body: [
229+
{
230+
body: {
231+
inputs: [
232+
{
233+
index: 1,
234+
txId: '434342da3f66f94d929d8c7a49484e1c212c74c6213d7b938119f6e0dcb9454c'
235+
}
236+
],
237+
outputs: [
238+
{
239+
address: Cardano.PaymentAddress('addr_test1wzlv9cslk9tcj0wpm9p5t6kajyt37ap5sc9rzkaxa9p67ys2ygypv'),
240+
datumHash: '51f55225cb45388c05903db1e5095382ceafa2d17ff13ffbecf31b037c7c4dc1' as Cardano.DatumHash,
241+
value: { coins: 1_724_100n }
242+
}
243+
]
244+
},
245+
inputSource: Cardano.InputSource.inputs,
246+
witness: {
247+
datums: [
248+
{
249+
cbor: 'd8799f4108d8799fd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ffd87a80ff1a002625a0d8799fd879801a4f2442c1d8799f1b000000108fdb12acffffff',
250+
constructor: 0n,
251+
fields: {
252+
cbor: '9f4108d8799fd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ffd87a80ff1a002625a0d8799fd879801a4f2442c1d8799f1b000000108fdb12acffffff',
253+
items: [
254+
new Uint8Array([8]),
255+
{
256+
cbor: 'd8799fd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ffd87a80ff',
257+
constructor: 0n,
258+
fields: {
259+
cbor: '9fd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ffd87a80ff',
260+
items: [
261+
{
262+
cbor: 'd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ff',
263+
constructor: 0n,
264+
fields: {
265+
cbor: '9fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ff',
266+
items: [
267+
{
268+
cbor: 'd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffff',
269+
constructor: 0n,
270+
fields: {
271+
cbor: '9fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffff',
272+
items: [
273+
{
274+
cbor: 'd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ff',
275+
constructor: 0n,
276+
fields: {
277+
cbor: '9f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ff',
278+
items: [
279+
new Uint8Array([
280+
82, 71, 221, 59, 223, 45, 47, 131, 138, 47, 12, 145, 179, 143, 18,
281+
117, 35, 119, 45, 36, 57, 57, 147, 225, 15, 187, 210, 53
282+
])
283+
]
284+
}
285+
},
286+
{
287+
cbor: 'd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffff',
288+
constructor: 0n,
289+
fields: {
290+
cbor: '9fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffff',
291+
items: [
292+
{
293+
cbor: 'd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffff',
294+
constructor: 0n,
295+
fields: {
296+
cbor: '9fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffff',
297+
items: [
298+
{
299+
cbor: 'd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ff',
300+
constructor: 0n,
301+
fields: {
302+
cbor: '9f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ff',
303+
items: [
304+
new Uint8Array([
305+
154, 69, 160, 29, 133, 196, 129, 130, 115, 37, 236, 160,
306+
83, 121, 87, 191, 4, 128, 236, 55, 233, 173, 167, 49, 176,
307+
100, 0, 208
308+
])
309+
]
310+
}
311+
}
312+
]
313+
}
314+
}
315+
]
316+
}
317+
}
318+
]
319+
}
320+
},
321+
{
322+
cbor: 'd87a80',
323+
constructor: 1n,
324+
fields: {
325+
cbor: '80',
326+
items: []
327+
}
328+
}
329+
]
330+
}
331+
},
332+
{
333+
cbor: 'd87a80',
334+
constructor: 1n,
335+
fields: {
336+
cbor: '80',
337+
items: []
338+
}
339+
}
340+
]
341+
}
342+
},
343+
2_500_000n,
344+
{
345+
cbor: 'd8799fd879801a4f2442c1d8799f1b000000108fdb12acffff',
346+
constructor: 0n,
347+
fields: {
348+
cbor: '9fd879801a4f2442c1d8799f1b000000108fdb12acffff',
349+
items: [
350+
{
351+
cbor: 'd87980',
352+
constructor: 0n,
353+
fields: {
354+
cbor: '80',
355+
items: []
356+
}
357+
},
358+
1_327_776_449n,
359+
{
360+
cbor: 'd8799f1b000000108fdb12acff',
361+
constructor: 0n,
362+
fields: {
363+
cbor: '9f1b000000108fdb12acff',
364+
items: [71_132_975_788n]
365+
}
366+
}
367+
]
368+
}
369+
}
370+
]
371+
}
372+
}
373+
]
374+
}
375+
}
376+
]
377+
}
378+
} as ProjectionEvent).pipe(withUtxo())
379+
);
380+
expect(produced[0][1].datum).toBeTruthy();
381+
});
382+
217383
it('when inputSource is collateral: maps consumed/produced utxo from collateral/collateralReturn', async () => {
218384
const {
219385
utxo: { consumed, produced }

0 commit comments

Comments
 (0)