Skip to content

Commit

Permalink
fix: project cip68 metadata when datum resides in tx witness
Browse files Browse the repository at this point in the history
Some CIP-68 NFTs are created by using transaction
witness datum instead of inline datum
  • Loading branch information
mkazlauskas committed Oct 11, 2024
1 parent 2e0e2c7 commit 164032d
Show file tree
Hide file tree
Showing 6 changed files with 13,770 additions and 11 deletions.
14 changes: 11 additions & 3 deletions packages/projection-typeorm/src/operators/storeUtxo.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { Cardano, Serialization } from '@cardano-sdk/core';
import { ChainSyncEventType, Mappers } from '@cardano-sdk/projection';
import { Hash32ByteBase16 } from '@cardano-sdk/crypto';
import { ObjectLiteral } from 'typeorm';
import { OutputEntity, TokensEntity } from '../entity';
import { typeormOperator } from './util';

const serializeDatumIfExists = (datum: Cardano.PlutusData | undefined) =>
datum ? Serialization.PlutusData.fromCore(datum).toCbor() : undefined;
const serializeInlineDatumIfExists = (
datum: Cardano.PlutusData | undefined,
datumHash: Hash32ByteBase16 | undefined
) => {
// withUtxo mapper hydrates utxo with datum from witness
// we probably don't need to store it in the db
if (datumHash) return;
return datum ? Serialization.PlutusData.fromCore(datum).toCbor() : undefined;
};

export interface WithStoredProducedUtxo {
storedProducedUtxo: Map<Mappers.ProducedUtxo, ObjectLiteral>;
Expand All @@ -27,7 +35,7 @@ export const storeUtxo = typeormOperator<Mappers.WithUtxo, WithStoredProducedUtx
address,
block: { slot: header.slot },
coins: value.coins,
datum: serializeDatumIfExists(datum),
datum: serializeInlineDatumIfExists(datum, datumHash),
datumHash,
outputIndex: index,
scriptReference,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,25 @@ describe('storeNftMetadata', () => {
const metadata = await nftMetadataRepo.findOneBy({ userTokenAssetId });
expect(metadata).toBeTruthy();
});

it('stores metadata from witness datum', async () => {
const USER_TOKEN_ASSET_ID = Cardano.AssetId(
'ecd0970cb2d599a8bdb61f6eb597e25eaf34d76bdf8ade17b6a8fa59000de140534832303037'
);
const REFERENCE_TOKEN_ASSET_ID = Cardano.AssetId(
'ecd0970cb2d599a8bdb61f6eb597e25eaf34d76bdf8ade17b6a8fa59000643b0534832303037'
);
const eventsWithCip68Handle = filterAssets(chainSyncData(ChainSyncDataSet.Cip68WitnessDatumProblem), [
REFERENCE_TOKEN_ASSET_ID,
USER_TOKEN_ASSET_ID
]);
const evt = await firstValueFrom(project$(eventsWithCip68Handle));
const nftMetadata = evt.nftMetadata.find(({ userTokenAssetId }) => userTokenAssetId === USER_TOKEN_ASSET_ID);
expect(nftMetadata).toBeTruthy();

const storedMetadata = await nftMetadataRepo.findOneBy({ userTokenAssetId: USER_TOKEN_ASSET_ID });
expect(typeof storedMetadata?.image).toBe('string');
});
});

describe('willStoreNftMetadata', () => {
Expand Down
19 changes: 16 additions & 3 deletions packages/projection/src/operators/Mappers/withUtxo.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Cardano } from '@cardano-sdk/core';
import { Cardano, Serialization } from '@cardano-sdk/core';
import { FilterByPolicyIds } from './types';
import { ProjectionOperator } from '../../types';
import { map } from 'rxjs';
import { unifiedProjectorOperator } from '../utils';

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

export interface WithUtxo {
Expand All @@ -14,15 +15,27 @@ export interface WithUtxo {
};
}

const attemptHydrateDatum = (txOut: Cardano.TxOut, witness: Cardano.Witness): Cardano.TxOut => {
if (!txOut.datumHash) return txOut;
const witnessDatum = witness.datums?.find(
(datum) => Serialization.PlutusData.fromCore(datum).hash() === txOut.datumHash
);
if (!witnessDatum) return txOut;
return {
...txOut,
datum: witnessDatum
};
};

export const withUtxo = unifiedProjectorOperator<{}, WithUtxo>((evt) => {
const produced = evt.block.body.flatMap(({ body: { outputs, collateralReturn }, inputSource, id }) =>
const produced = evt.block.body.flatMap(({ body: { outputs, collateralReturn }, inputSource, id, witness }) =>
(inputSource === Cardano.InputSource.inputs ? outputs : collateralReturn ? [collateralReturn] : []).map(
(txOut, outputIndex): [Cardano.TxIn, Cardano.TxOut] => [
{
index: outputIndex,
txId: id
},
txOut
attemptHydrateDatum(txOut, witness)
]
)
);
Expand Down
176 changes: 171 additions & 5 deletions packages/projection/test/operators/Mappers/withUtxo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const validTxSource$ = of({
}
]
},
inputSource: Cardano.InputSource.inputs
inputSource: Cardano.InputSource.inputs,
witness: {}
},
{
body: {
Expand Down Expand Up @@ -71,7 +72,8 @@ export const validTxSource$ = of({
}
]
},
inputSource: Cardano.InputSource.inputs
inputSource: Cardano.InputSource.inputs,
witness: {}
},
{
body: {
Expand Down Expand Up @@ -108,7 +110,8 @@ export const validTxSource$ = of({
}
]
},
inputSource: Cardano.InputSource.inputs
inputSource: Cardano.InputSource.inputs,
witness: {}
}
]
}
Expand Down Expand Up @@ -151,7 +154,8 @@ describe('withUtxo', () => {
}
]
},
inputSource: Cardano.InputSource.collaterals
inputSource: Cardano.InputSource.collaterals,
witness: {}
},
{
body: {
Expand Down Expand Up @@ -200,7 +204,8 @@ describe('withUtxo', () => {
}
]
},
inputSource: Cardano.InputSource.collaterals
inputSource: Cardano.InputSource.collaterals,
witness: {}
}
]
}
Expand All @@ -214,6 +219,167 @@ describe('withUtxo', () => {
expect(produced).toHaveLength(5);
});

