Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: project cip68 metadata when datum resides in tx witness #1510

Merged
merged 2 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/core/src/Asset/NftMetadata/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import * as CidValidator from '@biglup/is-cid';
import { InvalidStringError, OpaqueString } from '@cardano-sdk/util';
import type { Metadatum } from '../../Cardano';

const looksLikeIpfsUrlWithoutProtocol = (uri: string) => {
const [cid] = uri.split('/');
if (!cid) return false;
return CidValidator.isValid(cid);
};

export type Uri = OpaqueString<'Uri'>;

export const Uri = (uri: string) => {
Expand All @@ -12,7 +18,7 @@ export const Uri = (uri: string) => {
if (uri.startsWith('data:')) {
return uri as unknown as Uri;
}
if (CidValidator.isValid(uri)) {
if (looksLikeIpfsUrlWithoutProtocol(uri)) {
return `ipfs://${uri}` as unknown as Uri;
}
throw new InvalidStringError(
Expand Down
7 changes: 7 additions & 0 deletions packages/core/test/Asset/NftMetadata/fromMetadatum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('NftMetadata.fromMetadatum', () => {
const assetNameString = Buffer.from(assetNameStringUtf8).toString('hex');
const policyIdString = 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7';
const assetImageIPFS = 'ipfs://QmWS6DgF8Ma8oooBn7CtD3ChHyzzMw5NXWfnDbVFTip8af';
const assetImageIPFSNoProtocol = 'QmbhD98wpxQ8dqQkjv5Z3U59g3UqVQWV7szZWuD5CZy6iV/1.jpg';
const assetImageHTTPS = 'https://tokens.cardano.org';
const ipfsUrl = 'ipfs://image';

Expand Down Expand Up @@ -258,6 +259,12 @@ describe('NftMetadata.fromMetadatum', () => {
expect(result?.image).toEqual('ipfs://bafybeihtdkq3ntfcewytdaimnrslpxsatsg47e3bqlsgi3jkax65pypymi');
});

it('supports image without protocol for ipfs uri', () => {
const metadatum = createMetadatumWithFiles([], assetImageIPFSNoProtocol, assetNameStringUtf8);
const result = Asset.NftMetadata.fromMetadatum(validAsset, metadatum, logger);
expect(result?.image).toEqual(`ipfs://${assetImageIPFSNoProtocol}`);
});

it('supports image with ipfs protocol as array', () => {
const metadatum = createMetadatumWithFiles([], assetImageIPFS, assetNameStringUtf8);
const result = Asset.NftMetadata.fromMetadatum(validAsset, metadatum, logger);
Expand Down
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
Loading