Skip to content

Commit

Permalink
Merge pull request #141 from input-output-hk/feat/asset-info
Browse files Browse the repository at this point in the history
feat: asset info
  • Loading branch information
rhyslbw committed Nov 22, 2021
2 parents 7e96a8e + 090947c commit e36fa7d
Show file tree
Hide file tree
Showing 54 changed files with 751 additions and 143 deletions.
3 changes: 2 additions & 1 deletion .gitignore
@@ -1,2 +1,3 @@
node_modules
docs
docs
.env
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -22,6 +22,7 @@
"mainnet:up": "DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose -p sdk-mainnet up",
"mainnet:down": "docker-compose -p sdk-mainnet down",
"test": "yarn workspaces run test",
"test:e2e": "yarn workspaces run test:e2e",
"test:debug": "DEBUG=true yarn workspaces run test",
"testnet:up": "DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 OGMIOS_PORT=1338 NETWORK=testnet docker-compose -p sdk-testnet up",
"testnet:down": "docker-compose -p sdk-testnet down",
Expand Down
2 changes: 2 additions & 0 deletions packages/blockfrost/.env.example
@@ -0,0 +1,2 @@
BLOCKFROST_API_KEY=testnetNElagmhpQDubE6Ic4XBUVJjV5DROyijO
NETWORK_ID=0
1 change: 1 addition & 0 deletions packages/blockfrost/e2e.jest.config.js
@@ -0,0 +1 @@
module.exports = require('../../test/e2e.jest.config');
3 changes: 2 additions & 1 deletion packages/blockfrost/package.json
Expand Up @@ -17,14 +17,15 @@
"lint": "eslint --ignore-path ../../.eslintignore \"**/*.ts\"",
"lint:fix": "eslint --fix --ignore-path ../../.eslintignore \"**/*.ts\"",
"test": "jest -c ./jest.config.js",
"test:e2e": "jest -c ./e2e.jest.config.js",
"coverage": "shx echo No coverage report for this package",
"prepack": "yarn build"
},
"devDependencies": {
"shx": "^0.3.3"
},
"dependencies": {
"@blockfrost/blockfrost-js": "^1.3.0",
"@blockfrost/blockfrost-js": "2.0.2",
"@cardano-sdk/core": " 0.1.7"
},
"files": [
Expand Down
64 changes: 64 additions & 0 deletions packages/blockfrost/src/blockfrostAssetProvider.ts
@@ -0,0 +1,64 @@
import { Asset, AssetProvider, Cardano, util } from '@cardano-sdk/core';
import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js';
import { Options } from '@blockfrost/blockfrost-js/lib/types';
import { fetchSequentially, withProviderErrors } from './util';

const mapMetadata = (
onChain: Responses['asset']['onchain_metadata'],
offChain: Responses['asset']['metadata']
): Cardano.AssetMetadata => {
const metadata = { ...onChain, ...offChain };
return {
...util.replaceNullsWithUndefineds(metadata),
desc: metadata.description,
// The other type option is any[] - not sure what it means, omitting if no string.
image: typeof metadata.image === 'string' ? metadata.image : undefined
};
};

/**
* Connect to the [Blockfrost service](https://docs.blockfrost.io/)
*
* @param {Options} options BlockFrostAPI options
* @returns {AssetProvider} WalletProvider
* @throws ProviderFailure
*/
export const blockfrostAssetProvider = (options: Options): AssetProvider => {
const blockfrost = new BlockFrostAPI(options);

const getAssetHistory = async (assetId: string): Promise<Cardano.AssetMintOrBurn[]> =>
fetchSequentially({
arg: assetId,
request: blockfrost.assetsHistory,
responseTranslator: (response): Cardano.AssetMintOrBurn[] =>
response.map(({ action, amount, tx_hash }) => ({
action: action === 'minted' ? Cardano.AssetProvisioning.Mint : Cardano.AssetProvisioning.Burn,
quantity: BigInt(amount),
transactionId: tx_hash
}))
});

const getAsset: AssetProvider['getAsset'] = async (assetId) => {
const response = await blockfrost.assetsById(assetId);
const name = Buffer.from(Asset.util.assetNameFromAssetId(assetId), 'hex').toString('utf-8');
const quantity = BigInt(response.quantity);
return {
assetId,
fingerprint: response.fingerprint,
history:
response.mint_or_burn_count === 1
? [{ action: Cardano.AssetProvisioning.Mint, quantity, transactionId: response.initial_mint_tx_hash }]
: await getAssetHistory(assetId),
metadata: mapMetadata(response.onchain_metadata, response.metadata),
name,
policyId: response.policy_id,
quantity
};
};

const providerFunctions: AssetProvider = {
getAsset
};

return withProviderErrors(providerFunctions);
};
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
BigIntMath,
Cardano,
Expand All @@ -8,93 +7,36 @@ import {
ProviderFailure,
WalletProvider
} from '@cardano-sdk/core';
import { BlockFrostAPI, Error as BlockfrostError, Responses } from '@blockfrost/blockfrost-js';
import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js';
import { BlockfrostToCore, BlockfrostTransactionContent, BlockfrostUtxo } from './BlockfrostToCore';
import { Options, PaginationOptions } from '@blockfrost/blockfrost-js/lib/types';
import { dummyLogger } from 'ts-log';
import { fetchSequentially, formatBlockfrostError, withProviderErrors } from './util';
import { flatten, groupBy } from 'lodash-es';

const formatBlockfrostError = (error: unknown) => {
const blockfrostError = error as BlockfrostError;
if (typeof blockfrostError === 'string') {
throw new ProviderError(ProviderFailure.Unknown, error, blockfrostError);
}
if (typeof blockfrostError !== 'object') {
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (response type)');
}
const errorAsType1 = blockfrostError as {
status_code: number;
message: string;
error: string;
};
if (errorAsType1.status_code) {
return errorAsType1;
}
const errorAsType2 = blockfrostError as {
errno: number;
message: string;
code: string;
};
if (errorAsType2.code) {
const status_code = Number.parseInt(errorAsType2.code);
if (!status_code) {
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (status code)');
}
return {
error: errorAsType2.errno.toString(),
message: errorAsType1.message,
status_code
};
}
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (response json)');
};

const toProviderError = (error: unknown) => {
const { status_code } = formatBlockfrostError(error);
if (status_code === 404) {
throw new ProviderError(ProviderFailure.NotFound);
}
throw new ProviderError(ProviderFailure.Unknown, error, `status_code: ${status_code}`);
};
const fetchByAddressSequentially = async <Item, Response>(props: {
address: Cardano.Address;
request: (address: Cardano.Address, pagination: PaginationOptions) => Promise<Response[]>;
responseTranslator?: (address: Cardano.Address, response: Response[]) => Item[];
}): Promise<Item[]> =>
fetchSequentially({
arg: props.address,
request: props.request,
responseTranslator: props.responseTranslator
? (response, arg) => props.responseTranslator!(arg, response)
: undefined
});

/**
* Connect to the [Blockfrost service](https://docs.blockfrost.io/)
*
* @param {Options} options BlockFrostAPI options
* @returns {WalletProvider} WalletProvider
* @throws {ProviderFailure}
*/
export const blockfrostProvider = (options: Options, logger = dummyLogger): WalletProvider => {
export const blockfrostWalletProvider = (options: Options, logger = dummyLogger): WalletProvider => {
const blockfrost = new BlockFrostAPI(options);

const fetchByAddressSequentially = async <Item, Response>(
props: {
address: Cardano.Address;
request: (address: Cardano.Address, pagination: PaginationOptions) => Promise<Response[]>;
responseTranslator?: (address: Cardano.Address, response: Response[]) => Item[];
},
accumulated: Item[] = [],
count = 0,
page = 1
): Promise<Item[]> => {
try {
const response = await props.request(props.address, { count, page });
const totalCount = count + response.length;
const maybeTranslatedResponse = props.responseTranslator
? props.responseTranslator(props.address, response)
: response;
const newAccumulatedItems = [...accumulated, ...maybeTranslatedResponse] as Item[];
if (response.length === 100) {
return fetchByAddressSequentially<Item, Response>(props, newAccumulatedItems, totalCount, page + 1);
}
return newAccumulatedItems;
} catch (error) {
if (error.status_code === 404) {
return [];
}
throw error;
}
};

const ledgerTip: WalletProvider['ledgerTip'] = async () => {
const block = await blockfrost.blocksLatest();
return BlockfrostToCore.blockToTip(block);
Expand Down Expand Up @@ -172,7 +114,7 @@ export const blockfrostProvider = (options: Options, logger = dummyLogger): Wall
};
return { delegationAndRewards, utxo };
} catch (error) {
if (error.status_code === 404) {
if (formatBlockfrostError(error).status_code === 404) {
return { utxo };
}
throw error;
Expand Down Expand Up @@ -306,12 +248,37 @@ export const blockfrostProvider = (options: Options, logger = dummyLogger): Wall
];
};

const fetchJsonMetadata = async (txHash: Cardano.Hash16): Promise<Cardano.MetadatumMap | null> => {
try {
const response = await blockfrost.txsMetadata(txHash);
return response.reduce((map, metadatum) => {
// Not sure if types are correct, missing 'label', but it's present in docs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { json_metadata, label } = metadatum as any;
if (!json_metadata || !label) return map;
map[label] = json_metadata;
return map;
}, {} as Cardano.MetadatumMap);
} catch (error) {
if (formatBlockfrostError(error).status_code === 404) {
return null;
}
throw error;
}
};

// eslint-disable-next-line unicorn/consistent-function-scoping
const parseValidityInterval = (num: string | null) => Number.parseInt(num || '') || undefined;
const fetchTransaction = async (hash: string): Promise<Cardano.TxAlonzo> => {
const { inputs, outputs } = BlockfrostToCore.transactionUtxos(await blockfrost.txsUtxos(hash));
const response = await blockfrost.txs(hash);
const metadata = await fetchJsonMetadata(hash);
return {
auxiliaryData: metadata
? {
body: { blob: metadata }
}
: undefined,
blockHeader: {
blockHash: response.block,
blockHeight: response.block_height,
Expand Down Expand Up @@ -340,7 +307,6 @@ export const blockfrostProvider = (options: Options, logger = dummyLogger): Wall
redeemers: await fetchRedeemers(response),
signatures: {}
}
// TODO: fetch metadata; not sure we can get the metadata hash and scripts from Blockfrost
};
};

Expand Down Expand Up @@ -468,8 +434,5 @@ export const blockfrostProvider = (options: Options, logger = dummyLogger): Wall
utxoDelegationAndRewards
};

return Object.keys(providerFunctions).reduce((provider, key) => {
provider[key] = (...args: any[]) => (providerFunctions as any)[key](...args).catch(toProviderError);
return provider;
}, {} as any) as WalletProvider;
return withProviderErrors(providerFunctions);
};
3 changes: 2 additions & 1 deletion packages/blockfrost/src/index.ts
@@ -1,3 +1,4 @@
export { WalletProvider } from '@cardano-sdk/core';
export * from './blockfrostProvider';
export * from './blockfrostWalletProvider';
export * from './blockfrostAssetProvider';
export { Options } from '@blockfrost/blockfrost-js/lib/types';
79 changes: 79 additions & 0 deletions packages/blockfrost/src/util.ts
@@ -0,0 +1,79 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Error as BlockfrostError } from '@blockfrost/blockfrost-js';
import { PaginationOptions } from '@blockfrost/blockfrost-js/lib/types';
import { ProviderError, ProviderFailure } from '@cardano-sdk/core';