it('hydrates produced output datum from witness', async () => {
const {
utxo: { produced }
} = await firstValueFrom(
of({
block: {
body: [
{
body: {
inputs: [
{
index: 1,
txId: '434342da3f66f94d929d8c7a49484e1c212c74c6213d7b938119f6e0dcb9454c'
}
],
outputs: [
{
address: Cardano.PaymentAddress('addr_test1wzlv9cslk9tcj0wpm9p5t6kajyt37ap5sc9rzkaxa9p67ys2ygypv'),
datumHash: '51f55225cb45388c05903db1e5095382ceafa2d17ff13ffbecf31b037c7c4dc1' as Cardano.DatumHash,
value: { coins: 1_724_100n }
}
]
},
inputSource: Cardano.InputSource.inputs,
witness: {
datums: [
{
cbor: 'd8799f4108d8799fd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ffd87a80ff1a002625a0d8799fd879801a4f2442c1d8799f1b000000108fdb12acffffff',
constructor: 0n,
fields: {
cbor: '9f4108d8799fd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ffd87a80ff1a002625a0d8799fd879801a4f2442c1d8799f1b000000108fdb12acffffff',
items: [
new Uint8Array([8]),
{
cbor: 'd8799fd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ffd87a80ff',
constructor: 0n,
fields: {
cbor: '9fd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ffd87a80ff',
items: [
{
cbor: 'd8799fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ff',
constructor: 0n,
fields: {
cbor: '9fd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffffd87a80ff',
items: [
{
cbor: 'd8799fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffff',
constructor: 0n,
fields: {
cbor: '9fd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ffd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffffff',
items: [
{
cbor: 'd8799f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ff',
constructor: 0n,
fields: {
cbor: '9f581c5247dd3bdf2d2f838a2f0c91b38f127523772d24393993e10fbbd235ff',
items: [
new Uint8Array([
82, 71, 221, 59, 223, 45, 47, 131, 138, 47, 12, 145, 179, 143, 18,
117, 35, 119, 45, 36, 57, 57, 147, 225, 15, 187, 210, 53
])
]
}
},
{
cbor: 'd8799fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffff',
constructor: 0n,
fields: {
cbor: '9fd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffffff',
items: [
{
cbor: 'd8799fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffff',
constructor: 0n,
fields: {
cbor: '9fd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ffff',
items: [
{
cbor: 'd8799f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ff',
constructor: 0n,
fields: {
cbor: '9f581c9a45a01d85c481827325eca0537957bf0480ec37e9ada731b06400d0ff',
items: [
new Uint8Array([
154, 69, 160, 29, 133, 196, 129, 130, 115, 37, 236, 160,
83, 121, 87, 191, 4, 128, 236, 55, 233, 173, 167, 49, 176,
100, 0, 208
])
]
}
}
]
}
}
]
}
}
]
}
},
{
cbor: 'd87a80',
constructor: 1n,
fields: {
cbor: '80',
items: []
}
}
]
}
},
{
cbor: 'd87a80',
constructor: 1n,
fields: {
cbor: '80',
items: []
}
}
]
}
},
2_500_000n,
{
cbor: 'd8799fd879801a4f2442c1d8799f1b000000108fdb12acffff',
constructor: 0n,
fields: {
cbor: '9fd879801a4f2442c1d8799f1b000000108fdb12acffff',
items: [
{
cbor: 'd87980',
constructor: 0n,
fields: {
cbor: '80',
items: []
}
},
1_327_776_449n,
{
cbor: 'd8799f1b000000108fdb12acff',
constructor: 0n,
fields: {
cbor: '9f1b000000108fdb12acff',
items: [71_132_975_788n]
}
}
]
}
}
]
}
}
]
}
}
]
}
} as ProjectionEvent).pipe(withUtxo())
);
expect(produced[0][1].datum).toBeTruthy();
});

it('when inputSource is collateral: maps consumed/produced utxo from collateral/collateralReturn', async () => {
const {
utxo: { consumed, produced }
Expand Down
Loading

0 comments on commit 164032d

Please sign in to comment.