Skip to content

Commit

Permalink
feat: add actual data instead of counts to TransactionDetails
Browse files Browse the repository at this point in the history
  • Loading branch information
mkazlauskas committed Oct 26, 2021
1 parent 6f52f5e commit 19be1c1
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 106 deletions.
145 changes: 129 additions & 16 deletions packages/blockfrost/src/blockfrostProvider.ts
@@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { WalletProvider, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { BlockFrostAPI, Error as BlockfrostError } from '@blockfrost/blockfrost-js';
import { WalletProvider, ProviderError, ProviderFailure, Ogmios, Transaction } from '@cardano-sdk/core';
import { BlockFrostAPI, Error as BlockfrostError, Responses } from '@blockfrost/blockfrost-js';
import { Options } from '@blockfrost/blockfrost-js/lib/types';
import { BlockfrostToOgmios } from './BlockfrostToOgmios';
import { BlockfrostToCore } from './blockfrostToCore';
import { dummyLogger } from 'ts-log';

const formatBlockfrostError = (error: unknown) => {
const blockfrostError = error as BlockfrostError;
Expand Down Expand Up @@ -54,7 +55,7 @@ const toProviderError = (error: unknown) => {
* @param {Options} options BlockFrostAPI options
* @returns {WalletProvider} WalletProvider
*/
export const blockfrostProvider = (options: Options): WalletProvider => {
export const blockfrostProvider = (options: Options, logger = dummyLogger): WalletProvider => {
const blockfrost = new BlockFrostAPI(options);

const ledgerTip: WalletProvider['ledgerTip'] = async () => {
Expand Down Expand Up @@ -153,30 +154,142 @@ export const blockfrostProvider = (options: Options): WalletProvider => {
return BlockfrostToOgmios.currentWalletProtocolParameters(response.data);
};

const fetchRedeemers = async ({
redeemer_count,
hash
}: Responses['tx_content']): Promise<Transaction.Redeemer[] | undefined> => {
if (!redeemer_count) return;
const response = await blockfrost.txsRedeemers(hash);
return response.map(
({ datum_hash, fee, purpose, script_hash, unit_mem, unit_steps, tx_index }): Transaction.Redeemer => ({
index: tx_index,
datumHash: datum_hash,
executionUnits: {
memory: Number.parseInt(unit_mem),
steps: Number.parseInt(unit_steps)
},
fee: BigInt(fee),
purpose,
scriptHash: script_hash
})
);
};

const fetchWithdrawals = async ({
withdrawal_count,
hash
}: Responses['tx_content']): Promise<Transaction.Withdrawal[] | undefined> => {
if (!withdrawal_count) return;
const response = await blockfrost.txsWithdrawals(hash);
return response.map(
({ address, amount }): Transaction.Withdrawal => ({
address,
quantity: BigInt(amount)
})
);
};

const fetchMint = async ({
asset_mint_or_burn_count,
hash
}: Responses['tx_content']): Promise<Ogmios.TokenMap | undefined> => {
if (!asset_mint_or_burn_count) return;
logger.warn(`Skipped fetching asset mint/burn for tx "${hash}": not implemented for Blockfrost provider`);
};

const fetchPoolRetireCerts = async (hash: string): Promise<Transaction.PoolCertificate[]> => {
const response = await blockfrost.txsPoolRetires(hash);
return response.map(({ cert_index, pool_id, retiring_epoch }) => ({
epoch: retiring_epoch,
certIndex: cert_index,
poolId: pool_id,
type: Transaction.CertificateType.PoolRetirement
}));
};

const fetchPoolUpdateCerts = async (hash: string): Promise<Transaction.PoolCertificate[]> => {
const response = await blockfrost.txsPoolUpdates(hash);
return response.map(({ cert_index, pool_id, active_epoch }) => ({
epoch: active_epoch,
certIndex: cert_index,
poolId: pool_id,
type: Transaction.CertificateType.PoolRegistration
}));
};

const fetchMirCerts = async (hash: string): Promise<Transaction.MirCertificate[]> => {
const response = await blockfrost.txsMirs(hash);
return response.map(({ address, amount, cert_index, pot }) => ({
type: Transaction.CertificateType.MIR,
address,
quantity: BigInt(amount),
certIndex: cert_index,
pot
}));
};

const fetchStakeCerts = async (hash: string): Promise<Transaction.StakeAddressCertificate[]> => {
const response = await blockfrost.txsStakes(hash);
return response.map(({ address, cert_index, registration }) => ({
type: registration
? Transaction.CertificateType.StakeRegistration
: Transaction.CertificateType.StakeDeregistration,
address,
certIndex: cert_index
}));
};

const fetchDelegationCerts = async (hash: string): Promise<Transaction.StakeDelegationCertificate[]> => {
const response = await blockfrost.txsDelegations(hash);
return response.map(({ cert_index, index, address, active_epoch, pool_id }) => ({
type: Transaction.CertificateType.StakeDelegation,
certIndex: cert_index,
delegationIndex: index,
address,
epoch: active_epoch,
poolId: pool_id
}));
};

const fetchCertificates = async ({
pool_retire_count,
pool_update_count,
mir_cert_count,
stake_cert_count,
delegation_count,
hash
}: Responses['tx_content']): Promise<Transaction.Certificate[] | undefined> => {
if (pool_retire_count + pool_update_count + mir_cert_count + stake_cert_count + delegation_count === 0) return;
return [
...(await fetchPoolRetireCerts(hash)),
...(await fetchPoolUpdateCerts(hash)),
...(await fetchMirCerts(hash)),
...(await fetchStakeCerts(hash)),
...(await fetchDelegationCerts(hash))
];
};

// eslint-disable-next-line unicorn/consistent-function-scoping
const parseValidityInterval = (num: string | null) => Number.parseInt(num || '') || undefined;
const transactionDetails: WalletProvider['transactionDetails'] = async (hash) => {
const response = await blockfrost.txs(hash);
return {
slot: response.slot,
block: response.block,
blockHeight: response.block_height,
block: {
slot: response.slot,
blockNo: response.block_height,
hash: response.block
},
index: response.index,
deposit: BigInt(response.deposit),
fee: BigInt(response.fees),
index: response.index,
numAssetMintOrBurn: response.asset_mint_or_burn_count,
numRedeemer: response.redeemer_count,
numUtxos: response.utxo_count,
numWithdrawals: response.withdrawal_count,
size: response.size,
validContract: response.valid_contract,
invalidBefore: parseValidityInterval(response.invalid_before),
invalidHereafter: parseValidityInterval(response.invalid_hereafter),
numCerts: {
delegation: response.delegation_count,
stakeKey: response.stake_cert_count,
stakePool: response.pool_update_count + response.pool_retire_count
}
redeemers: await fetchRedeemers(response),
withdrawals: await fetchWithdrawals(response),
mint: await fetchMint(response),
certificates: await fetchCertificates(response)
};
};

Expand Down
119 changes: 61 additions & 58 deletions packages/blockfrost/test/blockfrostProvider.test.ts
Expand Up @@ -383,65 +383,68 @@ describe('blockfrostProvider', () => {
});
});

test('transactionDetails', async () => {
const mockedResponse = {
hash: '1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477',
block: '356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940',
block_height: 123_456,
slot: 42_000_000,
index: 1,
output_amount: [
{
unit: 'lovelace',
quantity: '42000000'
describe('transactionDetails', () => {
it('without extra tx properties', async () => {
const mockedResponse = {
hash: '1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477',
block: '356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940',
block_height: 123_456,
slot: 42_000_000,
index: 1,
output_amount: [
{
unit: 'lovelace',
quantity: '42000000'
},
{
unit: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e',
quantity: '12'
}
],
fees: '182485',
deposit: '5',
size: 433,
invalid_before: null,
invalid_hereafter: '13885913',
utxo_count: 2,
withdrawal_count: 0,
mir_cert_count: 0,
delegation_count: 0,
stake_cert_count: 0,
pool_update_count: 0,
pool_retire_count: 0,
asset_mint_or_burn_count: 0,
redeemer_count: 0,
valid_contract: true
};
BlockFrostAPI.prototype.txs = jest.fn().mockResolvedValue(mockedResponse) as any;
const client = blockfrostProvider({ projectId: apiKey, isTestnet: true });
const response = await client.transactionDetails(
'1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477'
);

expect(response).toMatchObject({
block: {
slot: 42_000_000,
hash: '356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940',
blockNo: 123_456
},
{
unit: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e',
quantity: '12'
}
],
fees: '182485',
deposit: '5',
size: 433,
invalid_before: null,
invalid_hereafter: '13885913',
utxo_count: 4,
withdrawal_count: 1,
mir_cert_count: 2,
delegation_count: 3,
stake_cert_count: 4,
pool_update_count: 5,
pool_retire_count: 6,
asset_mint_or_burn_count: 7,
redeemer_count: 8,
valid_contract: true
};
BlockFrostAPI.prototype.txs = jest.fn().mockResolvedValue(mockedResponse) as any;
const client = blockfrostProvider({ projectId: apiKey, isTestnet: true });
const response = await client.transactionDetails(
'1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477'
);

expect(response).toMatchObject({
block: '356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940',
blockHeight: 123_456,
deposit: 5n,
fee: 182_485n,
index: 1,
numAssetMintOrBurn: 7,
numRedeemer: 8,
numWithdrawals: 1,
numUtxos: 4,
size: 433,
slot: 42_000_000,
validContract: true,
invalidHereafter: 13_885_913,
numCerts: {
delegation: 3,
stakeKey: 4,
stakePool: 11
}
} as Transaction.TxDetails);
deposit: 5n,
fee: 182_485n,
index: 1,
size: 433,
validContract: true,
invalidHereafter: 13_885_913
} as Transaction.TxDetails);
});
it.todo('with withdrawals');
it.todo('with redeemer');
it.todo('with mint');
it.todo('with MIR cert');
it.todo('with delegation cert');
it.todo('with stake certs');
it.todo('with pool update certs');
it.todo('with pool retire certs');
});

test('currentWalletProtocolParameters', async () => {
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/Asset/util.ts
@@ -1,15 +1,17 @@
import { CSL } from '../CSL';

export const policyIdFromAssetId = (assetId: string): string => assetId.slice(0, 56);
export const assetNameFromAssetId = (assetId: string): string => assetId.slice(56);
export type AssetId = string;

export const policyIdFromAssetId = (assetId: AssetId): string => assetId.slice(0, 56);
export const assetNameFromAssetId = (assetId: AssetId): string => assetId.slice(56);

/**
* @returns {string} concatenated hex-encoded policy id and asset name
*/
export const createAssetId = (scriptHash: CSL.ScriptHash, assetName: CSL.AssetName): string =>
export const createAssetId = (scriptHash: CSL.ScriptHash, assetName: CSL.AssetName): AssetId =>
Buffer.from(scriptHash.to_bytes()).toString('hex') + Buffer.from(assetName.name()).toString('hex');

export const parseAssetId = (assetId: string) => {
export const parseAssetId = (assetId: AssetId) => {
const policyId = policyIdFromAssetId(assetId);
const assetName = assetNameFromAssetId(assetId);
return {
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/CSL/cslToCore.ts
Expand Up @@ -2,8 +2,6 @@ import { Transaction } from '@emurgo/cardano-serialization-lib-nodejs';
import { Tx, TxDetails } from '../Transaction';

// TODO: probably move stuff over from cslToOgmios;
// Review: need to figure out a way to determine which ogmios
// conversion utils are going to be needed to interface directly with ogmios?
export const tx = (_input: Transaction): Tx & { details: TxDetails } => {
throw new Error('Not implemented');
};
2 changes: 1 addition & 1 deletion packages/core/src/Cardano/types/StakePool.ts
Expand Up @@ -27,7 +27,7 @@ export interface Cip6MetadataFields {
* the public Key for verification
* optional, 68 Characters
*/
extVkey?: Hash16; // Review: is this the correct type alias?
extVkey?: Hash16; // TODO: figure out if this is the correct type alias
}

export interface StakePoolMetadataFields {
Expand Down

0 comments on commit 19be1c1

Please sign in to comment.