Skip to content

Commit

Permalink
feat: add WalletProvider.transactionDetails, add address to TxIn
Browse files Browse the repository at this point in the history
  • Loading branch information
mkazlauskas committed Oct 27, 2021
1 parent 1d86393 commit 889a39b
Show file tree
Hide file tree
Showing 17 changed files with 458 additions and 68 deletions.
5 changes: 2 additions & 3 deletions packages/blockfrost/src/BlockfrostToOgmios.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Responses } from '@blockfrost/blockfrost-js';
import * as OgmiosSchema from '@cardano-ogmios/schema';
import { ProtocolParametersRequiredByWallet, Transaction } from '@cardano-sdk/core';
import { ProtocolParametersRequiredByWallet } from '@cardano-sdk/core';

type Unpacked<T> = T extends (infer U)[] ? U : T;
type BlockfrostAddressUtxoContent = Responses['address_utxo_content'];
Expand Down Expand Up @@ -41,8 +41,7 @@ export const BlockfrostToOgmios = {
outputs: (outputs: BlockfrostOutputs): OgmiosSchema.TxOut[] =>
outputs.map((output) => BlockfrostToOgmios.txOut(output)),

txContentUtxo: (blockfrost: Responses['tx_content_utxo']): Transaction.WithHash => ({
hash: blockfrost.hash,
txContentUtxo: (blockfrost: Responses['tx_content_utxo']): OgmiosSchema.Tx => ({
inputs: BlockfrostToOgmios.inputs(blockfrost.inputs),
outputs: BlockfrostToOgmios.outputs(blockfrost.outputs)
}),
Expand Down
152 changes: 147 additions & 5 deletions packages/blockfrost/src/blockfrostProvider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +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 @@ -53,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 @@ -111,7 +113,7 @@ export const blockfrostProvider = (options: Options): WalletProvider => {
const utxoDelegationAndRewards: WalletProvider['utxoDelegationAndRewards'] = async (addresses, stakeKeyHash) => {
const utxoResults = await Promise.all(
addresses.map(async (address) =>
blockfrost.addressesUtxosAll(address).then((result) => BlockfrostToOgmios.addressUtxoContent(address, result))
blockfrost.addressesUtxosAll(address).then((result) => BlockfrostToCore.addressUtxoContent(address, result))
)
);
const utxo = utxoResults.flat(1);
Expand All @@ -127,7 +129,7 @@ export const blockfrostProvider = (options: Options): WalletProvider => {

const queryTransactionsByHashes: WalletProvider['queryTransactionsByHashes'] = async (hashes) => {
const transactions = await Promise.all(hashes.map(async (hash) => blockfrost.txsUtxos(hash)));
return transactions.map((tx) => BlockfrostToOgmios.txContentUtxo(tx));
return transactions.map(BlockfrostToCore.transaction);
};

const queryTransactionsByAddresses: WalletProvider['queryTransactionsByAddresses'] = async (addresses) => {
Expand All @@ -152,12 +154,152 @@ 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 {
block: {
slot: response.slot,
blockNo: response.block_height,
hash: response.block
},
index: response.index,
deposit: BigInt(response.deposit),
fee: BigInt(response.fees),
size: response.size,
validContract: response.valid_contract,
invalidBefore: parseValidityInterval(response.invalid_before),
invalidHereafter: parseValidityInterval(response.invalid_hereafter),
redeemers: await fetchRedeemers(response),
withdrawals: await fetchWithdrawals(response),
mint: await fetchMint(response),
certificates: await fetchCertificates(response)
};
};

const providerFunctions: WalletProvider = {
ledgerTip,
networkInfo,
stakePoolStats,
submitTx,
utxoDelegationAndRewards,
transactionDetails,
queryTransactionsByAddresses,
queryTransactionsByHashes,
currentWalletProtocolParameters
Expand Down
19 changes: 19 additions & 0 deletions packages/blockfrost/src/blockfrostToCore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Responses } from '@blockfrost/blockfrost-js';
import { Cardano } from '@cardano-sdk/core';
import { BlockfrostToOgmios } from './BlockfrostToOgmios';

export const BlockfrostToCore = {
addressUtxoContent: (address: string, blockfrost: Responses['address_utxo_content']): Cardano.Utxo[] =>
blockfrost.map((utxo) => [
{ ...BlockfrostToOgmios.txIn(BlockfrostToOgmios.inputFromUtxo(address, utxo)), address },
BlockfrostToOgmios.txOut(BlockfrostToOgmios.outputFromUtxo(address, utxo))
]),
transaction: (tx: Responses['tx_content_utxo']) => ({
inputs: tx.inputs.map((input) => ({
...BlockfrostToOgmios.txIn(input),
address: input.address
})),
outputs: tx.outputs.map(BlockfrostToOgmios.txOut),
hash: tx.hash
})
};
74 changes: 71 additions & 3 deletions packages/blockfrost/test/blockfrostProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-len */

import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js';
Expand Down Expand Up @@ -258,10 +259,12 @@ describe('blockfrostProvider', () => {
]);

expect(response).toHaveLength(1);
expect(response[0]).toMatchObject<Transaction.WithHash>({
expect(response[0]).toMatchObject<Transaction.Tx>({
hash: '4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6',
inputs: [
{
address:
'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2',
txId: '6d50c330a6fba79de6949a8dcd5e4b7ffa3f9442f0c5bed7a78fa6d786c6c863',
index: 1
}
Expand Down Expand Up @@ -346,10 +349,12 @@ describe('blockfrostProvider', () => {
]);

expect(response).toHaveLength(1);
expect(response[0]).toMatchObject<Transaction.WithHash>({
expect(response[0]).toMatchObject<Transaction.Tx>({
hash: '4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6',
inputs: [
{
address:
'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2',
txId: '6d50c330a6fba79de6949a8dcd5e4b7ffa3f9442f0c5bed7a78fa6d786c6c863',
index: 1
}
Expand Down Expand Up @@ -378,6 +383,70 @@ describe('blockfrostProvider', () => {
});
});

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
},
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 () => {
const mockedResponse = {
data: {
Expand All @@ -394,7 +463,6 @@ describe('blockfrostProvider', () => {
coins_per_utxo_word: '0'
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
BlockFrostAPI.prototype.axiosInstance = jest.fn().mockResolvedValue(mockedResponse) as any;

const client = blockfrostProvider({ projectId: apiKey, isTestnet: true });
Expand Down
31 changes: 19 additions & 12 deletions packages/cardano-graphql-db-sync/src/CardanoGraphqlToOgmios.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Schema as Cardano } from '@cardano-ogmios/client';
import { ProtocolParametersRequiredByWallet, Transaction } from '@cardano-sdk/core';
import { ProtocolParametersRequiredByWallet } from '@cardano-sdk/core';
import { Block } from '@cardano-graphql/client-ts';

type GraphqlTransaction = {
hash: Cardano.Hash16;
inputs: { txHash: Cardano.Hash16; sourceTxIndex: number }[];
inputs: { txHash: Cardano.Hash16; sourceTxIndex: number; address: Cardano.Address }[];
outputs: {
address: Cardano.Address;
value: string;
Expand All @@ -30,18 +30,25 @@ export type GraphqlCurrentWalletProtocolParameters = {

export type CardanoGraphQlTip = Pick<Block, 'hash' | 'number' | 'slotNo'>;

export const CardanoGraphqlToOgmios = {
graphqlTransactionsToCardanoTxs: (transactions: GraphqlTransaction[]): Transaction.WithHash[] =>
transactions.map((tx) => ({
hash: tx.hash,
inputs: tx.inputs.map((index) => ({ txId: index.txHash, index: index.sourceTxIndex })),
outputs: tx.outputs.map((output) => {
const assets: Cardano.Value['assets'] = {};
const txIn = ({ sourceTxIndex, txHash }: GraphqlTransaction['inputs'][0]): Cardano.TxIn => ({
txId: txHash,
index: sourceTxIndex
});

const txOut = ({ address, tokens, value }: GraphqlTransaction['outputs'][0]) => {
const assets: Cardano.Value['assets'] = {};
for (const token of tokens) assets[token.asset.assetId] = BigInt(token.quantity);
return { address, value: { coins: Number(value), assets } };
};

for (const token of output.tokens) assets[token.asset.assetId] = BigInt(token.quantity);
export const CardanoGraphqlToOgmios = {
txIn,
txOut,

return { address: output.address, value: { coins: Number(output.value), assets } };
})
graphqlTransactionsToCardanoTxs: (transactions: GraphqlTransaction[]): Cardano.Tx[] =>
transactions.map((tx) => ({
inputs: tx.inputs.map(txIn),
outputs: tx.outputs.map(txOut)
})),

currentWalletProtocolParameters: (
Expand Down
Loading

0 comments on commit 889a39b

Please sign in to comment.