Skip to content

Commit

Permalink
Merge pull request #769 from input-output-hk/feat/plutus-data-seriali…
Browse files Browse the repository at this point in the history
…zation

feat: nft metadata service
  • Loading branch information
AngelCastilloB committed Sep 15, 2023
2 parents 6ed8510 + 78460d3 commit 948d377
Show file tree
Hide file tree
Showing 333 changed files with 27,333 additions and 11,802 deletions.
1 change: 0 additions & 1 deletion .github/workflows/continuous-integration-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ env:

on:
pull_request:
branches: ['master']
push:
branches: ['master']
tags: ['*.*.*']
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/continuous-integration-unit-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ env:

on:
pull_request:
branches: ['master']
push:
branches: ['master']
tags: ['*.*.*']
Expand Down
3 changes: 1 addition & 2 deletions packages/cardano-services-client/src/HttpProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { HttpProviderConfigPaths, Provider, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { Logger } from 'ts-log';
import { fromSerializableObject, toSerializableObject } from '@cardano-sdk/util';
import { apiVersion as staticApiVersion } from './version';
import axios, { AxiosAdapter, AxiosRequestConfig, AxiosResponseTransformer } from 'axios';
import packageJson from '../package.json';

Expand Down Expand Up @@ -104,7 +103,7 @@ export const createHttpProvider = <T extends Provider>({
const req: AxiosRequestConfig = {
...axiosOptions,
adapter,
baseURL: `${baseUrl.replace(/\/$/, '')}/v${staticApiVersion.root}/${serviceSlug}`,
baseURL: `${baseUrl.replace(/\/$/, '')}/v${apiVersion}/${serviceSlug}`,
data: { ...args[0] },
headers: {
...axiosOptions?.headers,
Expand Down
6 changes: 3 additions & 3 deletions packages/cardano-services-client/src/version.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// auto-generated using ../scripts/createVersionSource.js
export const apiVersion = {
assetInfo: '1.0.0',
chainHistory: '1.0.0',
chainHistory: '2.0.0',
handle: '1.0.0',
networkInfo: '1.0.0',
rewards: '1.0.0',
root: '1.0.0',
stakePool: '1.0.0',
txSubmit: '1.0.0',
utxo: '1.0.0'
txSubmit: '2.0.0',
utxo: '2.0.0'
};
8 changes: 4 additions & 4 deletions packages/cardano-services-client/test/HttpProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import path from 'path';

const packageJson = require(path.join(__dirname, '..', 'package.json'));

type ComplexArg2 = { map: Map<string, Buffer> };
type ComplexResponse = Map<bigint, Buffer>[];
type ComplexArg2 = { map: Map<string, Uint8Array> };
type ComplexResponse = Map<bigint, Uint8Array>[];
interface TestProvider extends Provider {
noArgsEmptyReturn(): Promise<void>;
complexArgsAndReturn({ arg1, arg2 }: { arg1: bigint; arg2: ComplexArg2 }): Promise<ComplexResponse>;
Expand Down Expand Up @@ -95,8 +95,8 @@ describe('createHttpProvider', () => {
describe('method with complex args and return', () => {
it('serializes args and deserializes response using core serializableObject', async () => {
const arg1 = 123n;
const arg2: ComplexArg2 = { map: new Map([['key', Buffer.from('abc')]]) };
const expectedResponse: ComplexResponse = [new Map([[1234n, Buffer.from('response data')]])];
const arg2: ComplexArg2 = { map: new Map([['key', new Uint8Array(Buffer.from('abc'))]]) };
const expectedResponse: ComplexResponse = [new Map([[1234n, new Uint8Array(Buffer.from('response data'))]])];
const provider = createTxSubmitProviderClient();
closeServer = await createStubHttpProviderServer(port, stubProviderPaths.complexArgsAndReturn, (req, res) => {
expect(fromSerializableObject(req.body)).toEqual({ arg1, arg2 });
Expand Down
6 changes: 3 additions & 3 deletions packages/cardano-services-client/version.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"assetInfo": "1.0.0",
"chainHistory": "1.0.0",
"chainHistory": "2.0.0",
"handle": "1.0.0",
"networkInfo": "1.0.0",
"rewards": "1.0.0",
"root": "1.0.0",
"stakePool": "1.0.0",
"txSubmit": "1.0.0",
"utxo": "1.0.0"
"txSubmit": "2.0.0",
"utxo": "2.0.0"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Cardano } from '@cardano-sdk/core';
import { LastMintTxModel, MultiAssetHistoryModel, MultiAssetModel } from './types';
import { LastMintTxModel, MultiAssetModel } from './types';
import { Logger } from 'ts-log';
import { Pool } from 'pg';
import Queries from './queries';
Expand Down Expand Up @@ -32,14 +32,4 @@ export class AssetBuilder {
});
return result.rows[0];
}

public async queryMultiAssetHistory(policyId: Cardano.PolicyId, name: Cardano.AssetName) {
this.#logger.debug('About to query multi asset history', { name, policyId });
const result = await this.#db.query<MultiAssetHistoryModel>({
name: 'find_multi_asset_history',
text: Queries.findMultiAssetHistory,
values: [Buffer.from(policyId, 'hex'), Buffer.from(name, 'hex')]
});
return result.rows;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { AssetBuilder } from './AssetBuilder';
import { AssetPolicyIdAndName, NftMetadataService, TokenMetadataService } from '../types';
import { DB_CACHE_TTL_DEFAULT, InMemoryCache, NoCache } from '../../InMemoryCache';
import { DbSyncProvider, DbSyncProviderDependencies } from '../../util/DbSyncProvider';
import { DbSyncProvider, DbSyncProviderDependencies } from '../../util';

/**
* Properties that are need to create DbSyncAssetProvider
Expand Down Expand Up @@ -74,7 +74,6 @@ export class DbSyncAssetProvider extends DbSyncProvider() implements AssetProvid
async getAsset({ assetId, extraData }: GetAssetArgs) {
const assetInfo = await this.#getAssetInfo(assetId);

if (extraData?.history) await this.loadHistory(assetInfo);
if (extraData?.nftMetadata) assetInfo.nftMetadata = await this.#getNftMetadata(assetInfo);
if (extraData?.tokenMetadata) {
try {
Expand Down Expand Up @@ -133,15 +132,6 @@ export class DbSyncAssetProvider extends DbSyncProvider() implements AssetProvid
return Promise.all(assetIds.map((_) => getAssetInfo(_)));
}

private async loadHistory(assetInfo: Asset.AssetInfo) {
assetInfo.history = (
await this.#builder.queryMultiAssetHistory(assetInfo.policyId, assetInfo.name)
).map<Asset.AssetMintOrBurn>(({ hash, quantity }) => ({
quantity: BigInt(quantity),
transactionId: hash.toString('hex') as unknown as Cardano.TransactionId
}));
}

async #getNftMetadata(asset: AssetPolicyIdAndName): Promise<Asset.NftMetadata | null> {
return this.#cache.get(
nftMetadataCacheKey(Cardano.AssetId.fromParts(asset.policyId, asset.name)),
Expand All @@ -166,9 +156,8 @@ export class DbSyncAssetProvider extends DbSyncProvider() implements AssetProvid
const supply = BigInt(multiAsset.sum);
// Backwards compatibility
const quantity = supply;
const mintOrBurnCount = Number(multiAsset.count);

return { assetId, fingerprint, mintOrBurnCount, name, policyId, quantity, supply };
return { assetId, fingerprint, name, policyId, quantity, supply };
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ export class DbSyncNftMetadataService implements NftMetadataService {
this.#logger.debug('Querying tx metadata', lastMintedTxId);
const metadatas = await this.#metadataService.queryTxMetadataByHashes([lastMintedTxId]);
const metadata = metadatas.get(lastMintedTxId);
return Asset.util.metadatumToCip25(assetInfo, metadata, this.#logger);
return Asset.NftMetadata.fromMetadatum(assetInfo, metadata, this.#logger);
}
}
37 changes: 37 additions & 0 deletions packages/cardano-services/src/Asset/TypeOrmNftMetadataService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Asset, Cardano } from '@cardano-sdk/core';
import { AssetPolicyIdAndName, NftMetadataService } from './types';
import { NftMetadataEntity } from '@cardano-sdk/projection-typeorm';
import { TypeormProviderDependencies, TypeormService } from '../util';

export class TypeOrmNftMetadataService extends TypeormService implements NftMetadataService {
constructor({ connectionConfig$, logger, entities }: TypeormProviderDependencies) {
super('TypeOrmNftMetadataService', { connectionConfig$, entities, logger });
}

async getNftMetadata(assetInfo: AssetPolicyIdAndName): Promise<Asset.NftMetadata | null> {
const assetId = Cardano.AssetId.fromParts(assetInfo.policyId, assetInfo.name);
const stringAssetName = Buffer.from(assetInfo.name, 'hex').toString('utf8');
return this.withDataSource(async (dataSource) => {
const queryRunner = dataSource.createQueryRunner();
const nftMetadataRepository = queryRunner.manager.getRepository(NftMetadataEntity);

const asset = await nftMetadataRepository.findOneBy({
name: stringAssetName,
userTokenAsset: { id: assetId }
});

if (!asset) {
return null;
}

return {
image: asset.image,
name: asset.name,
...(asset.description && { description: asset.description }),
...(asset.files && { files: asset.files }),
...(asset.mediaType && { mediaType: asset.mediaType }),
...(asset.otherProperties && { otherProperties: asset.otherProperties })
} as unknown as Asset.NftMetadata;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
Asset,
AssetProvider,
Cardano,
GetAssetArgs,
GetAssetsArgs,
ProviderError,
ProviderFailure
} from '@cardano-sdk/core';
import { AssetEntity } from '@cardano-sdk/projection-typeorm';
import { TokenMetadataService } from '../types';
import { TypeOrmNftMetadataService } from '../TypeOrmNftMetadataService';
import { TypeormProvider, TypeormProviderDependencies } from '../../util';

interface TypeormAssetProviderProps {
paginationPageSizeLimit: number;
}

interface TypeormAssetProviderDependencies extends TypeormProviderDependencies {
tokenMetadataService: TokenMetadataService;
}

export class TypeormAssetProvider extends TypeormProvider implements AssetProvider {
#dependencies: TypeormAssetProviderDependencies;
#nftMetadataService: TypeOrmNftMetadataService;
#paginationPageSizeLimit: number;

constructor({ paginationPageSizeLimit }: TypeormAssetProviderProps, dependencies: TypeormAssetProviderDependencies) {
const { connectionConfig$, entities, logger } = dependencies;
super('TypeormAssetProvider', { connectionConfig$, entities, logger });

this.#dependencies = dependencies;
this.#paginationPageSizeLimit = paginationPageSizeLimit;
this.#nftMetadataService = new TypeOrmNftMetadataService({ connectionConfig$, entities, logger });
}

async getAsset({ assetId, extraData }: GetAssetArgs): Promise<Asset.AssetInfo> {
const assetInfo = await this.#getAssetInfo(assetId);

if (extraData?.nftMetadata) assetInfo.nftMetadata = await this.#getNftMetadata(assetInfo);
if (extraData?.tokenMetadata) {
assetInfo.tokenMetadata = (await this.#fetchTokenMetadataList([assetId]))[0];
}

return assetInfo;
}

async getAssets({ assetIds, extraData }: GetAssetsArgs): Promise<Asset.AssetInfo[]> {
if (assetIds.length > this.#paginationPageSizeLimit) {
throw new ProviderError(
ProviderFailure.BadRequest,
undefined,
`AssetIds count of ${assetIds.length} can not be greater than ${this.#paginationPageSizeLimit}`
);
}

const assetInfoList = await Promise.all(assetIds.map((assetId) => this.#getAssetInfo(assetId)));

if (extraData?.nftMetadata) {
await Promise.all(
assetInfoList.map(async (assetInfo) => {
assetInfo.nftMetadata = await this.#getNftMetadata(assetInfo);
})
);
}

if (extraData?.tokenMetadata) {
const tokenMetadataList = await this.#fetchTokenMetadataList(assetIds);

for (const [index, assetInfo] of assetInfoList.entries()) {
assetInfo.tokenMetadata = tokenMetadataList[index];
}
}

return assetInfoList;
}

async #fetchTokenMetadataList(assetIds: Cardano.AssetId[]) {
let tokenMetadataList: (Asset.TokenMetadata | null | undefined)[] = [];

try {
tokenMetadataList = await this.#dependencies.tokenMetadataService.getTokenMetadata(assetIds);
} catch (error) {
if (error instanceof ProviderError && error.reason === ProviderFailure.Unhealthy) {
this.logger.error(`Failed to fetch token metadata for assets ${assetIds} due to: ${error.message}`);
tokenMetadataList = Array.from({ length: assetIds.length });
} else {
throw error;
}
}

return tokenMetadataList;
}

async #getNftMetadata(asset: Asset.AssetInfo): Promise<Asset.NftMetadata | null | undefined> {
try {
return this.#nftMetadataService.getNftMetadata({
name: asset.name,
policyId: asset.policyId
});
} catch (error) {
this.logger.error('Failed to get nft metadata', asset.assetId, error);
}
}

async #getAssetInfo(assetId: Cardano.AssetId): Promise<Asset.AssetInfo> {
const assetName = Cardano.AssetId.getAssetName(assetId);
const policyId = Cardano.AssetId.getPolicyId(assetId);
const fingerprint = Cardano.AssetFingerprint.fromParts(policyId, Cardano.AssetName(assetName));

return this.withDataSource(async (dataSource) => {
const queryRunner = dataSource.createQueryRunner();
const assetRepository = queryRunner.manager.getRepository(AssetEntity);

const asset = await assetRepository.findOneBy({ id: assetId });

if (!asset) throw new ProviderError(ProviderFailure.NotFound, undefined, `Asset not found '${assetId}'`);
const supply = asset.supply!;

return {
assetId,
fingerprint,
name: assetName,
policyId,
quantity: supply,
supply
};
});
}

async initializeImpl() {
await super.initializeImpl();
await this.#nftMetadataService.initialize();
}

async startImpl() {
await super.startImpl();
await this.#nftMetadataService.start();
}

async shutdownImpl() {
await super.shutdownImpl();
await this.#nftMetadataService.shutdown();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TypeormAssetProvider';
2 changes: 2 additions & 0 deletions packages/cardano-services/src/Asset/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export * from './CardanoTokenRegistry';
export * from './StubTokenMetadataService';
export * from './DbSyncNftMetadataService';
export * from './DbSyncAssetProvider';
export * from './TypeOrmNftMetadataService';
export * from './TypeormAssetProvider';
export * from './types';
6 changes: 0 additions & 6 deletions packages/cardano-services/src/Asset/openApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,6 @@
"fingerprint": {
"type": "string"
},
"mintOrBurnCount": {
"type": "number"
},
"name": {
"type": "string"
},
Expand Down Expand Up @@ -155,9 +152,6 @@
"ExtraData": {
"type": "object",
"properties": {
"history": {
"type": "boolean"
},
"nftMetadata": {
"type": "boolean"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export class DbSyncChainHistoryProvider extends DbSyncProvider() implements Chai
this.#builder.queryTxMintByIds(ids),
this.#builder.queryWithdrawalsByTxIds(ids),
this.#builder.queryRedeemersByIds(ids),
// Missing witness datums
this.#metadataService.queryTxMetadataByRecordIds(ids),
this.#builder.queryTransactionInputsByIds(ids, true),
this.#builder.queryCertificatesByIds(ids)
Expand Down
Loading

0 comments on commit 948d377

Please sign in to comment.