From 287edbf7ae4233c20cfa368e80b8115131cdc5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Mon, 23 Sep 2024 13:34:48 +0300 Subject: [PATCH] feat: enable blockfrost enabled e2e tests --- ...ontinuous-integration-blockfrost-e2e.yaml} | 13 +- compose/common.yml | 14 +- .../BlockfrostChainHistoryProvider.ts | 573 +++++++++---- .../BlockfrostNetworkInfoProvider.ts | 2 +- .../src/Program/programs/providerServer.ts | 32 +- .../BlockfrostUtxoProvider.ts | 51 +- .../BlockfrostClientFactory.ts | 6 +- .../BlockfrostProvider/BlockfrostProvider.ts | 2 +- .../BlockfrostProvider/BlockfrostToCore.ts | 71 +- .../BlockfrostChainHistoryProvider.test.ts | 809 +++++++++--------- packages/core/src/Provider/providerUtil.ts | 2 + .../AuxiliaryData/AuxiliaryData.ts | 9 +- .../core/src/Serialization/CBOR/CborReader.ts | 3 +- .../Certificates/PoolParams/PoolParams.ts | 7 +- .../Serialization/PlutusData/PlutusData.ts | 2 + .../core/src/Serialization/Transaction.ts | 9 +- .../TransactionBody/TransactionBody.ts | 19 +- .../TransactionBody/TransactionOutput.ts | 17 +- .../Serialization/TransactionBody/Utils.ts | 13 + .../Update/ProtocolParamUpdate.ts | 11 +- packages/e2e/docker-compose.yml | 1 - .../templates/babbage/db-sync-config.json | 5 +- .../blockfrost-ryo/local-network.yaml | 3 +- packages/e2e/package.json | 3 +- packages/e2e/test/ws-server/webSocket.test.ts | 9 +- 25 files changed, 1051 insertions(+), 635 deletions(-) rename .github/workflows/{continuous-integration-e2e.yaml => continuous-integration-blockfrost-e2e.yaml} (91%) diff --git a/.github/workflows/continuous-integration-e2e.yaml b/.github/workflows/continuous-integration-blockfrost-e2e.yaml similarity index 91% rename from .github/workflows/continuous-integration-e2e.yaml rename to .github/workflows/continuous-integration-blockfrost-e2e.yaml index 30900c70f8f..f8a7046cc2a 100644 --- a/.github/workflows/continuous-integration-e2e.yaml +++ b/.github/workflows/continuous-integration-blockfrost-e2e.yaml @@ -1,4 +1,4 @@ -name: Continuous Integration - E2E +name: Continuous Integration (Blockfrost) - E2E env: TL_DEPTH: ${{ github.event.pull_request.head.repo.fork && '0' || fromJson(vars.TL_DEPTH) }} @@ -28,6 +28,17 @@ env: TEST_CLIENT_STAKE_POOL_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}' WS_PROVIDER_URL: 'http://localhost:4100/ws' + # Blockfrost enablement + BLOCKFROST_CUSTOM_BACKEND_URL: 'http://blockfrost-ryo:3000' + USE_TYPEORM_ASSET_PROVIDER: 'false' + ASSET_PROVIDER: 'blockfrost' + UTXO_PROVIDER: 'blockfrost' + CHAIN_HISTORY_PROVIDER: 'blockfrost' + REWARDS_PROVIDER: 'blockfrost' + NETWORK_INFO_PROVIDER: 'blockfrost' + #blockfrost-ryo doesn't have submit API + #TX_SUBMIT_PROVIDER: 'blockfrost' + on: pull_request: push: diff --git a/compose/common.yml b/compose/common.yml index b963e2be790..7930764e855 100644 --- a/compose/common.yml +++ b/compose/common.yml @@ -78,9 +78,10 @@ x-provider-server-environment: &provider-server-environment NETWORK_INFO_PROVIDER: ${NETWORK_INFO_PROVIDER:-dbsync} TX_SUBMIT_PROVIDER: ${TX_SUBMIT_PROVIDER:-submit-node} STAKE_POOL_PROVIDER: ${STAKE_POOL_PROVIDER:-dbsync} + USE_TYPEORM_ASSET_PROVIDER: ${USE_TYPEORM_ASSET_PROVIDER:-true} NETWORK: ${NETWORK:-mainnet} - BLOCKFROST_API_KEY: ${BLOCKFROST_API_KEY} - BLOCKFROST_CUSTOM_BACKEND_URL: ${BLOCKFROST_CUSTOM_BACKEND_URL} + BLOCKFROST_API_KEY: ${BLOCKFROST_API_KEY:-} + BLOCKFROST_CUSTOM_BACKEND_URL: ${BLOCKFROST_CUSTOM_BACKEND_URL:-} x-sdk-environment: &sdk-environment LOGGER_MIN_SEVERITY: ${LOGGER_MIN_SEVERITY:-info} @@ -133,6 +134,8 @@ services: condition: service_started ports: - "3015:3000" + healthcheck: + test: [ 'CMD-SHELL', 'curl -s --fail http://localhost:3000/health' ] cardano-db-sync: <<: @@ -352,6 +355,10 @@ services: - *provider-server-environment ports: - ${API_PORT:-4000}:3000 + - 9229:9229 + depends_on: + blockfrost-ryo: + condition: service_healthy stake-pool-provider-server: <<: @@ -380,12 +387,13 @@ services: depends_on: asset-projector: condition: service_healthy + blockfrost-ryo: + condition: service_healthy environment: <<: - *sdk-environment - *provider-server-environment SERVICE_NAMES: asset - USE_TYPEORM_ASSET_PROVIDER: true ports: - ${HANDLE_API_PORT:-4014}:3000 diff --git a/packages/cardano-services/src/ChainHistory/BlockrostChainHistoryProvider/BlockfrostChainHistoryProvider.ts b/packages/cardano-services/src/ChainHistory/BlockrostChainHistoryProvider/BlockfrostChainHistoryProvider.ts index 7b568b9c660..9caae90f563 100644 --- a/packages/cardano-services/src/ChainHistory/BlockrostChainHistoryProvider/BlockfrostChainHistoryProvider.ts +++ b/packages/cardano-services/src/ChainHistory/BlockrostChainHistoryProvider/BlockfrostChainHistoryProvider.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line jsdoc/check-param-names import * as Crypto from '@cardano-sdk/crypto'; -import { BlockfrostProvider } from '../../util/BlockfrostProvider/BlockfrostProvider'; +import { BlockfrostProvider, BlockfrostProviderDependencies } from '../../util/BlockfrostProvider/BlockfrostProvider'; import { BlockfrostToCore, BlockfrostTransactionContent, @@ -13,46 +13,64 @@ import { BlocksByIdsArgs, Cardano, ChainHistoryProvider, + NetworkInfoProvider, Paginated, ProviderError, ProviderFailure, + Serialization, TransactionsByAddressesArgs, - TransactionsByIdsArgs + TransactionsByIdsArgs, + createSlotEpochCalc } from '@cardano-sdk/core'; +import { DB_MAX_SAFE_INTEGER } from '../DbSyncChainHistory/queries'; import { Responses } from '@blockfrost/blockfrost-js'; +import { Schemas } from '@blockfrost/blockfrost-js/lib/types/open-api'; +import omit from 'lodash/omit.js'; type WithCertIndex = T & { cert_index: number }; +export interface BlockfrostChainHistoryProviderDependencies extends BlockfrostProviderDependencies { + networkInfoProvider: NetworkInfoProvider; +} + export class BlockfrostChainHistoryProvider extends BlockfrostProvider implements ChainHistoryProvider { + private networkInfoProvider: NetworkInfoProvider; + + constructor({ logger, blockfrost, networkInfoProvider }: BlockfrostChainHistoryProviderDependencies) { + super({ blockfrost, logger }); + this.networkInfoProvider = networkInfoProvider; + } + protected async fetchRedeemers({ hash, redeemer_count }: Responses['tx_content']): Promise { if (!redeemer_count) return; - const response = await this.blockfrost.txsRedeemers(hash); - return response.map( - ({ purpose, script_hash, unit_mem, unit_steps, tx_index }): Cardano.Redeemer => ({ - data: Buffer.from(script_hash), - executionUnits: { - memory: Number.parseInt(unit_mem), - steps: Number.parseInt(unit_steps) - }, - index: tx_index, - purpose: ((): Cardano.Redeemer['purpose'] => { - switch (purpose) { - case 'cert': - return Cardano.RedeemerPurpose.certificate; - case 'reward': - return Cardano.RedeemerPurpose.withdrawal; - case 'mint': - return Cardano.RedeemerPurpose.mint; - case 'spend': - return Cardano.RedeemerPurpose.spend; - default: - return purpose; - } - })() - }) + return this.blockfrost.txsRedeemers(hash).then((response) => + response.map( + ({ purpose, script_hash, unit_mem, unit_steps, tx_index }): Cardano.Redeemer => ({ + data: Buffer.from(script_hash), + executionUnits: { + memory: Number.parseInt(unit_mem), + steps: Number.parseInt(unit_steps) + }, + index: tx_index, + purpose: ((): Cardano.Redeemer['purpose'] => { + switch (purpose) { + case 'cert': + return Cardano.RedeemerPurpose.certificate; + case 'reward': + return Cardano.RedeemerPurpose.withdrawal; + case 'mint': + return Cardano.RedeemerPurpose.mint; + case 'spend': + return Cardano.RedeemerPurpose.spend; + default: + return purpose; + } + })() + }) + ) ); } @@ -61,14 +79,16 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement hash }: Responses['tx_content']): Promise { if (!withdrawal_count) return; - const response = await this.blockfrost.txsWithdrawals(hash); - return response.map( - ({ address, amount }): Cardano.Withdrawal => ({ - quantity: BigInt(amount), - stakeAddress: Cardano.RewardAccount(address) - }) + return this.blockfrost.txsWithdrawals(hash).then((response) => + response.map( + ({ address, amount }): Cardano.Withdrawal => ({ + quantity: BigInt(amount), + stakeAddress: Cardano.RewardAccount(address) + }) + ) ); } + /** This method gathers mints by finding the amounts that doesn't exist in 'inputs' but exist in 'outputs'. */ protected gatherMintsFromUtxos( { asset_mint_or_burn_count }: Responses['tx_content'], @@ -87,65 +107,101 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement } protected async fetchPoolRetireCerts(hash: string): Promise[]> { - const response = await this.blockfrost.txsPoolRetires(hash); - return response.map(({ pool_id, retiring_epoch, cert_index }) => ({ - __typename: Cardano.CertificateType.PoolRetirement, - cert_index, - epoch: Cardano.EpochNo(retiring_epoch), - poolId: Cardano.PoolId(pool_id) - })); + return this.blockfrost.txsPoolRetires(hash).then((response) => + response.map(({ pool_id, retiring_epoch, cert_index }) => ({ + __typename: Cardano.CertificateType.PoolRetirement, + cert_index, + epoch: Cardano.EpochNo(retiring_epoch), + poolId: Cardano.PoolId(pool_id) + })) + ); } protected async fetchPoolUpdateCerts(hash: string): Promise[]> { - const response = await this.blockfrost.txsPoolUpdates(hash); - return response.map(({ pool_id, cert_index }) => ({ - __typename: Cardano.CertificateType.PoolRegistration, - cert_index, - poolId: Cardano.PoolId(pool_id), - poolParameters: ((): Cardano.PoolParameters => { - this.logger.warn('Omitting poolParameters for certificate in tx', hash); - return null as unknown as Cardano.PoolParameters; - })() - })); + return this.blockfrost.txsPoolUpdates(hash).then((response) => + response.map(({ pool_id, cert_index, fixed_cost, margin_cost, pledge, reward_account, vrf_key }) => ({ + __typename: Cardano.CertificateType.PoolRegistration, + cert_index, + poolId: Cardano.PoolId(pool_id), + poolParameters: { + cost: BigInt(fixed_cost), + id: pool_id as Cardano.PoolId, + margin: Cardano.FractionUtils.toFraction(margin_cost), + owners: [], + pledge: BigInt(pledge), + relays: [], + rewardAccount: reward_account as Cardano.RewardAccount, + vrf: vrf_key as Cardano.VrfVkHex + } + })) + ); + } + + async fetchCBOR(hash: string): Promise { + return this.blockfrost + .instance(`txs/${hash}/cbor`) + .then((response) => { + if (response.body.cbor) return response.body.cbor; + throw new Error('CBOR is null'); + }) + .catch((_error) => { + throw new Error('CBOR fetch failed'); + }); + } + + protected async fetchDetailsFromCBOR(hash: string) { + return this.fetchCBOR(hash) + .then((cbor) => { + const tx = Serialization.Transaction.fromCbor(Serialization.TxCBOR(cbor)).toCore(); + this.logger.info('Fetched details from CBOR for tx', hash); + return tx; + }) + .catch((error) => { + this.logger.warn('Failed to fetch details from CBOR for tx', hash, error); + return null; + }); } protected async fetchMirCerts(hash: string): Promise[]> { - const response = await this.blockfrost.txsMirs(hash); - return response.map(({ address, amount, cert_index, pot }) => ({ - __typename: Cardano.CertificateType.MIR, - cert_index, - kind: Cardano.MirCertificateKind.ToStakeCreds, - pot: pot === 'reserve' ? Cardano.MirCertificatePot.Reserves : Cardano.MirCertificatePot.Treasury, - quantity: BigInt(amount), - rewardAccount: Cardano.RewardAccount(address) - })); + return this.blockfrost.txsMirs(hash).then((response) => + response.map(({ address, amount, cert_index, pot }) => ({ + __typename: Cardano.CertificateType.MIR, + cert_index, + kind: Cardano.MirCertificateKind.ToStakeCreds, + pot: pot === 'reserve' ? Cardano.MirCertificatePot.Reserves : Cardano.MirCertificatePot.Treasury, + quantity: BigInt(amount), + rewardAccount: Cardano.RewardAccount(address) + })) + ); } protected async fetchStakeCerts(hash: string): Promise[]> { - const response = await this.blockfrost.txsStakes(hash); - return response.map(({ address, cert_index, registration }) => ({ - __typename: registration - ? Cardano.CertificateType.StakeRegistration - : Cardano.CertificateType.StakeDeregistration, - cert_index, - stakeCredential: { - hash: Cardano.RewardAccount.toHash(Cardano.RewardAccount(address)) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } - })); + return this.blockfrost.txsStakes(hash).then((response) => + response.map(({ address, cert_index, registration }) => ({ + __typename: registration + ? Cardano.CertificateType.StakeRegistration + : Cardano.CertificateType.StakeDeregistration, + cert_index, + stakeCredential: { + hash: Cardano.RewardAccount.toHash(Cardano.RewardAccount(address)) as unknown as Crypto.Hash28ByteBase16, + type: Cardano.CredentialType.KeyHash + } + })) + ); } protected async fetchDelegationCerts(hash: string): Promise[]> { - const response = await this.blockfrost.txsDelegations(hash); - return response.map(({ address, pool_id, cert_index }) => ({ - __typename: Cardano.CertificateType.StakeDelegation, - cert_index, - poolId: Cardano.PoolId(pool_id), - stakeCredential: { - hash: Cardano.RewardAccount.toHash(Cardano.RewardAccount(address)) as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash - } - })); + return this.blockfrost.txsDelegations(hash).then((response) => + response.map(({ address, pool_id, cert_index }) => ({ + __typename: Cardano.CertificateType.StakeDelegation, + cert_index, + poolId: Cardano.PoolId(pool_id), + stakeCredential: { + hash: Cardano.RewardAccount.toHash(Cardano.RewardAccount(address)) as unknown as Crypto.Hash28ByteBase16, + type: Cardano.CredentialType.KeyHash + } + })) + ); } protected async fetchCertificates({ @@ -157,86 +213,233 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement hash }: Responses['tx_content']): Promise { if (pool_retire_count + pool_update_count + mir_cert_count + stake_cert_count + delegation_count === 0) return; - return [ - ...(pool_retire_count ? await this.fetchPoolRetireCerts(hash) : []), - ...(pool_update_count ? await this.fetchPoolUpdateCerts(hash) : []), - ...(mir_cert_count ? await this.fetchMirCerts(hash) : []), - ...(stake_cert_count ? await this.fetchStakeCerts(hash) : []), - ...(delegation_count ? await this.fetchDelegationCerts(hash) : []) - ] - .sort((a, b) => b.cert_index - a.cert_index) - .map((cert) => cert as Cardano.Certificate); + + return Promise.all([ + pool_retire_count ? this.fetchPoolRetireCerts(hash) : [], + pool_update_count ? this.fetchPoolUpdateCerts(hash) : [], + mir_cert_count ? this.fetchMirCerts(hash) : [], + stake_cert_count ? this.fetchStakeCerts(hash) : [], + delegation_count ? this.fetchDelegationCerts(hash) : [] + ]).then((results) => + results + .flat() + .sort((a, b) => b.cert_index - a.cert_index) + .map((cert) => cert as Cardano.Certificate) + ); } - protected async fetchJsonMetadata(txHash: Cardano.TransactionId): Promise { - try { - const response = await this.blockfrost.txsMetadata(txHash.toString()); - // Not sure if types are correct, missing 'label', but it's present in docs - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return blockfrostMetadataToTxMetadata(response as any); - } catch (error) { - if (isBlockfrostNotFoundError(error)) { - return null; - } - throw error; - } + protected async fetchJsonMetadataAsAuxiliaryData(txHash: string): Promise { + const UNDEFINED = undefined; + return this.blockfrost + .txsMetadata(txHash) + .then((m) => { + const metadata = blockfrostMetadataToTxMetadata(m); + return metadata && metadata.size > 0 + ? { + blob: metadata + } + : UNDEFINED; + }) + .catch((error) => { + if (isBlockfrostNotFoundError(error)) { + return UNDEFINED; + } + throw error; + }); } // eslint-disable-next-line unicorn/consistent-function-scoping protected parseValidityInterval = (num: string | null) => Cardano.Slot(Number.parseInt(num || '')) || undefined; - protected async fetchTransaction(hash: Cardano.TransactionId): Promise { - try { - const utxos: Responses['tx_content_utxo'] = await this.blockfrost.txsUtxos(hash.toString()); - const { inputs, outputs, collaterals } = BlockfrostToCore.transactionUtxos(utxos); - - const response = await this.blockfrost.txs(hash.toString()); - const metadata = await this.fetchJsonMetadata(hash); - const certificates = await this.fetchCertificates(response); - const withdrawals = await this.fetchWithdrawals(response); - const inputSource: Cardano.InputSource = response.valid_contract - ? Cardano.InputSource.inputs - : Cardano.InputSource.collaterals; - - return { - auxiliaryData: metadata - ? { - blob: metadata - } - : undefined, + protected async fetchEpochNo(slotNo: Cardano.Slot) { + const calc = await this.networkInfoProvider.eraSummaries().then(createSlotEpochCalc); + return calc(slotNo); + } - blockHeader: { - blockNo: Cardano.BlockNo(response.block_height), - hash: Cardano.BlockId(response.block), - slot: Cardano.Slot(response.slot) - }, - body: { - certificates, - collaterals, - fee: BigInt(response.fees), - inputs, - mint: this.gatherMintsFromUtxos(response, utxos), - outputs, - validityInterval: { - invalidBefore: this.parseValidityInterval(response.invalid_before), - invalidHereafter: this.parseValidityInterval(response.invalid_hereafter) - }, - withdrawals - }, - id: hash, - index: response.index, - inputSource, - txSize: response.size, - witness: { - redeemers: await this.fetchRedeemers(response), - signatures: new Map() // not available in blockfrost + protected async fetchEpochParameters(epochNo: Cardano.EpochNo): Promise { + return await this.blockfrost.epochsParameters(epochNo); + } + + protected async processCertificates( + txContent: Schemas['tx_content'], + certificates?: Cardano.Certificate[] + ): Promise { + if (!certificates) return; + + const epochNo = await this.fetchEpochNo(Cardano.Slot(txContent.slot)); + const { pool_deposit, key_deposit } = await this.fetchEpochParameters(epochNo); + + return certificates.map((c) => { + const cert = omit(c, 'cert_index') as Cardano.Certificate; + switch (cert.__typename) { + case Cardano.CertificateType.PoolRegistration: { + cert.poolParameters.owners = []; + cert.poolParameters.relays = []; + const deposit = + txContent.deposit === undefined || txContent.deposit === '' || txContent.deposit === '0' + ? 0n + : BigInt(pool_deposit); + + delete cert.poolParameters.metadataJson; + + return { ...cert, deposit }; + } + case Cardano.CertificateType.StakeRegistration: { + const deposit = BigInt(key_deposit); + + return { ...cert, __typename: Cardano.CertificateType.Registration, deposit }; } - }; + case Cardano.CertificateType.StakeDeregistration: { + const deposit = BigInt(key_deposit); + + return { ...cert, __typename: Cardano.CertificateType.Unregistration, deposit }; + } + default: + return cert; + } + }); + } + + protected async transactionDetailsUsingAPIs(txContent: Responses['tx_content']): Promise { + const id = Cardano.TransactionId(txContent.hash); + + const [certificates, withdrawals, utxos, auxiliaryData] = await Promise.all([ + this.fetchCertificates(txContent), + this.fetchWithdrawals(txContent), + this.fetchUtxos(id), + this.fetchJsonMetadataAsAuxiliaryData(id) + ]); + + const { inputs, outputs, collaterals } = this.transactionUtxos(utxos); + + const mintPreOrder = this.gatherMintsFromUtxos(txContent, utxos); + const mint = mintPreOrder ? new Map([...mintPreOrder].sort()) : mintPreOrder; + + const inputSource: Cardano.InputSource = txContent.valid_contract + ? Cardano.InputSource.inputs + : Cardano.InputSource.collaterals; + + const body: Cardano.HydratedTxBody = this.mapTxBody( + { + certificates: await this.processCertificates(txContent, certificates), + collateralReturn: undefined, + collaterals, + fee: BigInt(txContent.fees), + inputs, + mint, + outputs, + proposalProcedures: undefined, + validityInterval: { + invalidBefore: this.parseValidityInterval(txContent.invalid_before), + invalidHereafter: this.parseValidityInterval(txContent.invalid_hereafter) + }, + votingProcedures: undefined, + withdrawals + }, + inputSource + ); + + return { + auxiliaryData, + blockHeader: this.mapBlockHeader(txContent), + body, + id, + index: txContent.index, + inputSource, + txSize: txContent.size, + witness: this.witnessFromRedeemers(await this.fetchRedeemers(txContent)) + }; + } + + private witnessFromRedeemers(redeemers: Cardano.Redeemer[] | undefined): Cardano.Witness { + // Although cbor has the data, this stub is used for compatibility with DbSyncChainHistoryProvider + const stubRedeemerData = Buffer.from('not implemented'); + + if (redeemers) { + for (const redeemer of redeemers) { + redeemer.data = stubRedeemerData; + } + } + + return { + redeemers, + signatures: new Map() // available in cbor, but skipped for compatibility with DbSyncChainHistoryProvider + }; + } + + protected async transactionDetailsUsingCBOR( + txContent: Responses['tx_content'] + ): Promise { + const id = Cardano.TransactionId(txContent.hash); + + const txFromCBOR = await this.fetchDetailsFromCBOR(id); + if (!txFromCBOR) return; + + const utxos: Schemas['tx_content_utxo'] = (await this.blockfrost.txsUtxos(id)) as Schemas['tx_content_utxo']; + + // We can't use txFromCBOR.body.inputs since it misses HydratedTxIn.address + const { inputs, outputs, collaterals } = this.transactionUtxos(utxos, txFromCBOR); + + // txFromCBOR.isValid can be also be used + const inputSource: Cardano.InputSource = txContent.valid_contract + ? Cardano.InputSource.inputs + : Cardano.InputSource.collaterals; + + const body: Cardano.HydratedTxBody = this.mapTxBody( + { + certificates: await this.processCertificates(txContent, txFromCBOR.body.certificates), + collateralReturn: txFromCBOR.body.collateralReturn, + collaterals, + fee: txFromCBOR.body.fee, + inputs, + mint: txFromCBOR.body.mint ? new Map([...txFromCBOR.body.mint].sort()) : undefined, + outputs, + proposalProcedures: txFromCBOR.body.proposalProcedures, + validityInterval: txFromCBOR.body.validityInterval, + votingProcedures: txFromCBOR.body.votingProcedures, + withdrawals: txFromCBOR.body.withdrawals + }, + inputSource + ); + + return { + auxiliaryData: txFromCBOR.auxiliaryData, + blockHeader: this.mapBlockHeader(txContent), + body, + id, + index: txContent.index, + inputSource, + txSize: txContent.size, + witness: this.witnessFromRedeemers(txFromCBOR.witness.redeemers) + }; + } + + protected async fetchTransaction(txId: Cardano.TransactionId): Promise { + try { + const txContent = await this.blockfrost.txs(txId.toString()); + + return (await this.transactionDetailsUsingCBOR(txContent)) ?? (await this.transactionDetailsUsingAPIs(txContent)); } catch (error) { throw blockfrostToProviderError(error); } } + private transactionUtxos(utxoResponse: Responses['tx_content_utxo'], txContent?: Cardano.Tx) { + const collaterals = utxoResponse.inputs.filter((input) => input.collateral).map(BlockfrostToCore.hydratedTxIn); + const inputs = utxoResponse.inputs + .filter((input) => !input.collateral && !input.reference) + .map(BlockfrostToCore.hydratedTxIn); + const outputPromises: Cardano.TxOut[] = utxoResponse.outputs + .filter((output) => !output.collateral) + .map((output) => { + const foundScript = txContent?.body.outputs.find((o) => o.address === output.address); + + return BlockfrostToCore.txOut(output, foundScript); + }); + + return { collaterals, inputs, outputs: outputPromises }; + } + public async blocksByHashes({ ids }: BlocksByIdsArgs): Promise { try { const responses = await Promise.all(ids.map((id) => this.blockfrost.blocks(id.toString()))); @@ -277,11 +480,17 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement } } + // eslint-disable-next-line sonarjs/cognitive-complexity public async transactionsByAddresses({ addresses, + pagination, blockRange }: TransactionsByAddressesArgs): Promise> { + this.logger.info(`transactionsByAddresses: ${JSON.stringify(blockRange)} ${JSON.stringify(addresses)}`); try { + const lowerBound = blockRange?.lowerBound ?? 0; + const upperBound = blockRange?.upperBound ?? DB_MAX_SAFE_INTEGER; + const addressTransactions = await Promise.all( addresses.map(async (address) => fetchByAddressSequentially< @@ -295,18 +504,22 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement transactions[transactions.length - 1].block_height < blockRange!.lowerBound! : undefined, request: (addr: Cardano.PaymentAddress, paginationOptions) => - this.blockfrost.addressesTransactions(addr.toString(), paginationOptions) + this.blockfrost.addressesTransactions(addr.toString(), paginationOptions, { + from: blockRange?.lowerBound ? blockRange?.lowerBound.toString() : undefined, + to: blockRange?.upperBound ? blockRange?.upperBound.toString() : undefined + }) }) ) ); - const allTransactions = addressTransactions - .flat(1) - .sort((a, b) => b.block_height - a.block_height || b.tx_index - a.tx_index); - const addressTransactionsSinceBlock = blockRange?.lowerBound - ? allTransactions.filter(({ block_height }) => block_height >= blockRange!.lowerBound!) - : allTransactions; - const ids = addressTransactionsSinceBlock.map(({ tx_hash }) => Cardano.TransactionId(tx_hash)); + const allTransactions = addressTransactions.flat(1); + + const ids = allTransactions + .filter(({ block_height }) => block_height >= lowerBound && block_height <= upperBound) + .sort((a, b) => a.block_height - b.block_height || a.tx_index - b.tx_index) + .map(({ tx_hash }) => Cardano.TransactionId(tx_hash)) + .splice(pagination.startAt, pagination.limit); + const pageResults = await this.transactionsByHashes({ ids }); return { pageResults, totalResultCount: allTransactions.length }; @@ -314,4 +527,58 @@ export class BlockfrostChainHistoryProvider extends BlockfrostProvider implement throw blockfrostToProviderError(error); } } + + private mapTxBody( + { + collateralReturn, + collaterals, + fee, + inputs, + outputs, + mint, + proposalProcedures, + validityInterval, + votingProcedures, + withdrawals, + certificates + }: Cardano.HydratedTxBody, + inputSource: Cardano.InputSource + ) { + return { + ...(inputSource === Cardano.InputSource.collaterals + ? { + collateralReturn: outputs.length > 0 ? outputs[0] : undefined, + collaterals: inputs, + fee: BigInt(0), + inputs: [], + outputs: [], + totalCollateral: fee + } + : { + collateralReturn: collateralReturn ?? undefined, + collaterals, + fee, + inputs, + outputs + }), + certificates, + mint, + proposalProcedures, + validityInterval, + votingProcedures, + withdrawals + }; + } + + private mapBlockHeader({ block, block_height, slot }: Responses['tx_content']): Cardano.PartialBlockHeader { + return { + blockNo: Cardano.BlockNo(block_height), + hash: Cardano.BlockId(block), + slot: Cardano.Slot(slot) + }; + } + + private fetchUtxos(id: Cardano.TransactionId): Promise { + return this.blockfrost.txsUtxos(id); + } } diff --git a/packages/cardano-services/src/NetworkInfo/BlockfrostNetworkInfoProvider/BlockfrostNetworkInfoProvider.ts b/packages/cardano-services/src/NetworkInfo/BlockfrostNetworkInfoProvider/BlockfrostNetworkInfoProvider.ts index 73395340d2b..3090c9250c0 100644 --- a/packages/cardano-services/src/NetworkInfo/BlockfrostNetworkInfoProvider/BlockfrostNetworkInfoProvider.ts +++ b/packages/cardano-services/src/NetworkInfo/BlockfrostNetworkInfoProvider/BlockfrostNetworkInfoProvider.ts @@ -80,7 +80,7 @@ export class BlockfrostNetworkInfoProvider extends BlockfrostProvider implements try { // Although Blockfrost have the endpoint, the blockfrost-js library don't have a call for it // https://github.com/blockfrost/blockfrost-js/issues/294 - const response = await this.blockfrost.instance('network-eras'); + const response = await this.blockfrost.instance('network/eras'); return response.body; } catch (error) { throw handleError(error); diff --git a/packages/cardano-services/src/Program/programs/providerServer.ts b/packages/cardano-services/src/Program/programs/providerServer.ts index c616557f64a..0d24d38ae7e 100644 --- a/packages/cardano-services/src/Program/programs/providerServer.ts +++ b/packages/cardano-services/src/Program/programs/providerServer.ts @@ -6,6 +6,7 @@ import { CardanoNode, ChainHistoryProvider, HandleProvider, + NetworkInfoProvider, Provider, RewardsProvider, Seconds, @@ -330,8 +331,17 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => { }); }, ServiceNames.NetworkInfo); - const getBlockfrostChainHistoryProvider = () => - new BlockfrostChainHistoryProvider({ blockfrost: getBlockfrostApi(), logger }); + let networkInfoProvider: NetworkInfoProvider; + const getNetworkInfoProvider = () => { + if (!networkInfoProvider) + networkInfoProvider = + args.networkInfoProvider === ProviderImplementation.BLOCKFROST + ? getBlockfrostNetworkInfoProvider() + : getDbSyncNetworkInfoProvider(); + return networkInfoProvider; + }; + const getBlockfrostChainHistoryProvider = (nInfoProvider: NetworkInfoProvider | DbSyncNetworkInfoProvider) => + new BlockfrostChainHistoryProvider({ blockfrost: getBlockfrostApi(), logger, networkInfoProvider: nInfoProvider }); const getBlockfrostRewardsProvider = () => new BlockfrostRewardsProvider({ blockfrost: getBlockfrostApi(), logger }); @@ -395,7 +405,10 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => { new ChainHistoryHttpService({ chainHistoryProvider: selectProviderImplementation( args.chainHistoryProvider ?? ProviderImplementation.DBSYNC, - { blockfrost: getBlockfrostChainHistoryProvider, dbsync: getDbSyncChainHistoryProvider }, + { + blockfrost: () => getBlockfrostChainHistoryProvider(getNetworkInfoProvider()), + dbsync: getDbSyncChainHistoryProvider + }, logger, ServiceNames.ChainHistory ), @@ -412,16 +425,11 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => { ServiceNames.Rewards ) }), - [ServiceNames.NetworkInfo]: async () => { - const networkInfoProvider = - args.networkInfoProvider === ProviderImplementation.BLOCKFROST - ? getBlockfrostNetworkInfoProvider() - : getDbSyncNetworkInfoProvider(); - return new NetworkInfoHttpService({ + [ServiceNames.NetworkInfo]: async () => + new NetworkInfoHttpService({ logger, - networkInfoProvider - }); - }, + networkInfoProvider: getNetworkInfoProvider() + }), [ServiceNames.TxSubmit]: async () => { const txSubmitProvider = args.useSubmitApi ? getSubmitApiProvider() diff --git a/packages/cardano-services/src/Utxo/BlockfrostUtxoProvider/BlockfrostUtxoProvider.ts b/packages/cardano-services/src/Utxo/BlockfrostUtxoProvider/BlockfrostUtxoProvider.ts index 703d4c39bdc..cdffc7f872b 100644 --- a/packages/cardano-services/src/Utxo/BlockfrostUtxoProvider/BlockfrostUtxoProvider.ts +++ b/packages/cardano-services/src/Utxo/BlockfrostUtxoProvider/BlockfrostUtxoProvider.ts @@ -1,19 +1,56 @@ import { BlockfrostProvider } from '../../util/BlockfrostProvider/BlockfrostProvider'; -import { BlockfrostToCore, BlockfrostUtxo, blockfrostToProviderError, fetchByAddressSequentially } from '../../util'; -import { Cardano, UtxoByAddressesArgs, UtxoProvider } from '@cardano-sdk/core'; +import { BlockfrostToCore, blockfrostToProviderError, fetchByAddressSequentially } from '../../util'; +import { Cardano, Serialization, UtxoByAddressesArgs, UtxoProvider } from '@cardano-sdk/core'; +import { PaginationOptions } from '@blockfrost/blockfrost-js/lib/types'; import { Responses } from '@blockfrost/blockfrost-js'; +import { Schemas } from '@blockfrost/blockfrost-js/lib/types/open-api'; export class BlockfrostUtxoProvider extends BlockfrostProvider implements UtxoProvider { + protected async fetchUtxos(addr: Cardano.PaymentAddress, pagination: PaginationOptions): Promise { + const utxos: Responses['address_utxo_content'] = (await this.blockfrost.addressesUtxos( + addr.toString(), + pagination + )) as Responses['address_utxo_content']; + + const utxoPromises = utxos.map((utxo) => + this.fetchDetailsFromCBOR(utxo.tx_hash).then((tx) => { + const txOut = tx ? tx.body.outputs.find((output) => output.address === utxo.address) : undefined; + return BlockfrostToCore.addressUtxoContent(addr.toString(), utxo, txOut); + }) + ); + return Promise.all(utxoPromises); + } + + async fetchCBOR(hash: string): Promise { + return this.blockfrost + .instance(`txs/${hash}/cbor`) + .then((response) => { + if (response.body.cbor) return response.body.cbor; + throw new Error('CBOR is null'); + }) + .catch((_error) => { + throw new Error('CBOR fetch failed'); + }); + } + protected async fetchDetailsFromCBOR(hash: string) { + return this.fetchCBOR(hash) + .then((cbor) => { + const tx = Serialization.Transaction.fromCbor(Serialization.TxCBOR(cbor)).toCore(); + this.logger.info('Fetched details from CBOR for tx', hash); + return tx; + }) + .catch((error) => { + this.logger.warn('Failed to fetch details from CBOR for tx', hash, error); + return null; + }); + } public async utxoByAddresses({ addresses }: UtxoByAddressesArgs): Promise { try { const utxoResults = await Promise.all( addresses.map(async (address) => - fetchByAddressSequentially({ + fetchByAddressSequentially({ address, - request: (addr: Cardano.PaymentAddress, pagination) => - this.blockfrost.addressesUtxos(addr.toString(), pagination), - responseTranslator: (addr: Cardano.PaymentAddress, response: Responses['address_utxo_content']) => - BlockfrostToCore.addressUtxoContent(addr.toString(), response) + request: async (addr: Cardano.PaymentAddress, pagination) => await this.fetchUtxos(addr, pagination) }) ) ); diff --git a/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostClientFactory.ts b/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostClientFactory.ts index 86b3d8e8ec7..a90f25d390d 100644 --- a/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostClientFactory.ts +++ b/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostClientFactory.ts @@ -14,7 +14,8 @@ export const getBlockfrostApi = () => { // custom hosted instance if (process.env.BLOCKFROST_CUSTOM_BACKEND_URL && process.env.BLOCKFROST_CUSTOM_BACKEND_URL !== '') { blockfrostApi = new BlockFrostAPI({ - customBackend: process.env.BLOCKFROST_CUSTOM_BACKEND_URL + customBackend: process.env.BLOCKFROST_CUSTOM_BACKEND_URL, + rateLimiter: false }); return blockfrostApi; @@ -29,7 +30,8 @@ export const getBlockfrostApi = () => { // network is not mandatory, we keep it for safety. blockfrostApi = new BlockFrostAPI({ network: process.env.NETWORK as AvailableNetworks, - projectId: process.env.BLOCKFROST_API_KEY + projectId: process.env.BLOCKFROST_API_KEY, + rateLimiter: false }); return blockfrostApi; diff --git a/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostProvider.ts b/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostProvider.ts index d88030662a0..f72253695fb 100644 --- a/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostProvider.ts +++ b/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostProvider.ts @@ -3,7 +3,7 @@ import { HealthCheckResponse, Provider, ProviderDependencies } from '@cardano-sd import { blockfrostToProviderError } from './blockfrostUtil'; import type { Logger } from 'ts-log'; -/** Properties that are need to create a BlockfrostProvider */ +/** Properties needed to create a BlockfrostProvider */ export interface BlockfrostProviderDependencies extends ProviderDependencies { blockfrost: BlockFrostAPI; logger: Logger; diff --git a/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostToCore.ts b/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostToCore.ts index 26dde88bcc2..68133a340e8 100644 --- a/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostToCore.ts +++ b/packages/cardano-services/src/util/BlockfrostProvider/BlockfrostToCore.ts @@ -1,4 +1,6 @@ -import { Cardano } from '@cardano-sdk/core'; +import { Cardano, Serialization } from '@cardano-sdk/core'; +import { Hash32ByteBase16 } from '@cardano-sdk/crypto'; +import { HexBlob } from '@cardano-sdk/util'; import { Responses } from '@blockfrost/blockfrost-js'; type Unpacked = T extends (infer U)[] ? U : T; @@ -11,11 +13,15 @@ export type BlockfrostTransactionContent = Unpacked; export const BlockfrostToCore = { - addressUtxoContent: (address: string, blockfrost: Responses['address_utxo_content']): Cardano.Utxo[] => - blockfrost.map((utxo) => [ + addressUtxoContent: ( + address: string, + utxo: Responses['address_utxo_content'][0], + txOutFromCbor?: Cardano.TxOut + ): Cardano.Utxo => + [ BlockfrostToCore.hydratedTxIn(BlockfrostToCore.inputFromUtxo(address, utxo)), - BlockfrostToCore.txOut(BlockfrostToCore.outputFromUtxo(address, utxo)) - ]) as Cardano.Utxo[], + BlockfrostToCore.txOut(BlockfrostToCore.outputFromUtxo(address, utxo), txOutFromCbor) + ] as Cardano.Utxo, blockToTip: (block: Responses['block_content']): Cardano.Tip => ({ blockNo: Cardano.BlockNo(block.height!), @@ -92,30 +98,51 @@ export const BlockfrostToCore = { treasuryExpansion: blockfrost.tau.toString() }), - transactionUtxos: (utxoResponse: Responses['tx_content_utxo']) => ({ - collaterals: utxoResponse.inputs.filter((input) => input.collateral).map(BlockfrostToCore.hydratedTxIn), - inputs: utxoResponse.inputs.filter((input) => !input.collateral).map(BlockfrostToCore.hydratedTxIn), - outputs: utxoResponse.outputs.map(BlockfrostToCore.txOut) - }), - - txContentUtxo: (blockfrost: Responses['tx_content_utxo']) => ({ - hash: blockfrost.hash, - inputs: BlockfrostToCore.inputs(blockfrost.inputs), - outputs: BlockfrostToCore.outputs(blockfrost.outputs) - }), + txOut: (blockfrost: BlockfrostOutput, txOutFromCbor?: Cardano.TxOut): Cardano.TxOut => { + const value: Cardano.Value = { + coins: BigInt(blockfrost.amount.find(({ unit }) => unit === 'lovelace')!.quantity) + }; - txOut: (blockfrost: BlockfrostOutput): Cardano.TxOut => { const assets: Cardano.TokenMap = new Map(); for (const { quantity, unit } of blockfrost.amount) { if (unit === 'lovelace') continue; assets.set(Cardano.AssetId(unit), BigInt(quantity)); } - return { + + if (assets.size > 0) value.assets = assets; + + const txOut: Cardano.TxOut = { address: Cardano.PaymentAddress(blockfrost.address), - value: { - assets, - coins: BigInt(blockfrost.amount.find(({ unit }) => unit === 'lovelace')!.quantity) - } + value }; + + if (blockfrost.inline_datum) + txOut.datum = Serialization.PlutusData.fromCbor(HexBlob(blockfrost.inline_datum)).toCore(); + if (blockfrost.data_hash) txOut.datumHash = Hash32ByteBase16(blockfrost.data_hash); + + if (txOutFromCbor?.scriptReference) txOut.scriptReference = txOutFromCbor.scriptReference; + /* + if (scriptCbor && scriptCbor.cbor) { + try { + txOut.scriptReference = Serialization.Script.fromCbor(HexBlob(scriptCbor.cbor)).toCore(); + } catch (error) { + // eslint-disable-next-line no-console + console.log('Error in BlockfrostToCore.txOut:', error); + } + } + */ + /* + if (blockfrost.reference_script_hash) { + try { + txOut.scriptReference = Serialization.Script.fromCbor(HexBlob(blockfrost.reference_script_hash)).toCore(); + } catch (error) { + // eslint-disable-next-line no-console + console.log('Error in BlockfrostToCore.txOut:', error); + } + } +*/ + // if (txOut.scriptReference) delete txOut.scriptReference; + // txOut.scriptReference = {} as Script; + return txOut; } }; diff --git a/packages/cardano-services/test/ChainHistory/BlockfrostChainHistoryProvider/BlockfrostChainHistoryProvider.test.ts b/packages/cardano-services/test/ChainHistory/BlockfrostChainHistoryProvider/BlockfrostChainHistoryProvider.test.ts index 629e3bce19b..6743a00bc5c 100644 --- a/packages/cardano-services/test/ChainHistory/BlockfrostChainHistoryProvider/BlockfrostChainHistoryProvider.test.ts +++ b/packages/cardano-services/test/ChainHistory/BlockfrostChainHistoryProvider/BlockfrostChainHistoryProvider.test.ts @@ -1,6 +1,6 @@ -import { BlockFrostAPI, Responses } from '@blockfrost/blockfrost-js'; +import { BlockFrostAPI } from '@blockfrost/blockfrost-js'; import { BlockfrostChainHistoryProvider } from '../../../src'; -import { Cardano } from '@cardano-sdk/core'; +import { Cardano, NetworkInfoProvider } from '@cardano-sdk/core'; import { dummyLogger as logger } from 'ts-log'; jest.mock('@blockfrost/blockfrost-js'); @@ -8,254 +8,405 @@ jest.mock('@blockfrost/blockfrost-js'); describe('blockfrostChainHistoryProvider', () => { const apiKey = 'someapikey'; - describe('transactionsBy*', () => { - const txsUtxosResponse = { - hash: '4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6', - inputs: [ + const txsUtxosResponse = { + hash: '4123d70f66414cc921f6ffc29a899aafc7137a99a0fd453d6b200863ef5702d6', + inputs: [ + { + address: + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', + amount: [ + { + quantity: '9732978705764', + unit: 'lovelace' + } + ], + output_index: 1, + tx_hash: '6d50c330a6fba79de6949a8dcd5e4b7ffa3f9442f0c5bed7a78fa6d786c6c863' + } + ], + outputs: [ + { + address: + 'addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y', + amount: [ + { + quantity: '1000000000', + unit: 'lovelace' + }, + { + quantity: '63', + unit: '06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108617364' + }, + { + quantity: '22', + unit: '06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108646464' + } + ] + }, + { + address: + 'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x', + amount: [ + { + quantity: '9731978536963', + unit: 'lovelace' + } + ] + } + ] + }; + const mockedTxResponse = { + asset_mint_or_burn_count: 5, + block: '356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940', + block_height: 123_456, + delegation_count: 0, + fees: '182485', + hash: '1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477', + index: 1, + invalid_before: null, + invalid_hereafter: '13885913', + mir_cert_count: 1, + output_amount: [ + { + quantity: '42000000', + unit: 'lovelace' + }, + { + quantity: '12', + unit: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e' + } + ], + pool_retire_count: 1, + pool_update_count: 1, + redeemer_count: 1, + size: 433, + slot: 42_000_000, + stake_cert_count: 1, + utxo_count: 5, + valid_contract: true, + withdrawal_count: 1 + }; + const mockedMetadataResponse = [ + { + json_metadata: { + hash: '6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af', + metadata: 'https://nut.link/metadata.json' + }, + label: '1967' + }, + { + json_metadata: { + ADAUSD: [ + { + source: 'ergoOracles', + value: 3 + } + ] + }, + label: '1968' + } + ]; + const mockedMirResponse = [ + { + address: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc', + amount: '431833601', + cert_index: 0, + pot: 'reserve' + } + ]; + const mockedPoolUpdateResponse = [ + { + active_epoch: 210, + cert_index: 0, + fixed_cost: '340000000', + margin_cost: 0.05, + metadata: { + description: 'The best pool ever', + hash: '47c0c68cb57f4a5b4a87bad896fc274678e7aea98e200fa14a1cb40c0cab1d8c', + homepage: 'https://stakentus.com/', + name: 'Stake Nuts', + ticker: 'NUTS', + url: 'https://stakenuts.com/mainnet.json' + }, + owners: ['stake1u98nnlkvkk23vtvf9273uq7cph5ww6u2yq2389psuqet90sv4xv9v'], + pledge: '5000000000', + pool_id: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy', + relays: [ { - address: - 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2', - amount: [ - { - quantity: '9732978705764', - unit: 'lovelace' - } - ], - output_index: 1, - tx_hash: '6d50c330a6fba79de6949a8dcd5e4b7ffa3f9442f0c5bed7a78fa6d786c6c863' + dns: 'relay1.stakenuts.com', + dns_srv: '_relays._tcp.relays.stakenuts.com', + ipv4: '4.4.4.4', + ipv6: 'https://stakenuts.com/mainnet.json', + port: 3001 } ], - outputs: [ + reward_account: 'stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f', + vrf_key: '0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233' + } + ]; + const mockedPoolRetireResponse = [ + { + cert_index: 0, + pool_id: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy', + retiring_epoch: 216 + } + ]; + const mockedStakeResponse = [ + { + address: 'stake1u9t3a0tcwune5xrnfjg4q7cpvjlgx9lcv0cuqf5mhfjwrvcwrulda', + cert_index: 0, + registration: true + } + ]; + const mockedDelegationResponse = [ + { + active_epoch: 210, + address: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc', + cert_index: 0, + pool_id: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy' + } + ]; + const mockedWithdrawalResponse = [ + { + address: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc', + amount: '431833601' + } + ]; + const mockedReedemerResponse = [ + { + fee: '172033', + purpose: 'spend', + redeemer_data_hash: '923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec', + script_hash: 'ec26b89af41bef0f7585353831cb5da42b5b37185e0c8a526143b824', + tx_index: 0, + unit_mem: '1700', + unit_steps: '476468' + } + ]; + const mockedAddressTransactionResponse = [ + { + block_height: 123, + block_time: 131_322, + tx_hash: '1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477', + tx_index: 0 + } + ]; + const mockedEpochParametersResponse = { key_deposit: '0', pool_deposit: '0' }; + const expectedHydratedTx = { + auxiliaryData: { + blob: new Map([ + [ + 1967n, + new Map([ + ['hash', '6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af'], + ['metadata', 'https://nut.link/metadata.json'] + ]) + ], + [ + 1968n, + new Map([ + [ + 'ADAUSD', + [ + new Map([ + ['source', 'ergoOracles'], + ['value', 3n] + ]) + ] + ] + ]) + ] + ]) + }, + blockHeader: { + blockNo: Cardano.BlockNo(123_456), + hash: Cardano.BlockId('356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940'), + slot: Cardano.Slot(42_000_000) + }, + body: { + certificates: [ { - address: - 'addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y', - amount: [ - { - quantity: '1000000000', - unit: 'lovelace' - }, - { - quantity: '63', - unit: '06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108617364' + __typename: Cardano.CertificateType.PoolRetirement, + epoch: 216, + poolId: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy' + } as unknown as Cardano.HydratedCertificate, + { + __typename: 'PoolRegistrationCertificate', + deposit: 0n, + poolId: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy', + poolParameters: { + cost: 340_000_000n, + id: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy', + margin: { + denominator: 20, + numerator: 1 }, - { - quantity: '22', - unit: '06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108646464' - } - ] + owners: [], + pledge: 5_000_000_000n, + relays: [], + rewardAccount: 'stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f', + vrf: '0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233' + } }, { - address: - 'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x', - amount: [ - { - quantity: '9731978536963', - unit: 'lovelace' - } - ] + __typename: 'MirCertificate', + kind: 'ToStakeCreds', + pot: 'reserve', + quantity: 431_833_601n, + rewardAccount: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc' + }, + { + __typename: 'RegistrationCertificate', + deposit: 0n, + stakeCredential: { + hash: '571ebd7877279a18734c91507b0164be8317f863f1c0269bba64e1b3', + type: 0 + } } - ] - }; - const mockedTxResponse = { - asset_mint_or_burn_count: 5, - block: '356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940', - block_height: 123_456, - delegation_count: 0, - fees: '182485', - hash: '1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477', - index: 1, - invalid_before: null, - invalid_hereafter: '13885913', - mir_cert_count: 1, - output_amount: [ + ], + collateralReturn: undefined, + collaterals: new Array(), + fee: 182_485n, + inputs: [ + { + address: Cardano.PaymentAddress( + 'addr_test1qr05llxkwg5t6c4j3ck5mqfax9wmz35rpcgw3qthrn9z7xcxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknstdz3k2' + ), + index: 1, + txId: Cardano.TransactionId('6d50c330a6fba79de6949a8dcd5e4b7ffa3f9442f0c5bed7a78fa6d786c6c863') + } + ], + mint: new Map([ + [Cardano.AssetId('06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108617364'), 63n], + [Cardano.AssetId('06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108646464'), 22n] + ]), + outputs: [ { - quantity: '42000000', - unit: 'lovelace' + address: Cardano.PaymentAddress( + 'addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y' + ), + value: { + assets: new Map([ + [Cardano.AssetId('06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108617364'), 63n], + [Cardano.AssetId('06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108646464'), 22n] + ]), + coins: 1_000_000_000n + } }, { - quantity: '12', - unit: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e' + address: Cardano.PaymentAddress( + 'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x' + ), + value: { + coins: 9_731_978_536_963n + } } ], - pool_retire_count: 1, - pool_update_count: 1, - redeemer_count: 1, - size: 433, - slot: 42_000_000, - stake_cert_count: 1, - utxo_count: 5, - valid_contract: true, - withdrawal_count: 1 - }; - const mockedMetadataResponse = [ - { - json_metadata: { - hash: '6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af', - metadata: 'https://nut.link/metadata.json' - }, - label: '1967' + proposalProcedures: undefined, + validityInterval: { + invalidBefore: undefined, + invalidHereafter: Cardano.Slot(13_885_913) }, + votingProcedures: undefined, + withdrawals: [ + { + quantity: 431_833_601n, + stakeAddress: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc' + } + ] + }, + id: Cardano.TransactionId('1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477'), + index: 1, + inputSource: Cardano.InputSource.inputs, + txSize: 433, + witness: { + redeemers: [ + { + data: Buffer.from(new Uint8Array([110, 111, 116, 32, 105, 109, 112, 108, 101, 109, 101, 110, 116, 101, 100])), + executionUnits: { + memory: 1700, + steps: 476_468 + }, + index: 0, + purpose: 'spend' + } as Cardano.Redeemer + ], + signatures: new Map() // not available in blockfrost + } + } as Cardano.HydratedTx; + + const mockedNetworkInfoProvider = { + eraSummaries: jest.fn().mockResolvedValue([ { - json_metadata: { - ADAUSD: [ - { - source: 'ergoOracles', - value: 3 - } - ] - }, - label: '1968' - } - ]; - const mockedMirResponse = [ - { - address: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc', - amount: '431833601', - cert_index: 0, - pot: 'reserve' - } - ]; - const mockedPoolUpdateResponse = [ - { - active_epoch: 210, - cert_index: 0, - fixed_cost: '340000000', - margin_cost: 0.05, - metadata: { - description: 'The best pool ever', - hash: '47c0c68cb57f4a5b4a87bad896fc274678e7aea98e200fa14a1cb40c0cab1d8c', - homepage: 'https://stakentus.com/', - name: 'Stake Nuts', - ticker: 'NUTS', - url: 'https://stakenuts.com/mainnet.json' - }, - owners: ['stake1u98nnlkvkk23vtvf9273uq7cph5ww6u2yq2389psuqet90sv4xv9v'], - pledge: '5000000000', - pool_id: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy', - relays: [ - { - dns: 'relay1.stakenuts.com', - dns_srv: '_relays._tcp.relays.stakenuts.com', - ipv4: '4.4.4.4', - ipv6: 'https://stakenuts.com/mainnet.json', - port: 3001 - } - ], - reward_account: 'stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f', - vrf_key: '0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233' - } - ]; - const mockedPoolRetireResponse = [ - { - cert_index: 0, - pool_id: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy', - retiring_epoch: 216 - } - ]; - const mockedStakeResponse = [ - { - address: 'stake1u9t3a0tcwune5xrnfjg4q7cpvjlgx9lcv0cuqf5mhfjwrvcwrulda', - cert_index: 0, - registration: true - } - ]; - const mockedDelegationResponse = [ - { - active_epoch: 210, - address: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc', - cert_index: 0, - pool_id: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy' - } - ]; - const mockedWithdrawalResponse = [ - { - address: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc', - amount: '431833601' - } - ]; - const mockedReedemerResponse = [ - { - fee: '172033', - purpose: 'spend', - redeemer_data_hash: '923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec', - script_hash: 'ec26b89af41bef0f7585353831cb5da42b5b37185e0c8a526143b824', - tx_index: 0, - unit_mem: '1700', - unit_steps: '476468' - } - ]; - const mockedAddressTransactionResponse = [ - { - block_height: 123, - block_time: 131_322, - tx_hash: '1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477', - tx_index: 0 + end: { slot: 100, time: new Date(1_506_203_092_000) }, + parameters: { epochLength: 100, safeZone: 0, slotLength: 1 }, + start: { slot: 0, time: new Date(1_506_203_091_000) } } - ]; - const expectedHydratedTx = { - auxiliaryData: { - blob: new Map([ - [ - 1967n, - new Map([ - ['hash', '6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af'], - ['metadata', 'https://nut.link/metadata.json'] - ]) - ], - [ - 1968n, - new Map([ - [ - 'ADAUSD', - [ - new Map([ - ['source', 'ergoOracles'], - ['value', 3n] - ]) - ] - ] - ]) - ] - ]) - }, + ]) + } as unknown as NetworkInfoProvider; + + describe('transactionsBy*', () => { + let blockfrost: BlockFrostAPI; + let provider: BlockfrostChainHistoryProvider; + beforeEach(() => { + BlockFrostAPI.prototype.txsUtxos = jest.fn().mockResolvedValue(txsUtxosResponse); + BlockFrostAPI.prototype.txs = jest.fn().mockResolvedValue(mockedTxResponse); + BlockFrostAPI.prototype.txsMetadata = jest.fn().mockResolvedValue(mockedMetadataResponse); + BlockFrostAPI.prototype.txsMirs = jest.fn().mockResolvedValue(mockedMirResponse); + BlockFrostAPI.prototype.txsPoolUpdates = jest.fn().mockResolvedValue(mockedPoolUpdateResponse); + BlockFrostAPI.prototype.txsPoolRetires = jest.fn().mockResolvedValue(mockedPoolRetireResponse); + BlockFrostAPI.prototype.txsStakes = jest.fn().mockResolvedValue(mockedStakeResponse); + BlockFrostAPI.prototype.txsDelegations = jest.fn().mockResolvedValue(mockedDelegationResponse); + BlockFrostAPI.prototype.txsWithdrawals = jest.fn().mockResolvedValue(mockedWithdrawalResponse); + BlockFrostAPI.prototype.txsRedeemers = jest.fn().mockResolvedValue(mockedReedemerResponse); + BlockFrostAPI.prototype.addressesTransactions = jest.fn().mockResolvedValue(mockedAddressTransactionResponse); + BlockFrostAPI.prototype.epochsParameters = jest.fn().mockResolvedValue(mockedEpochParametersResponse); + + blockfrost = new BlockFrostAPI({ network: 'preprod', projectId: apiKey }); + + provider = new BlockfrostChainHistoryProvider({ + blockfrost, + logger, + networkInfoProvider: mockedNetworkInfoProvider + }); + provider.fetchCBOR = jest.fn().mockRejectedValue('CBOR is null'); + }); + describe('transactionsByAddresses', () => { + test('converts responses correctly', async () => { + const response = await provider.transactionsByAddresses({ + addresses: [Cardano.PaymentAddress('2cWKMJemoBai9J7kVvRTukMmdfxtjL9z7c396rTfrrzfAZ6EeQoKLC2y1k34hswwm4SVr')], + pagination: { limit: 20, startAt: 0 } + }); + + expect(response.totalResultCount).toBe(1); + expect(response.pageResults[0]).toEqual(expectedHydratedTx); + }); + }); + + describe('transactionsByHashes', () => { + test('converts responses correctly', async () => { + const response = await provider.transactionsByHashes({ + ids: ['1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477'].map(Cardano.TransactionId) + }); + + expect(response).toHaveLength(1); + expect(response[0]).toEqual(expectedHydratedTx); + }); + }); + }); + describe('transactionsBy* with CBOR', () => { + const expectedHydratedTxCBOR = { + auxiliaryData: undefined, blockHeader: { blockNo: Cardano.BlockNo(123_456), hash: Cardano.BlockId('356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940'), slot: Cardano.Slot(42_000_000) }, body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRetirement, - cert_index: 0, - epoch: 216, - poolId: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy' - } as unknown as Cardano.HydratedCertificate, - { - __typename: 'PoolRegistrationCertificate', - cert_index: 0, - poolId: 'pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy', - poolParameters: null - }, - { - __typename: 'MirCertificate', - cert_index: 0, - kind: 'ToStakeCreds', - pot: 'reserve', - quantity: 431_833_601n, - rewardAccount: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc' - }, - { - __typename: 'StakeRegistrationCertificate', - cert_index: 0, - stakeCredential: { - hash: '571ebd7877279a18734c91507b0164be8317f863f1c0269bba64e1b3', - type: 0 - } - } - ], + certificates: undefined, + collateralReturn: undefined, collaterals: new Array(), - fee: 182_485n, + fee: 261_983n, inputs: [ { address: Cardano.PaymentAddress( @@ -265,10 +416,7 @@ describe('blockfrostChainHistoryProvider', () => { txId: Cardano.TransactionId('6d50c330a6fba79de6949a8dcd5e4b7ffa3f9442f0c5bed7a78fa6d786c6c863') } ], - mint: new Map([ - [Cardano.AssetId('06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108617364'), 63n], - [Cardano.AssetId('06f8c5655b4e2b5911fee8ef2fc66b4ce64c8835642695c730a3d108646464'), 22n] - ]), + mint: undefined, outputs: [ { address: Cardano.PaymentAddress( @@ -287,21 +435,17 @@ describe('blockfrostChainHistoryProvider', () => { 'addr_test1qra788mu4sg8kwd93ns9nfdh3k4ufxwg4xhz2r3n064tzfgxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flkns6cy45x' ), value: { - assets: new Map(), coins: 9_731_978_536_963n } } ], + proposalProcedures: undefined, validityInterval: { - invalidBefore: undefined, - invalidHereafter: Cardano.Slot(13_885_913) + invalidBefore: Cardano.Slot(72_258_832), + invalidHereafter: Cardano.Slot(72_259_732) }, - withdrawals: [ - { - quantity: 431_833_601n, - stakeAddress: 'stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc' - } - ] + votingProcedures: undefined, + withdrawals: undefined }, id: Cardano.TransactionId('1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477'), index: 1, @@ -311,25 +455,21 @@ describe('blockfrostChainHistoryProvider', () => { redeemers: [ { data: Buffer.from( - new Uint8Array([ - 101, 99, 50, 54, 98, 56, 57, 97, 102, 52, 49, 98, 101, 102, 48, 102, 55, 53, 56, 53, 51, 53, 51, 56, 51, - 49, 99, 98, 53, 100, 97, 52, 50, 98, 53, 98, 51, 55, 49, 56, 53, 101, 48, 99, 56, 97, 53, 50, 54, 49, - 52, 51, 98, 56, 50, 52 - ]) + new Uint8Array([110, 111, 116, 32, 105, 109, 112, 108, 101, 109, 101, 110, 116, 101, 100]) ), executionUnits: { - memory: 1700, - steps: 476_468 + memory: 436_212, + steps: 179_492_261 }, index: 0, purpose: 'spend' - } as Cardano.Redeemer + } ], - signatures: new Map() // not available in blockfrost + signatures: new Map() } } as Cardano.HydratedTx; let blockfrost: BlockFrostAPI; - + let provider: BlockfrostChainHistoryProvider; beforeEach(() => { BlockFrostAPI.prototype.txsUtxos = jest.fn().mockResolvedValue(txsUtxosResponse); BlockFrostAPI.prototype.txs = jest.fn().mockResolvedValue(mockedTxResponse); @@ -342,175 +482,42 @@ describe('blockfrostChainHistoryProvider', () => { BlockFrostAPI.prototype.txsWithdrawals = jest.fn().mockResolvedValue(mockedWithdrawalResponse); BlockFrostAPI.prototype.txsRedeemers = jest.fn().mockResolvedValue(mockedReedemerResponse); BlockFrostAPI.prototype.addressesTransactions = jest.fn().mockResolvedValue(mockedAddressTransactionResponse); + BlockFrostAPI.prototype.epochsParameters = jest.fn().mockResolvedValue(mockedEpochParametersResponse); blockfrost = new BlockFrostAPI({ network: 'preprod', projectId: apiKey }); + + provider = new BlockfrostChainHistoryProvider({ + blockfrost, + logger, + networkInfoProvider: mockedNetworkInfoProvider + }); + provider.fetchCBOR = jest + .fn() + .mockResolvedValue( + '84a90082825820262e95982cfe9fbc565a0e9a5343d323be8e08e51de23a5262b75ce6984179a900825820262e95982cfe9fbc565a0e9a5343d323be8e08e51de23a5262b75ce6984179a9010d81825820262e95982cfe9fbc565a0e9a5343d323be8e08e51de23a5262b75ce6984179a90112818258205de304d9c8884dd62ad8535d529e3d8fd5f212cc3c0c39410e4f3bccfca6e46b010182a300581d7039a4c3afe97b4c2d3385fefd5206d1865c74786b7ce955ebb6532e7a01821a001b9f18a1581cdab1406f1c769fbdb00514c494eed47a54c7ffc5d7aafc524cca069aa14d446a65644f7261636c654e465401028201d81858a6d8799f58404fe28ad1e94e742a6c79eb8f7cc44128965579792e4b5be940bfa61c3797914970fe2055016c2b7c3bbd9de43194e82b22a4ccdbee80b4099f73a304d9550707d8799fd8799f1a000f42401a00053dc9ffd8799fd8799fd87a9f1b00000192515efa80ffd87a80ffd8799fd87a9f1b00000192516cb620ffd87a80ffff43555344ff581cdab1406f1c769fbdb00514c494eed47a54c7ffc5d7aafc524cca069aff82583900f0a26fc170ad82b64a3d43dede08e78ea6e2028b5101058d0263a80a4c0ec23eba3aa27a4b8d61107f59f3e0d24b7bb6d7ae1a4bbd689c8d1abe8373ea021a0003ff5f031a044e9894081a044e95100e81581cf0a26fc170ad82b64a3d43dede08e78ea6e2028b5101058d0263a80a0b58205b3d8c3bc5032e4d2dbe3c07d23d7fb5133f4ec7466e73744317cebe2ed12610a200818258205401b7f67442e6cc870fbcffe921d24ab03e97e03bb655a24b4f424fd832c61558405bddd2f0d88e254ef1f0a84276aaadea4df3b05f8c441d9774b6a8d1c2556437a79e5131ea4475169d45e6e02bb3b31cd93216c5499e2c316aa68e485d6800000581840000d87980821a0006a7f41a0ab2d5a5f5f6' + ); }); - describe('transactionsByAddresses', () => { - test('converts responses correctly', async () => { - const provider = new BlockfrostChainHistoryProvider({ blockfrost, logger }); + describe('transactionsByAddresses (CBOR)', () => { + test('converts responses correctly (CBOR)', async () => { const response = await provider.transactionsByAddresses({ addresses: [Cardano.PaymentAddress('2cWKMJemoBai9J7kVvRTukMmdfxtjL9z7c396rTfrrzfAZ6EeQoKLC2y1k34hswwm4SVr')], pagination: { limit: 20, startAt: 0 } }); expect(response.totalResultCount).toBe(1); - expect(response.pageResults[0]).toEqual(expectedHydratedTx); + expect(response.pageResults[0]).toEqual(expectedHydratedTxCBOR); }); }); - describe('transactionsByHashes', () => { - test('converts responses correctly', async () => { - const provider = new BlockfrostChainHistoryProvider({ blockfrost, logger }); + describe('transactionsByHashes (CBOR)', () => { + test('converts responses correctly (CBOR)', async () => { const response = await provider.transactionsByHashes({ ids: ['1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477'].map(Cardano.TransactionId) }); expect(response).toHaveLength(1); - expect(response[0]).toEqual(expectedHydratedTx); + expect(response[0]).toEqual(expectedHydratedTxCBOR); }); }); }); - - describe('transactionsBy* throws', () => { - let blockfrost: BlockFrostAPI; - const mockedError = { - error: 'Forbidden', - message: 'Invalid project token.', - status_code: 403, - url: 'test' - }; - - const mockedErrorMethod = jest.fn().mockRejectedValue(mockedError); - beforeAll(() => { - BlockFrostAPI.prototype.txsUtxos = mockedErrorMethod; - BlockFrostAPI.prototype.txs = mockedErrorMethod; - BlockFrostAPI.prototype.txsMetadata = mockedErrorMethod; - BlockFrostAPI.prototype.txsMirs = mockedErrorMethod; - BlockFrostAPI.prototype.txsPoolUpdates = mockedErrorMethod; - BlockFrostAPI.prototype.txsPoolRetires = mockedErrorMethod; - BlockFrostAPI.prototype.txsStakes = mockedErrorMethod; - BlockFrostAPI.prototype.txsDelegations = mockedErrorMethod; - BlockFrostAPI.prototype.txsWithdrawals = mockedErrorMethod; - BlockFrostAPI.prototype.txsRedeemers = mockedErrorMethod; - BlockFrostAPI.prototype.addressesTransactions = mockedErrorMethod; - - blockfrost = new BlockFrostAPI({ network: 'preprod', projectId: apiKey }); - }); - beforeEach(() => { - mockedErrorMethod.mockClear(); - }); - describe('transactionsByAddresses', () => { - test('throws', async () => { - const provider = new BlockfrostChainHistoryProvider({ blockfrost, logger }); - - await expect(() => - provider.transactionsByAddresses({ - addresses: [ - Cardano.PaymentAddress('2cWKMJemoBai9J7kVvRTukMmdfxtjL9z7c396rTfrrzfAZ6EeQoKLC2y1k34hswwm4SVr') - ], - pagination: { limit: 20, startAt: 0 } - }) - ).rejects.toThrow(); - - expect(mockedErrorMethod).toBeCalledTimes(1); - }); - }); - - describe('transactionsByHashes', () => { - test('throws', async () => { - const provider = new BlockfrostChainHistoryProvider({ blockfrost, logger }); - - await expect(() => - provider.transactionsByHashes({ - ids: ['1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477'].map(Cardano.TransactionId) - }) - ).rejects.toThrow(); - expect(mockedErrorMethod).toBeCalledTimes(1); - }); - }); - }); - describe('blocksByHashes', () => { - const blockResponse = { - block_vrf: 'vrf_vk19j362pkr4t9y0m3qxgmrv0365vd7c4ze03ny4jh84q8agjy4ep4s99zvg8', - confirmations: 0, - epoch: 157, - epoch_slot: 312_794, - fees: '513839', - hash: '86e837d8a6cdfddaf364525ce9857eb93430b7e59a5fd776f0a9e11df476a7e5', - height: 2_927_618, - next_block: null, - output: '9249073880', - previous_block: 'da56fa53483a3a087c893b41aa0d73a303148c2887b3f7535e0b505ea5dc10aa', - size: 1050, - slot: 37_767_194, - slot_leader: 'pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh', - time: 1_632_136_410, - tx_count: 3 - } as Responses['block_content']; - test('blocksByHashes', async () => { - BlockFrostAPI.prototype.blocks = jest.fn().mockResolvedValue(blockResponse); - - const blockfrost = new BlockFrostAPI({ network: 'preprod', projectId: apiKey }); - const provider = new BlockfrostChainHistoryProvider({ blockfrost, logger }); - const response = await provider.blocksByHashes({ - ids: [Cardano.BlockId('0dbe461fb5f981c0d01615332b8666340eb1a692b3034f46bcb5f5ea4172b2ed')] - }); - - expect(response).toMatchObject([ - { - confirmations: 0, - date: new Date(1_632_136_410_000), - epoch: 157, - epochSlot: 312_794, - fees: 513_839n, - header: { - blockNo: 2_927_618, - hash: Cardano.BlockId('86e837d8a6cdfddaf364525ce9857eb93430b7e59a5fd776f0a9e11df476a7e5'), - slot: 37_767_194 - }, - nextBlock: undefined, - previousBlock: Cardano.BlockId('da56fa53483a3a087c893b41aa0d73a303148c2887b3f7535e0b505ea5dc10aa'), - size: 1050, - slotLeader: Cardano.PoolId('pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh'), - totalOutput: 9_249_073_880n, - txCount: 3, - vrf: Cardano.VrfVkBech32('vrf_vk19j362pkr4t9y0m3qxgmrv0365vd7c4ze03ny4jh84q8agjy4ep4s99zvg8') - } as Cardano.ExtendedBlockInfo - ]); - }); - - test('blocksByHashes, genesis delegate slot leader', async () => { - const slotLeader = 'ShelleyGenesis-eff1b5b26e65b791'; - BlockFrostAPI.prototype.blocks = jest.fn().mockResolvedValue({ ...blockResponse, slot_leader: slotLeader }); - - const blockfrost = new BlockFrostAPI({ network: 'preprod', projectId: apiKey }); - const provider = new BlockfrostChainHistoryProvider({ blockfrost, logger }); - const response = await provider.blocksByHashes({ - ids: [Cardano.BlockId('0dbe461fb5f981c0d01615332b8666340eb1a692b3034f46bcb5f5ea4172b2ed')] - }); - - expect(response[0].slotLeader).toBe(slotLeader); - }); - test('throws', async () => { - const mockedError = { - error: 'Forbidden', - message: 'Invalid project token.', - status_code: 403, - url: 'test' - }; - const mockedErrorMethod = jest.fn().mockRejectedValue(mockedError); - - BlockFrostAPI.prototype.blocks = mockedErrorMethod; - - const blockfrost = new BlockFrostAPI({ network: 'preprod', projectId: apiKey }); - const provider = new BlockfrostChainHistoryProvider({ blockfrost, logger }); - - await expect(() => - provider.blocksByHashes({ - ids: [Cardano.BlockId('0dbe461fb5f981c0d01615332b8666340eb1a692b3034f46bcb5f5ea4172b2ed')] - }) - ).rejects.toThrow(); - expect(mockedErrorMethod).toBeCalledTimes(1); - }); - }); }); diff --git a/packages/core/src/Provider/providerUtil.ts b/packages/core/src/Provider/providerUtil.ts index 067b432c012..8a3f5531515 100644 --- a/packages/core/src/Provider/providerUtil.ts +++ b/packages/core/src/Provider/providerUtil.ts @@ -17,6 +17,8 @@ export const withProviderErrors = (providerImplementation: T, toPr const tryParseBigIntKey = (key: string) => { // skip converting hex values if (key.startsWith('0x')) return key.slice(2); + if (key.length === 0) return key; + try { return BigInt(key); } catch { diff --git a/packages/core/src/Serialization/AuxiliaryData/AuxiliaryData.ts b/packages/core/src/Serialization/AuxiliaryData/AuxiliaryData.ts index 92e85c0d0bc..089f1b9ee75 100644 --- a/packages/core/src/Serialization/AuxiliaryData/AuxiliaryData.ts +++ b/packages/core/src/Serialization/AuxiliaryData/AuxiliaryData.ts @@ -228,10 +228,13 @@ export class AuxiliaryData { */ toCore(): Cardano.AuxiliaryData { const scripts = this.#getCoreScripts(); - return { - blob: this.#metadata ? this.#metadata.toCore() : undefined, - scripts: scripts.length > 0 ? scripts : undefined + const auxiliaryData: Cardano.AuxiliaryData = { + blob: this.#metadata ? this.#metadata.toCore() : undefined }; + + if (scripts.length > 0) auxiliaryData.scripts = scripts; + + return auxiliaryData; } /** diff --git a/packages/core/src/Serialization/CBOR/CborReader.ts b/packages/core/src/Serialization/CBOR/CborReader.ts index bdb4d8a2996..79814fbcaa2 100644 --- a/packages/core/src/Serialization/CBOR/CborReader.ts +++ b/packages/core/src/Serialization/CBOR/CborReader.ts @@ -525,7 +525,8 @@ export class CborReader { ); } - if (expectedType && expectedType !== nextByte.getMajorType()) + const next = nextByte.getMajorType(); + if (expectedType && expectedType !== next) throw new CborInvalidOperationException( `Major type mismatch, expected type ${expectedType} but got ${nextByte.getMajorType()}` ); diff --git a/packages/core/src/Serialization/Certificates/PoolParams/PoolParams.ts b/packages/core/src/Serialization/Certificates/PoolParams/PoolParams.ts index 73bf485c9c4..f55523b1e99 100644 --- a/packages/core/src/Serialization/Certificates/PoolParams/PoolParams.ts +++ b/packages/core/src/Serialization/Certificates/PoolParams/PoolParams.ts @@ -194,11 +194,10 @@ export class PoolParams { toCore(): Cardano.PoolParameters { const rewardAccountAddress = this.#rewardAccount.toAddress(); - return { + const poolParams: Cardano.PoolParameters = { cost: this.#cost, id: PoolId.fromKeyHash(this.#operator), margin: this.#margin.toCore(), - metadataJson: this.#poolMetadata?.toCore(), owners: this.#poolOwners .toCore() .map((keyHash) => createRewardAccount(keyHash, rewardAccountAddress.getNetworkId())), @@ -207,6 +206,10 @@ export class PoolParams { rewardAccount: this.#rewardAccount.toAddress().toBech32() as Cardano.RewardAccount, vrf: this.#vrfKeyHash }; + + if (this.#poolMetadata) poolParams.metadataJson = this.#poolMetadata.toCore(); + + return poolParams; } /** diff --git a/packages/core/src/Serialization/PlutusData/PlutusData.ts b/packages/core/src/Serialization/PlutusData/PlutusData.ts index 97ef6e204a5..48f40a1e032 100644 --- a/packages/core/src/Serialization/PlutusData/PlutusData.ts +++ b/packages/core/src/Serialization/PlutusData/PlutusData.ts @@ -473,6 +473,8 @@ export class PlutusData { * @param buffer The buffer to be converted to bigint. * @returns The resulting bigint; */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore private static bufferToBigint(buffer: Uint8Array): bigint { let ret = 0n; for (const i of buffer.values()) { diff --git a/packages/core/src/Serialization/Transaction.ts b/packages/core/src/Serialization/Transaction.ts index c922e806a4a..6547666b53e 100644 --- a/packages/core/src/Serialization/Transaction.ts +++ b/packages/core/src/Serialization/Transaction.ts @@ -122,13 +122,18 @@ export class Transaction { * @returns The Core Tx object. */ toCore(): Cardano.Tx { - return { - auxiliaryData: this.#auxiliaryData ? this.#auxiliaryData.toCore() : undefined, + const tx: Cardano.Tx = { body: this.#body.toCore(), id: this.getId(), isValid: this.#isValid, witness: this.#witnessSet.toCore() }; + + if (this.#auxiliaryData) { + tx.auxiliaryData = this.#auxiliaryData.toCore(); + } + + return tx; } /** diff --git a/packages/core/src/Serialization/TransactionBody/TransactionBody.ts b/packages/core/src/Serialization/TransactionBody/TransactionBody.ts index 1e28e482690..f4b596c43a0 100644 --- a/packages/core/src/Serialization/TransactionBody/TransactionBody.ts +++ b/packages/core/src/Serialization/TransactionBody/TransactionBody.ts @@ -400,6 +400,14 @@ export class TransactionBody { * @returns The Core TransactionBody object. */ toCore(): Cardano.TxBody { + /* + if (this.#mint) response.mint = this.#mint; + if (this.#withdrawals) + response.withdrawals = [...this.#withdrawals].map(([stakeAddress, quantity]) => ({ quantity, stakeAddress })); + if (this.#collateral) response.collaterals = this.#collateral.toCore(); + if (this.#proposalProcedures) response.proposalProcedures = this.#proposalProcedures.toCore(); + if (this.#votingProcedures) response.votingProcedures = this.#votingProcedures.toCore(); +*/ return { auxiliaryDataHash: this.#auxiliaryDataHash, certificates: this.#certs?.values() ? this.#certs.toCore() : undefined, @@ -418,13 +426,10 @@ export class TransactionBody { totalCollateral: this.#totalCollateral, treasuryValue: this.#currentTreasuryValue, update: this.#update ? this.#update.toCore() : undefined, - validityInterval: - this.#ttl || this.#validityStartInterval - ? { - invalidBefore: this.#validityStartInterval ? this.#validityStartInterval : undefined, - invalidHereafter: this.#ttl ? this.#ttl : undefined - } - : undefined, + validityInterval: { + invalidBefore: this.#validityStartInterval ? this.#validityStartInterval : undefined, + invalidHereafter: this.#ttl ? this.#ttl : undefined + }, votingProcedures: this.#votingProcedures ? this.#votingProcedures.toCore() : undefined, withdrawals: this.#withdrawals ? [...this.#withdrawals].map(([stakeAddress, quantity]) => ({ quantity, stakeAddress })) diff --git a/packages/core/src/Serialization/TransactionBody/TransactionOutput.ts b/packages/core/src/Serialization/TransactionBody/TransactionOutput.ts index d516e59d51a..5b7b34cbee1 100644 --- a/packages/core/src/Serialization/TransactionBody/TransactionOutput.ts +++ b/packages/core/src/Serialization/TransactionBody/TransactionOutput.ts @@ -208,16 +208,21 @@ export class TransactionOutput { * @returns The Core TransactionOutput object. */ toCore(): Cardano.TxOut { - return { + const value = this.#amount.toCore(); + if (!value.assets) delete value.assets; + + const txOut: Cardano.TxOut = { address: this.#address.asByron() ? this.#address.toBase58() : (this.#address.toBech32() as unknown as Cardano.PaymentAddress), - datum: - this.#datum && this.#datum.kind() === DatumKind.InlineData ? this.#datum.asInlineData()?.toCore() : undefined, - datumHash: this.#datum && this.#datum.kind() === DatumKind.DataHash ? this.#datum.asDataHash() : undefined, - scriptReference: this.#scriptRef ? this.#scriptRef.toCore() : undefined, - value: this.#amount.toCore() + value }; + + if (this.#datum && this.#datum.kind() === DatumKind.InlineData) txOut.datum = this.#datum.asInlineData()?.toCore(); + if (this.#datum && this.#datum.kind() === DatumKind.DataHash) txOut.datumHash = this.#datum.asDataHash(); + if (this.#scriptRef) txOut.scriptReference = this.#scriptRef.toCore(); + + return txOut; } /** diff --git a/packages/core/src/Serialization/TransactionBody/Utils.ts b/packages/core/src/Serialization/TransactionBody/Utils.ts index c4010db1e92..51ebd132e78 100644 --- a/packages/core/src/Serialization/TransactionBody/Utils.ts +++ b/packages/core/src/Serialization/TransactionBody/Utils.ts @@ -15,6 +15,19 @@ export const sortCanonically = (lhs: [string, unknown], rhs: [string, unknown]) return -1; }; +/** + * Sorts the given map entry reverse canonically. + * + * @param lhs The left hand side. + * @param rhs The right hand side. + */ +export const sortCanonicallyReverse = (lhs: [string, unknown], rhs: [string, unknown]) => { + if (lhs[0].length === rhs[0].length) { + return lhs[0] > rhs[0] ? -1 : 1; + } else if (lhs[0].length > rhs[0].length) return -1; + return 1; +}; + /** * transform a token map into a CDDL compatible multiasset structure. * diff --git a/packages/core/src/Serialization/Update/ProtocolParamUpdate.ts b/packages/core/src/Serialization/Update/ProtocolParamUpdate.ts index 4f3b39eedb0..efaa9e07a9c 100644 --- a/packages/core/src/Serialization/Update/ProtocolParamUpdate.ts +++ b/packages/core/src/Serialization/Update/ProtocolParamUpdate.ts @@ -412,7 +412,7 @@ export class ProtocolParamUpdate { * @returns The Core ProtocolParamUpdate object. */ toCore(): Cardano.ProtocolParametersUpdate { - return { + const protocolParametersUpdate: Cardano.ProtocolParametersUpdate = { coinsPerUtxoByte: this.#adaPerUtxoByte ? Number(this.#adaPerUtxoByte) : undefined, collateralPercentage: this.#collateralPercentage, committeeTermLimit: this.#committeeTermLimit ? EpochNo(this.#committeeTermLimit) : undefined, @@ -420,9 +420,7 @@ export class ProtocolParamUpdate { dRepDeposit: this.#drepDeposit, dRepInactivityPeriod: this.#drepInactivityPeriod ? EpochNo(this.#drepInactivityPeriod) : undefined, dRepVotingThresholds: this.#drepVotingThresholds?.toCore(), - decentralizationParameter: this.#d ? this.#d.toFloat().toString() : undefined, desiredNumberOfPools: this.#nOpt, - extraEntropy: this.#extraEntropy, governanceActionDeposit: this.#governanceActionDeposit, governanceActionValidityPeriod: this.#governanceActionValidityPeriod ? EpochNo(this.#governanceActionValidityPeriod) @@ -447,10 +445,15 @@ export class ProtocolParamUpdate { poolRetirementEpochBound: this.#maxEpoch, poolVotingThresholds: this.#poolVotingThresholds?.toCore(), prices: this.#executionCosts?.toCore(), - protocolVersion: this.#protocolVersion?.toCore(), stakeKeyDeposit: this.#keyDeposit ? Number(this.#keyDeposit) : undefined, treasuryExpansion: this.#treasuryGrowthRate ? this.#treasuryGrowthRate.toFloat().toString() : undefined }; + + if (this.#d) protocolParametersUpdate.decentralizationParameter = this.#d.toFloat().toString(); + if (this.#extraEntropy) protocolParametersUpdate.extraEntropy = this.#extraEntropy; + if (this.#protocolVersion) protocolParametersUpdate.protocolVersion = this.#protocolVersion.toCore(); + + return protocolParametersUpdate; } /** diff --git a/packages/e2e/docker-compose.yml b/packages/e2e/docker-compose.yml index f4ca9a127d3..76c21b8c0a2 100644 --- a/packages/e2e/docker-compose.yml +++ b/packages/e2e/docker-compose.yml @@ -15,7 +15,6 @@ services: NODE_ENV: local-network volumes: - ./local-network/config/network/blockfrost-ryo:/app/config - profiles: [blockfrost-ryo] local-testnet: <<: *logging diff --git a/packages/e2e/local-network/templates/babbage/db-sync-config.json b/packages/e2e/local-network/templates/babbage/db-sync-config.json index 733b54ece5f..a117060f50b 100644 --- a/packages/e2e/local-network/templates/babbage/db-sync-config.json +++ b/packages/e2e/local-network/templates/babbage/db-sync-config.json @@ -111,5 +111,8 @@ "scName": "stdout", "scRotation": null } - ] + ], + "insert_options": { + "tx_cbor": "enable" + } } diff --git a/packages/e2e/local-network/templates/blockfrost-ryo/local-network.yaml b/packages/e2e/local-network/templates/blockfrost-ryo/local-network.yaml index 468c196276d..d1b14d16c6e 100644 --- a/packages/e2e/local-network/templates/blockfrost-ryo/local-network.yaml +++ b/packages/e2e/local-network/templates/blockfrost-ryo/local-network.yaml @@ -11,4 +11,5 @@ dbSync: maxConnections: 5 network: "custom" genesisDataFolder: '/app/config' -tokenRegistryUrl: "https://metadata.cardano-testnet.iohkdev.io" +tokenRegistryUrl: "https://tokens.cardano.org" +tokenRegistryEnabled: false diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 2c6665c4fb7..1a116b4c4a1 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -46,9 +46,8 @@ "test:web-extension:watch": "run-s test:web-extension:build test:web-extension:watch:bg", "test:web-extension:watch:bg": "run-p test:web-extension:watch:build test:web-extension:watch:run", "test:ws": "jest -c jest.config.js --forceExit --selectProjects ws-server --runInBand --verbose", - "local-network:common": "DISABLE_DB_CACHE=${DISABLE_DB_CACHE:-true} SUBMIT_API_ARGS='--testnet-magic 888' USE_BLOCKFROST=false __FIX_UMASK__=$(chmod -R a+r ../../compose/placeholder-secrets) docker compose --env-file ../cardano-services/environments/.env.local -p local-network-e2e -f docker-compose.yml -f ../../compose/common.yml -f ../../compose/$(uname -m).yml $FILES --profile ${DOCKER_COMPOSE_PROFILE:-none} up", + "local-network:common": "DISABLE_DB_CACHE=${DISABLE_DB_CACHE:-true} SUBMIT_API_ARGS='--testnet-magic 888' USE_BLOCKFROST=false __FIX_UMASK__=$(chmod -R a+r ../../compose/placeholder-secrets) docker compose --env-file ../cardano-services/environments/.env.local -p local-network-e2e -f docker-compose.yml -f ../../compose/common.yml -f ../../compose/$(uname -m).yml $FILES up", "local-network:up": "FILES='' yarn local-network:common", - "local-network:blockfrost:up": "FILES='' DOCKER_COMPOSE_PROFILE='blockfrost-ryo' yarn local-network:common", "local-network:single:up": "FILES='' yarn local-network:common cardano-node file-server local-testnet ogmios postgres", "local-network:profile:up": "FILES='-f ../../compose/pg-agent.yml' yarn local-network:common", "local-network:down": "docker compose -p local-network-e2e -f docker-compose.yml -f ../../compose/common.yml -f ../../compose/pg-agent.yml down -v --remove-orphans", diff --git a/packages/e2e/test/ws-server/webSocket.test.ts b/packages/e2e/test/ws-server/webSocket.test.ts index 22545c98ac5..6354a1f245f 100644 --- a/packages/e2e/test/ws-server/webSocket.test.ts +++ b/packages/e2e/test/ws-server/webSocket.test.ts @@ -381,8 +381,13 @@ WHERE tx_out_id IS NULL GROUP BY address HAVING COUNT(DISTINCT tx_id) < 1000 ORD await client.chainHistoryProvider.transactionsByAddresses(request); - const wsUtxos = await client.utxoProvider.utxoByAddresses(request); - const httpUtxos = await utxoProvider.utxoByAddresses(request); + const wsUtxos = (await client.utxoProvider.utxoByAddresses(request)).sort( + ([txIn1, _txOut1], [txIn2, _txOut2]) => txIn1.txId.localeCompare(txIn2.txId) || txIn1.index - txIn2.index + ); + const httpUtxos = (await utxoProvider.utxoByAddresses(request)).sort( + ([txIn1, _txOut1], [txIn2, _txOut2]) => txIn1.txId.localeCompare(txIn2.txId) || txIn1.index - txIn2.index + ); + expect(toSerializableObject(wsUtxos)).toEqual(toSerializableObject(httpUtxos)); }); });