-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cardano-services): implement missing Blockfrost providers featur…
…es and tests
- Loading branch information
Showing
22 changed files
with
1,500 additions
and
1,142 deletions.
There are no files selected for viewing
122 changes: 56 additions & 66 deletions
122
packages/cardano-services/src/Asset/BlockfrostAssetProvider/BlockfrostAssetProvider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,95 +1,85 @@ | ||
/* eslint-disable unicorn/no-nested-ternary */ | ||
import { Asset, AssetProvider, Cardano, ProviderUtil } from '@cardano-sdk/core'; | ||
import { Asset, AssetProvider, Cardano, GetAssetArgs, GetAssetsArgs } from '@cardano-sdk/core'; | ||
import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js'; | ||
import { blockfrostMetadataToTxMetadata, fetchSequentially, healthCheck, toProviderError } from '../../util'; | ||
import { BlockfrostProvider } from '../../util/BlockfrostProvider/BlockfrostProvider'; | ||
import { blockfrostMetadataToTxMetadata, blockfrostToProviderError, fetchSequentially } from '../../util'; | ||
import { replaceNullsWithUndefineds } from '@cardano-sdk/util'; | ||
|
||
const mapMetadata = ( | ||
assetId: Cardano.AssetId, | ||
offChain: Responses['asset']['metadata'] | ||
): Asset.TokenMetadata | null => { | ||
const { logo, ...metadata } = { ...offChain }; | ||
|
||
if (Object.values(metadata).every((value) => value === undefined || value === null)) return null; | ||
|
||
return { | ||
...replaceNullsWithUndefineds(metadata), | ||
assetId, | ||
desc: metadata.description, | ||
// The other type option is any[] - not sure what it means, omitting if no string. | ||
icon: typeof logo === 'string' ? logo : undefined | ||
}; | ||
}; | ||
|
||
/** | ||
* Connect to the [Blockfrost service](https://docs.blockfrost.io/) | ||
* | ||
* @param {BlockFrostAPI} blockfrost BlockFrostAPI instance | ||
* @returns {AssetProvider} AssetProvider | ||
* @throws ProviderFailure | ||
*/ | ||
export const blockfrostAssetProvider = (blockfrost: BlockFrostAPI): AssetProvider => { | ||
const getLastMintedTx = async (assetId: Cardano.AssetId): Promise<Responses['asset_history'][number] | undefined> => { | ||
export class BlockfrostAssetProvider extends BlockfrostProvider implements AssetProvider { | ||
protected async getLastMintedTx(assetId: Cardano.AssetId): Promise<Responses['asset_history'][number] | undefined> { | ||
const [lastMintedTx] = await fetchSequentially({ | ||
arg: assetId.toString(), | ||
haveEnoughItems: (items: Responses['asset_history']): boolean => items.length > 0, | ||
paginationOptions: { order: 'desc' }, | ||
request: blockfrost.assetsHistory.bind<BlockFrostAPI['assetsHistory']>(blockfrost), | ||
request: this.blockfrost.assetsHistory.bind<BlockFrostAPI['assetsHistory']>(this.blockfrost), | ||
responseTranslator: (response): Responses['asset_history'] => response.filter((tx) => tx.action === 'minted') | ||
}); | ||
|
||
if (!lastMintedTx) return undefined; | ||
return lastMintedTx; | ||
}; | ||
} | ||
|
||
const getNftMetadata = async ( | ||
protected async getNftMetadata( | ||
asset: Pick<Asset.AssetInfo, 'name' | 'policyId'>, | ||
lastMintedTxHash: string | ||
): Promise<Asset.NftMetadata | null> => { | ||
const metadata = await blockfrost.txsMetadata(lastMintedTxHash); | ||
): Promise<Asset.NftMetadata | null> { | ||
const metadata = await this.blockfrost.txsMetadata(lastMintedTxHash); | ||
// Not sure if types are correct, missing 'label', but it's present in docs | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const metadatumMap = blockfrostMetadataToTxMetadata(metadata as any); | ||
return Asset.NftMetadata.fromMetadatum(asset, metadatumMap, console) ?? null; | ||
}; | ||
} | ||
|
||
const getAsset: AssetProvider['getAsset'] = async ({ assetId, extraData }) => { | ||
const response = await blockfrost.assetsById(assetId.toString()); | ||
const name = Cardano.AssetId.getAssetName(assetId); | ||
const policyId = Cardano.PolicyId(response.policy_id); | ||
const quantity = BigInt(response.quantity); | ||
protected mapMetadata( | ||
assetId: Cardano.AssetId, | ||
offChain: Responses['asset']['metadata'] | ||
): Asset.TokenMetadata | null { | ||
const { logo, ...metadata } = { ...offChain }; | ||
|
||
const nftMetadata = async () => { | ||
let lastMintedTxHash: string = response.initial_mint_tx_hash; | ||
if (response.mint_or_burn_count > 1) { | ||
const lastMintedTx = await getLastMintedTx(assetId); | ||
if (lastMintedTx) lastMintedTxHash = lastMintedTx.tx_hash; | ||
} | ||
return getNftMetadata({ name, policyId }, lastMintedTxHash); | ||
}; | ||
if (Object.values(metadata).every((value) => value === undefined || value === null)) return null; | ||
|
||
return { | ||
...replaceNullsWithUndefineds(metadata), | ||
assetId, | ||
fingerprint: Cardano.AssetFingerprint(response.fingerprint), | ||
// history: extraData?.history ? await history() : undefined, | ||
mintOrBurnCount: response.mint_or_burn_count, | ||
name, | ||
nftMetadata: extraData?.nftMetadata ? await nftMetadata() : undefined, | ||
policyId, | ||
quantity, | ||
supply: quantity, | ||
tokenMetadata: extraData?.tokenMetadata ? mapMetadata(assetId, response.metadata) : undefined | ||
desc: metadata.description, | ||
// The other type option is any[] - not sure what it means, omitting if no string. | ||
icon: typeof logo === 'string' ? logo : undefined | ||
}; | ||
}; | ||
} | ||
|
||
const getAssets: AssetProvider['getAssets'] = async ({ assetIds, extraData }) => | ||
Promise.all(assetIds.map((assetId) => getAsset({ assetId, extraData }))); | ||
async getAsset({ assetId, extraData }: GetAssetArgs) { | ||
try { | ||
const response = await this.blockfrost.assetsById(assetId.toString()); | ||
const name = Cardano.AssetId.getAssetName(assetId); | ||
const policyId = Cardano.PolicyId(response.policy_id); | ||
const quantity = BigInt(response.quantity); | ||
|
||
const providerFunctions: AssetProvider = { | ||
getAsset, | ||
getAssets, | ||
healthCheck: healthCheck.bind(undefined, blockfrost) | ||
}; | ||
const nftMetadata = async () => { | ||
let lastMintedTxHash: string = response.initial_mint_tx_hash; | ||
if (response.mint_or_burn_count > 1) { | ||
const lastMintedTx = await this.getLastMintedTx(assetId); | ||
if (lastMintedTx) lastMintedTxHash = lastMintedTx.tx_hash; | ||
} | ||
return this.getNftMetadata({ name, policyId }, lastMintedTxHash); | ||
}; | ||
|
||
return ProviderUtil.withProviderErrors(providerFunctions, toProviderError); | ||
}; | ||
return { | ||
assetId, | ||
fingerprint: Cardano.AssetFingerprint(response.fingerprint), | ||
// history: extraData?.history ? await history() : undefined, | ||
mintOrBurnCount: response.mint_or_burn_count, | ||
name, | ||
nftMetadata: extraData?.nftMetadata ? await nftMetadata() : undefined, | ||
policyId, | ||
quantity, | ||
supply: quantity, | ||
tokenMetadata: extraData?.tokenMetadata ? this.mapMetadata(assetId, response.metadata) : undefined | ||
}; | ||
} catch (error) { | ||
throw blockfrostToProviderError(error); | ||
} | ||
} | ||
async getAssets({ assetIds, extraData }: GetAssetsArgs) { | ||
return Promise.all(assetIds.map((assetId) => this.getAsset({ assetId, extraData }))); | ||
} | ||
} |
Oops, something went wrong.