From 390e2ff799a30ffbb98c011765d690864edb00e0 Mon Sep 17 00:00:00 2001 From: Carlos Amaro Date: Tue, 4 Jun 2024 12:41:28 +0100 Subject: [PATCH] feat(bungee-hermes): ability to use connectors without instanciating APIs Signed-off-by: Carlos Amaro refactor(bungee): besu strategy error handling and input validation Notice a few things: 1. The http-errors-enhanced-cjs library is being used to encode information about whether an error is consdiered (by us) as a user error or a bug in our code (e.g. it signals who do we think are at fault which is the important question when handling errors.). If we think the input was valid an we should hae succeeded with the function execution, then we throw an InternalServerError. Otherwise if we think the input was not in the required format, we throw a BadRequestError notifying the user (or developer who is calling our function) that they need to fix their input that they provided us with. 2. The error handling code and the output validation code are flattened out as much as possible so that it's easier to read and understand what the code does in what scenario and as the execution progresses the possible bad situations are slowly but surely eliminated and by the end we are confident that the output was in the expected format and that we can return it as-is and we don't need to have any type casting at all. 3. I also added the `pluginRegistry` parameter to the bungee plugin's options because this is an object that the API server passes in to every plugin that it instanties which is meant to facilitate the cross-plugin interaction, e.g. you can use in each plugin the passed in pluginRegistry to look up other plugins instances directly and invoke them without needing an API client object for doing so (which also adds a huge overhead of network latency) The relevant piece of code to be aware of to see why this is working is in the API server class, as pasted below where you can see that the pluginRegistry gets appended to all the plugin options regardless of which kind of plugin it is: ```typescript private async instantiatePlugin( pluginImport: PluginImport, registry: PluginRegistry, ): Promise { const fnTag = `${this.className}#instantiatePlugin`; const { logLevel } = this.options.config; const { packageName, options } = pluginImport; this.log.info(`Creating plugin from package: ${packageName}`, options); const pluginOptions = { ...options, logLevel, pluginRegistry: registry }; ``` Signed-off-by: Peter Somogyvari refactor(bungee): besu, ethereum and fabric strategy error handling and input validation Signed-off-by: Carlos Amaro --- .../cactus-plugin-bungee-hermes/package.json | 6 + .../main/typescript/plugin-bungee-hermes.ts | 2 + .../strategy/obtain-ledger-strategy.ts | 4 +- .../main/typescript/strategy/strategy-besu.ts | 357 +++++++++++++---- .../typescript/strategy/strategy-ethereum.ts | 253 ++++++++---- .../typescript/strategy/strategy-fabric.ts | 293 ++++++++++---- .../integration/besu-test-basic.test.ts | 237 +++++++----- .../integration/besu-test-pruning.test.ts | 184 +++++---- .../integration/bungee-api-test.test.ts | 68 ++-- .../integration/bungee-merge-views.test.ts | 363 ++++++++++-------- .../integration/bungee-process-views.test.ts | 166 ++++---- .../integration/ethereum-test-basic.test.ts | 297 ++++++++------ .../integration/fabric-test-basic.test.ts | 244 ++++++------ .../integration/fabric-test-pruning.test.ts | 202 +++++----- yarn.lock | 1 + 15 files changed, 1704 insertions(+), 973 deletions(-) diff --git a/packages/cactus-plugin-bungee-hermes/package.json b/packages/cactus-plugin-bungee-hermes/package.json index 519af8fb72e..7fff8ec2025 100644 --- a/packages/cactus-plugin-bungee-hermes/package.json +++ b/packages/cactus-plugin-bungee-hermes/package.json @@ -35,6 +35,11 @@ "name": "André Augusto", "email": "andre.augusto@tecnico.ulisboa.pt", "url": "https://github.com/AndreAugusto11" + }, + { + "name": "Carlos Amaro", + "email": "carlosrscamaro@tecnico.ulisboa.pt", + "url": "https://github.com/LordKubaya" } ], "main": "dist/lib/main/typescript/index.js", @@ -64,6 +69,7 @@ "axios": "1.7.2", "body-parser": "1.20.2", "fs-extra": "11.2.0", + "http-errors-enhanced-cjs": "2.0.1", "key-encoder": "2.0.3", "merkletreejs": "0.3.11", "typescript-optional": "2.0.1", diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/plugin-bungee-hermes.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/plugin-bungee-hermes.ts index a660eab1597..d8373dce38b 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/plugin-bungee-hermes.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/plugin-bungee-hermes.ts @@ -48,6 +48,7 @@ import { MergeViewsEndpointV1 } from "./web-services/merge-views-endpoint"; import { ProcessViewEndpointV1 } from "./web-services/process-view-endpoint"; import { PrivacyPolicies } from "./view-creation/privacy-policies"; +import { PluginRegistry } from "@hyperledger/cactus-core"; export interface IKeyPair { publicKey: Uint8Array; @@ -56,6 +57,7 @@ export interface IKeyPair { export interface IPluginBungeeHermesOptions extends ICactusPluginOptions { instanceId: string; + readonly pluginRegistry: PluginRegistry; keyPair?: IKeyPair; logLevel?: LogLevelDesc; diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/obtain-ledger-strategy.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/obtain-ledger-strategy.ts index eeb1b68aa12..d4f412116c0 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/obtain-ledger-strategy.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/obtain-ledger-strategy.ts @@ -1,8 +1,10 @@ import { Logger } from "@hyperledger/cactus-common"; import { State } from "../view-creation/state"; +import { IPluginLedgerConnector } from "@hyperledger/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-plugin-ledger-connector"; export interface NetworkDetails { - connectorApiPath: string; + connectorApiPath?: string; + connector?: IPluginLedgerConnector; participant: string; } export interface ObtainLedgerStrategy { diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-besu.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-besu.ts index cf26e702bdd..9178e8f2fe6 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-besu.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-besu.ts @@ -10,10 +10,12 @@ import { EthContractInvocationType, EvmBlock, EvmLog, + EvmTransaction, GetBlockV1Request, GetPastLogsV1Request, GetTransactionV1Request, InvokeContractV1Request, + PluginLedgerConnectorBesu, Web3SigningCredential, } from "@hyperledger/cactus-plugin-ledger-connector-besu"; import { State } from "../view-creation/state"; @@ -23,12 +25,14 @@ import { Transaction } from "../view-creation/transaction"; import Web3 from "web3"; import { Proof } from "../view-creation/proof"; import { TransactionProof } from "../view-creation/transaction-proof"; +import { BadRequestError, InternalServerError } from "http-errors-enhanced-cjs"; export interface BesuNetworkDetails extends NetworkDetails { signingCredential: Web3SigningCredential; keychainId: string; contractName: string; contractAddress: string; } + export class StrategyBesu implements ObtainLedgerStrategy { public static readonly CLASS_NAME = "StrategyBesu"; @@ -44,25 +48,40 @@ export class StrategyBesu implements ObtainLedgerStrategy { stateIds: string[], networkDetails: BesuNetworkDetails, ): Promise> { - const fnTag = `${StrategyBesu.CLASS_NAME}#generateLedgerStates()`; + const fn = `${StrategyBesu.CLASS_NAME}#generateLedgerStates()`; this.log.debug(`Generating ledger snapshot`); - Checks.truthy(networkDetails, `${fnTag} networkDetails`); + Checks.truthy(networkDetails, `${fn} networkDetails`); - const config = new Configuration({ - basePath: networkDetails.connectorApiPath, - }); - const besuApi = new BesuApi(config); + let besuApi: BesuApi | undefined; + let connector: PluginLedgerConnectorBesu | undefined; + + if (networkDetails.connector) { + connector = networkDetails.connector as PluginLedgerConnectorBesu; + } else if (networkDetails.connectorApiPath) { + const config = new Configuration({ + basePath: networkDetails.connectorApiPath, + }); + besuApi = new BesuApi(config); + } else { + throw new Error( + `${fn} networkDetails must have either connector or connectorApiPath`, + ); + } + const connectorOrApiClient = networkDetails.connector ? connector : besuApi; + if (!connectorOrApiClient) { + throw new InternalServerError(`${fn} got neither connector nor BesuAPI`); + } const ledgerStates = new Map(); const assetsKey = stateIds.length == 0 - ? await this.getAllAssetsKey(networkDetails, besuApi) + ? await this.getAllAssetsKey(networkDetails, connectorOrApiClient) : stateIds; this.log.debug("Current assets detected to capture: " + assetsKey); for (const assetKey of assetsKey) { const { transactions, values, blocks } = await this.getAllInfoByKey( assetKey, networkDetails, - besuApi, + connectorOrApiClient, ); const state = new State(assetKey, values, transactions); @@ -92,7 +111,7 @@ export class StrategyBesu implements ObtainLedgerStrategy { async getAllAssetsKey( networkDetails: BesuNetworkDetails, - api: BesuApi, + connectorOrApiClient: PluginLedgerConnectorBesu | BesuApi, ): Promise { const parameters = { contractName: networkDetails.contractName, @@ -103,29 +122,17 @@ export class StrategyBesu implements ObtainLedgerStrategy { signingCredential: networkDetails.signingCredential, gas: 1000000, }; - const response = await api.invokeContractV1( + const response = await this.invokeContract( parameters as InvokeContractV1Request, + connectorOrApiClient, ); - - if (response.status >= 200 && response.status < 300) { - if (response.data.callOutput) { - return response.data.callOutput as string[]; - } else { - throw new Error( - `${StrategyBesu.CLASS_NAME}#getAllAssetsKey: contract ${networkDetails.contractName} method getAllAssetsIDs output is falsy`, - ); - } - } - throw new Error( - `${StrategyBesu.CLASS_NAME}#getAllAssetsKey: BesuAPI error with status ${response.status}: ` + - response.data, - ); + return response; } async getAllInfoByKey( key: string, networkDetails: BesuNetworkDetails, - api: BesuApi, + connectorOrApiClient: PluginLedgerConnectorBesu | BesuApi, ): Promise<{ transactions: Transaction[]; values: string[]; @@ -137,57 +144,27 @@ export class StrategyBesu implements ObtainLedgerStrategy { address: networkDetails.contractAddress, topics: [[null], [Web3.utils.keccak256(key)]], //filter logs by asset key }; - const response = await api.getPastLogsV1(req as GetPastLogsV1Request); - if (response.status < 200 || response.status >= 300) { - throw new Error( - `${StrategyBesu.CLASS_NAME}#getAllInfoByKey: BesuAPI getPastLogsV1 error with status ${response.status}: ` + - response.data, - ); - } - if (!response.data.logs) { - throw new Error( - `${StrategyBesu.CLASS_NAME}#getAllInfoByKey: BesuAPI getPastLogsV1 API call successfull but output data is falsy`, - ); - } - const decoded = response.data.logs as EvmLog[]; + const decoded = await this.getPastLogs(req, connectorOrApiClient); const transactions: Transaction[] = []; const blocks: Map = new Map(); const values: string[] = []; this.log.debug("Getting transaction logs for asset: " + key); for (const log of decoded) { - const txTx = await api.getTransactionV1({ - transactionHash: log.transactionHash, - } as GetTransactionV1Request); - - if (txTx.status < 200 || txTx.status >= 300) { - throw new Error( - `${StrategyBesu.CLASS_NAME}#getAllInfoByKey: BesuAPI getTransactionV1 error with status ${txTx.status}: ` + - txTx.data, - ); - } - if (!txTx.data.transaction) { - throw new Error( - `${StrategyBesu.CLASS_NAME}#getAllInfoByKey: BesuAPI getTransactionV1 call successfull but output data is falsy`, - ); - } - - const txBlock = await api.getBlockV1({ - blockHashOrBlockNumber: log.blockHash, - } as GetBlockV1Request); + const txTx = await this.getTransaction( + { + transactionHash: log.transactionHash, + } as GetTransactionV1Request, + connectorOrApiClient, + ); - if (txBlock.status < 200 || txBlock.status >= 300) { - throw new Error( - `${StrategyBesu.CLASS_NAME}#getAllInfoByKey: BesuAPI getBlockV1 error with status ${txBlock.status}: ` + - txBlock.data, - ); - } - if (!txBlock.data.block) { - throw new Error( - `${StrategyBesu.CLASS_NAME}#getAllInfoByKey: BesuAPI getBlockV1 call successfull but output data is falsy`, - ); - } + const txBlock = await this.getBlock( + { + blockHashOrBlockNumber: log.blockHash, + } as GetBlockV1Request, + connectorOrApiClient, + ); this.log.debug( "Transaction: " + @@ -197,24 +174,256 @@ export class StrategyBesu implements ObtainLedgerStrategy { "\n =========== \n", ); const proof = new Proof({ - creator: txTx.data.transaction.from as string, //no sig for besu + creator: txTx.from as string, //no sig for besu }); const transaction: Transaction = new Transaction( log.transactionHash, - txBlock.data.block.timestamp, + txBlock.timestamp, new TransactionProof(proof, log.transactionHash), ); transaction.setStateId(key); transaction.setTarget(networkDetails.contractAddress as string); - transaction.setPayload( - txTx.data.transaction.input ? txTx.data.transaction.input : "", - ); //FIXME: payload = transaction input ? + transaction.setPayload(txTx.input ? txTx.input : ""); //FIXME: payload = transaction input ? transactions.push(transaction); values.push(JSON.stringify(log.data)); - blocks.set(transaction.getId(), txBlock.data.block); + blocks.set(transaction.getId(), txBlock); } return { transactions: transactions, values: values, blocks: blocks }; } + + async invokeContract( + parameters: InvokeContractV1Request, + connectorOrApiClient: PluginLedgerConnectorBesu | BesuApi, + ): Promise { + const fn = `${StrategyBesu.CLASS_NAME}#invokeContract()`; + if (!connectorOrApiClient) { + // throw BadRequestError because it is not our fault that we did not get + // all the needed parameters, e.g. we are signaling that this is a "user error" + // where the "user" is the other developer who called our function. + throw new BadRequestError(`${fn} connectorOrApiClient is falsy`); + } else if (connectorOrApiClient instanceof PluginLedgerConnectorBesu) { + const connector: PluginLedgerConnectorBesu = connectorOrApiClient; + const response = await connector.invokeContract(parameters); + if (!response) { + // We throw an InternalServerError because the user is not responsible + // for us not being able to obtain a result from the contract invocation. + // They provided us parameters for the call (which we then validated and + // accepted) an therefore now if something goes wrong we have to throw + // an exception accordingly (e.g. us "admitting fault") + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.callOutput) { + throw new InternalServerError(`${fn} response.callOutput is falsy`); + } + const { callOutput } = response; + if (!Array.isArray(callOutput)) { + throw new InternalServerError(`${fn} callOutput not an array`); + } + const allItemsAreStrings = callOutput.every((x) => typeof x === "string"); + if (!allItemsAreStrings) { + throw new InternalServerError(`${fn} callOutput has non-string items`); + } + return response.callOutput as string[]; + } else if (connectorOrApiClient instanceof BesuApi) { + const api: BesuApi = connectorOrApiClient; + const response = await api.invokeContractV1(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + // We log the error here on the debug level so that later on we can inspect the contents + // of it in the logs if we need to. The reason that this is important is because we do not + // want to dump the full response onto our own error response that is going back to the caller + // due to that potentially being a security issue that we are exposing internal data via the + // error responses. + // With that said, we still need to make sure that we can determine the root cause of any + // issues after the fact and therefore we must save the error response details somewhere (the logs) + this.log.debug("BesuAPI non-2xx HTTP response:", data, status, config); + + // For the caller/client we just send back a generic error admitting that we somehow messed up: + const errorMessage = `${fn} BesuAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + if (!data.callOutput) { + throw new InternalServerError(`${fn} data.callOutput is falsy`); + } + const { callOutput } = data; + if (!Array.isArray(callOutput)) { + throw new InternalServerError(`${fn} callOutput not an array`); + } + const allItemsAreStrings = callOutput.every((x) => typeof x === "string"); + if (!allItemsAreStrings) { + throw new InternalServerError(`${fn} callOutput has non-string items`); + } + return response.data.callOutput; + } + throw new InternalServerError(`${fn}: neither BesuAPI nor Connector given`); + } + + async getPastLogs( + req: GetPastLogsV1Request, + connectorOrApiClient: PluginLedgerConnectorBesu | BesuApi, + ): Promise { + const fn = `${StrategyBesu.CLASS_NAME}#getPastLogs()`; + if (!connectorOrApiClient) { + throw new BadRequestError(`${fn} connectorOrApiClient is falsy`); + } else if (connectorOrApiClient instanceof PluginLedgerConnectorBesu) { + const connector: PluginLedgerConnectorBesu = connectorOrApiClient; + const response = await connector.getPastLogs(req); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.logs) { + throw new InternalServerError(`${fn} response.logs is falsy`); + } + const { logs } = response; + if (!Array.isArray(logs)) { + throw new InternalServerError(`${fn} logs not an array`); + } + const allItemsAreEvmLog = logs.every((x) => this.isEvmLog(x)); + if (!allItemsAreEvmLog) { + throw new InternalServerError(`${fn} logs has non-EvmLog items`); + } + return response.logs as EvmLog[]; + } else if (connectorOrApiClient instanceof BesuApi) { + const api: BesuApi = connectorOrApiClient; + const response = await api.getPastLogsV1(req); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + this.log.debug("BesuAPI non-2xx HTTP response:", data, status, config); + const errorMessage = `${fn} BesuAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + if (!data.logs) { + throw new InternalServerError(`${fn} data.logs is falsy`); + } + const { logs } = data; + if (!Array.isArray(logs)) { + throw new InternalServerError(`${fn} logs not an array`); + } + const allItemsAreEvmLog = logs.every((x) => this.isEvmLog(x)); + if (!allItemsAreEvmLog) { + throw new InternalServerError(`${fn} logs has non-EvmLog items`); + } + return response.data.logs; + } + throw new InternalServerError(`${fn}: neither BesuAPI nor Connector given`); + } + + isEvmLog(x: any): x is EvmLog { + return ( + "address" in x && + "data" in x && + "blockHash" in x && + "transactionHash" in x && + "topics" in x && + "blockNumber" in x && + "logIndex" in x && + "transactionIndex" in x + ); + } + + async getTransaction( + req: GetTransactionV1Request, + connectorOrApiClient: PluginLedgerConnectorBesu | BesuApi, + ): Promise { + const fn = `${StrategyBesu.CLASS_NAME}#getTransaction()`; + if (!connectorOrApiClient) { + } else if (connectorOrApiClient instanceof PluginLedgerConnectorBesu) { + const connector: PluginLedgerConnectorBesu = connectorOrApiClient; + const response = await connector.getTransaction(req); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.transaction) { + throw new InternalServerError(`${fn} response.transaction is falsy`); + } + return response.transaction; + } else if (connectorOrApiClient instanceof BesuApi) { + const api: BesuApi = connectorOrApiClient; + const response = await api.getTransactionV1(req); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + this.log.debug("BesuAPI non-2xx HTTP response:", data, status, config); + + const errorMessage = `${fn} BesuAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + if (!data.transaction) { + throw new InternalServerError(`${fn} data.transaction is falsy`); + } + return response.data.transaction; + } + throw new InternalServerError(`${fn}: neither BesuAPI nor Connector given`); + } + + async getBlock( + req: GetBlockV1Request, + connectorOrApiClient: PluginLedgerConnectorBesu | BesuApi, + ): Promise { + const fn = `${StrategyBesu.CLASS_NAME}#getBlock()`; + if (!connectorOrApiClient) { + } else if (connectorOrApiClient instanceof PluginLedgerConnectorBesu) { + const connector: PluginLedgerConnectorBesu = connectorOrApiClient; + const response = await connector.getBlock(req); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.block) { + throw new InternalServerError(`${fn} response.block is falsy`); + } + return response.block; + } else if (connectorOrApiClient instanceof BesuApi) { + const api: BesuApi = connectorOrApiClient; + const response = await api.getBlockV1(req); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + this.log.debug("BesuAPI non-2xx HTTP response:", data, status, config); + + const errorMessage = `${fn} BesuAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + if (!data.block) { + throw new InternalServerError(`${fn} data.block is falsy`); + } + return data.block; + } + throw new InternalServerError(`${fn}: neither BesuAPI nor Connector given`); + } } diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-ethereum.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-ethereum.ts index c77824b7274..28e189c4159 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-ethereum.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-ethereum.ts @@ -10,6 +10,8 @@ import { EthContractInvocationType, InvokeContractV1Request, InvokeRawWeb3EthMethodV1Request, + PluginLedgerConnectorEthereum, + InvokeRawWeb3EthMethodV1Response, } from "@hyperledger/cactus-plugin-ledger-connector-ethereum"; import { NetworkDetails, ObtainLedgerStrategy } from "./obtain-ledger-strategy"; import { Configuration } from "@hyperledger/cactus-core-api"; @@ -19,6 +21,7 @@ import Web3 from "web3"; import { Proof } from "../view-creation/proof"; import { TransactionProof } from "../view-creation/transaction-proof"; import { Transaction } from "../view-creation/transaction"; +import { BadRequestError, InternalServerError } from "http-errors-enhanced-cjs"; interface EvmLog { address: string; @@ -73,26 +76,44 @@ export class StrategyEthereum implements ObtainLedgerStrategy { stateIds: string[], networkDetails: EthereumNetworkDetails, ): Promise> { - const fnTag = `${StrategyEthereum.CLASS_NAME}#generateLedgerStates()`; + const fn = `${StrategyEthereum.CLASS_NAME}#generateLedgerStates()`; this.log.debug(`Generating ledger snapshot`); - Checks.truthy(networkDetails, `${fnTag} networkDetails`); + Checks.truthy(networkDetails, `${fn} networkDetails`); - const config = new Configuration({ - basePath: networkDetails.connectorApiPath, - }); - const ethereumApi = new EthereumApi(config); + let ethereumApi: EthereumApi | undefined; + let connector: PluginLedgerConnectorEthereum | undefined; + + if (networkDetails.connector) { + connector = networkDetails.connector as PluginLedgerConnectorEthereum; + } else if (networkDetails.connectorApiPath) { + const config = new Configuration({ + basePath: networkDetails.connectorApiPath, + }); + ethereumApi = new EthereumApi(config); + } else { + throw new Error( + `${StrategyEthereum.CLASS_NAME}#generateLedgerStates: networkDetails must have either connector or connectorApiPath`, + ); + } + + const connectorOrApiClient = connector ? connector : ethereumApi; + if (!connectorOrApiClient) { + throw new InternalServerError( + `${fn} got neither connector nor EthereumApi`, + ); + } const ledgerStates = new Map(); const assetsKey = stateIds.length == 0 - ? await this.getAllAssetsKey(networkDetails, ethereumApi) + ? await this.getAllAssetsKey(networkDetails, connectorOrApiClient) : stateIds; this.log.debug("Current assets detected to capture: " + assetsKey); for (const assetKey of assetsKey) { const { transactions, values, blocks } = await this.getAllInfoByKey( assetKey, networkDetails, - ethereumApi, + connectorOrApiClient, ); const state = new State(assetKey, values, transactions); @@ -122,7 +143,7 @@ export class StrategyEthereum implements ObtainLedgerStrategy { async getAllAssetsKey( networkDetails: EthereumNetworkDetails, - api: EthereumApi, + connectorOrApiClient: PluginLedgerConnectorEthereum | EthereumApi, ): Promise { const parameters = { contract: { @@ -134,28 +155,17 @@ export class StrategyEthereum implements ObtainLedgerStrategy { params: [], signingCredential: networkDetails.signingCredential, }; - const response = await api.invokeContractV1( + const response = await this.invokeContract( parameters as InvokeContractV1Request, + connectorOrApiClient, ); - if (response.status >= 200 && response.status < 300) { - if (response.data.callOutput) { - return response.data.callOutput as string[]; - } else { - throw new Error( - `${StrategyEthereum.CLASS_NAME}#getAllAssetsKey: contract ${networkDetails.contractName} method getAllAssetsIDs output is falsy`, - ); - } - } - throw new Error( - `${StrategyEthereum.CLASS_NAME}#getAllAssetsKey: EthereumAPI error with status ${response.status}: ` + - response.data, - ); + return response; } async getAllInfoByKey( key: string, networkDetails: EthereumNetworkDetails, - api: EthereumApi, + connectorOrApiClient: PluginLedgerConnectorEthereum | EthereumApi, ): Promise<{ transactions: Transaction[]; values: string[]; @@ -171,19 +181,11 @@ export class StrategyEthereum implements ObtainLedgerStrategy { methodName: "getPastLogs", params: [filter], }; - const response = await api.invokeWeb3EthMethodV1(getLogsReq); - if (response.status < 200 || response.status >= 300) { - throw new Error( - `${StrategyEthereum.CLASS_NAME}#getAllInfoByKey: BesuAPI getPastLogsV1 error with status ${response.status}: ` + - response.data, - ); - } - if (!response.data.data) { - throw new Error( - `${StrategyEthereum.CLASS_NAME}#getAllInfoByKey: BesuAPI getPastLogsV1 API call successfull but output data is falsy`, - ); - } - const decoded = response.data.data as EvmLog[]; + const response = await this.invokeWeb3EthMethod( + getLogsReq, + connectorOrApiClient, + ); + const decoded = response.data as EvmLog[]; const transactions: Transaction[] = []; const blocks: Map = new Map(); const values: string[] = []; @@ -194,37 +196,19 @@ export class StrategyEthereum implements ObtainLedgerStrategy { methodName: "getTransaction", params: [log.transactionHash], }; - const txTx = await api.invokeWeb3EthMethodV1(getTransactionReq); - - if (txTx.status < 200 || txTx.status >= 300) { - throw new Error( - `${StrategyEthereum.CLASS_NAME}#getAllInfoByKey: EthereumAPI invokeWeb3EthMethodV1 getTransaction error with status ${txTx.status}: ` + - txTx.data, - ); - } - if (!txTx.data.data) { - throw new Error( - `${StrategyEthereum.CLASS_NAME}#getAllInfoByKey: EthereumAPI invokeWeb3EthMethodV1 getTransaction successfull but output data is falsy`, - ); - } + const txTx = await this.invokeWeb3EthMethod( + getTransactionReq, + connectorOrApiClient, + ); const getBlockReq: InvokeRawWeb3EthMethodV1Request = { methodName: "getBlock", params: [log.blockHash], }; - const txBlock = await api.invokeWeb3EthMethodV1(getBlockReq); - - if (txBlock.status < 200 || txBlock.status >= 300) { - throw new Error( - `${StrategyEthereum.CLASS_NAME}#getAllInfoByKey: EthereumAPI invokeWeb3EthMethodV1 getBlock error with status ${txBlock.status}: ` + - txBlock.data, - ); - } - if (!txBlock.data.data) { - throw new Error( - `${StrategyEthereum.CLASS_NAME}#getAllInfoByKey: EthereumAPI invokeWeb3EthMethodV1 getBlock successfull but output data is falsy`, - ); - } + const txBlock = await this.invokeWeb3EthMethod( + getBlockReq, + connectorOrApiClient, + ); this.log.debug( "Transaction: " + @@ -234,22 +218,157 @@ export class StrategyEthereum implements ObtainLedgerStrategy { "\n =========== \n", ); const proof = new Proof({ - creator: txTx.data.data.from as string, //no sig for ethereum + creator: txTx.data.from as string, //no sig for ethereum }); const transaction: Transaction = new Transaction( log.transactionHash, - txBlock.data.data.timestamp, + txBlock.data.timestamp, new TransactionProof(proof, log.transactionHash), ); transaction.setStateId(key); transaction.setTarget(networkDetails.contractAddress as string); - transaction.setPayload(txTx.data.data.input ? txTx.data.data.input : ""); //FIXME: payload = transaction input ? + transaction.setPayload(txTx.data.input ? txTx.data.input : ""); //FIXME: payload = transaction input ? transactions.push(transaction); values.push(JSON.stringify(log.data)); - blocks.set(transaction.getId(), txBlock.data.data); + blocks.set(transaction.getId(), txBlock.data); } return { transactions: transactions, values: values, blocks: blocks }; } + + async invokeContract( + parameters: InvokeContractV1Request, + connectorOrApiClient: PluginLedgerConnectorEthereum | EthereumApi, + ): Promise { + const fn = `${StrategyEthereum.CLASS_NAME}#invokeContract()`; + if (!connectorOrApiClient) { + // throw BadRequestError because it is not our fault that we did not get + // all the needed parameters, e.g. we are signaling that this is a "user error" + // where the "user" is the other developer who called our function. + throw new BadRequestError(`${fn} connectorOrApiClient is falsy`); + } else if (connectorOrApiClient instanceof PluginLedgerConnectorEthereum) { + const connector: PluginLedgerConnectorEthereum = connectorOrApiClient; + const response = await connector.invokeContract(parameters); + if (!response) { + // We throw an InternalServerError because the user is not responsible + // for us not being able to obtain a result from the contract invocation. + // They provided us parameters for the call (which we then validated and + // accepted) an therefore now if something goes wrong we have to throw + // an exception accordingly (e.g. us "admitting fault") + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.callOutput) { + throw new InternalServerError(`${fn} response.callOutput is falsy`); + } + const { callOutput } = response; + if (!Array.isArray(callOutput)) { + throw new InternalServerError(`${fn} callOutput not an array`); + } + const allItemsAreStrings = callOutput.every((x) => typeof x === "string"); + if (!allItemsAreStrings) { + throw new InternalServerError(`${fn} callOutput has non-string items`); + } + return response.callOutput as string[]; + } else if (connectorOrApiClient instanceof EthereumApi) { + const api: EthereumApi = connectorOrApiClient; + const response = await api.invokeContractV1(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + // We log the error here on the debug level so that later on we can inspect the contents + // of it in the logs if we need to. The reason that this is important is because we do not + // want to dump the full response onto our own error response that is going back to the caller + // due to that potentially being a security issue that we are exposing internal data via the + // error responses. + // With that said, we still need to make sure that we can determine the root cause of any + // issues after the fact and therefore we must save the error response details somewhere (the logs) + this.log.debug( + "EthereumApi non-2xx HTTP response:", + data, + status, + config, + ); + + // For the caller/client we just send back a generic error admitting that we somehow messed up: + const errorMessage = `${fn} EthereumApi error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + if (!data.callOutput) { + throw new InternalServerError(`${fn} data.callOutput is falsy`); + } + const { callOutput } = data; + if (!Array.isArray(callOutput)) { + throw new InternalServerError(`${fn} callOutput not an array`); + } + const allItemsAreStrings = callOutput.every((x) => typeof x === "string"); + if (!allItemsAreStrings) { + throw new InternalServerError(`${fn} callOutput has non-string items`); + } + return response.data.callOutput; + } + throw new InternalServerError( + `${fn}: neither EthereumApi nor Connector given`, + ); + } + + async invokeWeb3EthMethod( + parameters: InvokeRawWeb3EthMethodV1Request, + connectorOrApiClient: PluginLedgerConnectorEthereum | EthereumApi, + ): Promise { + const fn = `${StrategyEthereum.CLASS_NAME}#invokeContract()`; + if (!connectorOrApiClient) { + throw new BadRequestError(`${fn} connectorOrApiClient is falsy`); + } else if (connectorOrApiClient instanceof PluginLedgerConnectorEthereum) { + const connector: PluginLedgerConnectorEthereum = connectorOrApiClient; + const response = { + data: await connector.invokeRawWeb3EthMethod(parameters), + } as InvokeRawWeb3EthMethodV1Response; + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + return response as InvokeRawWeb3EthMethodV1Response; + } else if (connectorOrApiClient instanceof EthereumApi) { + const api: EthereumApi = connectorOrApiClient; + const response = await api.invokeWeb3EthMethodV1(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + this.log.debug( + "EthereumAPI non-2xx HTTP response:", + data, + status, + config, + ); + const errorMessage = `${fn} EthereumAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + if (!data.data) { + throw new InternalServerError(`${fn} data.data is falsy`); + } + return data; + } + throw new InternalServerError( + `${fn}: neither EthereumAPI nor Connector given`, + ); + } } diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-fabric.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-fabric.ts index a08bbbff46f..c7ccbf60cc9 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-fabric.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/strategy/strategy-fabric.ts @@ -4,7 +4,7 @@ import { Configuration, FabricContractInvocationType, RunTransactionRequest, - GetBlockResponseTypeV1, + PluginLedgerConnectorFabric, } from "@hyperledger/cactus-plugin-ledger-connector-fabric"; import { NetworkDetails, ObtainLedgerStrategy } from "./obtain-ledger-strategy"; import { @@ -18,6 +18,7 @@ import { State } from "../view-creation/state"; import { StateProof } from "../view-creation/state-proof"; import { Proof } from "../view-creation/proof"; import { TransactionProof } from "../view-creation/transaction-proof"; +import { BadRequestError, InternalServerError } from "http-errors-enhanced-cjs"; export interface FabricNetworkDetails extends NetworkDetails { signingCredential: FabricSigningCredential; @@ -40,18 +41,38 @@ export class StrategyFabric implements ObtainLedgerStrategy { stateIds: string[], networkDetails: FabricNetworkDetails, ): Promise> { - const fnTag = `${StrategyFabric.CLASS_NAME}#generateLedgerStates()`; + const fn = `${StrategyFabric.CLASS_NAME}#generateLedgerStates()`; this.log.debug(`Generating ledger snapshot`); - Checks.truthy(networkDetails, `${fnTag} networkDetails`); + Checks.truthy(networkDetails, `${fn} networkDetails`); - const config = new Configuration({ - basePath: networkDetails.connectorApiPath, - }); - const fabricApi = new FabricApi(config); + let fabricApi: FabricApi | undefined; + let connector: PluginLedgerConnectorFabric | undefined; + if (networkDetails.connector) { + connector = networkDetails.connector as PluginLedgerConnectorFabric; + } else if (networkDetails.connectorApiPath) { + const config = new Configuration({ + basePath: networkDetails.connectorApiPath, + }); + fabricApi = new FabricApi(config); + } else { + throw new Error( + `${fn} networkDetails must have either connector or connectorApiPath`, + ); + } + const connectorOrApiClient = networkDetails.connector + ? connector + : fabricApi; + if (!connectorOrApiClient) { + throw new InternalServerError( + `${fn} got neither connector nor FabricAPI`, + ); + } const assetsKey = stateIds.length == 0 - ? (await this.getAllAssetsKey(fabricApi, networkDetails)).split(",") + ? ( + await this.getAllAssetsKey(networkDetails, connectorOrApiClient) + ).split(",") : stateIds; const ledgerStates = new Map(); //For each key in ledgerAssetsKey @@ -59,15 +80,19 @@ export class StrategyFabric implements ObtainLedgerStrategy { const assetValues: string[] = []; const txWithTimeS: Transaction[] = []; - const txs = await this.getAllTxByKey(assetKey, fabricApi, networkDetails); + const txs = await this.getAllTxByKey( + networkDetails, + assetKey, + connectorOrApiClient, + ); //For each tx get receipt let last_receipt; for (const tx of txs) { const receipt = JSON.parse( await this.fabricGetTxReceiptByTxIDV1( - tx.getId(), - fabricApi, networkDetails, + tx.getId(), + connectorOrApiClient, ), ); tx.getProof().setCreator( @@ -96,9 +121,9 @@ export class StrategyFabric implements ObtainLedgerStrategy { last_receipt = receipt; } const block = await this.fabricGetBlockByTxID( - txs[txs.length - 1].getId(), - fabricApi, networkDetails, + txs[txs.length - 1].getId(), + connectorOrApiClient, ); const state = new State(assetKey, assetValues, txWithTimeS); ledgerStates.set(assetKey, state); @@ -123,39 +148,74 @@ export class StrategyFabric implements ObtainLedgerStrategy { } async fabricGetTxReceiptByTxIDV1( - transactionId: string, - api: FabricApi, networkDetails: FabricNetworkDetails, + transactionId: string, + connectorOrApiClient: PluginLedgerConnectorFabric | FabricApi, ): Promise { - const receiptLockRes = await api.getTransactionReceiptByTxIDV1({ + const fn = `${StrategyFabric.CLASS_NAME}#fabricGetTxReceiptByTxIDV1()`; + const parameters = { signingCredential: networkDetails.signingCredential, channelName: networkDetails.channelName, contractName: "qscc", invocationType: FabricContractInvocationType.Call, methodName: "GetBlockByTxID", params: [networkDetails.channelName, transactionId], - } as RunTransactionRequest); + } as RunTransactionRequest; - if (receiptLockRes.status >= 200 && receiptLockRes.status < 300) { - if (receiptLockRes.data) { - return JSON.stringify(receiptLockRes.data); - } else { - throw new Error( - `${StrategyFabric.CLASS_NAME}#fabricGetTxReceiptByTxIDV1: contract qscc method GetBlockByTxID invocation output is falsy`, + if (!connectorOrApiClient) { + throw new BadRequestError(`${fn} connectorOrApiClient is falsy`); + } else if (connectorOrApiClient instanceof PluginLedgerConnectorFabric) { + const connector: PluginLedgerConnectorFabric = connectorOrApiClient; + const response = await connector.getTransactionReceiptByTxID(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + const receiptLockRes = JSON.stringify(response); + if (!receiptLockRes) { + throw new InternalServerError(`${fn} receiptLockRes is falsy`); + } + return receiptLockRes; + } else if (connectorOrApiClient instanceof FabricApi) { + const api: FabricApi = connectorOrApiClient; + const response = await api.getTransactionReceiptByTxIDV1(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + this.log.debug( + "FabricAPI non-2xx HTTP response:", + data, + status, + config, ); + const errorMessage = `${fn} FabricAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); } + + const receiptLockRes = JSON.stringify(data); + if (!receiptLockRes) { + throw new InternalServerError(`${fn} receiptLockRes is falsy`); + } + return receiptLockRes; } - throw new Error( - `${StrategyFabric.CLASS_NAME}#fabricGetTxReceiptByTxIDV1: FabricAPI error with status 500: ` + - receiptLockRes.data, + throw new InternalServerError( + `${fn}: neither FabricAPI nor Connector given`, ); } async fabricGetBlockByTxID( - txId: string, - api: FabricApi, networkDetails: FabricNetworkDetails, + txId: string, + connectorOrApiClient: PluginLedgerConnectorFabric | FabricApi, ): Promise<{ hash: string; signers: string[] }> { + const fn = `${StrategyFabric.CLASS_NAME}#fabricGetBlockByTxID()`; const gatewayOptions = { identity: networkDetails.signingCredential.keychainRef, wallet: { @@ -171,26 +231,51 @@ export class StrategyFabric implements ObtainLedgerStrategy { query: { transactionId: txId, }, - type: GetBlockResponseTypeV1.Full, + skipDecode: false, }; - const getBlockResponse = await api.getBlockV1(getBlockReq); + let block_data; - if (getBlockResponse.status < 200 || getBlockResponse.status >= 300) { - throw new Error( - `${StrategyFabric.CLASS_NAME}#fabricGetTxReceiptByTxIDV1: FabricAPI getBlockV1 error with status ${getBlockResponse.status}: ` + - getBlockResponse.data, - ); - } - if (!getBlockResponse.data) { - throw new Error( - `${StrategyFabric.CLASS_NAME}#fabricGetBlockByTxID: getBlockV1 API call output data is falsy`, + if (!connectorOrApiClient) { + throw new BadRequestError(`${fn} connectorOrApiClient is falsy`); + } else if (connectorOrApiClient instanceof PluginLedgerConnectorFabric) { + const connector: PluginLedgerConnectorFabric = connectorOrApiClient; + const response = await connector.getBlock(getBlockReq); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + block_data = response; + } else if (connectorOrApiClient instanceof FabricApi) { + const api: FabricApi = connectorOrApiClient; + const response = await api.getBlockV1(getBlockReq); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + this.log.debug( + "FabricAPI non-2xx HTTP response:", + data, + status, + config, + ); + const errorMessage = `${fn} FabricAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + block_data = response.data; + } else { + throw new InternalServerError( + `${fn}: neither FabricAPI nor Connector given`, ); } - const block = JSON.parse( - JSON.stringify(getBlockResponse?.data), - ).decodedBlock; + const block = JSON.parse(JSON.stringify(block_data)).decodedBlock; const blockSig = block.metadata.metadata[0].signatures; const sigs = []; @@ -213,59 +298,133 @@ export class StrategyFabric implements ObtainLedgerStrategy { } async getAllAssetsKey( - api: FabricApi, networkDetails: FabricNetworkDetails, + connectorOrApiClient: PluginLedgerConnectorFabric | FabricApi, ): Promise { - const response = await api.runTransactionV1({ + const fn = `${StrategyFabric.CLASS_NAME}#getAllAssetsKey()`; + const parameters = { signingCredential: networkDetails.signingCredential, channelName: networkDetails.channelName, contractName: networkDetails.contractName, methodName: "GetAllAssetsKey", invocationType: FabricContractInvocationType.Call, params: [], - } as RunTransactionRequest); + } as RunTransactionRequest; - if (response.status >= 200 && response.status < 300) { - if (response.data.functionOutput) { - return response.data.functionOutput; - } else { - throw new Error( - `${StrategyFabric.CLASS_NAME}#getAllAssetsKey: contract ${networkDetails.contractName} method GetAllAssetsKey invocation output is falsy`, + if (!connectorOrApiClient) { + throw new BadRequestError(`${fn} connectorOrApiClient is falsy`); + } else if (connectorOrApiClient instanceof PluginLedgerConnectorFabric) { + const connector: PluginLedgerConnectorFabric = connectorOrApiClient; + const response = await connector.transact(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.functionOutput) { + throw new InternalServerError(`${fn} response.functionOutput is falsy`); + } + const { functionOutput } = response; + if (!(typeof functionOutput === "string")) { + throw new InternalServerError( + `${fn} response.functionOutput is not a string`, + ); + } + return functionOutput; + } else if (connectorOrApiClient instanceof FabricApi) { + const api: FabricApi = connectorOrApiClient; + const response = await api.runTransactionV1(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + this.log.debug( + "FabricAPI non-2xx HTTP response:", + data, + status, + config, ); + const errorMessage = `${fn} FabricAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); } + if (!data.functionOutput) { + throw new InternalServerError(`${fn} response.functionOutput is falsy`); + } + const { functionOutput } = data; + if (!(typeof functionOutput === "string")) { + throw new InternalServerError( + `${fn} response.functionOutput is not a string`, + ); + } + return functionOutput; } - throw new Error( - `${StrategyFabric.CLASS_NAME}#getAllAssetsKey: FabricAPI error with status 500: ` + - response.data, + throw new InternalServerError( + `${fn}: neither FabricAPI nor Connector given`, ); } async getAllTxByKey( - key: string, - api: FabricApi, networkDetails: FabricNetworkDetails, + key: string, + connectorOrApiClient: PluginLedgerConnectorFabric | FabricApi, ): Promise { - const response = await api.runTransactionV1({ + const fn = `${StrategyFabric.CLASS_NAME}#getAllTxByKey()`; + const parameters = { signingCredential: networkDetails.signingCredential, channelName: networkDetails.channelName, contractName: networkDetails.contractName, methodName: "GetAllTxByKey", invocationType: FabricContractInvocationType.Call, params: [key], - } as RunTransactionRequest); + } as RunTransactionRequest; - if (response.status >= 200 && response.status < 300) { - if (response.data.functionOutput) { - return this.txsStringToTxs(response.data.functionOutput); - } else { - throw new Error( - `${StrategyFabric.CLASS_NAME}#getAllTxByKey: contract ${networkDetails.contractName} method GetAllTxByKey invocation output is falsy`, + if (!connectorOrApiClient) { + throw new BadRequestError(`${fn} connectorOrApiClient is falsy`); + } else if (connectorOrApiClient instanceof PluginLedgerConnectorFabric) { + const connector: PluginLedgerConnectorFabric = connectorOrApiClient; + const response = await connector.transact(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.functionOutput) { + throw new InternalServerError(`${fn} response.functionOutput is falsy`); + } + return this.txsStringToTxs(response.functionOutput); + } else if (connectorOrApiClient instanceof FabricApi) { + const api: FabricApi = connectorOrApiClient; + const response = await api.runTransactionV1(parameters); + if (!response) { + throw new InternalServerError(`${fn} response is falsy`); + } + if (!response.status) { + throw new InternalServerError(`${fn} response.status is falsy`); + } + const { status, data, statusText, config } = response; + if (response.status < 200 || response.status > 300) { + this.log.debug( + "FabricAPI non-2xx HTTP response:", + data, + status, + config, ); + const errorMessage = `${fn} FabricAPI error status: ${status}: ${statusText}`; + throw new InternalServerError(errorMessage); + } + if (!data) { + throw new InternalServerError(`${fn} response.data is falsy`); + } + if (!data.functionOutput) { + throw new InternalServerError(`${fn} response.functionOutput is falsy`); } + return this.txsStringToTxs(data.functionOutput); } - throw new Error( - `${StrategyFabric.CLASS_NAME}#getAllTxByKey: FabricAPI error with status 500: ` + - response.data, + throw new InternalServerError( + `${fn}: neither FabricAPI nor Connector given`, ); } diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/besu-test-basic.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/besu-test-basic.test.ts index b9967b9c7eb..b0211e41bb8 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/besu-test-basic.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/besu-test-basic.test.ts @@ -73,7 +73,9 @@ let bungeeContractAddress: string; let keychainPlugin: PluginKeychainMemory; -beforeAll(async () => { +let networkDetailsList: BesuNetworkDetails[]; + +beforeEach(async () => { pruneDockerAllIfGithubAction({ logLevel }) .then(() => { log.info("Pruning throw OK"); @@ -235,118 +237,147 @@ beforeAll(async () => { .contractAddress as string; pluginBungeeHermesOptions = { + pluginRegistry, keyPair: Secp256k1Keys.generateKeyPairsBuffer(), instanceId: uuidv4(), logLevel, }; } + networkDetailsList = [ + { + signingCredential: bungeeSigningCredential, + contractName, + connectorApiPath: besuPath, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + } as BesuNetworkDetails, + { + signingCredential: bungeeSigningCredential, + contractName, + connector: connector, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + } as BesuNetworkDetails, + ]; }); -test("test creation of views for different timeframes and states", async () => { - const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); - const strategy = "BESU"; - bungee.addStrategy(strategy, new StrategyBesu("INFO")); - const networkDetails: BesuNetworkDetails = { - signingCredential: bungeeSigningCredential, - contractName, - connectorApiPath: besuPath, - keychainId: bungeeKeychainId, - contractAddress: bungeeContractAddress, - participant: firstHighNetWorthAccount, - }; - - const snapshot = await bungee.generateSnapshot([], strategy, networkDetails); - const view = bungee.generateView( - snapshot, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - //expect to return a view - expect(view.view).toBeTruthy(); - expect(view.signature).toBeTruthy(); - - //expect the view to have capture the new asset BESU_ASSET_ID, and attributes to match - expect(snapshot.getStateBins().length).toEqual(1); - expect(snapshot.getStateBins()[0].getId()).toEqual(BESU_ASSET_ID); - expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); - - const view1 = bungee.generateView(snapshot, "0", "9999", undefined); - - //expects nothing to limit time of 9999 - expect(view1.view).toBeUndefined(); - expect(view1.signature).toBeUndefined(); - - //changing BESU_ASSET_ID value - const lockAsset = await connector.invokeContract({ - contractName, - keychainId: keychainPlugin.getKeychainId(), - invocationType: EthContractInvocationType.Send, - methodName: "lockAsset", - params: [BESU_ASSET_ID], - signingCredential: { - ethAccount: firstHighNetWorthAccount, - secret: besuKeyPair.privateKey, - type: Web3SigningCredentialType.PrivateKeyHex, - }, - gas: 1000000, - }); - expect(lockAsset).not.toBeUndefined(); - expect(lockAsset.success).toBeTrue(); - - //creating new asset - const new_asset_id = uuidv4(); - const depNew = await connector.invokeContract({ - contractName, - keychainId: keychainPlugin.getKeychainId(), - invocationType: EthContractInvocationType.Send, - methodName: "createAsset", - params: [new_asset_id, 10], - signingCredential: { - ethAccount: firstHighNetWorthAccount, - secret: besuKeyPair.privateKey, - type: Web3SigningCredentialType.PrivateKeyHex, - }, - gas: 1000000, - }); - expect(depNew).not.toBeUndefined(); - expect(depNew.success).toBeTrue(); - - const snapshot1 = await bungee.generateSnapshot([], strategy, networkDetails); - const view2 = bungee.generateView( - snapshot1, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - //expect to return a view - expect(view2.view).toBeTruthy(); - expect(view2.signature).toBeTruthy(); - - const stateBins = snapshot1.getStateBins(); - expect(stateBins.length).toEqual(2); //expect to have captured state for both assets - - const bins = [stateBins[0].getId(), stateBins[1].getId()]; - - //checks if values match: - // - new value of BESU_ASSET_ID state in new snapshot different than value from old snapshot) - // - successfully captured transaction that created the new asset - if (bins[0] === BESU_ASSET_ID) { - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(2); - expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( - snapshot.getStateBins()[0].getValue(), +test.each([{ apiPath: true }, { apiPath: false }])( + //test for both BesuApiPath and BesuConnector + "test creation of views for different timeframes and states using", + async ({ apiPath }) => { + let networkDetails: BesuNetworkDetails; + if (apiPath) { + networkDetails = networkDetailsList[0]; + } else { + networkDetails = networkDetailsList[1]; + } + const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); + const strategy = "BESU"; + bungee.addStrategy(strategy, new StrategyBesu("INFO")); + + const snapshot = await bungee.generateSnapshot( + [], + strategy, + networkDetails, ); - expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(1); - } else { - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); - expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(2); - expect(snapshot1.getStateBins()[1].getValue()).not.toEqual( - snapshot.getStateBins()[0].getValue(), + const view = bungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, ); - } -}); + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + //expect the view to have capture the new asset BESU_ASSET_ID, and attributes to match + expect(snapshot.getStateBins().length).toEqual(1); + expect(snapshot.getStateBins()[0].getId()).toEqual(BESU_ASSET_ID); + expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); + + const view1 = bungee.generateView(snapshot, "0", "9999", undefined); -afterAll(async () => { + //expects nothing to limit time of 9999 + expect(view1.view).toBeUndefined(); + expect(view1.signature).toBeUndefined(); + + //changing BESU_ASSET_ID value + const lockAsset = await connector?.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "lockAsset", + params: [BESU_ASSET_ID], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(lockAsset).not.toBeUndefined(); + expect(lockAsset.success).toBeTrue(); + + //creating new asset + const new_asset_id = uuidv4(); + const depNew = await connector?.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "createAsset", + params: [new_asset_id, 10], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(depNew).not.toBeUndefined(); + expect(depNew.success).toBeTrue(); + + const snapshot1 = await bungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view2 = bungee.generateView( + snapshot1, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view2.view).toBeTruthy(); + expect(view2.signature).toBeTruthy(); + + const stateBins = snapshot1.getStateBins(); + expect(stateBins.length).toEqual(2); //expect to have captured state for both assets + + const bins = [stateBins[0].getId(), stateBins[1].getId()]; + + //checks if values match: + // - new value of BESU_ASSET_ID state in new snapshot different than value from old snapshot) + // - successfully captured transaction that created the new asset + if (bins[0] === BESU_ASSET_ID) { + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(2); + expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( + snapshot.getStateBins()[0].getValue(), + ); + expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(1); + } else { + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); + expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(2); + expect(snapshot1.getStateBins()[1].getValue()).not.toEqual( + snapshot.getStateBins()[0].getValue(), + ); + } + }, +); + +afterEach(async () => { await Servers.shutdown(besuServer); await besuLedger.stop(); await besuLedger.destroy(); diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/besu-test-pruning.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/besu-test-pruning.test.ts index 4bd8df5822a..95fae810ef0 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/besu-test-pruning.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/besu-test-pruning.test.ts @@ -72,7 +72,9 @@ let bungeeContractAddress: string; let keychainPlugin: PluginKeychainMemory; -beforeAll(async () => { +let networkDetailsList: BesuNetworkDetails[]; + +beforeEach(async () => { pruneDockerAllIfGithubAction({ logLevel }) .then(() => { log.info("Pruning throw OK"); @@ -234,89 +236,119 @@ beforeAll(async () => { .contractAddress as string; pluginBungeeHermesOptions = { + pluginRegistry, keyPair: Secp256k1Keys.generateKeyPairsBuffer(), instanceId: uuidv4(), logLevel, }; } + networkDetailsList = [ + { + signingCredential: bungeeSigningCredential, + contractName, + connectorApiPath: besuPath, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + } as BesuNetworkDetails, + { + signingCredential: bungeeSigningCredential, + contractName, + connector: connector, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + } as BesuNetworkDetails, + ]; }); -test("test creation of views for specific timeframes", async () => { - const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); - const strategy = "BESU"; - bungee.addStrategy(strategy, new StrategyBesu("INFO")); - const netwokDetails: BesuNetworkDetails = { - signingCredential: bungeeSigningCredential, - contractName, - connectorApiPath: besuPath, - keychainId: bungeeKeychainId, - contractAddress: bungeeContractAddress, - participant: firstHighNetWorthAccount, - }; - - const snapshot = await bungee.generateSnapshot([], strategy, netwokDetails); - const view = bungee.generateView( - snapshot, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - - //expect to return a view - expect(view.view).toBeTruthy(); - expect(view.signature).toBeTruthy(); - - //expect the view to have capture the new asset BESU_ASSET_ID, and attributes to match - expect(snapshot.getStateBins().length).toEqual(1); - expect(snapshot.getStateBins()[0].getId()).toEqual(BESU_ASSET_ID); - expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); - - //changing BESU_ASSET_ID value - const lockAsset = await connector.invokeContract({ - contractName, - keychainId: keychainPlugin.getKeychainId(), - invocationType: EthContractInvocationType.Send, - methodName: "lockAsset", - params: [BESU_ASSET_ID], - signingCredential: { - ethAccount: firstHighNetWorthAccount, - secret: besuKeyPair.privateKey, - type: Web3SigningCredentialType.PrivateKeyHex, - }, - gas: 1000000, - }); - expect(lockAsset).not.toBeUndefined(); - expect(lockAsset.success).toBeTrue(); - - const snapshot1 = await bungee.generateSnapshot([], strategy, netwokDetails); - - //tI is the time of the first transaction + 1 - const tI = ( - parseInt(snapshot.getStateBins()[0].getTransactions()[0].getTimeStamp()) + 1 - ).toString(); - - const view1 = bungee.generateView( - snapshot1, - tI, - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - - //expect to return a view - expect(view1.view).toBeTruthy(); - expect(view1.signature).toBeTruthy(); - - expect(snapshot1.getStateBins().length).toEqual(1); - expect(snapshot1.getStateBins()[0].getId()).toEqual(BESU_ASSET_ID); - //expect the view to not include first transaction (made before tI) - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); - //expect old and new snapshot state values to differ - expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( - snapshot.getStateBins()[0].getValue(), - ); -}); +test.each([{ apiPath: true }, { apiPath: false }])( + //test for both BesuApiPath and BesuConnector + "test creation of views for specific timeframes", + async ({ apiPath }) => { + let networkDetails: BesuNetworkDetails; + if (apiPath) { + networkDetails = networkDetailsList[0]; + } else { + networkDetails = networkDetailsList[1]; + } + const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); + const strategy = "BESU"; + bungee.addStrategy(strategy, new StrategyBesu("INFO")); + + const snapshot = await bungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view = bungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + //expect the view to have capture the new asset BESU_ASSET_ID, and attributes to match + expect(snapshot.getStateBins().length).toEqual(1); + expect(snapshot.getStateBins()[0].getId()).toEqual(BESU_ASSET_ID); + expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); + + //changing BESU_ASSET_ID value + const lockAsset = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "lockAsset", + params: [BESU_ASSET_ID], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(lockAsset).not.toBeUndefined(); + expect(lockAsset.success).toBeTrue(); + + const snapshot1 = await bungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + + //tI is the time of the first transaction + 1 + const tI = ( + parseInt(snapshot.getStateBins()[0].getTransactions()[0].getTimeStamp()) + + 1 + ).toString(); + + const view1 = bungee.generateView( + snapshot1, + tI, + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + + //expect to return a view + expect(view1.view).toBeTruthy(); + expect(view1.signature).toBeTruthy(); + + expect(snapshot1.getStateBins().length).toEqual(1); + expect(snapshot1.getStateBins()[0].getId()).toEqual(BESU_ASSET_ID); + //expect the view to not include first transaction (made before tI) + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); + //expect old and new snapshot state values to differ + expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( + snapshot.getStateBins()[0].getValue(), + ); + }, +); -afterAll(async () => { +afterEach(async () => { await Servers.shutdown(besuServer); await besuLedger.stop(); await besuLedger.destroy(); diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-api-test.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-api-test.test.ts index 355da3bbe89..5526886ecdc 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-api-test.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-api-test.test.ts @@ -56,14 +56,6 @@ import { } from "@hyperledger/cactus-plugin-ledger-connector-fabric"; import path from "path"; import { DiscoveryOptions } from "fabric-network"; -import { - FabricNetworkDetails, - StrategyFabric, -} from "../../../main/typescript/strategy/strategy-fabric"; -import { - BesuNetworkDetails, - StrategyBesu, -} from "../../../main/typescript/strategy/strategy-besu"; import { PluginLedgerConnectorEthereum, DefaultApi as EthereumApi, @@ -72,10 +64,33 @@ import { GethTestLedger, WHALE_ACCOUNT_ADDRESS, } from "@hyperledger/cactus-test-geth-ledger"; -import { - EthereumNetworkDetails, - StrategyEthereum, -} from "../../../main/typescript/strategy/strategy-ethereum"; +import { StrategyEthereum } from "../../../main/typescript/strategy/strategy-ethereum"; +import { StrategyFabric } from "../../../main/typescript/strategy/strategy-fabric"; +import { StrategyBesu } from "../../../main/typescript/strategy/strategy-besu"; + +interface BesuNetworkDetails { + connectorApiPath: string; + participant: string; + signingCredential: Web3SigningCredential; + keychainId: string; + contractName: string; + contractAddress: string; +} +interface FabricNetworkDetails { + connectorApiPath: string; + participant: string; + signingCredential: FabricSigningCredential; + contractName: string; + channelName: string; +} +interface EthereumNetworkDetails { + connectorApiPath: string; + participant: string; + signingCredential: Web3SigningCredential; + keychainId: string; + contractName: string; + contractAddress: string; +} const logLevel: LogLevelDesc = "INFO"; @@ -101,10 +116,10 @@ const log = LoggerProvider.getOrCreate({ label: "BUNGEE - Hermes", }); -let ethereumSigningCredential; +let ethereumPath: string; +let ethereumSigningCredential: Web3SigningCredential; let ethereumKeychainId: string; let ethereumContractAddress: string; -let ethereumNetworkDetails: EthereumNetworkDetails; let ethereumServer: Server; let ethereumLedger: GethTestLedger; @@ -145,13 +160,16 @@ beforeAll(async () => { }); test("tests bungee api using different strategies", async () => { + const pluginRegistry = new PluginRegistry({ logLevel, plugins: [] }); const keyPair = Secp256k1Keys.generateKeyPairsBuffer(); pluginBungeeHermesOptions = { + pluginRegistry, keyPair, instanceId: uuidv4(), logLevel, }; const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); + pluginRegistry.add(bungee); //add strategies to BUNGEE - Hermes bungee.addStrategy(FABRIC_STRATEGY, new StrategyFabric("INFO")); @@ -175,6 +193,15 @@ test("tests bungee api using different strategies", async () => { participant: "Org1MSP", }; + const ethereumNetworkDetails: EthereumNetworkDetails = { + signingCredential: ethereumSigningCredential, + contractName: LockAssetContractJson.contractName, + connectorApiPath: ethereumPath, + keychainId: ethereumKeychainId, + contractAddress: ethereumContractAddress, + participant: WHALE_ACCOUNT_ADDRESS, + }; + const expressApp = express(); expressApp.use(bodyParser.json({ limit: "250mb" })); bungeeServer = http.createServer(expressApp); @@ -755,8 +782,8 @@ async function setupEthereumTestLedger(): Promise { }; const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; const { address, port } = addressInfo; - const apiHost = `http://${address}:${port}`; - const apiConfig = new Configuration({ basePath: apiHost }); + ethereumPath = `http://${address}:${port}`; + const apiConfig = new Configuration({ basePath: ethereumPath }); const apiClient = new EthereumApi(apiConfig); const rpcApiHttpHost = await ledger.getRpcApiHttpHost(); const web3 = new Web3(rpcApiHttpHost); @@ -872,15 +899,6 @@ async function setupEthereumTestLedger(): Promise { ethereumContractAddress = deployOut.data.transactionReceipt .contractAddress as string; - ethereumNetworkDetails = { - signingCredential: ethereumSigningCredential, - contractName: LockAssetContractJson.contractName, - connectorApiPath: apiHost, - keychainId: ethereumKeychainId, - contractAddress: ethereumContractAddress, - participant: WHALE_ACCOUNT_ADDRESS, - }; - return "Ethereum Network setup successful"; } diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-merge-views.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-merge-views.test.ts index b47eb21d641..8ddbfdec8d6 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-merge-views.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-merge-views.test.ts @@ -76,7 +76,9 @@ let bungeeServer: Server; let keychainPlugin: PluginKeychainMemory; -beforeAll(async () => { +let networkDetailsList: BesuNetworkDetails[]; + +beforeEach(async () => { pruneDockerAllIfGithubAction({ logLevel }) .then(() => { log.info("Pruning throw OK"); @@ -238,178 +240,209 @@ beforeAll(async () => { .contractAddress as string; pluginBungeeHermesOptions = { + pluginRegistry, keyPair: Secp256k1Keys.generateKeyPairsBuffer(), instanceId: uuidv4(), logLevel, }; } + networkDetailsList = [ + { + signingCredential: bungeeSigningCredential, + contractName, + connectorApiPath: besuPath, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + } as BesuNetworkDetails, + { + signingCredential: bungeeSigningCredential, + contractName, + connector: connector, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + } as BesuNetworkDetails, + ]; }); -test("test merging views, and integrated view proofs", async () => { - const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); - const strategy = "BESU"; - bungee.addStrategy(strategy, new StrategyBesu("INFO")); - const networkDetails: BesuNetworkDetails = { - signingCredential: bungeeSigningCredential, - contractName, - connectorApiPath: besuPath, - keychainId: bungeeKeychainId, - contractAddress: bungeeContractAddress, - participant: firstHighNetWorthAccount, - }; - - const snapshot = await bungee.generateSnapshot([], strategy, networkDetails); - const view = bungee.generateView( - snapshot, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - //expect to return a view - expect(view.view).toBeTruthy(); - expect(view.signature).toBeTruthy(); - - //changing BESU_ASSET_ID value - const lockAsset = await connector.invokeContract({ - contractName, - keychainId: keychainPlugin.getKeychainId(), - invocationType: EthContractInvocationType.Send, - methodName: "lockAsset", - params: [BESU_ASSET_ID], - signingCredential: { - ethAccount: firstHighNetWorthAccount, - secret: besuKeyPair.privateKey, - type: Web3SigningCredentialType.PrivateKeyHex, - }, - gas: 1000000, - }); - expect(lockAsset).not.toBeUndefined(); - expect(lockAsset.success).toBeTrue(); - - //creating new asset - const new_asset_id = uuidv4(); - const depNew = await connector.invokeContract({ - contractName, - keychainId: keychainPlugin.getKeychainId(), - invocationType: EthContractInvocationType.Send, - methodName: "createAsset", - params: [new_asset_id, 10], - signingCredential: { - ethAccount: firstHighNetWorthAccount, - secret: besuKeyPair.privateKey, - type: Web3SigningCredentialType.PrivateKeyHex, - }, - gas: 1000000, - }); - expect(depNew).not.toBeUndefined(); - expect(depNew.success).toBeTrue(); - - const snapshot1 = await bungee.generateSnapshot([], strategy, networkDetails); - const view2 = bungee.generateView( - snapshot1, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - //expect to return a view - expect(view2.view).toBeTruthy(); - expect(view2.signature).toBeTruthy(); - - const expressApp = express(); - expressApp.use(bodyParser.json({ limit: "250mb" })); - bungeeServer = http.createServer(expressApp); - const listenOptions: IListenOptions = { - hostname: "127.0.0.1", - port: 3000, - server: bungeeServer, - }; - const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; - const { address, port } = addressInfo; - - await bungee.getOrCreateWebServices(); - await bungee.registerWebServices(expressApp); - const bungeePath = `http://${address}:${port}`; - - const config = new Configuration({ basePath: bungeePath }); - const bungeeApi = new BungeeApi(config); - - const mergeViewsNoPolicyReq = await bungeeApi.mergeViewsV1({ - serializedViews: [ - JSON.stringify({ - view: JSON.stringify(view.view as View), - signature: view.signature, - }), - // eslint-disable-next-line prettier/prettier - JSON.stringify({ - view: JSON.stringify(view2.view as View), - signature: view2.signature, - }), - ], - mergePolicy: MergePolicyOpts.NONE, - }); - expect(mergeViewsNoPolicyReq.status).toBe(200); - - expect(mergeViewsNoPolicyReq.data.integratedView).toBeTruthy(); - expect(mergeViewsNoPolicyReq.data.signature).toBeTruthy(); - - const mergeViewsNoPolicy = bungee.mergeViews( - [view.view as View, view2.view as View], - [view.signature as string, view2.signature as string], - MergePolicyOpts.NONE, - [], - ); - //1 transaction captured in first view, and 3 in the second - expect(mergeViewsNoPolicy.integratedView.getAllTransactions().length).toBe(4); - //1 state captured in first view, and 2 in the second - expect(mergeViewsNoPolicy.integratedView.getAllStates().length).toBe(3); - - const transactionReceipts: string[] = []; - - mergeViewsNoPolicy.integratedView.getAllTransactions().forEach((t) => { - transactionReceipts.push(JSON.stringify(t.getProof())); - }); - expect( - ( - await bungeeApi.verifyMerkleRoot({ - input: transactionReceipts, - root: mergeViewsNoPolicy.integratedView.getIntegratedViewProof() - .transactionsMerkleRoot, - }) - ).data.result, - ).toBeTrue(); - - const mergeViewsWithPolicy = bungee.mergeViews( - [view.view as View, view2.view as View], - [view.signature as string, view2.signature as string], - MergePolicyOpts.PruneState, - [BESU_ASSET_ID], //should remove all states related to this asset - ); - - //0 transactions captured in first view, and 1 in the second (because of policy) - // eslint-disable-next-line prettier/prettier - expect(mergeViewsWithPolicy.integratedView.getAllTransactions().length).toBe( - 1, - ); - //0 state captured in first view, and 1 in the second (because of policy) - expect(mergeViewsWithPolicy.integratedView.getAllStates().length).toBe(1); - - const mergeViewsWithPolicy2 = bungee.mergeViews( - [view.view as View, view2.view as View], - [view.signature as string, view2.signature as string], - MergePolicyOpts.PruneStateFromView, - [BESU_ASSET_ID, view2.view?.getKey() as string], //should remove all states related to this asset - ); - - //1 transactions captured in first view, and 1 in the second (because of policy) - // eslint-disable-next-line prettier/prettier - expect(mergeViewsWithPolicy2.integratedView.getAllTransactions().length).toBe( - 2, - ); - //1 state captured in first view, and only 1 in the second (because of policy) - expect(mergeViewsWithPolicy2.integratedView.getAllStates().length).toBe(2); -}); +test.each([{ apiPath: true }, { apiPath: false }])( + //test for both BesuApiPath and BesuConnector + "test merging views, and integrated view proofs", + async ({ apiPath }) => { + let networkDetails: BesuNetworkDetails; + if (apiPath) { + networkDetails = networkDetailsList[0]; + } else { + networkDetails = networkDetailsList[1]; + } + const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); + const strategy = "BESU"; + bungee.addStrategy(strategy, new StrategyBesu("INFO")); + + const snapshot = await bungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view = bungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + //changing BESU_ASSET_ID value + const lockAsset = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "lockAsset", + params: [BESU_ASSET_ID], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(lockAsset).not.toBeUndefined(); + expect(lockAsset.success).toBeTrue(); + + //creating new asset + const new_asset_id = uuidv4(); + const depNew = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "createAsset", + params: [new_asset_id, 10], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(depNew).not.toBeUndefined(); + expect(depNew.success).toBeTrue(); + + const snapshot1 = await bungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view2 = bungee.generateView( + snapshot1, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view2.view).toBeTruthy(); + expect(view2.signature).toBeTruthy(); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + bungeeServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 3000, + server: bungeeServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await bungee.getOrCreateWebServices(); + await bungee.registerWebServices(expressApp); + const bungeePath = `http://${address}:${port}`; + + const config = new Configuration({ basePath: bungeePath }); + const bungeeApi = new BungeeApi(config); + + const mergeViewsNoPolicyReq = await bungeeApi.mergeViewsV1({ + serializedViews: [ + JSON.stringify({ + view: JSON.stringify(view.view as View), + signature: view.signature, + }), + // eslint-disable-next-line prettier/prettier + JSON.stringify({ + view: JSON.stringify(view2.view as View), + signature: view2.signature, + }), + ], + mergePolicy: MergePolicyOpts.NONE, + }); + expect(mergeViewsNoPolicyReq.status).toBe(200); + + expect(mergeViewsNoPolicyReq.data.integratedView).toBeTruthy(); + expect(mergeViewsNoPolicyReq.data.signature).toBeTruthy(); + + const mergeViewsNoPolicy = bungee.mergeViews( + [view.view as View, view2.view as View], + [view.signature as string, view2.signature as string], + MergePolicyOpts.NONE, + [], + ); + //1 transaction captured in first view, and 3 in the second + expect(mergeViewsNoPolicy.integratedView.getAllTransactions().length).toBe( + 4, + ); + //1 state captured in first view, and 2 in the second + expect(mergeViewsNoPolicy.integratedView.getAllStates().length).toBe(3); + + const transactionReceipts: string[] = []; + + mergeViewsNoPolicy.integratedView.getAllTransactions().forEach((t) => { + transactionReceipts.push(JSON.stringify(t.getProof())); + }); + expect( + ( + await bungeeApi.verifyMerkleRoot({ + input: transactionReceipts, + root: mergeViewsNoPolicy.integratedView.getIntegratedViewProof() + .transactionsMerkleRoot, + }) + ).data.result, + ).toBeTrue(); + + const mergeViewsWithPolicy = bungee.mergeViews( + [view.view as View, view2.view as View], + [view.signature as string, view2.signature as string], + MergePolicyOpts.PruneState, + [BESU_ASSET_ID], //should remove all states related to this asset + ); + + //0 transactions captured in first view, and 1 in the second (because of policy) + // eslint-disable-next-line prettier/prettier + expect( + mergeViewsWithPolicy.integratedView.getAllTransactions().length, + ).toBe(1); + //0 state captured in first view, and 1 in the second (because of policy) + expect(mergeViewsWithPolicy.integratedView.getAllStates().length).toBe(1); + + const mergeViewsWithPolicy2 = bungee.mergeViews( + [view.view as View, view2.view as View], + [view.signature as string, view2.signature as string], + MergePolicyOpts.PruneStateFromView, + [BESU_ASSET_ID, view2.view?.getKey() as string], //should remove all states related to this asset + ); -afterAll(async () => { + //1 transactions captured in first view, and 1 in the second (because of policy) + // eslint-disable-next-line prettier/prettier + expect( + mergeViewsWithPolicy2.integratedView.getAllTransactions().length, + ).toBe(2); + //1 state captured in first view, and only 1 in the second (because of policy) + expect(mergeViewsWithPolicy2.integratedView.getAllStates().length).toBe(2); + }, +); + +afterEach(async () => { await Servers.shutdown(besuServer); await Servers.shutdown(bungeeServer); await besuLedger.stop(); diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-process-views.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-process-views.test.ts index 2daeba45180..9902ede8b82 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-process-views.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-process-views.test.ts @@ -78,7 +78,9 @@ let bungeeServer: Server; let keychainPlugin: PluginKeychainMemory; -beforeAll(async () => { +let networkDetailsList: BesuNetworkDetails[]; + +beforeEach(async () => { pruneDockerAllIfGithubAction({ logLevel }) .then(() => { log.info("Pruning throw OK"); @@ -275,82 +277,108 @@ beforeAll(async () => { .contractAddress as string; pluginBungeeHermesOptions = { + pluginRegistry, keyPair: Secp256k1Keys.generateKeyPairsBuffer(), instanceId: uuidv4(), logLevel, }; } + networkDetailsList = [ + { + signingCredential: bungeeSigningCredential, + contractName, + connectorApiPath: besuPath, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + } as BesuNetworkDetails, + { + signingCredential: bungeeSigningCredential, + contractName, + connector: connector, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + } as BesuNetworkDetails, + ]; }); -test("test merging views, and integrated view proofs", async () => { - const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); - const strategy = "BESU"; - bungee.addStrategy(strategy, new StrategyBesu("INFO")); - const networkDetails: BesuNetworkDetails = { - signingCredential: bungeeSigningCredential, - contractName, - connectorApiPath: besuPath, - keychainId: bungeeKeychainId, - contractAddress: bungeeContractAddress, - participant: firstHighNetWorthAccount, - }; - - const snapshot = await bungee.generateSnapshot([], strategy, networkDetails); - const view = bungee.generateView( - snapshot, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - //expect to return a view - expect(view.view).toBeTruthy(); - expect(view.signature).toBeTruthy(); - - const expressApp = express(); - expressApp.use(bodyParser.json({ limit: "250mb" })); - bungeeServer = http.createServer(expressApp); - const listenOptions: IListenOptions = { - hostname: "127.0.0.1", - port: 3000, - server: bungeeServer, - }; - const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; - const { address, port } = addressInfo; - - await bungee.getOrCreateWebServices(); - await bungee.registerWebServices(expressApp); - const bungeePath = `http://${address}:${port}`; - - const config = new Configuration({ basePath: bungeePath }); - const bungeeApi = new BungeeApi(config); - - const processed = await bungeeApi.processViewV1({ - serializedView: JSON.stringify({ - view: JSON.stringify(view.view as View), - signature: view.signature, - }), - policyId: PrivacyPolicyOpts.PruneState, - policyArguments: [BESU_ASSET_ID], - }); - - expect(processed.status).toBe(200); - expect(processed.data.view).toBeTruthy(); - expect(processed.data.signature).toBeTruthy(); - - const processedView = deserializeView(JSON.stringify(processed.data)); - - //check view deserializer - expect(JSON.stringify(processedView)).toEqual(processed.data.view); - - expect(processedView.getPolicy()).toBeTruthy(); - expect(processedView.getOldVersionsMetadata().length).toBe(1); - expect(processedView.getOldVersionsMetadata()[0].signature).toBe( - view.signature, - ); - expect(processedView.getAllTransactions().length).toBe(1); -}); +test.each([{ apiPath: true }, { apiPath: false }])( + //test for both BesuApiPath and BesuConnector + "test merging views, and integrated view proofs", + async ({ apiPath }) => { + let networkDetails: BesuNetworkDetails; + if (apiPath) { + networkDetails = networkDetailsList[0]; + } else { + networkDetails = networkDetailsList[1]; + } + + const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); + const strategy = "BESU"; + bungee.addStrategy(strategy, new StrategyBesu("INFO")); + + const snapshot = await bungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view = bungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + bungeeServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 3000, + server: bungeeServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await bungee.getOrCreateWebServices(); + await bungee.registerWebServices(expressApp); + const bungeePath = `http://${address}:${port}`; + + const config = new Configuration({ basePath: bungeePath }); + const bungeeApi = new BungeeApi(config); + + const processed = await bungeeApi.processViewV1({ + serializedView: JSON.stringify({ + view: JSON.stringify(view.view as View), + signature: view.signature, + }), + policyId: PrivacyPolicyOpts.PruneState, + policyArguments: [BESU_ASSET_ID], + }); + + expect(processed.status).toBe(200); + expect(processed.data.view).toBeTruthy(); + expect(processed.data.signature).toBeTruthy(); + + const processedView = deserializeView(JSON.stringify(processed.data)); + + //check view deserializer + expect(JSON.stringify(processedView)).toEqual(processed.data.view); + + expect(processedView.getPolicy()).toBeTruthy(); + expect(processedView.getOldVersionsMetadata().length).toBe(1); + expect(processedView.getOldVersionsMetadata()[0].signature).toBe( + view.signature, + ); + expect(processedView.getAllTransactions().length).toBe(1); + }, +); -afterAll(async () => { +afterEach(async () => { await Servers.shutdown(besuServer); await Servers.shutdown(bungeeServer); await besuLedger.stop(); diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/ethereum-test-basic.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/ethereum-test-basic.test.ts index f4a6ec2987e..12a504aba5b 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/ethereum-test-basic.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/ethereum-test-basic.test.ts @@ -14,9 +14,9 @@ const testLogLevel: LogLevelDesc = "info"; import "jest-extended"; import express from "express"; import bodyParser from "body-parser"; -import http from "http"; import { v4 as uuidV4 } from "uuid"; import { AddressInfo } from "net"; +import http, { Server } from "http"; import { Server as SocketIoServer } from "socket.io"; import Web3 from "web3"; @@ -88,23 +88,16 @@ describe("Ethereum contract deploy and invoke using keychain", () => { let bungeeContractAddress: string; let pluginBungeeHermesOptions: IPluginBungeeHermesOptions; const ETH_ASSET_NAME = uuidV4(); - const expressApp = express(); - expressApp.use(bodyParser.json({ limit: "250mb" })); - const server = http.createServer(expressApp); - // set to address Type Error returned by Response.json() - // "Can't serialize BigInt" - expressApp.set("json replacer", stringifyBigIntReplacer); - - const wsApi = new SocketIoServer(server, { - path: Constants.SocketIoConnectionPathV1, - }); + let server: Server; ////////////////////////////////// // Setup ////////////////////////////////// - beforeAll(async () => { + let networkDetailsList: EthereumNetworkDetails[]; + + beforeEach(async () => { pruneDockerAllIfGithubAction({ logLevel: testLogLevel }) .then(() => { log.info("Pruning throw OK"); @@ -120,6 +113,15 @@ describe("Ethereum contract deploy and invoke using keychain", () => { }); await ledger.start(); + const expressApp = express(); + + expressApp.use(bodyParser.json({ limit: "250mb" })); + // set to address Type Error returned by Response.json() + // "Can't serialize BigInt" + expressApp.set("json replacer", stringifyBigIntReplacer); + + server = http.createServer(expressApp); + const listenOptions: IListenOptions = { hostname: "127.0.0.1", port: 5000, @@ -148,16 +150,21 @@ describe("Ethereum contract deploy and invoke using keychain", () => { LockAssetContractJson.contractName, JSON.stringify(LockAssetContractJson), ); + const pluginRegistry = new PluginRegistry({ plugins: [keychainPlugin] }); connector = new PluginLedgerConnectorEthereum({ instanceId: uuidV4(), rpcApiHttpHost, logLevel: testLogLevel, - pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + pluginRegistry, }); // Instantiate connector with the keychain plugin that already has the // private key we want to use for one of our tests await connector.getOrCreateWebServices(); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.registerWebServices(expressApp, wsApi); const initTransferValue = web3.utils.toWei("5000", "ether"); @@ -228,134 +235,170 @@ describe("Ethereum contract deploy and invoke using keychain", () => { .contractAddress as string; pluginBungeeHermesOptions = { + pluginRegistry, keyPair: Secp256k1Keys.generateKeyPairsBuffer(), instanceId: uuidV4(), logLevel: testLogLevel, }; + networkDetailsList = [ + { + signingCredential: bungeeSigningCredential, + contractName: LockAssetContractJson.contractName, + connectorApiPath: apiHost, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: WHALE_ACCOUNT_ADDRESS, + } as EthereumNetworkDetails, + { + signingCredential: bungeeSigningCredential, + contractName: LockAssetContractJson.contractName, + connector: connector, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: WHALE_ACCOUNT_ADDRESS, + } as EthereumNetworkDetails, + ]; }); - afterAll(async () => { + afterEach(async () => { + await Servers.shutdown(server); await ledger.stop(); await ledger.destroy(); - await Servers.shutdown(server); - const pruning = pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); - await expect(pruning).resolves.toBeTruthy(); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel: testLogLevel }); + fail("Pruning didn't throw OK"); + }); }); - test("test creation of views for different timeframes and states", async () => { - const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); - const strategy = "ETH"; - bungee.addStrategy(strategy, new StrategyEthereum("INFO")); - const networkDetails: EthereumNetworkDetails = { - signingCredential: bungeeSigningCredential, - contractName: LockAssetContractJson.contractName, - connectorApiPath: apiHost, - keychainId: bungeeKeychainId, - contractAddress: bungeeContractAddress, - participant: WHALE_ACCOUNT_ADDRESS, - }; - - const snapshot = await bungee.generateSnapshot( - [], - strategy, - networkDetails, - ); - const view = bungee.generateView( - snapshot, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - - //expect to return a view - expect(view.view).toBeTruthy(); - expect(view.signature).toBeTruthy(); - - //expect the view to have capture the new asset ETH_ASSET_NAME, and attributes to match - expect(snapshot.getStateBins().length).toEqual(1); - expect(snapshot.getStateBins()[0].getId()).toEqual(ETH_ASSET_NAME); - expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); - - const view1 = bungee.generateView(snapshot, "0", "9999", undefined); - - //expects nothing to limit time of 9999 - expect(view1.view).toBeUndefined(); - expect(view1.signature).toBeUndefined(); - - //changing ETH_ASSET_NAME value - const lockAsset = await apiClient.invokeContractV1({ - contract: { - contractName: LockAssetContractJson.contractName, - keychainId: keychainPlugin.getKeychainId(), - }, - invocationType: EthContractInvocationType.Send, - methodName: "lockAsset", - params: [ETH_ASSET_NAME], - web3SigningCredential: { - ethAccount: WHALE_ACCOUNT_ADDRESS, - secret: "", - type: Web3SigningCredentialType.GethKeychainPassword, - }, - }); - expect(lockAsset).not.toBeUndefined(); - expect(lockAsset.status).toBe(200); + test.each([{ apiPath: true }, { apiPath: false }])( + //test for both EthereumApiPath and EthereumConnector + "test creation of views for different timeframes and states", + async ({ apiPath }) => { + if (!apiPath) { + // set to address Type Error returned by Response.json() when using the connector by it self + // "Can't serialize BigInt" + const originalStringify = JSON.stringify; + const mock = jest.spyOn(JSON, "stringify"); + mock.mockImplementation((value: any) => { + return originalStringify(value, stringifyBigIntReplacer); + }); + } + + let networkDetails: EthereumNetworkDetails; + if (apiPath) { + networkDetails = networkDetailsList[0]; + } else { + networkDetails = networkDetailsList[1]; + } + const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); + const strategy = "ETH"; + bungee.addStrategy(strategy, new StrategyEthereum("INFO")); + const snapshot = await bungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view = bungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); - //changing ETH_ASSET_NAME value - const new_asset_id = uuidV4(); - const depNew = await apiClient.invokeContractV1({ - contract: { - contractName: LockAssetContractJson.contractName, - keychainId: keychainPlugin.getKeychainId(), - }, - invocationType: EthContractInvocationType.Send, - methodName: "createAsset", - params: [new_asset_id, 10], - web3SigningCredential: { - ethAccount: WHALE_ACCOUNT_ADDRESS, - secret: "", - type: Web3SigningCredentialType.GethKeychainPassword, - }, - }); - expect(depNew).not.toBeUndefined(); - expect(depNew.status).toBe(200); + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + //expect the view to have capture the new asset ETH_ASSET_NAME, and attributes to match + expect(snapshot.getStateBins().length).toEqual(1); + expect(snapshot.getStateBins()[0].getId()).toEqual(ETH_ASSET_NAME); + expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); + + const view1 = bungee.generateView(snapshot, "0", "9999", undefined); + + //expects nothing to limit time of 9999 + expect(view1.view).toBeUndefined(); + expect(view1.signature).toBeUndefined(); + + //changing ETH_ASSET_NAME value + const lockAsset = await apiClient.invokeContractV1({ + contract: { + contractName: LockAssetContractJson.contractName, + keychainId: keychainPlugin.getKeychainId(), + }, + invocationType: EthContractInvocationType.Send, + methodName: "lockAsset", + params: [ETH_ASSET_NAME], + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + expect(lockAsset).not.toBeUndefined(); + expect(lockAsset.status).toBe(200); + + //changing ETH_ASSET_NAME value + const new_asset_id = uuidV4(); + const depNew = await apiClient.invokeContractV1({ + contract: { + contractName: LockAssetContractJson.contractName, + keychainId: keychainPlugin.getKeychainId(), + }, + invocationType: EthContractInvocationType.Send, + methodName: "createAsset", + params: [new_asset_id, 10], + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + expect(depNew).not.toBeUndefined(); + expect(depNew.status).toBe(200); - const snapshot1 = await bungee.generateSnapshot( - [], - strategy, - networkDetails, - ); - const view2 = bungee.generateView( - snapshot1, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - //expect to return a view - expect(view2.view).toBeTruthy(); - expect(view2.signature).toBeTruthy(); - - const stateBins = snapshot1.getStateBins(); - expect(stateBins.length).toEqual(2); //expect to have captured state for both assets - - const bins = [stateBins[0].getId(), stateBins[1].getId()]; - - //checks if values match: - // - new value of ETH_ASSET_NAME state in new snapshot different than value from old snapshot) - // - successfully captured transaction that created the new asset - if (bins[0] === ETH_ASSET_NAME) { - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(2); - expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( - snapshot.getStateBins()[0].getValue(), + const snapshot1 = await bungee.generateSnapshot( + [], + strategy, + networkDetails, ); - expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(1); - } else { - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); - expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(2); - expect(snapshot1.getStateBins()[1].getValue()).not.toEqual( - snapshot.getStateBins()[0].getValue(), + const view2 = bungee.generateView( + snapshot1, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, ); - } - }); + //expect to return a view + expect(view2.view).toBeTruthy(); + expect(view2.signature).toBeTruthy(); + + const stateBins = snapshot1.getStateBins(); + expect(stateBins.length).toEqual(2); //expect to have captured state for both assets + + const bins = [stateBins[0].getId(), stateBins[1].getId()]; + + //checks if values match: + // - new value of ETH_ASSET_NAME state in new snapshot different than value from old snapshot) + // - successfully captured transaction that created the new asset + if (bins[0] === ETH_ASSET_NAME) { + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(2); + expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( + snapshot.getStateBins()[0].getValue(), + ); + expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(1); + } else { + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); + expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(2); + expect(snapshot1.getStateBins()[1].getValue()).not.toEqual( + snapshot.getStateBins()[0].getValue(), + ); + } + }, + ); }); function stringifyBigIntReplacer(key: string, value: bigint): string { diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-basic.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-basic.test.ts index 08c49cbf6a8..43cc97dd514 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-basic.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-basic.test.ts @@ -68,14 +68,14 @@ let pluginBungeeFabricOptions: IPluginBungeeHermesOptions; let pluginBungee: PluginBungeeHermes; const FABRIC_ASSET_ID = uuidv4(); -let networkDetails: FabricNetworkDetails; +let networkDetailsList: FabricNetworkDetails[]; const log = LoggerProvider.getOrCreate({ level: logLevel, label: "BUNGEE - Hermes", }); -beforeAll(async () => { +beforeEach(async () => { pruneDockerAllIfGithubAction({ logLevel }) .then(() => { log.info("Pruning throw OK"); @@ -329,124 +329,148 @@ beforeAll(async () => { ); pluginBungeeFabricOptions = { + pluginRegistry, keyPair: Secp256k1Keys.generateKeyPairsBuffer(), instanceId: uuidv4(), }; - networkDetails = { - connectorApiPath: fabricPath, - signingCredential: fabricSigningCredential, - channelName: fabricChannelName, - contractName: fabricContractName, - participant: "Org1MSP", - }; + networkDetailsList = [ + { + connectorApiPath: fabricPath, + signingCredential: fabricSigningCredential, + channelName: fabricChannelName, + contractName: fabricContractName, + participant: "Org1MSP", + }, + { + connector: fabricConnector, + signingCredential: fabricSigningCredential, + channelName: fabricChannelName, + contractName: fabricContractName, + participant: "Org1MSP", + }, + ]; pluginBungee = new PluginBungeeHermes(pluginBungeeFabricOptions); } }); -test("test creation of views for different timeframes and states", async () => { - const strategy = "FABRIC"; - pluginBungee.addStrategy(strategy, new StrategyFabric("INFO")); - - const snapshot = await pluginBungee.generateSnapshot( - [], - strategy, - networkDetails, - ); - const view = pluginBungee.generateView( - snapshot, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - - //expect to return a view - expect(view.view).toBeTruthy(); - expect(view.signature).toBeTruthy(); - - //expect the view to have capture the new asset Fabric_ASSET_ID, and attributes to match - expect(snapshot.getStateBins().length).toEqual(1); - expect(snapshot.getStateBins()[0].getId()).toEqual(FABRIC_ASSET_ID); - expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); - - //fabric transaction proofs include endorsements - expect( - snapshot.getStateBins()[0].getTransactions()[0].getProof().getEndorsements() - ?.length, - ).toEqual(2); - - //no valid states for this time frame - const view1 = pluginBungee.generateView(snapshot, "0", "9999", undefined); - expect(view1.view).toBeUndefined(); - expect(view1.signature).toBeUndefined(); - - //creating new asset - const new_asset_id = uuidv4(); - const createResponse = await apiClient.runTransactionV1({ - contractName: fabricContractName, - channelName: fabricChannelName, - params: [new_asset_id, "10"], - methodName: "CreateAsset", - invocationType: FabricContractInvocationType.Send, - signingCredential: fabricSigningCredential, - }); - expect(createResponse).not.toBeUndefined(); - expect(createResponse.status).toBeGreaterThan(199); - expect(createResponse.status).toBeLessThan(300); - - //changing FABRIC_ASSET_ID value - const modifyResponse = await apiClient.runTransactionV1({ - contractName: fabricContractName, - channelName: fabricChannelName, - params: [FABRIC_ASSET_ID, "18"], - methodName: "UpdateAsset", - invocationType: FabricContractInvocationType.Send, - signingCredential: fabricSigningCredential, - }); - expect(modifyResponse).not.toBeUndefined(); - expect(modifyResponse.status).toBeGreaterThan(199); - expect(modifyResponse.status).toBeLessThan(300); - - const snapshot1 = await pluginBungee.generateSnapshot( - [], - strategy, - networkDetails, - ); - const view2 = pluginBungee.generateView( - snapshot1, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - - //expect to return a view - expect(view2.view).toBeTruthy(); - expect(view2.signature).toBeTruthy(); - - //expect to have captured state for both assets - const stateBins = snapshot1.getStateBins(); - expect(stateBins.length).toEqual(2); - const bins = [stateBins[0].getId(), stateBins[1].getId()]; - - expect(bins.includes(FABRIC_ASSET_ID)).toBeTrue(); - expect(bins.includes(new_asset_id)).toBeTrue(); - - //checks if values match: - // - new value of FABRIC_ASSET_ID state in new snapshot equals to new value) - // - successfully captured transaction that created the new asset - if (bins[0] === FABRIC_ASSET_ID) { - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(2); - expect(snapshot1.getStateBins()[0].getValue()).toEqual("18"); - expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(1); - } else { - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); - expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(2); - expect(snapshot1.getStateBins()[1].getValue()).toEqual("18"); - } -}); +test.each([{ apiPath: true }, { apiPath: false }])( + //test for both FabricApiPath and FabricConnector + "test creation of views for different timeframes and states", + async ({ apiPath }) => { + let networkDetails: FabricNetworkDetails; + if (apiPath) { + networkDetails = networkDetailsList[0]; + } else { + networkDetails = networkDetailsList[1]; + } + + const strategy = "FABRIC"; + pluginBungee.addStrategy(strategy, new StrategyFabric("INFO")); + + const snapshot = await pluginBungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view = pluginBungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + //expect the view to have capture the new asset Fabric_ASSET_ID, and attributes to match + expect(snapshot.getStateBins().length).toEqual(1); + expect(snapshot.getStateBins()[0].getId()).toEqual(FABRIC_ASSET_ID); + expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); + + //fabric transaction proofs include endorsements + expect( + snapshot + .getStateBins()[0] + .getTransactions()[0] + .getProof() + .getEndorsements()?.length, + ).toEqual(2); + + //no valid states for this time frame + const view1 = pluginBungee.generateView(snapshot, "0", "9999", undefined); + expect(view1.view).toBeUndefined(); + expect(view1.signature).toBeUndefined(); + + //creating new asset + const new_asset_id = uuidv4(); + const createResponse = await apiClient.runTransactionV1({ + contractName: fabricContractName, + channelName: fabricChannelName, + params: [new_asset_id, "10"], + methodName: "CreateAsset", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + expect(createResponse).not.toBeUndefined(); + expect(createResponse.status).toBeGreaterThan(199); + expect(createResponse.status).toBeLessThan(300); + + //changing FABRIC_ASSET_ID value + const modifyResponse = await apiClient.runTransactionV1({ + contractName: fabricContractName, + channelName: fabricChannelName, + params: [FABRIC_ASSET_ID, "18"], + methodName: "UpdateAsset", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + expect(modifyResponse).not.toBeUndefined(); + expect(modifyResponse.status).toBeGreaterThan(199); + expect(modifyResponse.status).toBeLessThan(300); + + const snapshot1 = await pluginBungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view2 = pluginBungee.generateView( + snapshot1, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + + //expect to return a view + expect(view2.view).toBeTruthy(); + expect(view2.signature).toBeTruthy(); + + //expect to have captured state for both assets + const stateBins = snapshot1.getStateBins(); + expect(stateBins.length).toEqual(2); + const bins = [stateBins[0].getId(), stateBins[1].getId()]; + + expect(bins.includes(FABRIC_ASSET_ID)).toBeTrue(); + expect(bins.includes(new_asset_id)).toBeTrue(); + + //checks if values match: + // - new value of FABRIC_ASSET_ID state in new snapshot equals to new value) + // - successfully captured transaction that created the new asset + if (bins[0] === FABRIC_ASSET_ID) { + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(2); + expect(snapshot1.getStateBins()[0].getValue()).toEqual("18"); + expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(1); + } else { + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); + expect(snapshot1.getStateBins()[1].getTransactions().length).toEqual(2); + expect(snapshot1.getStateBins()[1].getValue()).toEqual("18"); + } + }, +); -afterAll(async () => { +afterEach(async () => { await fabricLedger.stop(); await fabricLedger.destroy(); await Servers.shutdown(fabricServer); diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-pruning.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-pruning.test.ts index 0a810c00fff..2bc97836907 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-pruning.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-pruning.test.ts @@ -68,14 +68,14 @@ let pluginBungeeFabricOptions: IPluginBungeeHermesOptions; let pluginBungee: PluginBungeeHermes; const FABRIC_ASSET_ID = uuidv4(); -let networkDetails: FabricNetworkDetails; +let networkDetailsList: FabricNetworkDetails[]; const log = LoggerProvider.getOrCreate({ level: logLevel, label: "BUNGEE - Hermes", }); -beforeAll(async () => { +beforeEach(async () => { pruneDockerAllIfGithubAction({ logLevel }) .then(() => { log.info("Pruning throw OK"); @@ -329,103 +329,127 @@ beforeAll(async () => { ); pluginBungeeFabricOptions = { + pluginRegistry, keyPair: Secp256k1Keys.generateKeyPairsBuffer(), instanceId: uuidv4(), }; - networkDetails = { - connectorApiPath: fabricPath, - signingCredential: fabricSigningCredential, - channelName: fabricChannelName, - contractName: fabricContractName, - participant: "Org1MSP", - }; + networkDetailsList = [ + { + connectorApiPath: fabricPath, + signingCredential: fabricSigningCredential, + channelName: fabricChannelName, + contractName: fabricContractName, + participant: "Org1MSP", + }, + { + connector: fabricConnector, + signingCredential: fabricSigningCredential, + channelName: fabricChannelName, + contractName: fabricContractName, + participant: "Org1MSP", + }, + ]; pluginBungee = new PluginBungeeHermes(pluginBungeeFabricOptions); } }); -test("test creation of views for specific timeframes", async () => { - const strategy = "FABRIC"; - pluginBungee.addStrategy(strategy, new StrategyFabric("INFO")); - - const snapshot = await pluginBungee.generateSnapshot( - [], - strategy, - networkDetails, - ); - const view = pluginBungee.generateView( - snapshot, - "0", - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - //expect to return a view - expect(view.view).toBeTruthy(); - expect(view.signature).toBeTruthy(); - - //expect the view to have capture the new asset FABRIC_ASSET_ID, and attributes to match - expect(snapshot.getStateBins().length).toEqual(1); - expect(snapshot.getStateBins()[0].getId()).toEqual(FABRIC_ASSET_ID); - expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); - //fabric transaction proofs include endorsements - expect( - snapshot.getStateBins()[0].getTransactions()[0].getProof().getEndorsements() - ?.length, - ).toEqual(2); - - //changing FABRIC_ASSET_ID value - const modifyResponse = await apiClient.runTransactionV1({ - contractName: fabricContractName, - channelName: fabricChannelName, - params: [FABRIC_ASSET_ID, "18"], - methodName: "UpdateAsset", - invocationType: FabricContractInvocationType.Send, - signingCredential: fabricSigningCredential, - }); - expect(modifyResponse).not.toBeUndefined(); - expect(modifyResponse.status).toBeGreaterThan(199); - expect(modifyResponse.status).toBeLessThan(300); - - const snapshot1 = await pluginBungee.generateSnapshot( - [], - strategy, - networkDetails, - ); - - //tI is the time of the first transaction +1 - const tI = ( - BigInt(snapshot.getStateBins()[0].getTransactions()[0].getTimeStamp()) + - BigInt(1) - ).toString(); - - expect(snapshot1.getStateBins().length).toEqual(1); - expect(snapshot1.getStateBins()[0].getId()).toEqual(FABRIC_ASSET_ID); - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(2); - expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( - snapshot.getStateBins()[0].getValue(), - ); - const view1 = pluginBungee.generateView( - snapshot1, - tI, - Number.MAX_SAFE_INTEGER.toString(), - undefined, - ); - //expect to return a view - expect(view1.view).toBeTruthy(); - expect(view1.signature).toBeTruthy(); - - expect(snapshot1.getStateBins().length).toEqual(1); - expect(snapshot1.getStateBins()[0].getId()).toEqual(FABRIC_ASSET_ID); - //expect the view to not include first transaction (made before tI) - expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); - //expect old and new snapshot state values to differ - expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( - snapshot.getStateBins()[0].getValue(), - ); -}); +test.each([{ apiPath: true }, { apiPath: false }])( + //test for both FabricApiPath and FabricConnector + "test creation of views for specific timeframes", + async ({ apiPath }) => { + let networkDetails: FabricNetworkDetails; + if (apiPath) { + networkDetails = networkDetailsList[0]; + } else { + networkDetails = networkDetailsList[1]; + } + + const strategy = "FABRIC"; + pluginBungee.addStrategy(strategy, new StrategyFabric("INFO")); + + const snapshot = await pluginBungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + const view = pluginBungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + //expect the view to have capture the new asset FABRIC_ASSET_ID, and attributes to match + expect(snapshot.getStateBins().length).toEqual(1); + expect(snapshot.getStateBins()[0].getId()).toEqual(FABRIC_ASSET_ID); + expect(snapshot.getStateBins()[0].getTransactions().length).toEqual(1); + //fabric transaction proofs include endorsements + expect( + snapshot + .getStateBins()[0] + .getTransactions()[0] + .getProof() + .getEndorsements()?.length, + ).toEqual(2); + + //changing FABRIC_ASSET_ID value + const modifyResponse = await apiClient.runTransactionV1({ + contractName: fabricContractName, + channelName: fabricChannelName, + params: [FABRIC_ASSET_ID, "18"], + methodName: "UpdateAsset", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + expect(modifyResponse).not.toBeUndefined(); + expect(modifyResponse.status).toBeGreaterThan(199); + expect(modifyResponse.status).toBeLessThan(300); + + const snapshot1 = await pluginBungee.generateSnapshot( + [], + strategy, + networkDetails, + ); + + //tI is the time of the first transaction +1 + const tI = ( + BigInt(snapshot.getStateBins()[0].getTransactions()[0].getTimeStamp()) + + BigInt(1) + ).toString(); + + expect(snapshot1.getStateBins().length).toEqual(1); + expect(snapshot1.getStateBins()[0].getId()).toEqual(FABRIC_ASSET_ID); + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(2); + expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( + snapshot.getStateBins()[0].getValue(), + ); + const view1 = pluginBungee.generateView( + snapshot1, + tI, + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view1.view).toBeTruthy(); + expect(view1.signature).toBeTruthy(); + + expect(snapshot1.getStateBins().length).toEqual(1); + expect(snapshot1.getStateBins()[0].getId()).toEqual(FABRIC_ASSET_ID); + //expect the view to not include first transaction (made before tI) + expect(snapshot1.getStateBins()[0].getTransactions().length).toEqual(1); + //expect old and new snapshot state values to differ + expect(snapshot1.getStateBins()[0].getValue()).not.toEqual( + snapshot.getStateBins()[0].getValue(), + ); + }, +); -afterAll(async () => { +afterEach(async () => { await fabricLedger.stop(); await fabricLedger.destroy(); await Servers.shutdown(fabricServer); diff --git a/yarn.lock b/yarn.lock index 26eba0089a0..6b1838be8e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9931,6 +9931,7 @@ __metadata: express: "npm:4.19.2" fabric-network: "npm:2.2.20" fs-extra: "npm:11.2.0" + http-errors-enhanced-cjs: "npm:2.0.1" key-encoder: "npm:2.0.3" merkletreejs: "npm:0.3.11" socket.io: "npm:4.6.2"