Skip to content

Commit

Permalink
chore(addresses): replace CIP-25 v2 validation for extended call
Browse files Browse the repository at this point in the history
  • Loading branch information
1000101 committed Nov 22, 2022
1 parent b2e1daf commit 7e4b3a7
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 114 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- refactor test environments
- production logger logs only with debug option
- replaced and improved (v2) CIP-25 validation logic for `/addresses/{address}/extended`

### Fixed

Expand Down
23 changes: 19 additions & 4 deletions src/routes/addresses/index.ts
Expand Up @@ -5,7 +5,11 @@ import { getDbSync } from '../../utils/database';
import { getSchemaForEndpoint } from '@blockfrost/openapi';
import { getAdditionalParametersFromRequest } from '../../utils/string-utils';
import { handle400Custom, handle404, handleInvalidAddress } from '../../utils/error-handler';
import { getAddressTypeAndPaymentCred, paymentCredToBech32Address } from '../../utils/validation';
import {
getAddressTypeAndPaymentCred,
getOnchainMetadata,
paymentCredToBech32Address,
} from '../../utils/validation';
import { SQLQuery } from '../../sql';
import { fetchAssetMetadata } from '../../utils/token-registry';
import { handleInvalidAsset } from '@blockfrost/blockfrost-utils/lib/fastify';
Expand Down Expand Up @@ -131,13 +135,24 @@ async function addresses(fastify: FastifyInstance) {

if (rows[0].amount) {
for (const asset of rows[0].amount) {
const registryData = await fetchAssetMetadata(asset.unit);
const unit = `${asset.policy_id}${asset.asset_name}`;
const registryData = await fetchAssetMetadata(unit);

let has_nft_onchain_metadata = false;

const validOnchainMetadata = getOnchainMetadata(
asset.onchain_metadata,
asset.asset_name,
asset.policy_id,
);

if (validOnchainMetadata) has_nft_onchain_metadata = true;

assetsAmount.push({
unit: asset.unit,
unit: unit,
quantity: asset.quantity,
decimals: registryData?.decimals ?? null,
has_nft_onchain_metadata: asset.has_nft_onchain_metadata,
has_nft_onchain_metadata: has_nft_onchain_metadata,
});
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/routes/assets/index.ts
Expand Up @@ -68,7 +68,12 @@ async function assets(fastify: FastifyInstance) {
}

const metadata = await fetchAssetMetadata(request.params.asset);
const onchainMetadata = getOnchainMetadata(rows[0]);
const onchainMetadata = getOnchainMetadata(
rows[0].onchain_metadata,
rows[0].asset_name,
rows[0].policy_id,
);

const fingerprint = AssetFingerprint.fromParts(
Uint8Array.from(Buffer.from(rows[0].policy_id, 'hex')),
Uint8Array.from(Buffer.from(rows[0].asset_name ?? '', 'hex')),
Expand Down
35 changes: 10 additions & 25 deletions src/sql/addresses/addresses_address_extended.sql
Expand Up @@ -49,23 +49,24 @@ SELECT (
(
SELECT json_agg(
json_build_object(
'unit',
'policy_id',
token_policy,
'asset_name',
token_name,
'quantity',
token_quantity::TEXT,
-- cast to TEXT to avoid number overflow
'has_nft_onchain_metadata',
has_nft_onchain_metadata
'onchain_metadata',
onchain_metadata
)
)
FROM (
SELECT CONCAT(encode(policy, 'hex'), encode(name, 'hex')) AS "token_name",
SELECT encode(policy, 'hex') AS "token_policy",
encode(name, 'hex') AS "token_name",
SUM(quantity) AS "token_quantity",
(
SELECT CASE
WHEN txm.json->encode(ma.policy, 'hex')->convert_from(ma.name, 'UTF8') IS NOT NULL THEN 'true'
ELSE 'false'
END
-- retrieve the latest metadata for further CIP-25 v2 validation outside of SQL
SELECT txm.json
FROM tx_metadata txm
WHERE txm.tx_id = (
SELECT MAX(txmmax.tx_id)
Expand All @@ -77,25 +78,9 @@ SELECT (
AND (
encode(mamax.policy, 'hex') || encode(mamax.name, 'hex')
) = encode(ma.policy, 'hex') || encode(ma.name, 'hex')
--AND bf_fn_is_valid_utf8(mamax.name) = 'true'
/*
^ To use the condition above, you first have to create
a custom function in dbsync:
CREATE OR REPLACE FUNCTION bf_fn_is_valid_utf8(BYTEA) RETURNS BOOLEAN AS $$
BEGIN
PERFORM convert_from($1, 'UTF8');
RETURN TRUE;
EXCEPTION
WHEN character_not_in_repertoire
THEN
RAISE WARNING '%', SQLERRM;
RETURN FALSE;
END; $$ LANGUAGE plpgsql;
*/
)
AND txm.key = 721
) AS "has_nft_onchain_metadata"
) AS "onchain_metadata"
FROM ma_tx_out mto
JOIN multi_asset ma ON (mto.ident = ma.id)
WHERE mto.id IN (
Expand Down
23 changes: 13 additions & 10 deletions src/types/common.ts
@@ -1,19 +1,22 @@
import { components } from '@blockfrost/openapi';

// generic types

export type Order = 'asc' | 'desc';
export type AddressType = 'byron' | 'shelley';
export interface Amount {
unit: string;
quantity: string;
}

export interface AmountExtended extends Amount {
decimals: null | number;
has_nft_onchain_metadata: boolean;
}
export const CARDANO_NETWORKS = ['mainnet', 'testnet', 'preview', 'preprod'];

export type Network = 'mainnet' | 'testnet' | 'preview' | 'preprod';

export interface ResultFound {
result: number;
}

export const CARDANO_NETWORKS = ['mainnet', 'testnet', 'preview', 'preprod'];
// less generic types

export type Network = 'mainnet' | 'testnet' | 'preview' | 'preprod';
type OnchainMetadataItem = components['schemas']['asset']['onchain_metadata'];

export type OnchainMetadata = {
version?: number;
} & Record<string, Record<string, OnchainMetadataItem>>;
21 changes: 14 additions & 7 deletions src/types/queries/addresses.ts
@@ -1,4 +1,4 @@
import { Amount, AmountExtended, Order } from '../common';
import { OnchainMetadata, Order } from '../common';
export type { ResultFound } from '../common';
export interface RequestParameters {
Params: {
Expand Down Expand Up @@ -52,6 +52,19 @@ export interface RequestParametersTransactions {
};
}

export interface Amount {
unit: string;
quantity: string;
}

export interface AmountExtended {
asset_name: string;
policy_id: string;
quantity: string;
decimals: null | number;
onchain_metadata: OnchainMetadata | null;
}

export interface AddressQuery {
address: string;
amount_lovelace: string;
Expand Down Expand Up @@ -89,12 +102,6 @@ export interface AddressUtxosQuery {
reference_script_hash: string;
}

export interface AmountExtendedQuery {
unit: string;
quantity: string;
has_nft_onchain_metadata: boolean;
}

export interface AddressExtendedQuery {
address: string;
amount_lovelace: string;
Expand Down
10 changes: 1 addition & 9 deletions src/types/queries/assets.ts
@@ -1,5 +1,4 @@
import { Order } from '../common';
import { components } from '@blockfrost/openapi';
import { OnchainMetadata, Order } from '../common';
export type { ResultFound } from '../common';

export interface AssetsPolicyFound {
Expand Down Expand Up @@ -74,17 +73,10 @@ interface Metadata {
logo: string | null;
}

type OnchainMetadataItem = components['schemas']['asset']['onchain_metadata'];

export type OnchainMetadata = {
version?: number;
} & Record<string, Record<string, OnchainMetadataItem>>;

export interface Asset {
asset: string;
policy_id: string;
asset_name: string | null;
asset_name_UTF8: string | null;
fingerprint: string;
quantity: string;
initial_mint_tx_id: string;
Expand Down
22 changes: 13 additions & 9 deletions src/utils/validation.ts
Expand Up @@ -48,29 +48,33 @@ export const getOnchainMetadataVersion = (onchainMetadata: Asset['onchain_metada
return 1;
};

export const getOnchainMetadata = (assetResponse: Asset) => {
if (!assetResponse.onchain_metadata || !assetResponse.asset_name) return null;
export const getOnchainMetadata = (
onchainMetadata: Asset['onchain_metadata'],
assetName: Asset['asset_name'],
policyId: Asset['policy_id'],
) => {
if (!onchainMetadata || !assetName) return null;

let assetName = assetResponse.asset_name;
const policyId = assetResponse.policy_id;
const version = getOnchainMetadataVersion(assetResponse.onchain_metadata);
const version = getOnchainMetadataVersion(onchainMetadata);

if (version === 1) {
assetName = Buffer.from(assetResponse.asset_name, 'hex').toString('utf8');
assetName = Buffer.from(assetName, 'hex').toString('utf8');
}

// version 2 is default

try {
const assetSchema = getSchemaForEndpoint('/assets/{asset}');
const onchainMetadataSchema = assetSchema['response']['200']['properties']['onchain_metadata'];
const validateOnchainMetadata = ajv.compile(onchainMetadataSchema);
const onchainMetadata = assetResponse.onchain_metadata[policyId][assetName];
const isSchemaValid = validateOnchainMetadata(onchainMetadata);
const onchainMetadataResult = onchainMetadata[policyId][assetName];
const isSchemaValid = validateOnchainMetadata(onchainMetadataResult);

if (!isSchemaValid) {
return null;
}

return onchainMetadata;
return onchainMetadataResult;
} catch (error) {
console.log(error);
return null;
Expand Down
5 changes: 2 additions & 3 deletions test/integration/fixtures/mainnet/assets.ts
Expand Up @@ -480,13 +480,12 @@ export default [
quantity: '1',
initial_mint_tx_hash: '7ffa840579680db30b5a8b4aa25642198bf02fb63e265c9021ee48f17392f33d',
mint_or_burn_count: 1,
onchain_metadata: null, // TODO: this should not be null, ref: https://github.com/input-output-hk/cardano-node/issues/3272
onchain_metadata: null,
metadata: null,
},
},
{
testName:
'assets/:asset - non-valid according to https://github.com/cardano-foundation/CIPs/pull/85/files',
testName: 'assets/:asset - non-valid according to https://cips.cardano.org/cips/cip25/',
endpoints: [
'assets/0e14267a8020229adc0184dd25fa3174c3f7d6caadcb4425c70e7c04756e7369673033323839',
],
Expand Down
8 changes: 6 additions & 2 deletions test/unit/tests/utils/validation.ts
Expand Up @@ -109,8 +109,12 @@ describe('validation-format-utils', () => {

parseOnChainMetadataFixtures.map(fixture => {
test(fixture.name, async () => {
// @ts-expect-error tests
const result = validationUtils.getOnchainMetadata(fixture.data);
const result = validationUtils.getOnchainMetadata(
// @ts-expect-error tests
fixture.data.onchain_metadata,
fixture.data.asset_name,
fixture.data.policy_id,
);

expect(result).toStrictEqual(fixture.response);
});
Expand Down

0 comments on commit 7e4b3a7

Please sign in to comment.