diff --git a/packages/blockfrost/src/BlockfrostToOgmios.ts b/packages/blockfrost/src/BlockfrostToOgmios.ts index 22d0ab0fce9..ca612bb20a3 100644 --- a/packages/blockfrost/src/BlockfrostToOgmios.ts +++ b/packages/blockfrost/src/BlockfrostToOgmios.ts @@ -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 extends (infer U)[] ? U : T; type BlockfrostAddressUtxoContent = Responses['address_utxo_content']; @@ -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) }), diff --git a/packages/blockfrost/src/blockfrostProvider.ts b/packages/blockfrost/src/blockfrostProvider.ts index 6ca51ba6b58..0c591159450 100644 --- a/packages/blockfrost/src/blockfrostProvider.ts +++ b/packages/blockfrost/src/blockfrostProvider.ts @@ -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; @@ -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 () => { @@ -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); @@ -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) => { @@ -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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 diff --git a/packages/blockfrost/src/blockfrostToCore.ts b/packages/blockfrost/src/blockfrostToCore.ts new file mode 100644 index 00000000000..c4e7e43ebc0 --- /dev/null +++ b/packages/blockfrost/src/blockfrostToCore.ts @@ -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 + }) +}; diff --git a/packages/blockfrost/test/blockfrostProvider.test.ts b/packages/blockfrost/test/blockfrostProvider.test.ts index e522adacc8f..ddd03fd5e3f 100644 --- a/packages/blockfrost/test/blockfrostProvider.test.ts +++ b/packages/blockfrost/test/blockfrostProvider.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable max-len */ import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js'; @@ -258,10 +259,12 @@ describe('blockfrostProvider', () => { ]); expect(response).toHaveLength(1); - expect(response[0]).toMatchObject({ + expect(response[0]).toMatchObject({ hash: '4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6', inputs: [ { + address: + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', txId: '6d50c330a6fba79de6949a8dcd5e4b7ffa3f9442f0c5bed7a78fa6d786c6c863', index: 1 } @@ -346,10 +349,12 @@ describe('blockfrostProvider', () => { ]); expect(response).toHaveLength(1); - expect(response[0]).toMatchObject({ + expect(response[0]).toMatchObject({ hash: '4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6', inputs: [ { + address: + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', txId: '6d50c330a6fba79de6949a8dcd5e4b7ffa3f9442f0c5bed7a78fa6d786c6c863', index: 1 } @@ -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: { @@ -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 }); diff --git a/packages/cardano-graphql-db-sync/src/CardanoGraphqlToOgmios.ts b/packages/cardano-graphql-db-sync/src/CardanoGraphqlToOgmios.ts index d1acda48b78..9e40b034c7e 100644 --- a/packages/cardano-graphql-db-sync/src/CardanoGraphqlToOgmios.ts +++ b/packages/cardano-graphql-db-sync/src/CardanoGraphqlToOgmios.ts @@ -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; @@ -30,18 +30,25 @@ export type GraphqlCurrentWalletProtocolParameters = { export type CardanoGraphQlTip = Pick; -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: ( diff --git a/packages/cardano-graphql-db-sync/src/cardanoGraphqlDbSyncProvider.ts b/packages/cardano-graphql-db-sync/src/cardanoGraphqlDbSyncProvider.ts index f8dcdabb06e..42ea0b41652 100644 --- a/packages/cardano-graphql-db-sync/src/cardanoGraphqlDbSyncProvider.ts +++ b/packages/cardano-graphql-db-sync/src/cardanoGraphqlDbSyncProvider.ts @@ -1,13 +1,13 @@ import { WalletProvider, ProviderError, ProviderFailure } from '@cardano-sdk/core'; import { gql, GraphQLClient } from 'graphql-request'; import { TransactionSubmitResponse } from '@cardano-graphql/client-ts'; -import { Schema as Cardano } from '@cardano-ogmios/client'; import { Buffer } from 'buffer'; import { CardanoGraphqlToOgmios, GraphqlCurrentWalletProtocolParameters, CardanoGraphQlTip } from './CardanoGraphqlToOgmios'; +import { CardanoGraphqlToCore, TransactionsResponse } from './cardanoGraphqlToCore'; /** * Connect to a [cardano-graphql (cardano-db-sync) service](https://github.com/input-output-hk/cardano-graphql) @@ -245,6 +245,7 @@ export const cardanoGraphqlDbSyncProvider = (uri: string): WalletProvider => { ) { hash inputs { + address txHash sourceTxIndex } @@ -262,22 +263,11 @@ export const cardanoGraphqlDbSyncProvider = (uri: string): WalletProvider => { } `; - type Response = { - transactions: { - hash: Cardano.Hash16; - inputs: { txHash: Cardano.Hash16; sourceTxIndex: number }[]; - outputs: { - address: Cardano.Address; - value: string; - tokens: { asset: { assetId: string }; quantity: string }[]; - }[]; - }[]; - }; type Variables = { addresses: string[] }; - const response = await client.request(query, { addresses }); + const response = await client.request(query, { addresses }); - return CardanoGraphqlToOgmios.graphqlTransactionsToCardanoTxs(response.transactions); + return CardanoGraphqlToCore.transactions(response); }; const queryTransactionsByHashes: WalletProvider['queryTransactionsByHashes'] = async (hashes) => { @@ -303,22 +293,11 @@ export const cardanoGraphqlDbSyncProvider = (uri: string): WalletProvider => { } `; - type Response = { - transactions: { - hash: Cardano.Hash16; - inputs: { txHash: Cardano.Hash16; sourceTxIndex: number }[]; - outputs: { - address: Cardano.Address; - value: string; - tokens: { asset: { assetId: string }; quantity: string }[]; - }[]; - }[]; - }; type Variables = { hashes: string[] }; - const response = await client.request(query, { hashes }); + const response = await client.request(query, { hashes }); - return CardanoGraphqlToOgmios.graphqlTransactionsToCardanoTxs(response.transactions); + return CardanoGraphqlToCore.transactions(response); }; const currentWalletProtocolParameters: WalletProvider['currentWalletProtocolParameters'] = async () => { @@ -356,12 +335,18 @@ export const cardanoGraphqlDbSyncProvider = (uri: string): WalletProvider => { return CardanoGraphqlToOgmios.currentWalletProtocolParameters(response.cardano.currentEpoch.protocolParams); }; + // eslint-disable-next-line unicorn/consistent-function-scoping + const transactionDetails: WalletProvider['transactionDetails'] = async () => { + throw new ProviderError(ProviderFailure.NotImplemented); + }; + return { ledgerTip, networkInfo, stakePoolStats, submitTx, utxoDelegationAndRewards, + transactionDetails, queryTransactionsByAddresses, queryTransactionsByHashes, currentWalletProtocolParameters diff --git a/packages/cardano-graphql-db-sync/src/cardanoGraphqlToCore.ts b/packages/cardano-graphql-db-sync/src/cardanoGraphqlToCore.ts new file mode 100644 index 00000000000..bb740368b99 --- /dev/null +++ b/packages/cardano-graphql-db-sync/src/cardanoGraphqlToCore.ts @@ -0,0 +1,29 @@ +import { Hash16 } from '@cardano-ogmios/schema'; +import { Cardano } from '@cardano-sdk/core'; +import { CardanoGraphqlToOgmios } from './CardanoGraphqlToOgmios'; + +export type CardanoGraphqlTxIn = { txHash: Hash16; sourceTxIndex: number; address: Cardano.Address }; +export type TransactionsResponse = { + transactions: { + hash: Hash16; + inputs: CardanoGraphqlTxIn[]; + outputs: { + address: Cardano.Address; + value: string; + tokens: { asset: { assetId: string }; quantity: string }[]; + }[]; + }[]; +}; + +export const CardanoGraphqlToCore = { + transactions: (response: TransactionsResponse) => + response.transactions.map(({ hash, inputs, outputs }) => ({ + hash, + inputs: inputs.map(CardanoGraphqlToCore.txIn), + outputs: outputs.map(CardanoGraphqlToOgmios.txOut) + })), + txIn: (input: CardanoGraphqlTxIn) => ({ + ...CardanoGraphqlToOgmios.txIn(input), + address: input.address + }) +}; diff --git a/packages/cardano-graphql-db-sync/test/cardanoGraphqlDbSyncProvider.test.ts b/packages/cardano-graphql-db-sync/test/cardanoGraphqlDbSyncProvider.test.ts index d8b55e9726a..cc1c2c6d3b4 100644 --- a/packages/cardano-graphql-db-sync/test/cardanoGraphqlDbSyncProvider.test.ts +++ b/packages/cardano-graphql-db-sync/test/cardanoGraphqlDbSyncProvider.test.ts @@ -16,6 +16,8 @@ describe('cardanoGraphqlDbSyncProvider', () => { hash: '886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8', inputs: [ { + address: + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', txHash: '886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8', sourceTxIndex: 1 } @@ -46,6 +48,8 @@ describe('cardanoGraphqlDbSyncProvider', () => { hash: '390ec1131b8cc95125f1dc2d2c02d54c79939f04f3f5723e47606279ddc822b3', inputs: [ { + address: + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', txHash: '390ec1131b8cc95125f1dc2d2c02d54c79939f04f3f5723e47606279ddc822b3', sourceTxIndex: 1 } @@ -75,10 +79,12 @@ describe('cardanoGraphqlDbSyncProvider', () => { expect(response).toHaveLength(2); - expect(response[0]).toMatchObject({ + expect(response[0]).toMatchObject({ hash: '886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8', inputs: [ { + address: + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', txId: '886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8', index: 1 } @@ -110,6 +116,8 @@ describe('cardanoGraphqlDbSyncProvider', () => { hash: '886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8', inputs: [ { + address: + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', txHash: '886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8', sourceTxIndex: 1 } @@ -147,10 +155,12 @@ describe('cardanoGraphqlDbSyncProvider', () => { ]); expect(response).toHaveLength(1); - expect(response[0]).toMatchObject({ + expect(response[0]).toMatchObject({ hash: '886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8', inputs: [ { + address: + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', txId: '886206542d63b23a047864021fbfccf291d78e47c1e59bd4c75fbc67b248c5e8', index: 1 } diff --git a/packages/core/src/Asset/util.ts b/packages/core/src/Asset/util.ts index e8c713fa7a8..2b919e0e005 100644 --- a/packages/core/src/Asset/util.ts +++ b/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 { diff --git a/packages/core/src/CSL/cslToCore.ts b/packages/core/src/CSL/cslToCore.ts new file mode 100644 index 00000000000..d7cb26856a5 --- /dev/null +++ b/packages/core/src/CSL/cslToCore.ts @@ -0,0 +1,7 @@ +import { Transaction } from '@emurgo/cardano-serialization-lib-nodejs'; +import { Tx, TxDetails } from '../Transaction'; + +// TODO: probably move stuff over from cslToOgmios; +export const tx = (_input: Transaction): Tx & { details: TxDetails } => { + throw new Error('Not implemented'); +}; diff --git a/packages/core/src/CSL/index.ts b/packages/core/src/CSL/index.ts index 43a7a87e915..be7ddee0eae 100644 --- a/packages/core/src/CSL/index.ts +++ b/packages/core/src/CSL/index.ts @@ -1,5 +1,6 @@ import * as CSL from '@emurgo/cardano-serialization-lib-nodejs'; export * as cslUtil from './util'; +export * as cslToCore from './cslToCore'; export * as CSL from '@emurgo/cardano-serialization-lib-nodejs'; export type CardanoSerializationLib = typeof CSL; diff --git a/packages/core/src/Cardano/types/StakePool.ts b/packages/core/src/Cardano/types/StakePool.ts index 20a9afa7113..817d384b5ab 100644 --- a/packages/core/src/Cardano/types/StakePool.ts +++ b/packages/core/src/Cardano/types/StakePool.ts @@ -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 { diff --git a/packages/core/src/Cardano/types/index.ts b/packages/core/src/Cardano/types/index.ts index 0199aea4901..1265124bfc9 100644 --- a/packages/core/src/Cardano/types/index.ts +++ b/packages/core/src/Cardano/types/index.ts @@ -1,2 +1,3 @@ export * from './StakePool'; export * from './ExtendedStakePoolMetadata'; +export * from './utxo'; diff --git a/packages/core/src/Cardano/types/utxo.ts b/packages/core/src/Cardano/types/utxo.ts new file mode 100644 index 00000000000..323935ea033 --- /dev/null +++ b/packages/core/src/Cardano/types/utxo.ts @@ -0,0 +1,9 @@ +import { TxOut, Address, TxIn as OgmiosTxIn } from '@cardano-ogmios/schema'; + +export { TxOut, Address } from '@cardano-ogmios/schema'; + +export interface TxIn extends OgmiosTxIn { + address: Address; +} + +export type Utxo = [TxIn, TxOut]; diff --git a/packages/core/src/Transaction/types.ts b/packages/core/src/Transaction/types.ts index e5235ddb9f2..813fb9ca69c 100644 --- a/packages/core/src/Transaction/types.ts +++ b/packages/core/src/Transaction/types.ts @@ -1,8 +1,112 @@ -import { Slot, Hash16, Tx } from '@cardano-ogmios/schema'; +import { Slot, Hash16, Tip, Address, Epoch, PoolId, ExUnits } from '@cardano-ogmios/schema'; +import { Ogmios, Cardano } from '..'; +import { Lovelace, TokenMap } from '../Ogmios'; export interface ValidityInterval { invalidBefore?: Slot; invalidHereafter?: Slot; } -export type WithHash = { hash: Hash16 } & Tx; +export interface Tx { + inputs: Cardano.TxIn[]; + outputs: Cardano.TxOut[]; + hash: Hash16; +} + +export type Block = Tip; + +export interface Withdrawal { + address: Address; + quantity: Lovelace; +} + +export enum CertificateType { + StakeRegistration = 'StakeRegistration', + StakeDeregistration = 'StakeDeregistration', + PoolRegistration = 'PoolRegistration', + PoolRetirement = 'PoolRetirement', + StakeDelegation = 'StakeDelegation', + MIR = 'MoveInstantaneousRewards', + GenesisKeyDelegation = 'GenesisKeyDelegation' +} + +export interface Redeemer { + index: number; + purpose: 'spend' | 'mint' | 'cert' | 'reward'; + scriptHash: Hash16; + datumHash: Hash16; + executionUnits: ExUnits; + fee: Lovelace; +} + +export interface StakeAddressCertificate { + type: CertificateType.StakeRegistration | CertificateType.StakeDeregistration; + certIndex: number; + address: Address; +} + +export interface PoolCertificate { + type: CertificateType.PoolRegistration | CertificateType.PoolRetirement; + certIndex: number; + poolId: PoolId; + epoch: Epoch; +} + +export interface StakeDelegationCertificate { + type: CertificateType.StakeDelegation; + certIndex: number; + delegationIndex: number; + address: Address; + poolId: PoolId; + epoch: Epoch; +} + +export interface MirCertificate { + type: CertificateType.MIR; + certIndex: number; + address: Address; + quantity: Lovelace; + pot: 'reserve' | 'treasury'; +} + +export interface GenesisKeyDelegationCertificate { + type: CertificateType.GenesisKeyDelegation; + certIndex: number; + genesisHash: Hash16; + genesisDelegateHash: Hash16; + vrfKeyHash: Hash16; +} + +export type Certificate = + | StakeAddressCertificate + | PoolCertificate + | StakeDelegationCertificate + | MirCertificate + | GenesisKeyDelegationCertificate; + +export interface TxDetails { + block: Block; + index: number; + fee: Ogmios.Lovelace; + deposit: Ogmios.Lovelace; + size: number; + invalidBefore?: Slot; + invalidHereafter?: Slot; + withdrawals?: Withdrawal[]; + certificates?: Certificate[]; + mint?: TokenMap; + redeemers?: Redeemer[]; + validContract?: boolean; +} + +// TODO: consider consolidating all core types from Cardano/ Core/ Ogmios/types, maybe under Cardano? + +export enum TransactionStatus { + Pending = 'pending', + Confirmed = 'confirmed' +} + +export interface DetailedTransaction extends Tx { + details: TxDetails; + status: TransactionStatus; +} diff --git a/packages/core/src/WalletProvider/types.ts b/packages/core/src/WalletProvider/types.ts index 3fdd600232a..e1ee30b155f 100644 --- a/packages/core/src/WalletProvider/types.ts +++ b/packages/core/src/WalletProvider/types.ts @@ -1,5 +1,5 @@ -import Cardano, { ProtocolParametersAlonzo } from '@cardano-ogmios/schema'; -import { Ogmios, Transaction, CSL } from '..'; +import OgmiosSchema, { ProtocolParametersAlonzo } from '@cardano-ogmios/schema'; +import { Ogmios, Transaction, CSL, Cardano } from '..'; export type ProtocolParametersRequiredByWallet = Pick< ProtocolParametersAlonzo, @@ -36,7 +36,7 @@ export type StakePoolStats = { export type NetworkInfo = { currentEpoch: { - number: Cardano.Epoch; + number: OgmiosSchema.Epoch; start: { /** Local date */ date: Date; @@ -51,7 +51,7 @@ export type NetworkInfo = { }; export interface WalletProvider { - ledgerTip: () => Promise; + ledgerTip: () => Promise; networkInfo: () => Promise; // TODO: move stakePoolStats out to other provider type, since it's not required for wallet operation stakePoolStats?: () => Promise; @@ -59,10 +59,16 @@ export interface WalletProvider { submitTx: (signedTransaction: CSL.Transaction) => Promise; // TODO: split utxoDelegationAndRewards this into 2 or 3 functions utxoDelegationAndRewards: ( - addresses: Cardano.Address[], - stakeKeyHash: Cardano.Hash16 - ) => Promise<{ utxo: Cardano.Utxo; delegationAndRewards: Cardano.DelegationsAndRewards }>; - queryTransactionsByAddresses: (addresses: Cardano.Address[]) => Promise; - queryTransactionsByHashes: (hashes: Cardano.Hash16[]) => Promise; + addresses: OgmiosSchema.Address[], + stakeKeyHash: OgmiosSchema.Hash16 + ) => Promise<{ utxo: Cardano.Utxo[]; delegationAndRewards: OgmiosSchema.DelegationsAndRewards }>; + transactionDetails: (hash: OgmiosSchema.Hash16) => Promise; + /** + * TODO: add an optional 'since: Slot' argument for querying transactions and utxos. + * When doing so we need to also consider how best we can use the volatile block range of the chain + * to minimise over-fetching and assist the application in handling rollback scenarios. + */ + queryTransactionsByAddresses: (addresses: OgmiosSchema.Address[]) => Promise; + queryTransactionsByHashes: (hashes: OgmiosSchema.Hash16[]) => Promise; currentWalletProtocolParameters: () => Promise; } diff --git a/packages/wallet/test/mocks/ProviderStub.ts b/packages/wallet/test/mocks/ProviderStub.ts index e6a1acd95de..c8c477f2e5d 100644 --- a/packages/wallet/test/mocks/ProviderStub.ts +++ b/packages/wallet/test/mocks/ProviderStub.ts @@ -132,6 +132,7 @@ export const providerStub = () => ({ } }), utxoDelegationAndRewards: jest.fn().mockResolvedValue({ utxo, delegationAndRewards }), + transactionDetails: jest.fn(), queryTransactionsByAddresses: queryTransactions(), queryTransactionsByHashes: queryTransactions(), currentWalletProtocolParameters: async () => ({