From 801e901a1e86c3a61b31da4f0c040a18d667f2de Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Mon, 21 Oct 2024 22:53:19 +0200 Subject: [PATCH 1/6] fix(core): do not convert empty strings in natural zero --- packages/core/src/Provider/providerUtil.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/Provider/providerUtil.ts b/packages/core/src/Provider/providerUtil.ts index 067b432c012..a46acad0cc9 100644 --- a/packages/core/src/Provider/providerUtil.ts +++ b/packages/core/src/Provider/providerUtil.ts @@ -17,6 +17,9 @@ export const withProviderErrors = (providerImplementation: T, toPr const tryParseBigIntKey = (key: string) => { // skip converting hex values if (key.startsWith('0x')) return key.slice(2); + // skip converting empty string + if (key.length === 0) return key; + try { return BigInt(key); } catch { From fcd8c2bd2375cf65cfe6115263a22a76a804b1f4 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Mon, 21 Oct 2024 22:55:57 +0200 Subject: [PATCH 2/6] refactor(core): do not add some undefined keys in deserialization Some deserialization functions add some keys with undefined value. The DbSyncChainHistoryProvider has a slightly different behavior not adding those keys. In order to unify the output of BlockfrostChainHistoryProvider (which deserializes transactions CBOR) with the one from DbSyncChainHistoryProvider these keys are no longer added from deserialization as well. --- .../AuxiliaryData/AuxiliaryData.ts | 9 ++++++--- .../Certificates/PoolParams/PoolParams.ts | 7 +++++-- packages/core/src/Serialization/Transaction.ts | 9 +++++++-- .../TransactionBody/TransactionOutput.ts | 17 +++++++++++------ .../Serialization/Update/ProtocolParamUpdate.ts | 11 +++++++---- 5 files changed, 36 insertions(+), 17 deletions(-) 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/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/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/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/Update/ProtocolParamUpdate.ts b/packages/core/src/Serialization/Update/ProtocolParamUpdate.ts index 4f3b39eedb0..632af8f47f9 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 !== undefined) protocolParametersUpdate.extraEntropy = this.#extraEntropy; + if (this.#protocolVersion) protocolParametersUpdate.protocolVersion = this.#protocolVersion.toCore(); + + return protocolParametersUpdate; } /** From 52271f5ec2f101f1da79b9f01b24a18a914a6b2b Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Mon, 21 Oct 2024 23:02:33 +0200 Subject: [PATCH 3/6] test(ogmios): align tests with new deserialization --- .../ObservableOgmiosCardanoNode.test.ts.snap | 22 +---- .../__snapshots__/block.test.ts.snap | 87 +------------------ 2 files changed, 2 insertions(+), 107 deletions(-) diff --git a/packages/ogmios/test/CardanoNode/__snapshots__/ObservableOgmiosCardanoNode.test.ts.snap b/packages/ogmios/test/CardanoNode/__snapshots__/ObservableOgmiosCardanoNode.test.ts.snap index 27aee5644cf..218b6dbc46f 100644 --- a/packages/ogmios/test/CardanoNode/__snapshots__/ObservableOgmiosCardanoNode.test.ts.snap +++ b/packages/ogmios/test/CardanoNode/__snapshots__/ObservableOgmiosCardanoNode.test.ts.snap @@ -34,7 +34,6 @@ Array [ "block": Object { "body": Array [ Object { - "auxiliaryData": undefined, "body": Object { "auxiliaryDataHash": undefined, "certificates": Array [ @@ -54,7 +53,6 @@ Array [ "denominator": 1, "numerator": 1, }, - "metadataJson": undefined, "owners": Array [ "stake_test1uztg6yppa0t30rslkrneva5c9qju40rhndjnuy356kxw83s6n95nu", ], @@ -94,7 +92,6 @@ Array [ "denominator": 1, "numerator": 1, }, - "metadataJson": undefined, "owners": Array [ "stake_test1urcnqgzt2x8hpsvej4zfudehahknm8lux894pmqwg5qshgcrn346q", ], @@ -134,7 +131,6 @@ Array [ "denominator": 1, "numerator": 1, }, - "metadataJson": undefined, "owners": Array [ "stake_test1uquj460qdrj4az6uy7kvtzct4w8226xq4t30dlzfhc360tgegny4m", ], @@ -174,41 +170,25 @@ Array [ "outputs": Array [ Object { "address": "addr_test1vz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclgmzkket", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 29699998493561943n, }, }, Object { "address": "addr_test1qz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclvk35gzr67hz78plv88jemfs2p9e2780xm98cfrf4vvu0rq83pdz2", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 100000000000000n, }, }, Object { "address": "addr_test1qz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzcl03xqsyk5v0wrqen92yncmn0m0d8k0lcvwt2rkqu3gppw3sdexkvh", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 100000000000000n, }, }, Object { "address": "addr_test1qz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclfe9t57q689t694cfavck9sh2uw545vp2hz7m7yn03r57kslz5rjf", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 100000000000000n, }, }, @@ -269,4 +249,4 @@ Array [ }, }, ] -`; \ No newline at end of file +`; diff --git a/packages/ogmios/test/ogmiosToCore/__snapshots__/block.test.ts.snap b/packages/ogmios/test/ogmiosToCore/__snapshots__/block.test.ts.snap index ceb03db80fa..26acd1e8c25 100644 --- a/packages/ogmios/test/ogmiosToCore/__snapshots__/block.test.ts.snap +++ b/packages/ogmios/test/ogmiosToCore/__snapshots__/block.test.ts.snap @@ -4,7 +4,6 @@ exports[`ogmiosToCore block babbage can translate from babbage block 1`] = ` Object { "body": Array [ Object { - "auxiliaryData": undefined, "body": Object { "auxiliaryDataHash": undefined, "certificates": Array [ @@ -31,11 +30,7 @@ Object { "outputs": Array [ Object { "address": "addr_test1qq2u3e0afx3dqk95hfptv7wm8rl0fht2ecehxzy2yr3g5z79dq6pms68sakpc70q0h37wcn92c9u5jaeu6he7dhypy3sxd05k6", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 9999134743n, }, }, @@ -73,7 +68,6 @@ Object { "blob": Map { 58n => "", }, - "scripts": undefined, }, "body": Object { "auxiliaryDataHash": "3f5d7adf353de22cf1fc514aa911901c5fbd1b5ee3ed57950a6dc5a0fe1dbb20", @@ -99,9 +93,6 @@ Object { "outputs": Array [ Object { "address": "addr_test1qq9tud0fzpj6pdeqeegrma25ngfrre9ruwwcuj2m6dnsreu2qtzuksrwqtv4stsk3hq2uscsh90rf5rdgg38k5t0f0nsp30w94", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { "assets": Map { "eb48b495393986032e8cef0b0a9b2ce64b3881e8a29347e169c7122c4654" => 70n, @@ -111,11 +102,7 @@ Object { }, Object { "address": "addr_test1qq9tud0fzpj6pdeqeegrma25ngfrre9ruwwcuj2m6dnsreu2qtzuksrwqtv4stsk3hq2uscsh90rf5rdgg38k5t0f0nsp30w94", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 9692318369n, }, }, @@ -159,7 +146,6 @@ Object { "blob": Map { 0n => Array [], }, - "scripts": undefined, }, "body": Object { "auxiliaryDataHash": "b93d8e428871a0b0e12c5c554653f515f50fe48dc5b55af7e3667e7d9661ea2c", @@ -183,21 +169,13 @@ Object { "outputs": Array [ Object { "address": "addr_test1qry0mqhwlf84py9nhz5qukujgjjx26cwg3zqvm2aqeuqqneyz5g72vhh8ws2fcjudfjesyl8fy4qss4rdx9v34e2pxps5s28ar", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 1000000n, }, }, Object { "address": "addr_test1qra40qtvuef6dr4ckvzgme7sg5thtdjhjrc268uvn22reqeyz5g72vhh8ws2fcjudfjesyl8fy4qss4rdx9v34e2pxpsglu3mg", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 1639738n, }, }, @@ -231,7 +209,6 @@ Object { }, }, Object { - "auxiliaryData": undefined, "body": Object { "auxiliaryDataHash": undefined, "certificates": undefined, @@ -257,19 +234,13 @@ Object { "outputs": Array [ Object { "address": "addr_test1qrc7mfr62uj0zg07ylfq37ukce6ahxtumdl0d3ar5aw35x758sc7z8763mfpes0j7wqrkkrdgna98c0rtlps585ypdnst69syh", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 2371174n, }, }, Object { "address": "addr_test1wrn96kc0xx774qnyzccygr0qdxs4escmy8ptfu38p3hfehsu5427k", - "datum": undefined, "datumHash": "118cd93427a8e035d484e55ddd4c70200ac5a4d8e63d9d1b89e8a8d49d229415", - "scriptReference": undefined, "value": Object { "assets": Map { "4d2047d7af3d8de799de5daa6e1b0ebfc039f4274084d5bb9ba3975d42594e45544c6963656e7365" => 1n, @@ -279,9 +250,7 @@ Object { }, Object { "address": "addr_test1wrn96kc0xx774qnyzccygr0qdxs4escmy8ptfu38p3hfehsu5427k", - "datum": undefined, "datumHash": "118cd93427a8e035d484e55ddd4c70200ac5a4d8e63d9d1b89e8a8d49d229415", - "scriptReference": undefined, "value": Object { "assets": Map { "4d2047d7af3d8de799de5daa6e1b0ebfc039f4274084d5bb9ba3975d42594e45544c6963656e7365" => 1n, @@ -380,7 +349,6 @@ Object { }, }, Object { - "auxiliaryData": undefined, "body": Object { "auxiliaryDataHash": undefined, "certificates": undefined, @@ -406,19 +374,13 @@ Object { "outputs": Array [ Object { "address": "addr_test1qrc7mfr62uj0zg07ylfq37ukce6ahxtumdl0d3ar5aw35x758sc7z8763mfpes0j7wqrkkrdgna98c0rtlps585ypdnst69syh", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 4444865n, }, }, Object { "address": "addr_test1wrn96kc0xx774qnyzccygr0qdxs4escmy8ptfu38p3hfehsu5427k", - "datum": undefined, "datumHash": "118cd93427a8e035d484e55ddd4c70200ac5a4d8e63d9d1b89e8a8d49d229415", - "scriptReference": undefined, "value": Object { "assets": Map { "4d2047d7af3d8de799de5daa6e1b0ebfc039f4274084d5bb9ba3975d42594e45544c6963656e7365" => 1n, @@ -428,9 +390,7 @@ Object { }, Object { "address": "addr_test1wrn96kc0xx774qnyzccygr0qdxs4escmy8ptfu38p3hfehsu5427k", - "datum": undefined, "datumHash": "118cd93427a8e035d484e55ddd4c70200ac5a4d8e63d9d1b89e8a8d49d229415", - "scriptReference": undefined, "value": Object { "assets": Map { "4d2047d7af3d8de799de5daa6e1b0ebfc039f4274084d5bb9ba3975d42594e45544c6963656e7365" => 1n, @@ -440,9 +400,7 @@ Object { }, Object { "address": "addr_test1wrn96kc0xx774qnyzccygr0qdxs4escmy8ptfu38p3hfehsu5427k", - "datum": undefined, "datumHash": "118cd93427a8e035d484e55ddd4c70200ac5a4d8e63d9d1b89e8a8d49d229415", - "scriptReference": undefined, "value": Object { "assets": Map { "4d2047d7af3d8de799de5daa6e1b0ebfc039f4274084d5bb9ba3975d42594e45544c6963656e7365" => 1n, @@ -545,7 +503,6 @@ Object { "blob": Map { 0n => Array [], }, - "scripts": undefined, }, "body": Object { "auxiliaryDataHash": "b93d8e428871a0b0e12c5c554653f515f50fe48dc5b55af7e3667e7d9661ea2c", @@ -573,21 +530,13 @@ Object { "outputs": Array [ Object { "address": "addr_test1qry0mqhwlf84py9nhz5qukujgjjx26cwg3zqvm2aqeuqqneyz5g72vhh8ws2fcjudfjesyl8fy4qss4rdx9v34e2pxps5s28ar", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 1000000n, }, }, Object { "address": "addr_test1qpavsa3m303vjnpsa4cpjzsgeqmkaa7wya3v94s9pq4wtrpyz5g72vhh8ws2fcjudfjesyl8fy4qss4rdx9v34e2pxpsnutk58", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 9986971687n, }, }, @@ -640,7 +589,6 @@ exports[`ogmiosToCore block can translate from allegra block 1`] = ` Object { "body": Array [ Object { - "auxiliaryData": undefined, "body": Object { "auxiliaryDataHash": undefined, "certificates": undefined, @@ -659,11 +607,7 @@ Object { "outputs": Array [ Object { "address": "addr_test1vz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclgmzkket", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 29699998493147869n, }, }, @@ -719,7 +663,6 @@ exports[`ogmiosToCore block can translate from alonzo block 1`] = ` Object { "body": Array [ Object { - "auxiliaryData": undefined, "body": Object { "auxiliaryDataHash": undefined, "certificates": undefined, @@ -738,11 +681,8 @@ Object { "outputs": Array [ Object { "address": "addr_test1vz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclgmzkket", - "datum": undefined, "datumHash": "c5dfa8c3cbd5a959829618a7b46e163078cb3f1b39f152514d0c3686d553529a", - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 29699998492735907n, }, }, @@ -854,7 +794,6 @@ exports[`ogmiosToCore block can translate from mary block 1`] = ` Object { "body": Array [ Object { - "auxiliaryData": undefined, "body": Object { "auxiliaryDataHash": undefined, "certificates": undefined, @@ -873,11 +812,7 @@ Object { "outputs": Array [ Object { "address": "addr_test1vz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclgmzkket", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 29699998492941888n, }, }, @@ -933,7 +868,6 @@ exports[`ogmiosToCore block can translate from shelley block 1`] = ` Object { "body": Array [ Object { - "auxiliaryData": undefined, "body": Object { "auxiliaryDataHash": undefined, "certificates": Array [ @@ -953,7 +887,6 @@ Object { "denominator": 1, "numerator": 1, }, - "metadataJson": undefined, "owners": Array [ "stake_test1uztg6yppa0t30rslkrneva5c9qju40rhndjnuy356kxw83s6n95nu", ], @@ -993,7 +926,6 @@ Object { "denominator": 1, "numerator": 1, }, - "metadataJson": undefined, "owners": Array [ "stake_test1urcnqgzt2x8hpsvej4zfudehahknm8lux894pmqwg5qshgcrn346q", ], @@ -1033,7 +965,6 @@ Object { "denominator": 1, "numerator": 1, }, - "metadataJson": undefined, "owners": Array [ "stake_test1uquj460qdrj4az6uy7kvtzct4w8226xq4t30dlzfhc360tgegny4m", ], @@ -1073,41 +1004,25 @@ Object { "outputs": Array [ Object { "address": "addr_test1vz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclgmzkket", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 29699998493561943n, }, }, Object { "address": "addr_test1qz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclvk35gzr67hz78plv88jemfs2p9e2780xm98cfrf4vvu0rq83pdz2", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 100000000000000n, }, }, Object { "address": "addr_test1qz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzcl03xqsyk5v0wrqen92yncmn0m0d8k0lcvwt2rkqu3gppw3sdexkvh", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 100000000000000n, }, }, Object { "address": "addr_test1qz09v9yfxguvlp0zsnrpa3tdtm7el8xufp3m5lsm7qxzclfe9t57q689t694cfavck9sh2uw545vp2hz7m7yn03r57kslz5rjf", - "datum": undefined, - "datumHash": undefined, - "scriptReference": undefined, "value": Object { - "assets": undefined, "coins": 100000000000000n, }, }, @@ -1159,4 +1074,4 @@ Object { "txCount": 1, "vrf": "vrf_vk1ny8dyz3pa9u7v7h8l5evsmxxjq07dk6j5uda60lxe3zsya5ea20s2c6jph", } -`; \ No newline at end of file +`; From 103f408929b0ebd709d8d027f24e36ca0f8ee952 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Mon, 21 Oct 2024 23:11:41 +0200 Subject: [PATCH 4/6] feat(cardano-services): improve transaction from blockfrost using cbor to reduce the nr of calls --- .../BlockfrostChainHistoryProvider.ts | 575 +++++++++---- .../BlockfrostNetworkInfoProvider.ts | 9 +- .../src/Program/programs/providerServer.ts | 32 +- .../BlockfrostUtxoProvider.ts | 51 +- .../BlockfrostClientFactory.ts | 6 +- .../BlockfrostProvider/BlockfrostProvider.ts | 2 +- .../BlockfrostProvider/BlockfrostToCore.ts | 50 +- .../util/BlockfrostProvider/blockfrostUtil.ts | 9 +- .../BlockfrostChainHistoryProvider.test.ts | 809 +++++++++--------- 9 files changed, 935 insertions(+), 608 deletions(-) diff --git a/packages/cardano-services/src/ChainHistory/BlockrostChainHistoryProvider/BlockfrostChainHistoryProvider.ts b/packages/cardano-services/src/ChainHistory/BlockrostChainHistoryProvider/BlockfrostChainHistoryProvider.ts index 7b568b9c660..bc75fee397f 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,235 @@ 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 + ? txFromCBOR.body.validityInterval + : { invalidBefore: undefined, invalidHereafter: undefined }, + 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 +482,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 +506,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 +529,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..ee06719853b 100644 --- a/packages/cardano-services/src/NetworkInfo/BlockfrostNetworkInfoProvider/BlockfrostNetworkInfoProvider.ts +++ b/packages/cardano-services/src/NetworkInfo/BlockfrostNetworkInfoProvider/BlockfrostNetworkInfoProvider.ts @@ -1,5 +1,5 @@ import { BlockfrostProvider } from '../../util/BlockfrostProvider/BlockfrostProvider'; -import { BlockfrostToCore, blockfrostToProviderError, networkMagicToIdMap } from '../../util'; +import { BlockfrostToCore, blockfrostToProviderError } from '../../util'; import { Cardano, EraSummary, @@ -63,7 +63,10 @@ export class BlockfrostNetworkInfoProvider extends BlockfrostProvider implements epochLength: response.epoch_length, maxKesEvolutions: response.max_kes_evolutions, maxLovelaceSupply: BigInt(response.max_lovelace_supply), - networkId: networkMagicToIdMap[response.network_magic], + networkId: + response.network_magic === Cardano.NetworkMagics.Mainnet + ? Cardano.NetworkId.Mainnet + : Cardano.NetworkId.Testnet, networkMagic: response.network_magic, securityParameter: response.security_param, slotLength: Seconds(response.slot_length), @@ -80,7 +83,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..9f3ade205ae 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,30 @@ 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; + + return txOut; } }; diff --git a/packages/cardano-services/src/util/BlockfrostProvider/blockfrostUtil.ts b/packages/cardano-services/src/util/BlockfrostProvider/blockfrostUtil.ts index dd0ae980c0f..8f0f7c47677 100644 --- a/packages/cardano-services/src/util/BlockfrostProvider/blockfrostUtil.ts +++ b/packages/cardano-services/src/util/BlockfrostProvider/blockfrostUtil.ts @@ -39,7 +39,7 @@ export const fetchSequentially = async ( request: (arg: Arg, pagination: PaginationOptions) => Promise; responseTranslator?: (response: Response[], arg: Arg) => Item[]; /** - * @returns true to indicatate that current result set should be returned + * @returns true to indicate that current result set should be returned */ haveEnoughItems?: (items: Item[]) => boolean; paginationOptions?: PaginationOptions; @@ -88,7 +88,7 @@ export const fetchByAddressSequentially = async (props: { request: (address: Cardano.PaymentAddress, pagination: PaginationOptions) => Promise; responseTranslator?: (address: Cardano.PaymentAddress, response: Response[]) => Item[]; /** - * @returns true to indicatate that current result set should be returned + * @returns true to indicate that current result set should be returned */ haveEnoughItems?: (items: Item[]) => boolean; paginationOptions?: PaginationOptions; @@ -103,11 +103,6 @@ export const fetchByAddressSequentially = async (props: { : undefined }); -export const networkMagicToIdMap: { [key in number]: Cardano.NetworkId } = { - [Cardano.NetworkMagics.Mainnet]: Cardano.NetworkId.Mainnet, - [Cardano.NetworkMagics.Preprod]: Cardano.NetworkId.Testnet -}; - // copied from util-dev export const testnetEraSummaries: EraSummary[] = [ { 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); - }); - }); }); From 8d9f847a2f948251695f6d09292f0e6bad79532f Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Mon, 21 Oct 2024 23:13:02 +0200 Subject: [PATCH 5/6] feat: add blockfrost based provider server --- ...continuous-integration-blockfrost-e2e.yaml | 93 +++++++++++++++++++ .../workflows/continuous-integration-e2e.yaml | 8 +- compose/common.yml | 35 ++++++- packages/cardano-services/package.json | 6 +- packages/e2e/.env.example | 7 ++ packages/e2e/docker-compose.yml | 2 - packages/e2e/local-network/Dockerfile | 2 +- .../templates/babbage/db-sync-config.json | 5 +- .../blockfrost-ryo/local-network.yaml | 3 +- packages/e2e/package.json | 3 +- .../test/blockfrost/queryTransactions.test.ts | 6 +- 11 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/continuous-integration-blockfrost-e2e.yaml diff --git a/.github/workflows/continuous-integration-blockfrost-e2e.yaml b/.github/workflows/continuous-integration-blockfrost-e2e.yaml new file mode 100644 index 00000000000..fab690b0d83 --- /dev/null +++ b/.github/workflows/continuous-integration-blockfrost-e2e.yaml @@ -0,0 +1,93 @@ +name: Continuous Integration - E2E BF + +env: + TL_DEPTH: ${{ github.event.pull_request.head.repo.fork && '0' || fromJson(vars.TL_DEPTH) }} + TL_LEVEL: ${{ github.event.pull_request.head.repo.fork && 'info' || vars.TL_LEVEL }} + # ----------------------------------------------------------------------------------------- + DB_SYNC_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/cexplorer' + KEY_MANAGEMENT_PROVIDER: 'inMemory' + KEY_MANAGEMENT_PARAMS: '{"bip32Ed25519": "Sodium", "accountIndex": 0, "chainId":{"networkId": 0, "networkMagic": 888}, "passphrase":"some_passphrase","mnemonic":"vacant violin soft weird deliver render brief always monitor general maid smart jelly core drastic erode echo there clump dizzy card filter option defense"}' + OGMIOS_URL: 'ws://localhost:1340/' + STAKE_POOL_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/stake_pool' + STAKE_POOL_TEST_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/stake_pool_test' + TEST_CLIENT_ASSET_PROVIDER: 'http' + TEST_CLIENT_ASSET_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4014/"}' + TEST_CLIENT_CHAIN_HISTORY_PROVIDER: 'http' + TEST_CLIENT_CHAIN_HISTORY_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4001/"}' + TEST_CLIENT_HANDLE_PROVIDER: 'http' + TEST_CLIENT_HANDLE_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4011/"}' + TEST_CLIENT_NETWORK_INFO_PROVIDER: 'ws' + TEST_CLIENT_NETWORK_INFO_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}' + TEST_CLIENT_REWARDS_PROVIDER: 'http' + TEST_CLIENT_REWARDS_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4001/"}' + TEST_CLIENT_TX_SUBMIT_PROVIDER: 'http' + TEST_CLIENT_TX_SUBMIT_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}' + TEST_CLIENT_UTXO_PROVIDER: 'http' + TEST_CLIENT_UTXO_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4001/"}' + TEST_CLIENT_STAKE_POOL_PROVIDER: 'http' + TEST_CLIENT_STAKE_POOL_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}' + WS_PROVIDER_URL: 'http://localhost:4100/ws' + +on: + pull_request: + push: + branches: ['master'] + tags: ['*.*.*'] + +jobs: + build_and_test: + strategy: + matrix: + os: [ubuntu-20.04] + runs-on: ${{ matrix.os }} + steps: + - name: 📥 Checkout repository + uses: actions/checkout@v3 + + - name: 🧰 Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.12.0 + + - name: 🔨 Build + run: | + yarn install --immutable --inline-builds --mode=skip-build + yarn workspace @cardano-sdk/cardano-services-client build:cjs + yarn workspace @cardano-sdk/cardano-services build:cjs + yarn workspace @cardano-sdk/e2e build:cjs + yarn workspace @cardano-sdk/util-dev build:cjs + docker build --no-cache . + env: + NODE_OPTIONS: '--max_old_space_size=8192' + + - name: 🌐 Setup local test network + working-directory: packages/e2e + run: | + yarn local-network:up -d + env: + CARDANO_NODE_CHAINDB_LOG_LEVEL: 'Warning' + CARDANO_NODE_LOG_LEVEL: 'Warning' + OGMIOS_PORT: '1340' + OGMIOS_URL: 'ws://ogmios:1340' + POSTGRES_PORT: '5435' + + - name: Wait for network init + run: | + yarn workspace @cardano-sdk/e2e wait-for-network-init + + - name: 🔬 Test - e2e - wallet at epoch 0 + run: | + yarn workspace @cardano-sdk/e2e test:wallet:epoch0 + + - name: Wait for epoch 3 + run: | + yarn workspace @cardano-sdk/e2e wait-for-network-epoch-3 + + - name: 🔬 Test - e2e - wallet at epoch 3 + run: | + yarn workspace @cardano-sdk/e2e test:wallet:epoch3 + yarn workspace @cardano-sdk/e2e test:blockfrost:providers + + - name: Dump docker logs + if: ${{ cancelled() || failure() }} + uses: jwalton/gh-docker-logs@v2 diff --git a/.github/workflows/continuous-integration-e2e.yaml b/.github/workflows/continuous-integration-e2e.yaml index 30900c70f8f..435063a37c2 100644 --- a/.github/workflows/continuous-integration-e2e.yaml +++ b/.github/workflows/continuous-integration-e2e.yaml @@ -4,20 +4,20 @@ env: TL_DEPTH: ${{ github.event.pull_request.head.repo.fork && '0' || fromJson(vars.TL_DEPTH) }} TL_LEVEL: ${{ github.event.pull_request.head.repo.fork && 'info' || vars.TL_LEVEL }} # ----------------------------------------------------------------------------------------- + DB_SYNC_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/cexplorer' KEY_MANAGEMENT_PROVIDER: 'inMemory' KEY_MANAGEMENT_PARAMS: '{"bip32Ed25519": "Sodium", "accountIndex": 0, "chainId":{"networkId": 0, "networkMagic": 888}, "passphrase":"some_passphrase","mnemonic":"vacant violin soft weird deliver render brief always monitor general maid smart jelly core drastic erode echo there clump dizzy card filter option defense"}' + OGMIOS_URL: 'ws://localhost:1340/' + STAKE_POOL_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/stake_pool' + STAKE_POOL_TEST_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/stake_pool_test' TEST_CLIENT_ASSET_PROVIDER: 'http' TEST_CLIENT_ASSET_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4014/"}' TEST_CLIENT_CHAIN_HISTORY_PROVIDER: 'ws' TEST_CLIENT_CHAIN_HISTORY_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}' - DB_SYNC_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/cexplorer' TEST_CLIENT_HANDLE_PROVIDER: 'http' TEST_CLIENT_HANDLE_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4011/"}' - STAKE_POOL_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/stake_pool' - STAKE_POOL_TEST_CONNECTION_STRING: 'postgresql://postgres:doNoUseThisSecret!@localhost:5435/stake_pool_test' TEST_CLIENT_NETWORK_INFO_PROVIDER: 'ws' TEST_CLIENT_NETWORK_INFO_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}' - OGMIOS_URL: 'ws://localhost:1340/' TEST_CLIENT_REWARDS_PROVIDER: 'http' TEST_CLIENT_REWARDS_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/"}' TEST_CLIENT_TX_SUBMIT_PROVIDER: 'http' diff --git a/compose/common.yml b/compose/common.yml index b963e2be790..9f3cc53fb16 100644 --- a/compose/common.yml +++ b/compose/common.yml @@ -79,8 +79,6 @@ x-provider-server-environment: &provider-server-environment TX_SUBMIT_PROVIDER: ${TX_SUBMIT_PROVIDER:-submit-node} STAKE_POOL_PROVIDER: ${STAKE_POOL_PROVIDER:-dbsync} NETWORK: ${NETWORK:-mainnet} - 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} @@ -118,21 +116,23 @@ x-sdk-environment: &sdk-environment POSTGRES_USER_FILE_STAKE_POOL: /run/secrets/postgres_user POSTGRES_USER_FILE_WALLET_API: /run/secrets/postgres_user TOKEN_METADATA_SERVER_URL: https://metadata.world.dev.cardano.org - USE_WEB_SOCKET_API: true WEB_SOCKET_API_URL: ws://ws-server:3000/ws services: blockfrost-ryo: build: - context: "https://github.com/ginnun/blockfrost-backend-ryo.git#feat/custom-network-support" + context: 'https://github.com/ginnun/blockfrost-backend-ryo.git#feat/custom-network-support' dockerfile: Dockerfile environment: BLOCKFROST_CONFIG_SERVER_LISTEN_ADDRESS: 0.0.0.0 depends_on: cardano-db-sync: condition: service_started + healthcheck: + test: ['CMD-SHELL', 'curl -s --fail http://localhost:3000/health'] ports: - - "3015:3000" + - 3015:3000 + restart: always cardano-db-sync: <<: @@ -350,9 +350,34 @@ services: <<: - *sdk-environment - *provider-server-environment + USE_WEB_SOCKET_API: true ports: - ${API_PORT:-4000}:3000 + blockfrost-provider-server: + <<: + - *from-sdk + - *logging + - *provider-server + - *with-postgres + depends_on: + blockfrost-ryo: + condition: service_healthy + environment: + <<: + - *sdk-environment + - *provider-server-environment + # ATM we don't have BlockfrostHandleProvider and BlockfrostStakePoolProvider + ASSET_PROVIDER: blockfrost + BLOCKFROST_CUSTOM_BACKEND_URL: 'http://blockfrost-ryo:3000' + CHAIN_HISTORY_PROVIDER: blockfrost + NETWORK_INFO_PROVIDER: blockfrost + REWARDS_PROVIDER: blockfrost + UTXO_PROVIDER: blockfrost + ports: + - ${API_PORT:-4001}:3000 + - 9229:9229 + stake-pool-provider-server: <<: - *from-sdk diff --git a/packages/cardano-services/package.json b/packages/cardano-services/package.json index d52d387646f..a8505d4260d 100644 --- a/packages/cardano-services/package.json +++ b/packages/cardano-services/package.json @@ -29,7 +29,7 @@ "cleanup": "rm -rf dist node_modules", "cli": "ts-node --transpile-only src/cli.ts", "compose:single:up": "yarn compose:up cardano-node ogmios postgres", - "compose:up": "__FIX_UMASK__=$(chmod -R a+r ../../compose/placeholder-secrets) docker compose --env-file environments/.env.$NETWORK -p cardano-services-$NETWORK -f docker-compose.yml -f ../../compose/common.yml -f ../../compose/pg-agent.yml -f ../../compose/$(uname -m).yml ${FILES:-} --profile ${DOCKER_COMPOSE_PROFILE:-none} up", + "compose:up": "__FIX_UMASK__=$(chmod -R a+r ../../compose/placeholder-secrets) docker compose --env-file environments/.env.$NETWORK -p cardano-services-$NETWORK -f docker-compose.yml -f ../../compose/common.yml -f ../../compose/pg-agent.yml -f ../../compose/$(uname -m).yml ${FILES:-} up", "compose:down": "docker compose -p cardano-services-$NETWORK -f docker-compose.yml -f ../../compose/common.yml -f ../../compose/pg-agent.yml -f ../../compose/$(uname -m).yml down -t 120", "coverage": "yarn test --coverage || true", "circular-deps:check": "madge --circular dist/cjs", @@ -38,21 +38,17 @@ "generate-migration": "typeorm-ts-node-commonjs migration:generate src/Projection/migrations/migrations -d src/migrationDataSource.ts", "mainnet:single:up": "NETWORK=mainnet yarn compose:single:up", "mainnet:up": "NETWORK=mainnet SUBMIT_API_ARGS=--mainnet yarn compose:up", - "mainnet:blockfrost:up": "DOCKER_COMPOSE_PROFILE='blockfrost-ryo' yarn mainnet:up", "mainnet:down": "NETWORK=mainnet yarn compose:down", "prepack": "yarn build", "preprod:single:up": "NETWORK=preprod yarn compose:single:up", "preprod:up": "NETWORK=preprod SUBMIT_API_ARGS='--testnet-magic 1' yarn compose:up", - "preprod:blockfrost:up": "DOCKER_COMPOSE_PROFILE='blockfrost-ryo' yarn preprod:up", "preprod:down": "NETWORK=preprod yarn compose:down", "pretest": "yarn build", "preview:single:up": "NETWORK=preview yarn compose:single:up", "preview:up": "NETWORK=preview SUBMIT_API_ARGS='--testnet-magic 2' yarn compose:up", - "preview:blockfrost:up": "DOCKER_COMPOSE_PROFILE='blockfrost-ryo' yarn preview:up", "preview:down": "NETWORK=preview yarn compose:down", "sanchonet:single:up": "NETWORK=sanchonet yarn compose:single:up", "sanchonet:up": "NETWORK=sanchonet SUBMIT_API_ARGS='--testnet-magic 4' yarn compose:up", - "sanchonet:blockfrost:up": "DOCKER_COMPOSE_PROFILE='blockfrost-ryo' yarn sanchonet:up", "sanchonet:down": "NETWORK=sanchonet yarn compose:down", "test": "jest --runInBand -c ./jest.config.js --selectProjects unit", "test:build:verify": "tsc --build ./test", diff --git a/packages/e2e/.env.example b/packages/e2e/.env.example index 7a9bd4986e1..24fc638e6ae 100644 --- a/packages/e2e/.env.example +++ b/packages/e2e/.env.example @@ -28,6 +28,13 @@ TEST_CLIENT_STAKE_POOL_PROVIDER=http TEST_CLIENT_STAKE_POOL_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/"}' WS_PROVIDER_URL='ws://localhost:4100/ws' +# Uncomment following to run e2e tests agains blockfrost providers locally +#TEST_CLIENT_CHAIN_HISTORY_PROVIDER=http +#TEST_CLIENT_CHAIN_HISTORY_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4001/"}' +#TEST_CLIENT_REWARDS_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4001/"}' +#TEST_CLIENT_UTXO_PROVIDER=http +#TEST_CLIENT_UTXO_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4001/"}' + # Required by test:ogmios, test:blockfrost DB_SYNC_CONNECTION_STRING='postgresql://postgres:doNoUseThisSecret!@localhost:5435/cexplorer' STAKE_POOL_CONNECTION_STRING='postgresql://postgres:doNoUseThisSecret!@localhost:5435/stake_pool' diff --git a/packages/e2e/docker-compose.yml b/packages/e2e/docker-compose.yml index f4ca9a127d3..8ffa7ba53cd 100644 --- a/packages/e2e/docker-compose.yml +++ b/packages/e2e/docker-compose.yml @@ -6,7 +6,6 @@ x-logging: &logging max-file: '10' services: - blockfrost-ryo: depends_on: local-testnet: @@ -15,7 +14,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/Dockerfile b/packages/e2e/local-network/Dockerfile index d8f9bf00b7c..a31cf59a79c 100644 --- a/packages/e2e/local-network/Dockerfile +++ b/packages/e2e/local-network/Dockerfile @@ -1,6 +1,6 @@ ARG UBUNTU_VERSION=20.04 -FROM ubuntu:${UBUNTU_VERSION} as builder +FROM ubuntu:${UBUNTU_VERSION} AS builder ENV DEBIAN_FRONTEND=nonintercative 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 f96389bd193..e5460a4aa78 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/blockfrost/queryTransactions.test.ts b/packages/e2e/test/blockfrost/queryTransactions.test.ts index 2997d78ebef..b16f8e57017 100644 --- a/packages/e2e/test/blockfrost/queryTransactions.test.ts +++ b/packages/e2e/test/blockfrost/queryTransactions.test.ts @@ -1,4 +1,4 @@ -import { BlockfrostChainHistoryProvider, util } from '@cardano-sdk/cardano-services'; +import { BlockfrostChainHistoryProvider, BlockfrostNetworkInfoProvider, util } from '@cardano-sdk/cardano-services'; import { Cardano, ChainHistoryProvider } from '@cardano-sdk/core'; import { logger } from '@cardano-sdk/util-dev'; @@ -7,9 +7,11 @@ describe.only('BlockfrostChainHistoryProvider', () => { let blockfrost; beforeAll(async () => { blockfrost = util.getBlockfrostApi(); + const networkInfoProvider = new BlockfrostNetworkInfoProvider({ blockfrost, logger }); chainHistoryProvider = new BlockfrostChainHistoryProvider({ blockfrost, - logger + logger, + networkInfoProvider }); }); From b4ce30b429b3b715cf9ea5ea88437413843fbd5f Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 22 Oct 2024 11:08:36 +0200 Subject: [PATCH 6/6] test(e2e): add comparison test skeleton for blockfrost network info provider --- packages/e2e/jest.config.js | 1 + packages/e2e/package.json | 1 + .../blockfrost-providers/networkInfo.test.ts | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 packages/e2e/test/blockfrost-providers/networkInfo.test.ts diff --git a/packages/e2e/jest.config.js b/packages/e2e/jest.config.js index 944d2806e22..a3731178edd 100644 --- a/packages/e2e/jest.config.js +++ b/packages/e2e/jest.config.js @@ -26,6 +26,7 @@ const realAdaTestFileNames = [ module.exports = { projects: [ { ...project('blockfrost'), globalSetup: './test/blockfrost/setup.ts' }, + project('blockfrost-providers'), project('local-network'), project('long-running'), project('ogmios'), diff --git a/packages/e2e/package.json b/packages/e2e/package.json index e5460a4aa78..3a86a161ab8 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -23,6 +23,7 @@ "load-test-custom:wallet-restoration": "ts-node test/load-test-custom/wallet-restoration/wallet-restoration.test.ts", "test": "echo 'test' command not implemented yet", "test:blockfrost": "jest -c jest.config.js --forceExit --selectProjects blockfrost --runInBand --verbose", + "test:blockfrost:providers": "jest -c jest.config.js --forceExit --selectProjects blockfrost-providers --runInBand --verbose", "test:utils": "jest -c jest.config.js --forceExit --selectProjects utils --verbose", "test:long-running": "jest -c jest.config.js --forceExit --selectProjects long-running --runInBand --verbose", "test:local-network": "jest -c jest.config.js --forceExit --selectProjects local-network --runInBand --verbose", diff --git a/packages/e2e/test/blockfrost-providers/networkInfo.test.ts b/packages/e2e/test/blockfrost-providers/networkInfo.test.ts new file mode 100644 index 00000000000..819b2a30dcb --- /dev/null +++ b/packages/e2e/test/blockfrost-providers/networkInfo.test.ts @@ -0,0 +1,26 @@ +// cSpell:ignore cardano utxos + +import { NetworkInfoProvider } from '@cardano-sdk/core'; +import { logger } from '@cardano-sdk/util-dev'; +import { networkInfoHttpProvider } from '@cardano-sdk/cardano-services-client'; +import { toSerializableObject } from '@cardano-sdk/util'; + +// LW-11697 to enable this +describe.skip('Web Socket', () => { + const legacyProvider = networkInfoHttpProvider({ baseUrl: 'http://localhost:4000/', logger }); + const provider = networkInfoHttpProvider({ baseUrl: 'http://localhost:4001/', logger }); + + const methods: (keyof NetworkInfoProvider)[] = [ + 'eraSummaries', + 'genesisParameters', + 'lovelaceSupply', + 'protocolParameters', + 'stake' + ]; + + test.each(methods)('compare %s', async (method) => { + const [legacyResponse, response] = await Promise.all([legacyProvider[method](), provider[method]()]); + + expect(toSerializableObject(response)).toEqual(toSerializableObject(legacyResponse)); + }); +});