export const formatBlockfrostError = (error: unknown) => {
const blockfrostError = error as BlockfrostError;
if (typeof blockfrostError === 'string') {
throw new ProviderError(ProviderFailure.Unknown, error, blockfrostError);
}
if (typeof blockfrostError !== 'object') {
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (response type)');
}
const errorAsType1 = blockfrostError as {
status_code: number;
message: string;
error: string;
};
if (errorAsType1.status_code) {
return errorAsType1;
}
const errorAsType2 = blockfrostError as {
errno: number;
message: string;
code: string;
};
if (errorAsType2.code) {
const status_code = Number.parseInt(errorAsType2.code);
if (!status_code) {
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (status code)');
}
return {
error: errorAsType2.errno.toString(),
message: errorAsType1.message,
status_code
};
}
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (response json)');
};

export const toProviderError = (error: unknown) => {
const { status_code } = formatBlockfrostError(error);
if (status_code === 404) {
throw new ProviderError(ProviderFailure.NotFound);
}
throw new ProviderError(ProviderFailure.Unknown, error, `status_code: ${status_code}`);
};

export const withProviderErrors = <T>(providerImplementation: T) =>
Object.keys(providerImplementation).reduce((provider, key) => {
provider[key] = (...args: any[]) => (providerImplementation as any)[key](...args).catch(toProviderError);
return provider;
}, {} as any) as T;

export const fetchSequentially = async <Item, Arg, Response>(
props: {
arg: Arg;
request: (arg: Arg, pagination: PaginationOptions) => Promise<Response[]>;
responseTranslator?: (response: Response[], arg: Arg) => Item[];
},
itemsPerPage = 100,
page = 1,
accumulated: Item[] = []
): Promise<Item[]> => {
try {
const response = await props.request(props.arg, { count: itemsPerPage, page });
const maybeTranslatedResponse = props.responseTranslator ? props.responseTranslator(response, props.arg) : response;
const newAccumulatedItems = [...accumulated, ...maybeTranslatedResponse] as Item[];
if (response.length === itemsPerPage) {
return fetchSequentially<Item, Arg, Response>(props, itemsPerPage, page + 1, newAccumulatedItems);
}
return newAccumulatedItems;
} catch (error) {
if (formatBlockfrostError(error).status_code === 404) {
return [];
}
throw error;
}
};

0 comments on commit e36fa7d

Please sign in to comment.