diff --git a/.cspell.json b/.cspell.json index 228d0f3d87a..4973ba70385 100644 --- a/.cspell.json +++ b/.cspell.json @@ -134,7 +134,8 @@ "wasm", "Xdai", "goquorum", - "outsh" + "hada", + "undici" ], "dictionaries": [ "typescript,node,npm,go,rust" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a2604ba05d..fa0ef9e605e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -821,6 +821,33 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- - run: ./tools/ci.sh + cactus-plugin-ledger-connector-iroha2: + continue-on-error: false + env: + FULL_BUILD_DISABLED: true + JEST_TEST_PATTERN: packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/(unit|integration|benchmark)/.*/*.test.ts + JEST_TEST_RUNNER_DISABLED: false + TAPE_TEST_RUNNER_DISABLED: true + needs: build-dev + runs-on: ubuntu-20.04 + steps: + - name: Use Node.js v16.14.2 + uses: actions/setup-node@v2.1.2 + with: + node-version: v16.14.2 + - uses: actions/checkout@v2.3.4 + - id: yarn-cache-dir-path + name: Get yarn cache directory path + run: echo "::set-output name=dir::$(yarn cache dir)" + - id: yarn-cache + name: Restore Yarn Cache + uses: actions/cache@v3.0.4 + with: + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + restore-keys: | + ${{ runner.os }}-yarn- + - run: ./tools/ci.sh cactus-plugin-ledger-connector-quorum: continue-on-error: false env: diff --git a/.gitignore b/.gitignore index be2b83ebe2a..b4bf1721657 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,6 @@ bin/ lerna-debug.log cactus-openapi-spec.json cactus-openapi-spec-*.json -.npmrc *.log build/ .gradle/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..6492e57642c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@iroha2:registry=https://nexus.iroha.tech/repository/npm-group/ \ No newline at end of file diff --git a/packages/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-socket-api-client.ts b/packages/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-socket-api-client.ts index 18554855ba9..bdb260acb2c 100644 --- a/packages/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-socket-api-client.ts +++ b/packages/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-socket-api-client.ts @@ -19,11 +19,7 @@ export interface ISocketApiClient { args: any, ): Promise; - watchBlocksV1?( - monitorOptions?: Record, - ): Observable; + watchBlocksV1?(monitorOptions?: any): Observable; - watchBlocksAsyncV1?( - monitorOptions?: Record, - ): Promise>; + watchBlocksAsyncV1?(monitorOptions?: any): Promise>; } diff --git a/packages/cactus-plugin-ledger-connector-iroha2/README.md b/packages/cactus-plugin-ledger-connector-iroha2/README.md new file mode 100644 index 00000000000..25aeef37aaf --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/README.md @@ -0,0 +1,345 @@ +# `@hyperledger/cactus-plugin-ledger-connector-iroha2` + +This plugin provides `Cactus` a way to interact with **Iroha V2** networks. Using this we can run various Iroha leger instructions and queries. +If you want to connect to Iroha V1 ledger, please use `@hyperledger/cactus-plugin-ledger-connector-iroha` instead. + +## Summary + +- [Remarks](#remarks) +- [Getting Started](#getting-started) +- [Endpoints](#endpoints) +- [Running the tests](#running-the-tests) +- [Contributing](#contributing) +- [License](#license) +- [Acknowledgments](#acknowledgments) + +## Remarks + +- Docker support is not implemented yet. +- There is no official Iroha V2 release yet. API and connector behavior can change before stable release. +- Query pagination is not supported yet. Querying large datasets (the ones with `All`) can be catastrophic. Pagination is not implemented in upstream javascript iroha sdk yet. + +## Getting Started + +Clone the git repository on your local machine. Follow these instructions that will get you a copy of the project up and running on your local machine for development and testing purposes. + +### Prerequisites + +In the root of the project, execute the command to install and build the dependencies. It will also build this connector: + +```sh +yarn run configure +``` + +### Usage + +Import `PluginFactoryLedgerConnector` from the connector package and use it to create a connector. + +```typescript +import { PluginFactoryLedgerConnector } from "@hyperledger/cactus-plugin-ledger-connector-iroha2"; +import { PluginImportType } from "@hyperledger/cactus-core-api"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { v4 as uuidv4 } from "uuid"; + +const defaultConfig = { + torii: { + apiURL: "http://127.0.0.1:8080", + }, + accountId: { + name: "alice", + domainId: "wonderland", + }, +}; + +const factory = new PluginFactoryLedgerConnector({ + pluginImportType: PluginImportType.Local, +}); + +const connector = await factory.create({ + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins }), + logLevel, + defaultConfig, +}); +``` + +Alternatively, you can instantiate a new `PluginLedgerConnectorIroha2` instance directly. + +```typescript +import { PluginLedgerConnectorIroha2 } from "@hyperledger/cactus-plugin-ledger-connector-iroha2"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { v4 as uuidv4 } from "uuid"; + +const defaultConfig = { + torii: { + apiURL: "http://127.0.0.1:8080", + }, + accountId: { + name: "alice", + domainId: "wonderland", + }, +}; + +const connector = new PluginLedgerConnectorIroha2({ + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins }), + logLevel, + defaultConfig, +}); +``` + +> `defaultConfig` in `PluginLedgerConnectorIroha2` options can be used to define a set of configurations common for +> all the requests. Specific, top-level keys of the config can be later overwritten by single requests. +> For example, each request can specify own credentials to be used, but they don't have to define peer address at all since it will +> be taken from the connector default config. + +> Connector plugin can export its web endpoints and be called remotely through an `ApiClient`. It is recommended +> to use `ApiServer` for remote connector setup, see `@hyperledger/cactus-cmd-api-server` for more details. The following steps assume +> using the connector directly which is suitable for testing. + +You can use the connector plugin to send transactions to or query the Iroha V2 ledger. +Here, for instance, we create and query a new domain: + +```typescript +import { IrohaInstruction } from "@hyperledger/cactus-plugin-ledger-connector-iroha2"; + +const transactionResponse = await connector.transact({ + // Note: `instruction` can be a list of instructions to be sent as single transaction. + instruction: { + name: IrohaInstruction.RegisterDomain, + params: ["newDomainName"], + }, + baseConfig: { + // Overwrite default connector config to use own credentials stored in a keychain plugin + signingCredential: { + keychainId, + keychainRef, + }, + }, +}); + +const queryResponse = await connector.query({ + queryName: IrohaQuery.FindDomainById, + params: ["newDomainName"], + baseConfig: { + signingCredential: { + keychainId, + keychainRef, + }, + }, +}); +``` + +> See [connector integration tests](./src/test/typescript/integration) for complete usage examples. + +> For the list of currently supported instructions and queries see [Endpoints](#endpoints) + +### Building/running the container image locally + +- This connector has no image yet. + +## Endpoints + +### TransactV1 (`/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/transact`) + +##### RegisterDomain + +- `domainName`: string + +##### RegisterAssetDefinition + +- `assetName`: string, +- `domainName`: string, +- `valueType`: "Fixed" | "Quantity" | "BigQuantity" | "Store", +- `mintable`: "Infinitely" | "Once" | "Not", + +##### RegisterAsset + +- `assetName`: string, +- `domainName`: string, +- `accountName`: string, +- `accountDomainName`: string, +- `value`: number | bigint | string, + +##### MintAsset + +- `assetName`: string, +- `domainName`: string, +- `accountName`: string, +- `accountDomainName`: string, +- `value`: number | bigint | string, + +##### BurnAsset + +- `assetName`: string, +- `domainName`: string, +- `accountName`: string, +- `accountDomainName`: string, +- `value`: number | bigint | string | Metadata, + +##### TransferAsset + +- `assetName`: string, +- `assetDomainName`: string, +- `sourceAccountName`: string, +- `sourceAccountDomain`: string, +- `targetAccountName`: string, +- `targetAccountDomain`: string, +- `valueToTransfer`: number | bigint | string | Metadata, + +##### RegisterAccount + +- `accountName`: string, +- `domainName`: string, +- `publicKeyPayload`: string, (hex encoded string) +- `publicKeyDigestFunction` = "ed25519", + +### QueryV1 (`/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/query`) + +#### FindAllDomains + +- None + +#### FindDomainById + +- `domainName`: string + +#### FindAssetDefinitionById + +- `name`: string, +- `domainName`: string, + +#### FindAllAssetsDefinitions + +- None + +#### FindAssetById + +- `assetName`: string, +- `assetDomainName`: string, +- `accountName`: string, +- `accountDomainName`: string, + +#### FindAllAssets + +- None + +#### FindAllPeers + +- None + +#### FindAllBlocks + +- None + +#### FindAccountById + +- `name`: string, +- `domainName`: string, + +#### FindAllAccounts + +- None + +#### FindAllTransactions + +- None + +#### FindTransactionByHash + +- `hash`: string (hex encoded string) + +### WatchBlocksV1 (SocketIO) + +#### Subscribe (Input) + +- Starts watching for new blocks that will be reported back with `Next` messages. +- Options: + - `type?: BlockTypeV1`: Specify response block type wanted. Defaults to JSON serialized in string. + - `startBlock?: string;`: Number of block to start monitoring from. + - `baseConfig?: Iroha2BaseConfig`: Iroha connection details (merged with default connector config). + +#### Unsubscribe (Input) + +#### Next (Output) + +- Block data encoded as requested in `type` option during subscription. + +#### Error (Output) + +- Error details. + +#### Monitoring new blocks with ApiClient (example) + +```typescript +const watchObservable = apiClient.watchBlocksV1({ + type: BlockTypeV1.Raw, + baseConfig: defaultBaseConfig, +}); + +const subscription = watchObservable.subscribe({ + next(block) { + console.log("New block:", block); + }, + error(err) { + console.error("Monitor error:", err); + subscription.unsubscribe(); + }, +}); +``` + +## Running the tests + +To run all the tests for this connector to ensure it's working correctly execute the following from the root of the `cactus` project: + +```sh +npx jest cactus-plugin-ledger-connector-iroha2 +``` + +## Contributing + +We welcome contributions to Hyperledger Cactus in many forms, and there’s always plenty to do! + +Please review [CONTIRBUTING.md](../../CONTRIBUTING.md) to get started. + +### Quick connector project walkthrough + +#### `./src/main/json/openapi.json` + +- Contains OpenAPI definition. + +#### `./src/main/typescript/plugin-ledger-connector-iroha2.ts` + +- Contains main connector class logic, including `transact` and `query` functions. + +#### `./src/main/typescript/utils.ts` + +- Utility functions used throughout the connector. If the file grows too big consider dividing it into more subject-related files. +- Should be internal to the connector, not exported in a public interface. + +#### `./src/main/typescript/api-client/iroha2-api-client.ts` + +- Contains implementation of ApiClient extension over client generated from OpenAPI definition. +- Should be used to connect to remote connector. + +#### `./src/main/typescript/web-services` + +- Folder that contains web service endpoint definitions. + +#### `./src/main/typescript/cactus-iroha-sdk-wrapper/` + +- Internal (not exported) wrappers around upstream Iroha javascript SDK. +- Provides convenient functions without need to manually build up message payload. + - `client.ts` Can be used to add multiple instructions into single transaction and send it. This is base entry for wrapper usage. + - `query.ts` Contain functions to query the ledger, should be used through client (i.e. `client.query.getSomething()`). + - `data-factories.ts` Functions that simplify creation of some commonly used structures in the wrapper. + +#### `./src/test/typescript/integration/` + +- Integration test of various connector functionalities. + +## License + +This distribution is published under the Apache License Version 2.0 found in the [LICENSE](../../LICENSE) file. + +## Acknowledgments diff --git a/packages/cactus-plugin-ledger-connector-iroha2/openapitools.json b/packages/cactus-plugin-ledger-connector-iroha2/openapitools.json new file mode 100644 index 00000000000..29f5d069907 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "5.2.0" + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/package.json b/packages/cactus-plugin-ledger-connector-iroha2/package.json new file mode 100644 index 00000000000..44cf9ef07bb --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/package.json @@ -0,0 +1,80 @@ +{ + "name": "@hyperledger/cactus-plugin-ledger-connector-iroha2", + "version": "1.0.0", + "description": "Allows Cactus nodes to connect to an Iroha V2 ledger.", + "keywords": [ + "Hyperledger", + "Cactus", + "Iroha", + "Iroha2", + "Iroha V2", + "Integration", + "Blockchain", + "Distributed Ledger Technology" + ], + "homepage": "https://github.com/hyperledger/cactus#readme", + "bugs": { + "url": "https://github.com/hyperledger/cactus/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hyperledger/cactus.git" + }, + "license": "Apache-2.0", + "author": { + "name": "Hyperledger Cactus Contributors", + "email": "cactus@lists.hyperledger.org", + "url": "https://www.hyperledger.org/use/cactus" + }, + "contributors": [ + { + "name": "Michal Bajer", + "email": "michal.bajer@fujitsu.com", + "url": "https://www.fujitsu.com/global/" + } + ], + "main": "dist/lib/main/typescript/index.js", + "module": "dist/lib/main/typescript/index.js", + "types": "dist/types/main/typescript/index.d.ts", + "files": [ + "dist/*" + ], + "scripts": { + "codegen": "run-p 'codegen:*'", + "codegen:openapi": "npm run generate-sdk", + "generate-sdk": "openapi-generator-cli generate -i ./src/main/json/openapi.json -g typescript-axios -o ./src/main/typescript/generated/openapi/typescript-axios/ --reserved-words-mappings protected=protected" + }, + "dependencies": { + "@hyperledger/cactus-common": "1.0.0", + "@hyperledger/cactus-core": "1.0.0", + "@hyperledger/cactus-core-api": "1.0.0", + "@iroha2/client": "3.0.0", + "@iroha2/crypto-core": "0.1.1", + "@iroha2/crypto-target-node": "0.4.0", + "@iroha2/data-model": "3.0.0", + "hada": "0.0.8", + "rxjs": "7.3.0", + "express": "4.17.1", + "socket.io": "4.4.1", + "fast-safe-stringify": "2.1.1", + "sanitize-html": "2.7.0", + "undici": "5.10.0" + }, + "devDependencies": { + "@hyperledger/cactus-test-tooling": "1.0.0", + "@hyperledger/cactus-plugin-keychain-memory": "1.0.0", + "@types/express": "4.17.8", + "@types/sanitize-html": "2.6.2", + "uuid": "8.3.2", + "body-parser": "1.19.0", + "jest": "28.1.0", + "jest-extended": "2.0.0" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-iroha2/src/main/json/openapi.json new file mode 100644 index 00000000000..ea9d4566224 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/json/openapi.json @@ -0,0 +1,532 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Hyperledger Cactus Plugin - Connector Iroha V2", + "description": "Can perform basic tasks on a Iroha V2 ledger", + "version": "1.0.0", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "components": { + "schemas": { + "IrohaInstruction": { + "type": "string", + "description": "Command names that correspond to Iroha Special Instructions (https://hyperledger.github.io/iroha-2-docs/guide/advanced/isi.html)", + "enum": [ + "registerDomain", + "registerAssetDefinition", + "registerAsset", + "mintAsset", + "burnAsset", + "transferAsset", + "registerAccount" + ], + "x-enum-descriptions": [ + "Register new domain", + "Register new asset definition", + "Register new asset", + "Mint asset value", + "Burn asset value", + "Transfer asset between accounts", + "Register new account" + ], + "x-enum-varnames": [ + "RegisterDomain", + "RegisterAssetDefinition", + "RegisterAsset", + "MintAsset", + "BurnAsset", + "TransferAsset", + "RegisterAccount" + ] + }, + "IrohaQuery": { + "type": "string", + "description": "Command names that correspond to Iroha queries (https://hyperledger.github.io/iroha-2-docs/guide/advanced/queries.html)", + "enum": [ + "findAllDomains", + "findDomainById", + "findAssetDefinitionById", + "findAllAssetsDefinitions", + "findAssetById", + "findAllAssets", + "findAllPeers", + "findAllBlocks", + "findAccountById", + "findAllAccounts", + "findAllTransactions", + "findTransactionByHash" + ], + "x-enum-descriptions": [ + "Get list of all registered domains", + "Get domain with specified ID", + "Get asset definition with specified ID", + "Get list of all registered asset definition", + "Get asset with specified ID", + "Get list of all registered assets", + "Get list of all ledger peers", + "Get list of all ledger blocks", + "Get account with specified ID", + "Get list of all registered accounts", + "Get list of all transactions", + "Get transaction with specified hash" + ], + "x-enum-varnames": [ + "FindAllDomains", + "FindDomainById", + "FindAssetDefinitionById", + "FindAllAssetsDefinitions", + "FindAssetById", + "FindAllAssets", + "FindAllPeers", + "FindAllBlocks", + "FindAccountById", + "FindAllAccounts", + "FindAllTransactions", + "FindTransactionByHash" + ] + }, + "WatchBlocksV1": { + "type": "string", + "description": "Websocket requests for monitoring new blocks.", + "enum": [ + "org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Subscribe", + "org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Next", + "org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Unsubscribe", + "org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Error", + "org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Complete" + ], + "x-enum-varnames": [ + "Subscribe", + "Next", + "Unsubscribe", + "Error", + "Complete" + ] + }, + "BlockTypeV1": { + "type": "string", + "description": "Iroha V2 block response type.", + "enum": [ + "raw", + "binary" + ], + "x-enum-descriptions": [ + "Default JSON-encoded string full block data.", + "Encoded format that must be decoded with Iroha SDK on client side before use" + ], + "x-enum-varnames": [ + "Raw", + "Binary" + ] + }, + "WatchBlocksOptionsV1": { + "type": "object", + "description": "Options passed when subscribing to block monitoring.", + "properties": { + "type": { + "$ref": "#/components/schemas/BlockTypeV1", + "description": "Type of response block to return.", + "default": "BlockTypeV1.Binary", + "nullable": false + }, + "startBlock": { + "type": "string", + "description": "Number of block to start monitoring from.", + "minLength": 1, + "nullable": false + }, + "baseConfig": { + "$ref": "#/components/schemas/Iroha2BaseConfig", + "description": "Iroha V2 connection configuration.", + "nullable": false + } + } + }, + "WatchBlocksRawResponseV1": { + "type": "object", + "description": "Default JSON-encoded string full block data.", + "required": [ + "blockData" + ], + "properties": { + "blockData": { + "type": "string", + "nullable": false + } + } + }, + "WatchBlocksBinaryResponseV1": { + "type": "object", + "description": "Binary encoded response of block data.", + "required": [ + "binaryBlock" + ], + "properties": { + "binaryBlock": { + "type": "string", + "format": "binary", + "nullable": false + } + } + }, + "WatchBlocksResponseV1": { + "oneOf": [ + { + "$ref": "#/components/schemas/WatchBlocksRawResponseV1", + "nullable": false + }, + { + "$ref": "#/components/schemas/WatchBlocksBinaryResponseV1", + "nullable": false + }, + { + "$ref": "#/components/schemas/ErrorExceptionResponseV1", + "nullable": false + } + ] + }, + "Iroha2AccountId": { + "type": "object", + "description": "Iroha V2 account ID.", + "additionalProperties": false, + "nullable": false, + "required": [ + "name", + "domainId" + ], + "properties": { + "name": { + "type": "string", + "nullable": false + }, + "domainId": { + "type": "string", + "nullable": false + } + } + }, + "Iroha2KeyJson": { + "type": "object", + "description": "Private/Public key JSON containing payload and digest function.", + "additionalProperties": false, + "nullable": false, + "required": [ + "digestFunction", + "payload" + ], + "properties": { + "digestFunction": { + "type": "string", + "nullable": false + }, + "payload": { + "type": "string", + "nullable": false + } + } + }, + "KeychainReference": { + "type": "object", + "description": "Reference to entry stored in Cactus keychain plugin.", + "required": [ + "keychainId", + "keychainRef" + ], + "properties": { + "keychainId": { + "type": "string", + "description": "Keychain plugin ID.", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "keychainRef": { + "type": "string", + "description": "Key reference name.", + "minLength": 1, + "maxLength": 100, + "nullable": false + } + } + }, + "Iroha2KeyPair": { + "type": "object", + "description": "Pair of Iroha account private and public keys.", + "required": [ + "privateKey", + "publicKey" + ], + "properties": { + "privateKey": { + "$ref": "#/components/schemas/Iroha2KeyJson", + "nullable": false + }, + "publicKey": { + "type": "string", + "nullable": false + } + } + }, + "Iroha2BaseConfigTorii": { + "type": "object", + "description": "Iroha V2 peer connection information.", + "additionalProperties": false, + "nullable": false, + "properties": { + "apiURL": { + "type": "string", + "nullable": false + }, + "telemetryURL": { + "type": "string", + "nullable": false + } + } + }, + "Iroha2BaseConfig": { + "type": "object", + "description": "Iroha V2 connection configuration.", + "additionalProperties": false, + "required": [ + "torii" + ], + "properties": { + "torii": { + "$ref": "#/components/schemas/Iroha2BaseConfigTorii", + "nullable": false + }, + "accountId": { + "$ref": "#/components/schemas/Iroha2AccountId", + "nullable": false + }, + "signingCredential": { + "oneOf": [ + { + "$ref": "#/components/schemas/Iroha2KeyPair", + "nullable": false + }, + { + "$ref": "#/components/schemas/KeychainReference", + "nullable": false + } + ] + } + } + }, + "IrohaInstructionRequestV1": { + "type": "object", + "description": "Single Iroha V2 instruction to be executed request.", + "required": [ + "name", + "params" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "IrohaInstruction", + "description": "Iroha V2 instruction name.", + "nullable": false + }, + "params": { + "description": "The list of arguments to pass with specified instruction.", + "type": "array", + "items": {} + } + } + }, + "TransactRequestV1": { + "type": "object", + "description": "Request to transact endpoint, can be passed one or multiple instructions to be executed.", + "required": [ + "instruction" + ], + "additionalProperties": false, + "properties": { + "instruction": { + "oneOf": [ + { + "$ref": "#/components/schemas/IrohaInstructionRequestV1", + "nullable": false + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/IrohaInstructionRequestV1" + } + } + ] + }, + "baseConfig": { + "$ref": "#/components/schemas/Iroha2BaseConfig", + "description": "Iroha V2 connection configuration.", + "nullable": false + } + } + }, + "TransactResponseV1": { + "type": "object", + "description": "Response from transaction endpoint with operation status.", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "nullable": false + } + } + }, + "QueryRequestV1": { + "type": "object", + "description": "Request to query endpoint.", + "required": [ + "queryName" + ], + "additionalProperties": false, + "properties": { + "queryName": { + "type": "IrohaQuery", + "description": "Name of the query to be executed.", + "nullable": false + }, + "baseConfig": { + "$ref": "#/components/schemas/Iroha2BaseConfig", + "description": "Iroha V2 connection configuration.", + "nullable": false + }, + "params": { + "description": "The list of arguments to pass with the query.", + "type": "array", + "items": {} + } + } + }, + "QueryResponseV1": { + "type": "object", + "description": "Response with query results.", + "required": [ + "response" + ], + "properties": { + "response": { + "description": "Query response data that varies between different queries.", + "nullable": false + } + } + }, + "ErrorExceptionResponseV1": { + "type": "object", + "description": "Error response from the connector.", + "required": [ + "message", + "error" + ], + "properties": { + "message": { + "type": "string", + "description": "Short error description message.", + "nullable": false + }, + "error": { + "type": "string", + "description": "Detailed error information.", + "nullable": false + } + } + } + } + }, + "paths": { + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/transact": { + "post": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/transact" + } + }, + "operationId": "TransactV1", + "summary": "Executes a transaction on a Iroha V2 ledger (by sending some instructions)", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactRequestV1" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactResponseV1" + } + } + } + }, + "500": { + "description": "Internal Server Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionResponseV1" + } + } + } + } + } + } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/query": { + "post": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/query" + } + }, + "operationId": "QueryV1", + "summary": "Executes a query on a Iroha V2 ledger and returns it's results.", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryRequestV1" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryResponseV1" + } + } + } + }, + "500": { + "description": "Internal Server Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionResponseV1" + } + } + } + } + } + } + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/api-client/iroha2-api-client.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/api-client/iroha2-api-client.ts new file mode 100644 index 00000000000..eab260f1669 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/api-client/iroha2-api-client.ts @@ -0,0 +1,126 @@ +/** + * Extension of ApiClient genereted from OpenAPI. + * Allows operations not handled by OpenAPI (i.e. socketIO or grpc endpoints). + */ + +import { Observable, ReplaySubject } from "rxjs"; +import { finalize } from "rxjs/operators"; +import { io } from "socket.io-client"; +import { Logger, Checks } from "@hyperledger/cactus-common"; +import { LogLevelDesc, LoggerProvider } from "@hyperledger/cactus-common"; +import { Constants, ISocketApiClient } from "@hyperledger/cactus-core-api"; +import { + DefaultApi, + WatchBlocksV1, + WatchBlocksOptionsV1, + WatchBlocksResponseV1, +} from "../generated/openapi/typescript-axios"; +import { Configuration } from "../generated/openapi/typescript-axios/configuration"; + +/** + * Configuration for Iroha2ApiClient + */ +export class Iroha2ApiClientOptions extends Configuration { + readonly logLevel?: LogLevelDesc; + readonly wsApiHost?: string; + readonly wsApiPath?: string; +} + +/** + * Extended ApiClient that can be used to communicate with Iroha2 connector. + */ +export class Iroha2ApiClient + extends DefaultApi + implements ISocketApiClient { + public readonly className = "Iroha2ApiClient"; + + private readonly log: Logger; + private readonly wsApiHost: string; + private readonly wsApiPath: string; + + /** + * Registry of started monitoring sessions. + */ + private monitorSubjects = new Map< + string, + ReplaySubject + >(); + + constructor(public readonly options: Iroha2ApiClientOptions) { + super(options); + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + + this.wsApiHost = options.wsApiHost || options.basePath || location.host; + this.wsApiPath = options.wsApiPath || Constants.SocketIoConnectionPathV1; + this.log.debug(`Created ${this.className} OK.`); + this.log.debug(`wsApiHost=${this.wsApiHost}`); + this.log.debug(`wsApiPath=${this.wsApiPath}`); + this.log.debug(`basePath=${this.options.basePath}`); + } + + /** + * Watch for new blocks on Iroha2 ledger. + * + * @param monitorOptions Monitoring configuration. + * + * @returns Observable that will receive new blocks once they appear. + */ + public watchBlocksV1( + monitorOptions: WatchBlocksOptionsV1, + ): Observable { + const socket = io(this.wsApiHost, { path: this.wsApiPath }); + const subject = new ReplaySubject(0); + + socket.on(WatchBlocksV1.Next, (data: WatchBlocksResponseV1) => { + this.log.debug("Received WatchBlocksV1.Next"); + subject.next(data); + }); + + socket.on(WatchBlocksV1.Error, (ex: string) => { + this.log.error("Received WatchBlocksV1.Error:", ex); + subject.error(ex); + }); + + socket.on(WatchBlocksV1.Complete, () => { + this.log.debug("Received WatchBlocksV1.Complete"); + subject.complete(); + }); + + socket.on("connect", () => { + this.log.info( + `Connected client '${socket.id}', sending WatchBlocksV1.Subscribe...`, + ); + this.monitorSubjects.set(socket.id, subject); + socket.emit(WatchBlocksV1.Subscribe, monitorOptions); + }); + + socket.connect(); + + return subject.pipe( + finalize(() => { + this.log.info( + `FINALIZE client ${socket.id} - unsubscribing from the stream...`, + ); + socket.emit(WatchBlocksV1.Unsubscribe); + socket.disconnect(); + this.monitorSubjects.delete(socket.id); + }), + ); + } + + /** + * Stop all ongoing monitors, terminate connections. + * + * @note Might take few seconds to clean up all the connections. + */ + public close(): void { + this.log.debug("Close all running monitors."); + this.monitorSubjects.forEach((subject) => subject.complete()); + this.monitorSubjects.clear(); + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/client.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/client.ts new file mode 100644 index 00000000000..bb4afd415c7 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/client.ts @@ -0,0 +1,621 @@ +/** + * Cactus wrapper around IrohaV2 Client and some related functions. + */ + +import { crypto } from "@iroha2/crypto-target-node"; +import { + Client, + Signer, + Torii, + setCrypto, + CreateToriiProps, +} from "@iroha2/client"; +import { + AssetDefinitionId, + AssetValueType, + DomainId, + EvaluatesToRegistrableBox, + Executable, + Expression, + IdentifiableBox, + Instruction, + MapNameValue, + Metadata, + Mintable, + Name as IrohaName, + Value as IrohaValue, + NewAssetDefinition, + NewDomain, + OptionIpfsPath, + RegisterBox, + VecInstruction, + Asset, + MintBox, + EvaluatesToValue, + EvaluatesToIdBox, + IdBox, + BurnBox, + PublicKey, + NewAccount, + VecPublicKey, + TransferBox, +} from "@iroha2/data-model"; +import { Key, KeyPair } from "@iroha2/crypto-core"; +const { adapter: irohaWSAdapter } = require("@iroha2/client/web-socket/node"); + +import { + Checks, + Logger, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; + +import { hexToBytes } from "hada"; +import { fetch as undiciFetch } from "undici"; + +import { CactusIrohaV2QueryClient } from "./query"; +import { + createAccountId, + createAssetId, + createAssetValue, + createIrohaValue, +} from "./data-factories"; + +setCrypto(crypto); + +/** + * Generates key pair compatible with IrohaV2 SDK client. + * + * @warning Returned `KeyPair` must be freed by the caller! (use `.free()` method) + * @param publicKeyMultihash public key in multihash format. + * @param privateKeyJson private key payload and digest function. + * @returns IrohaV2 SDK `KeyPair` + */ +export function generateIrohaV2KeyPair( + publicKeyMultihash: string, + privateKeyJson: Key, +): KeyPair { + const freeableKeys: { free(): void }[] = []; + + try { + const multihashBytes = Uint8Array.from(hexToBytes(publicKeyMultihash)); + + const multihash = crypto.createMultihashFromBytes(multihashBytes); + freeableKeys.push(multihash); + const publicKey = crypto.createPublicKeyFromMultihash(multihash); + freeableKeys.push(publicKey); + const privateKey = crypto.createPrivateKeyFromJsKey(privateKeyJson); + freeableKeys.push(privateKey); + + const keyPair = crypto.createKeyPairFromKeys(publicKey, privateKey); + + return keyPair; + } finally { + freeableKeys.forEach((x) => x.free()); + } +} + +/** + * Single instruction internal representation. + */ +interface NamedIrohaV2Instruction { + name: string; + instruction: Instruction; +} + +/** + * Cactus wrapper around Iroha V2 SDK Client. Should not be used outside of this connector. + * - Provides convenient functions to transact / query the ledger. + * - Each transaction method adds the instruction to the transaction list that is executed together during `send()` call. + * - Use `query` member to access query interface (that doesn't affect client transaction list) + * - Each method returns `this` so invocations can be chained. + */ +export class CactusIrohaV2Client { + private readonly log: Logger; + private readonly transactions: Array = []; + + /** + * Upstream IrohaV2 SDK client used by this wrapper. + */ + public readonly irohaClient: Client; + + /** + * Separate interface for making the IrohaV2 queries. + */ + public readonly query: CactusIrohaV2QueryClient; + + constructor( + public readonly toriiOptions: Omit, + public readonly signerOptions: ConstructorParameters, + private readonly logLevel: LogLevelDesc = "info", + ) { + Checks.truthy(toriiOptions.apiURL, "toriiOptions apiURL"); + Checks.truthy(toriiOptions.telemetryURL, "toriiOptions telemetryURL"); + Checks.truthy(signerOptions[0], "signerOptions account"); + Checks.truthy(signerOptions[1], "signerOptions keyPair"); + + const torii = new Torii({ + ...toriiOptions, + ws: irohaWSAdapter, + fetch: undiciFetch as any, + }); + + const signer = new Signer(...signerOptions); + + this.irohaClient = new Client({ torii, signer }); + + const label = this.constructor.name; + this.log = LoggerProvider.getOrCreate({ level: this.logLevel, label }); + + this.log.debug(`${label} created`); + + this.query = new CactusIrohaV2QueryClient(this.irohaClient, this.log); + } + + /** + * Add instruction to register a new domain. + * + * @param domainName + * @returns this + */ + public registerDomain(domainName: IrohaName): this { + Checks.truthy(domainName, "registerDomain arg domainName"); + + const registerBox = RegisterBox({ + object: EvaluatesToRegistrableBox({ + expression: Expression( + "Raw", + IrohaValue( + "Identifiable", + IdentifiableBox( + "NewDomain", + NewDomain({ + id: DomainId({ + name: domainName, + }), + metadata: Metadata({ map: MapNameValue(new Map()) }), + logo: OptionIpfsPath("None"), + }), + ), + ), + ), + }), + }); + + const description = `RegisterDomain '${domainName}'`; + this.transactions.push({ + name: description, + instruction: Instruction("Register", registerBox), + }); + this.log.debug(`Added ${description} to transactions`); + + return this; + } + + /** + * Add instruction to register a new asset definition. + * Each asset must be define before it's created or mined. + * + * @param assetName + * @param domainName + * @param valueType Type of stored asset value (e.g. `Quantity`, `Fixed`) + * @param mintable How asset can be minted (e.g. "Infinitely", "Not") + * @param metadata + * @returns this + */ + public registerAssetDefinition( + assetName: IrohaName, + domainName: IrohaName, + valueType: "Fixed" | "Quantity" | "BigQuantity" | "Store", + mintable: "Infinitely" | "Once" | "Not", + metadata: Map = new Map(), + ): this { + Checks.truthy(assetName, "registerAsset arg assetName"); + Checks.truthy(domainName, "registerAsset arg domainName"); + Checks.truthy(valueType, "registerAsset arg valueType"); + Checks.truthy(mintable, "registerAsset arg mintable"); + + const assetDefinition = NewAssetDefinition({ + id: AssetDefinitionId({ + name: assetName, + domain_id: DomainId({ name: domainName }), + }), + value_type: AssetValueType(valueType), + metadata: Metadata({ + map: MapNameValue(metadata), + }), + mintable: Mintable(mintable), + }); + + const registerBox = RegisterBox({ + object: EvaluatesToRegistrableBox({ + expression: Expression( + "Raw", + IrohaValue( + "Identifiable", + IdentifiableBox("NewAssetDefinition", assetDefinition), + ), + ), + }), + }); + + const description = `RegisterAssetDefinition '${assetName}#${domainName}', type: ${valueType}, mintable: ${mintable}`; + this.transactions.push({ + name: description, + instruction: Instruction("Register", registerBox), + }); + this.log.debug(`Added ${description} to transactions`); + + return this; + } + + /** + * Add instruction to register an asset that has been previously defined. + * + * @param assetName + * @param domainName + * @param accountName Asset owner name + * @param accountDomainName Asset owner domain name + * @param value Asset value must match `AssetValueType` from asset definition. + * @returns this + */ + public registerAsset( + assetName: IrohaName, + domainName: IrohaName, + accountName: IrohaName, + accountDomainName: IrohaName, + value: Parameters[0], + ): this { + Checks.truthy(assetName, "registerAsset arg assetName"); + Checks.truthy(domainName, "registerAsset arg domainName"); + Checks.truthy(accountName, "registerAsset arg accountName"); + Checks.truthy(accountDomainName, "registerAsset arg accountDomainName"); + + const assetDefinition = Asset({ + id: createAssetId(assetName, domainName, accountName, accountDomainName), + value: createAssetValue(value), + }); + + const registerBox = RegisterBox({ + object: EvaluatesToRegistrableBox({ + expression: Expression( + "Raw", + IrohaValue("Identifiable", IdentifiableBox("Asset", assetDefinition)), + ), + }), + }); + + const description = `RegisterAsset '${assetName}#${domainName}', value: ${value}`; + this.transactions.push({ + name: description, + instruction: Instruction("Register", registerBox), + }); + this.log.debug(`Added ${description} to transactions`); + + return this; + } + + /** + * Add instruction to mint specified amount of an asset. + * + * @param assetName + * @param domainName + * @param accountName Asset owner name + * @param accountDomainName Asset owner domain name + * @param value Asset value must match `AssetValueType` from asset definition. + * @returns this + */ + public mintAsset( + assetName: IrohaName, + domainName: IrohaName, + accountName: IrohaName, + accountDomainName: IrohaName, + value: Parameters[0], + ): this { + Checks.truthy(assetName, "mintAsset arg assetName"); + Checks.truthy(domainName, "mintAsset arg domainName"); + Checks.truthy(accountName, "mintAsset arg accountName"); + Checks.truthy(accountDomainName, "mintAsset arg accountDomainName"); + Checks.truthy(value, "mintAsset arg value"); + + const mintBox = MintBox({ + object: EvaluatesToValue({ + expression: Expression("Raw", createIrohaValue(value)), + }), + destination_id: EvaluatesToIdBox({ + expression: Expression( + "Raw", + IrohaValue( + "Id", + IdBox( + "AssetId", + createAssetId( + assetName, + domainName, + accountName, + accountDomainName, + ), + ), + ), + ), + }), + }); + + const description = `MintAsset '${assetName}#${domainName}', value: ${value}`; + this.transactions.push({ + name: description, + instruction: Instruction("Mint", mintBox), + }); + this.log.debug(`Added ${description} to transactions`); + + return this; + } + + /** + * Add instruction to burn specified amount of an asset. + * + * @param assetName + * @param domainName + * @param accountName Asset owner name + * @param accountDomainName Asset owner domain name + * @param value Asset value to burn must match `AssetValueType` from asset definition. + * @returns this + */ + public burnAsset( + assetName: IrohaName, + domainName: IrohaName, + accountName: IrohaName, + accountDomainName: IrohaName, + value: number | bigint | string | Metadata, + ): this { + Checks.truthy(assetName, "burnAsset arg assetName"); + Checks.truthy(domainName, "burnAsset arg domainName"); + Checks.truthy(accountName, "burnAsset arg accountName"); + Checks.truthy(accountDomainName, "burnAsset arg accountDomainName"); + Checks.truthy(value, "burnAsset arg value"); + + const burnBox = BurnBox({ + object: EvaluatesToValue({ + expression: Expression("Raw", createIrohaValue(value)), + }), + destination_id: EvaluatesToIdBox({ + expression: Expression( + "Raw", + IrohaValue( + "Id", + IdBox( + "AssetId", + createAssetId( + assetName, + domainName, + accountName, + accountDomainName, + ), + ), + ), + ), + }), + }); + + const description = `BurnAsset '${assetName}#${domainName}', value: ${value}`; + this.transactions.push({ + name: description, + instruction: Instruction("Burn", burnBox), + }); + this.log.debug(`Added ${description} to transactions`); + + return this; + } + + /** + * Add instruction to transfer asset between two accounts. + * + * @param assetName + * @param assetDomainName + * @param sourceAccountName Origin account name. + * @param sourceAccountDomain Origin account domain name. + * @param targetAccountName Target account name. + * @param targetAccountDomain Target account domain name. + * @param valueToTransfer Asset value to transfer must match `AssetValueType` from asset definition. + * @returns this + */ + public transferAsset( + assetName: IrohaName, + assetDomainName: IrohaName, + sourceAccountName: IrohaName, + sourceAccountDomain: IrohaName, + targetAccountName: IrohaName, + targetAccountDomain: IrohaName, + valueToTransfer: number | bigint | string | Metadata, + ): this { + Checks.truthy(assetName, "transferAsset arg assetName"); + Checks.truthy(assetDomainName, "transferAsset arg assetDomainName"); + Checks.truthy(sourceAccountName, "transferAsset arg sourceAccountName"); + Checks.truthy(sourceAccountDomain, "transferAsset arg sourceAccountDomain"); + Checks.truthy(targetAccountName, "transferAsset arg targetAccountName"); + Checks.truthy(targetAccountDomain, "transferAsset arg targetAccountDomain"); + Checks.truthy(valueToTransfer, "transferAsset arg valueToTransfer"); + + const transferBox = TransferBox({ + source_id: EvaluatesToIdBox({ + expression: Expression( + "Raw", + IrohaValue( + "Id", + IdBox( + "AssetId", + createAssetId( + assetName, + assetDomainName, + sourceAccountName, + sourceAccountDomain, + ), + ), + ), + ), + }), + object: EvaluatesToValue({ + expression: Expression("Raw", createIrohaValue(valueToTransfer)), + }), + destination_id: EvaluatesToIdBox({ + expression: Expression( + "Raw", + IrohaValue( + "Id", + IdBox( + "AssetId", + createAssetId( + assetName, + assetDomainName, + targetAccountName, + targetAccountDomain, + ), + ), + ), + ), + }), + }); + + const description = `TransferAsset '${assetName}#${assetDomainName}',\ + from: ${sourceAccountName}@${sourceAccountDomain}\ + to ${targetAccountName}@${targetAccountDomain}`; + + this.transactions.push({ + name: description, + instruction: Instruction("Transfer", transferBox), + }); + this.log.debug(`Added ${description} to transactions`); + + return this; + } + + /** + * Add instruction to register new account on the ledger. + * + * @param accountName + * @param domainName + * @param publicKeyPayload Public key, either HEX encoded string or raw `Uint8Array` bytes. + * @param publicKeyDigestFunction + * @param metadata + * @returns this + */ + public registerAccount( + accountName: IrohaName, + domainName: IrohaName, + publicKeyPayload: string | Uint8Array, + publicKeyDigestFunction = "ed25519", + metadata: Map = new Map(), + ): this { + Checks.truthy(accountName, "registerAccount arg accountName"); + Checks.truthy(domainName, "registerAccount arg domainName"); + Checks.truthy(publicKeyPayload, "registerAccount arg publicKeyPayload"); + Checks.truthy( + publicKeyDigestFunction, + "registerAccount arg publicKeyDigestFunction", + ); + + let publicKeyBytes: Uint8Array; + if (typeof publicKeyPayload === "string") { + publicKeyBytes = Uint8Array.from(hexToBytes(publicKeyPayload)); + } else { + publicKeyBytes = publicKeyPayload; + } + + const publicKey = PublicKey({ + payload: publicKeyBytes, + digest_function: publicKeyDigestFunction, + }); + + const registerBox = RegisterBox({ + object: EvaluatesToRegistrableBox({ + expression: Expression( + "Raw", + IrohaValue( + "Identifiable", + IdentifiableBox( + "NewAccount", + NewAccount({ + id: createAccountId(accountName, domainName), + signatories: VecPublicKey([publicKey]), + metadata: Metadata({ map: MapNameValue(metadata) }), + }), + ), + ), + ), + }), + }); + + const description = `RegisterAccount '${accountName}@${domainName}'`; + this.transactions.push({ + name: description, + instruction: Instruction("Register", registerBox), + }); + this.log.debug(`Added ${description} to transactions`); + + return this; + } + + /** + * Clear all the instructions stored in current transaction. + * + * @returns this + */ + public clear(): this { + this.transactions.length = 0; + return this; + } + + /** + * Get summary report of all instructions stored in a current transaction. + * + * @returns printable string report + */ + public getTransactionSummary(): string { + const header = `Transaction Summary (total: ${this.transactions.length}):\n`; + const instructions = this.transactions.map( + (instruction, index) => + ` - Instruction #${index}: ` + instruction.name + "\n", + ); + + return header.concat(...instructions); + } + + /** + * Send all the stored instructions as single Iroha transaction. + * + * @returns this + */ + public async send(): Promise { + if (this.transactions.length === 0) { + this.log.warn("send() ignored - no instructions to be sent!"); + return this; + } + + const irohaInstructions = this.transactions.map( + (entry) => entry.instruction, + ); + this.log.info( + `Send transaction with ${irohaInstructions.length} instructions to Iroha ledger`, + ); + this.log.debug(this.getTransactionSummary()); + + await this.irohaClient.submitExecutable( + Executable("Instructions", VecInstruction(irohaInstructions)), + ); + + this.clear(); + + return this; + } + + /** + * Free all allocated resources. + * Should be called before the shutdown. + */ + public free(): void { + this.log.debug("Free CactusIrohaV2Client key pair"); + // TODO - Investigate if signer keypair not leaking now + //this.irohaClient.keyPair?.free(); + this.clear(); + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/data-factories.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/data-factories.ts new file mode 100644 index 00000000000..c2e29161ea6 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/data-factories.ts @@ -0,0 +1,121 @@ +/** + * Helper factory functions to simplify construction of complex input values used by the upstream Iroha V2 SDK. + */ + +import { + AssetDefinitionId, + DomainId, + Metadata, + Name as IrohaName, + Value as IrohaValue, + AssetId, + AccountId, + AssetValue, +} from "@iroha2/data-model"; + +/** + * JS types that can be converted to IrohaV2 `AssetValue` + */ +export type AssetValueInput = number | bigint | string | Metadata; + +/** + * JS types that can be converted to IrohaV2 `IrohaValue` + */ +export type IrohaValueInput = number | bigint | string | Metadata; + +/** + * Convert JS value into matching `AssetValue` + * + * @param value + * @returns AssetValue + */ +export function createAssetValue(value: AssetValueInput): AssetValue { + switch (typeof value) { + case "number": + return AssetValue("Quantity", value); + case "bigint": + return AssetValue("BigQuantity", value); + case "string": + return AssetValue("Fixed", value); + case "object": + return AssetValue("Store", value); + default: + throw new Error(`Unknown AssetValue: ${value}, type: ${typeof value}`); + } +} + +/** + * Convert JS value into matching `IrohaValue` + * + * @param value + * @returns IrohaValue + */ +export function createIrohaValue(value: IrohaValueInput): IrohaValue { + switch (typeof value) { + case "number": + return IrohaValue("U32", value); + case "bigint": + return IrohaValue("U128", value); + case "string": + return IrohaValue("Fixed", value); + case "object": + return IrohaValue("LimitedMetadata", value); + default: + throw new Error(`Unknown IrohaValue: ${value}, type: ${typeof value}`); + } +} + +/** + * Create `AccountId` from it's name and domain. + * + * @param accountName + * @param domainName + * @returns AccountId + */ +export function createAccountId( + accountName: IrohaName, + domainName: IrohaName, +): AccountId { + return AccountId({ + name: accountName, + domain_id: DomainId({ + name: domainName, + }), + }); +} + +/** + * Create `AssetDefinitionId` from it's name and domain. + * + * @param assetName + * @param domainName + * @returns AssetDefinitionId + */ +export function createAssetDefinitionId( + assetName: IrohaName, + domainName: IrohaName, +): AccountId { + return AssetDefinitionId({ + name: assetName, + domain_id: DomainId({ name: domainName }), + }); +} + +/** + * Create `AssetId` from it's name and domain and account information. + * + * @param assetName + * @param domainName + * @returns AssetDefinitionId + */ +export function createAssetId( + assetName: IrohaName, + domainName: IrohaName, + accountName: IrohaName, + accountDomainName = domainName, +): AssetId { + return AssetId({ + definition_id: createAssetDefinitionId(assetName, domainName), + account_id: createAccountId(accountName, accountDomainName), + }); +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/query.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/query.ts new file mode 100644 index 00000000000..11a3b92e626 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/query.ts @@ -0,0 +1,450 @@ +/** + * Cactus wrapper around IrohaV2 Client query utilities. + * Intended to be used through `CactusIrohaV2Client` interface but can be instantiated separately if needed. + */ + +import { Client, ToriiQueryResult } from "@iroha2/client"; +import { + DomainId, + Expression, + QueryBox, + Value, + FindDomainById, + EvaluatesToDomainId, + IdBox, + Name as IrohaName, + FindAssetById, + EvaluatesToAssetId, + FindAssetDefinitionById, + EvaluatesToAssetDefinitionId, + FindAccountById, + EvaluatesToAccountId, + FindTransactionByHash, + EvaluatesToHash, + Domain, + VecValue, + AssetDefinition, + Asset, + Account, + TransactionValue, + Peer, +} from "@iroha2/data-model"; + +import { Checks, Logger } from "@hyperledger/cactus-common"; + +import safeStringify from "fast-safe-stringify"; +import { hexToBytes } from "hada"; +import { + createAccountId, + createAssetDefinitionId, + createAssetId, +} from "./data-factories"; + +/** + * Cactus wrapper around IrohaV2 Client query utilities. + * Intended to be used through `CactusIrohaV2Client` interface but can be instantiated separately if needed. + * + * @todo Implement pagination once it's supported by the upstream iroha-javascript SDK. + */ +export class CactusIrohaV2QueryClient { + constructor( + private readonly irohaClient: Client, + private readonly log: Logger, + ) { + this.log.debug("CactusIrohaV2QueryClient created."); + } + + /** + * Helper function to match vector response from Iroha and handle possible errors. + * + * @param result Query result object + * @param queryName Query name for diagnostics. + * @returns Vector result + */ + private matchVectorResult( + result: ToriiQueryResult, + queryName: string, + ): VecValue { + return result.match({ + Ok: (res) => res.result.as("Vec"), + Err: (error) => { + throw new Error(`${queryName} query error: ${safeStringify(error)}`); + }, + }); + } + + // Domains + + /** + * Query all the domains details in the ledger. + * Can return a lot of data. + * + * @returns domain list + */ + public async findAllDomains(): Promise { + const result = await this.irohaClient.requestWithQueryBox( + QueryBox("FindAllDomains", null), + ); + + const vectorResult = this.matchVectorResult(result, "findAllDomains"); + const domains = vectorResult.map((i) => i.as("Identifiable").as("Domain")); + + this.log.debug("findAllDomains:", domains); + return domains; + } + + /** + * Query single domain by it's name + * + * @param domainName + * @returns Domain data + */ + public async findDomainById(domainName: IrohaName): Promise { + Checks.truthy(domainName, "findDomainById arg domainName"); + + const result = await this.irohaClient.requestWithQueryBox( + QueryBox( + "FindDomainById", + FindDomainById({ + id: EvaluatesToDomainId({ + expression: Expression( + "Raw", + Value( + "Id", + IdBox( + "DomainId", + DomainId({ + name: domainName, + }), + ), + ), + ), + }), + }), + ), + ); + + const domain = result.match({ + Ok: (res) => res.result.as("Identifiable").as("Domain"), + Err: (error) => { + throw new Error( + `findDomainById query error: ${safeStringify(error.toJSON())}`, + ); + }, + }); + + this.log.debug("findDomainById:", domain); + return domain; + } + + // Assets + + /** + * Query single asset definition using it's name and domain. + * + * @param name + * @param domainName + * @returns Asset definition + */ + public async findAssetDefinitionById( + name: IrohaName, + domainName: IrohaName, + ): Promise { + Checks.truthy(name, "findAssetDefinitionById arg name"); + Checks.truthy(domainName, "findAssetDefinitionById arg domainName"); + + const result = await this.irohaClient.requestWithQueryBox( + QueryBox( + "FindAssetDefinitionById", + FindAssetDefinitionById({ + id: EvaluatesToAssetDefinitionId({ + expression: Expression( + "Raw", + Value( + "Id", + IdBox( + "AssetDefinitionId", + createAssetDefinitionId(name, domainName), + ), + ), + ), + }), + }), + ), + ); + + const assetDef = result.match({ + Ok: (res) => res.result.as("Identifiable").as("AssetDefinition"), + Err: (error) => { + throw new Error( + `findAssetDefinitionById query error: ${safeStringify(error)}`, + ); + }, + }); + + this.log.debug("findAssetDefinitionById:", assetDef); + return assetDef; + } + + /** + * Query all defined asset definitions. + * Can return a lot of data. + * + * @returns List of asset definitions. + */ + public async findAllAssetsDefinitions(): Promise { + const result = await this.irohaClient.requestWithQueryBox( + QueryBox("FindAllAssetsDefinitions", null), + ); + + const vectorResult = this.matchVectorResult( + result, + "findAllAssetsDefinitions", + ); + const assetDefs = vectorResult.map((d) => + d.as("Identifiable").as("AssetDefinition"), + ); + + this.log.debug("findAllAssetsDefinitions:", assetDefs); + return assetDefs; + } + + /** + * Query single asset by it's name, domain and account definition. + * + * @param assetName + * @param assetDomainName + * @param accountName Owner account name + * @param accountDomainName Owner account domain name + * @returns Asset + */ + public async findAssetById( + assetName: IrohaName, + assetDomainName: IrohaName, + accountName: IrohaName, + accountDomainName: IrohaName, + ): Promise { + Checks.truthy(assetName, "findAssetById arg assetName"); + Checks.truthy(assetDomainName, "findAssetById arg assetDomainName"); + Checks.truthy(accountName, "findAssetById arg accountName"); + Checks.truthy(accountDomainName, "findAssetById arg accountDomainName"); + + const result = await this.irohaClient.requestWithQueryBox( + QueryBox( + "FindAssetById", + FindAssetById({ + id: EvaluatesToAssetId({ + expression: Expression( + "Raw", + Value( + "Id", + IdBox( + "AssetId", + createAssetId( + assetName, + assetDomainName, + accountName, + accountDomainName, + ), + ), + ), + ), + }), + }), + ), + ); + + const asset = result.match({ + Ok: (res) => res.result.as("Identifiable").as("Asset"), + Err: (error) => { + throw new Error(`findAssetById query error: ${safeStringify(error)}`); + }, + }); + + this.log.debug("findAssetById:", asset); + return asset; + } + + /** + * Query all assets on the ledger. + * Can return a lot of data. + * + * @returns List of assets. + */ + public async findAllAssets(): Promise { + const result = await this.irohaClient.requestWithQueryBox( + QueryBox("FindAllAssets", null), + ); + + const vectorResult = this.matchVectorResult(result, "findAllAssets"); + const assets = vectorResult.map((i) => i.as("Identifiable").as("Asset")); + + this.log.debug("findAllAssets:", assets); + return assets; + } + + // Account + + /** + * Query single account by it's name and domain. + * + * @param name + * @param domainName + * @returns Account + */ + public async findAccountById( + name: IrohaName, + domainName: IrohaName, + ): Promise { + Checks.truthy(name, "findAccountById arg name"); + Checks.truthy(domainName, "findAccountById arg domainName"); + + const result = await this.irohaClient.requestWithQueryBox( + QueryBox( + "FindAccountById", + FindAccountById({ + id: EvaluatesToAccountId({ + expression: Expression( + "Raw", + Value( + "Id", + IdBox("AccountId", createAccountId(name, domainName)), + ), + ), + }), + }), + ), + ); + + const account = result.match({ + Ok: (res) => res.result.as("Identifiable").as("Account"), + Err: (error) => { + throw new Error(`findAccountById query error: ${safeStringify(error)}`); + }, + }); + + this.log.debug("findAccountById:", account); + return account; + } + + /** + * Query all accounts on the ledger. + * Can return a lot of data. + * + * @returns List of accounts. + */ + public async findAllAccounts(): Promise { + const result = await this.irohaClient.requestWithQueryBox( + QueryBox("FindAllAccounts", null), + ); + + const vectorResult = this.matchVectorResult(result, "findAllAccounts"); + const accounts = vectorResult.map((i) => + i.as("Identifiable").as("Account"), + ); + + this.log.debug("findAllAccounts:", accounts); + return accounts; + } + + // Transactions + + /** + * Query all transactions on the ledger. + * Can return a lot of data. + * + * @returns List of transactions. + */ + public async findAllTransactions(): Promise { + const result = await this.irohaClient.requestWithQueryBox( + QueryBox("FindAllTransactions", null), + ); + + const vectorResult = this.matchVectorResult(result, "findAllTransactions"); + const transactions = vectorResult.map((i) => i.as("TransactionValue")); + + this.log.debug("findAllTransactions:", transactions); + return transactions; + } + + /** + * Query single transaction using it's hash. + * + * @param hash Either HEX encoded string or raw `Uint8Array` bytes. + * @returns Transaction + */ + public async findTransactionByHash( + hash: string | Uint8Array, + ): Promise { + Checks.truthy(hash, "findTransactionByHash arg hash"); + + this.log.debug("findTransactionByHash - search for", hash); + let hashBytes: Uint8Array; + if (typeof hash === "string") { + hashBytes = Uint8Array.from(hexToBytes(hash)); + } else { + hashBytes = hash; + } + + const result = await this.irohaClient.requestWithQueryBox( + QueryBox( + "FindTransactionByHash", + FindTransactionByHash({ + hash: EvaluatesToHash({ + expression: Expression("Raw", Value("Hash", hashBytes)), + }), + }), + ), + ); + + const transaction = result.match({ + Ok: (res) => res.result.as("TransactionValue"), + Err: (error) => { + throw new Error( + `findTransactionByHash query error: ${safeStringify(error)}`, + ); + }, + }); + + this.log.debug("findTransactionByHash:", transaction); + return transaction; + } + + // Misc + + /** + * Query all peers on the ledger. + * Can return a lot of data. + * + * @returns List of peers. + */ + public async findAllPeers(): Promise { + const result = await this.irohaClient.requestWithQueryBox( + QueryBox("FindAllPeers", null), + ); + + const vectorResult = this.matchVectorResult(result, "findAllPeers"); + const peers = vectorResult.map((i) => i.as("Identifiable").as("Peer")); + + this.log.debug("findAllPeers:", peers); + return peers; + } + + /** + * Query all blocks on the ledger. + * Can return a lot of data. + * + * @returns List of blocks. + */ + public async findAllBlocks(): Promise { + const result = await this.irohaClient.requestWithQueryBox( + QueryBox("FindAllBlocks", null), + ); + + const vectorResult = this.matchVectorResult(result, "findAllBlocks"); + const blocks = vectorResult.map((i) => i.as("Block")); + + this.log.debug(`findAllBlocks: Total ${blocks.length}`); + return blocks; + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator-ignore b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator-ignore new file mode 100644 index 00000000000..57cdd7b74b9 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator-ignore @@ -0,0 +1,27 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md + +git_push.sh +.npmignore +.gitignore diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/FILES b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/FILES new file mode 100644 index 00000000000..53250c02696 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/FILES @@ -0,0 +1,5 @@ +api.ts +base.ts +common.ts +configuration.ts +index.ts diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION new file mode 100644 index 00000000000..7cbea073bea --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION @@ -0,0 +1 @@ +5.2.0 \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/api.ts new file mode 100644 index 00000000000..bdb0c274fef --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -0,0 +1,606 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Hyperledger Cactus Plugin - Connector Iroha V2 + * Can perform basic tasks on a Iroha V2 ledger + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import { Configuration } from './configuration'; +import globalAxios, { AxiosPromise, AxiosInstance } from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base'; + +/** + * Iroha V2 block response type. + * @export + * @enum {string} + */ + +export enum BlockTypeV1 { + /** + * Default JSON-encoded string full block data. + */ + Raw = 'raw', + /** + * Encoded format that must be decoded with Iroha SDK on client side before use + */ + Binary = 'binary' +} + +/** + * Error response from the connector. + * @export + * @interface ErrorExceptionResponseV1 + */ +export interface ErrorExceptionResponseV1 { + /** + * Short error description message. + * @type {string} + * @memberof ErrorExceptionResponseV1 + */ + message: string; + /** + * Detailed error information. + * @type {string} + * @memberof ErrorExceptionResponseV1 + */ + error: string; +} +/** + * Iroha V2 account ID. + * @export + * @interface Iroha2AccountId + */ +export interface Iroha2AccountId { + /** + * + * @type {string} + * @memberof Iroha2AccountId + */ + name: string; + /** + * + * @type {string} + * @memberof Iroha2AccountId + */ + domainId: string; +} +/** + * Iroha V2 connection configuration. + * @export + * @interface Iroha2BaseConfig + */ +export interface Iroha2BaseConfig { + /** + * + * @type {Iroha2BaseConfigTorii} + * @memberof Iroha2BaseConfig + */ + torii: Iroha2BaseConfigTorii; + /** + * + * @type {Iroha2AccountId} + * @memberof Iroha2BaseConfig + */ + accountId?: Iroha2AccountId; + /** + * + * @type {Iroha2KeyPair | KeychainReference} + * @memberof Iroha2BaseConfig + */ + signingCredential?: Iroha2KeyPair | KeychainReference; +} +/** + * Iroha V2 peer connection information. + * @export + * @interface Iroha2BaseConfigTorii + */ +export interface Iroha2BaseConfigTorii { + /** + * + * @type {string} + * @memberof Iroha2BaseConfigTorii + */ + apiURL?: string; + /** + * + * @type {string} + * @memberof Iroha2BaseConfigTorii + */ + telemetryURL?: string; +} +/** + * Private/Public key JSON containing payload and digest function. + * @export + * @interface Iroha2KeyJson + */ +export interface Iroha2KeyJson { + /** + * + * @type {string} + * @memberof Iroha2KeyJson + */ + digestFunction: string; + /** + * + * @type {string} + * @memberof Iroha2KeyJson + */ + payload: string; +} +/** + * Pair of Iroha account private and public keys. + * @export + * @interface Iroha2KeyPair + */ +export interface Iroha2KeyPair { + /** + * + * @type {Iroha2KeyJson} + * @memberof Iroha2KeyPair + */ + privateKey: Iroha2KeyJson; + /** + * + * @type {string} + * @memberof Iroha2KeyPair + */ + publicKey: string; +} +/** + * Command names that correspond to Iroha Special Instructions (https://hyperledger.github.io/iroha-2-docs/guide/advanced/isi.html) + * @export + * @enum {string} + */ + +export enum IrohaInstruction { + /** + * Register new domain + */ + RegisterDomain = 'registerDomain', + /** + * Register new asset definition + */ + RegisterAssetDefinition = 'registerAssetDefinition', + /** + * Register new asset + */ + RegisterAsset = 'registerAsset', + /** + * Mint asset value + */ + MintAsset = 'mintAsset', + /** + * Burn asset value + */ + BurnAsset = 'burnAsset', + /** + * Transfer asset between accounts + */ + TransferAsset = 'transferAsset', + /** + * Register new account + */ + RegisterAccount = 'registerAccount' +} + +/** + * Single Iroha V2 instruction to be executed request. + * @export + * @interface IrohaInstructionRequestV1 + */ +export interface IrohaInstructionRequestV1 { + /** + * Iroha V2 instruction name. + * @type {IrohaInstruction} + * @memberof IrohaInstructionRequestV1 + */ + name: IrohaInstruction; + /** + * The list of arguments to pass with specified instruction. + * @type {Array} + * @memberof IrohaInstructionRequestV1 + */ + params: Array; +} +/** + * Command names that correspond to Iroha queries (https://hyperledger.github.io/iroha-2-docs/guide/advanced/queries.html) + * @export + * @enum {string} + */ + +export enum IrohaQuery { + /** + * Get list of all registered domains + */ + FindAllDomains = 'findAllDomains', + /** + * Get domain with specified ID + */ + FindDomainById = 'findDomainById', + /** + * Get asset definition with specified ID + */ + FindAssetDefinitionById = 'findAssetDefinitionById', + /** + * Get list of all registered asset definition + */ + FindAllAssetsDefinitions = 'findAllAssetsDefinitions', + /** + * Get asset with specified ID + */ + FindAssetById = 'findAssetById', + /** + * Get list of all registered assets + */ + FindAllAssets = 'findAllAssets', + /** + * Get list of all ledger peers + */ + FindAllPeers = 'findAllPeers', + /** + * Get list of all ledger blocks + */ + FindAllBlocks = 'findAllBlocks', + /** + * Get account with specified ID + */ + FindAccountById = 'findAccountById', + /** + * Get list of all registered accounts + */ + FindAllAccounts = 'findAllAccounts', + /** + * Get list of all transactions + */ + FindAllTransactions = 'findAllTransactions', + /** + * Get transaction with specified hash + */ + FindTransactionByHash = 'findTransactionByHash' +} + +/** + * Reference to entry stored in Cactus keychain plugin. + * @export + * @interface KeychainReference + */ +export interface KeychainReference { + /** + * Keychain plugin ID. + * @type {string} + * @memberof KeychainReference + */ + keychainId: string; + /** + * Key reference name. + * @type {string} + * @memberof KeychainReference + */ + keychainRef: string; +} +/** + * Request to query endpoint. + * @export + * @interface QueryRequestV1 + */ +export interface QueryRequestV1 { + /** + * Name of the query to be executed. + * @type {IrohaQuery} + * @memberof QueryRequestV1 + */ + queryName: IrohaQuery; + /** + * + * @type {Iroha2BaseConfig} + * @memberof QueryRequestV1 + */ + baseConfig?: Iroha2BaseConfig; + /** + * The list of arguments to pass with the query. + * @type {Array} + * @memberof QueryRequestV1 + */ + params?: Array; +} +/** + * Response with query results. + * @export + * @interface QueryResponseV1 + */ +export interface QueryResponseV1 { + /** + * Query response data that varies between different queries. + * @type {any} + * @memberof QueryResponseV1 + */ + response: any; +} +/** + * Request to transact endpoint, can be passed one or multiple instructions to be executed. + * @export + * @interface TransactRequestV1 + */ +export interface TransactRequestV1 { + /** + * + * @type {IrohaInstructionRequestV1 | Array} + * @memberof TransactRequestV1 + */ + instruction: IrohaInstructionRequestV1 | Array; + /** + * + * @type {Iroha2BaseConfig} + * @memberof TransactRequestV1 + */ + baseConfig?: Iroha2BaseConfig; +} +/** + * Response from transaction endpoint with operation status. + * @export + * @interface TransactResponseV1 + */ +export interface TransactResponseV1 { + /** + * + * @type {string} + * @memberof TransactResponseV1 + */ + status: string; +} +/** + * Binary encoded response of block data. + * @export + * @interface WatchBlocksBinaryResponseV1 + */ +export interface WatchBlocksBinaryResponseV1 { + /** + * + * @type {any} + * @memberof WatchBlocksBinaryResponseV1 + */ + binaryBlock: any; +} +/** + * Options passed when subscribing to block monitoring. + * @export + * @interface WatchBlocksOptionsV1 + */ +export interface WatchBlocksOptionsV1 { + /** + * + * @type {BlockTypeV1} + * @memberof WatchBlocksOptionsV1 + */ + type?: BlockTypeV1; + /** + * Number of block to start monitoring from. + * @type {string} + * @memberof WatchBlocksOptionsV1 + */ + startBlock?: string; + /** + * + * @type {Iroha2BaseConfig} + * @memberof WatchBlocksOptionsV1 + */ + baseConfig?: Iroha2BaseConfig; +} +/** + * Default JSON-encoded string full block data. + * @export + * @interface WatchBlocksRawResponseV1 + */ +export interface WatchBlocksRawResponseV1 { + /** + * + * @type {string} + * @memberof WatchBlocksRawResponseV1 + */ + blockData: string; +} +/** + * @type WatchBlocksResponseV1 + * @export + */ +export type WatchBlocksResponseV1 = ErrorExceptionResponseV1 | WatchBlocksBinaryResponseV1 | WatchBlocksRawResponseV1; + +/** + * Websocket requests for monitoring new blocks. + * @export + * @enum {string} + */ + +export enum WatchBlocksV1 { + Subscribe = 'org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Subscribe', + Next = 'org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Next', + Unsubscribe = 'org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Unsubscribe', + Error = 'org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Error', + Complete = 'org.hyperledger.cactus.api.async.hliroha2.WatchBlocksV1.Complete' +} + + +/** + * DefaultApi - axios parameter creator + * @export + */ +export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Executes a query on a Iroha V2 ledger and returns it\'s results. + * @param {QueryRequestV1} [queryRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + queryV1: async (queryRequestV1?: QueryRequestV1, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/query`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(queryRequestV1, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Executes a transaction on a Iroha V2 ledger (by sending some instructions) + * @param {TransactRequestV1} [transactRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + transactV1: async (transactRequestV1?: TransactRequestV1, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/transact`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(transactRequestV1, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * DefaultApi - functional programming interface + * @export + */ +export const DefaultApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Executes a query on a Iroha V2 ledger and returns it\'s results. + * @param {QueryRequestV1} [queryRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async queryV1(queryRequestV1?: QueryRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.queryV1(queryRequestV1, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary Executes a transaction on a Iroha V2 ledger (by sending some instructions) + * @param {TransactRequestV1} [transactRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async transactV1(transactRequestV1?: TransactRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.transactV1(transactRequestV1, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * DefaultApi - factory interface + * @export + */ +export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = DefaultApiFp(configuration) + return { + /** + * + * @summary Executes a query on a Iroha V2 ledger and returns it\'s results. + * @param {QueryRequestV1} [queryRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + queryV1(queryRequestV1?: QueryRequestV1, options?: any): AxiosPromise { + return localVarFp.queryV1(queryRequestV1, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Executes a transaction on a Iroha V2 ledger (by sending some instructions) + * @param {TransactRequestV1} [transactRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + transactV1(transactRequestV1?: TransactRequestV1, options?: any): AxiosPromise { + return localVarFp.transactV1(transactRequestV1, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +export class DefaultApi extends BaseAPI { + /** + * + * @summary Executes a query on a Iroha V2 ledger and returns it\'s results. + * @param {QueryRequestV1} [queryRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public queryV1(queryRequestV1?: QueryRequestV1, options?: any) { + return DefaultApiFp(this.configuration).queryV1(queryRequestV1, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Executes a transaction on a Iroha V2 ledger (by sending some instructions) + * @param {TransactRequestV1} [transactRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public transactV1(transactRequestV1?: TransactRequestV1, options?: any) { + return DefaultApiFp(this.configuration).transactV1(transactRequestV1, options).then((request) => request(this.axios, this.basePath)); + } +} + + diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/base.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/base.ts new file mode 100644 index 00000000000..876936a83ef --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/base.ts @@ -0,0 +1,71 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Hyperledger Cactus Plugin - Connector Iroha V2 + * Can perform basic tasks on a Iroha V2 ledger + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import { Configuration } from "./configuration"; +// Some imports not used depending on template conditions +// @ts-ignore +import globalAxios, { AxiosPromise, AxiosInstance } from 'axios'; + +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: any; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath || this.basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/common.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/common.ts new file mode 100644 index 00000000000..ed3f989cee9 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/common.ts @@ -0,0 +1,138 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Hyperledger Cactus Plugin - Connector Iroha V2 + * Can perform basic tasks on a Iroha V2 ledger + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import { Configuration } from "./configuration"; +import { RequiredError, RequestArgs } from "./base"; +import { AxiosInstance } from 'axios'; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + for (const object of objects) { + for (const key in object) { + if (Array.isArray(object[key])) { + searchParams.delete(key); + for (const item of object[key]) { + searchParams.append(key, item); + } + } else { + searchParams.set(key, object[key]); + } + } + } + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/configuration.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/configuration.ts new file mode 100644 index 00000000000..3e67b7e1720 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/configuration.ts @@ -0,0 +1,101 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Hyperledger Cactus Plugin - Connector Iroha V2 + * Can perform basic tasks on a Iroha V2 ledger + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/index.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/index.ts new file mode 100644 index 00000000000..e30d76a7bdb --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Hyperledger Cactus Plugin - Connector Iroha V2 + * Can perform basic tasks on a Iroha V2 ledger + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; + diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/index.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/index.ts new file mode 100755 index 00000000000..87cb558397c --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/index.ts @@ -0,0 +1 @@ +export * from "./public-api"; diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/index.web.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/index.web.ts new file mode 100755 index 00000000000..bdf54028d23 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/index.web.ts @@ -0,0 +1 @@ +export * from "./generated/openapi/typescript-axios/index"; diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-factory-ledger-connector.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-factory-ledger-connector.ts new file mode 100644 index 00000000000..fba8cf8b029 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-factory-ledger-connector.ts @@ -0,0 +1,20 @@ +import { + IPluginFactoryOptions, + PluginFactory, +} from "@hyperledger/cactus-core-api"; +import { + IPluginLedgerConnectorIroha2Options, + PluginLedgerConnectorIroha2, +} from "./plugin-ledger-connector-iroha2"; + +export class PluginFactoryLedgerConnector extends PluginFactory< + PluginLedgerConnectorIroha2, + IPluginLedgerConnectorIroha2Options, + IPluginFactoryOptions +> { + async create( + pluginOptions: IPluginLedgerConnectorIroha2Options, + ): Promise { + return new PluginLedgerConnectorIroha2(pluginOptions); + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-ledger-connector-iroha2.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-ledger-connector-iroha2.ts new file mode 100644 index 00000000000..67e2233485a --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-ledger-connector-iroha2.ts @@ -0,0 +1,629 @@ +/** + * Main IrohaV2 connector plugin class logic. + */ + +import type { Express } from "express"; +import type { + Server as SocketIoServer, + Socket as SocketIoSocket, +} from "socket.io"; + +import OAS from "../json/openapi.json"; + +import { + ConsensusAlgorithmFamily, + IPluginLedgerConnector, + IWebServiceEndpoint, + IPluginWebService, + ICactusPlugin, + ICactusPluginOptions, +} from "@hyperledger/cactus-core-api"; + +import { + consensusHasTransactionFinality, + PluginRegistry, +} from "@hyperledger/cactus-core"; + +import { + Checks, + Logger, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; + +import { + IrohaInstruction, + IrohaQuery, + TransactRequestV1, + TransactResponseV1, + Iroha2BaseConfig, + Iroha2KeyJson, + Iroha2KeyPair, + KeychainReference, + QueryRequestV1, + QueryResponseV1, + IrohaInstructionRequestV1, + WatchBlocksV1, + WatchBlocksOptionsV1, +} from "./generated/openapi/typescript-axios"; + +import { TransactEndpoint } from "./web-services/transact-endpoint"; +import { QueryEndpoint } from "./web-services/query-endpoint"; +import { WatchBlocksV1Endpoint } from "./web-services/watch-blocks-v1-endpoint"; + +import { KeyPair } from "@iroha2/crypto-core"; +import { AccountId, DomainId } from "@iroha2/data-model"; +import { + CactusIrohaV2Client, + generateIrohaV2KeyPair, +} from "./cactus-iroha-sdk-wrapper/client"; +import { CactusIrohaV2QueryClient } from "./cactus-iroha-sdk-wrapper/query"; +import { LengthOf, stringifyBigIntReplacer } from "./utils"; + +/** + * Input options for PluginLedgerConnectorIroha2. + */ +export interface IPluginLedgerConnectorIroha2Options + extends ICactusPluginOptions { + pluginRegistry: PluginRegistry; + logLevel?: LogLevelDesc; + defaultConfig?: Iroha2BaseConfig; +} + +/** + * Iroha V2 connector plugin. + */ +export class PluginLedgerConnectorIroha2 + implements + IPluginLedgerConnector, + ICactusPlugin, + IPluginWebService { + private readonly instanceId: string; + private readonly log: Logger; + private readonly defaultConfig: Iroha2BaseConfig | undefined; + private endpoints: IWebServiceEndpoint[] | undefined; + private runningWatchBlocksMonitors = new Set(); + + public readonly className: string; + + constructor(public readonly options: IPluginLedgerConnectorIroha2Options) { + this.className = this.constructor.name; + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.instanceId, `${fnTag} options.instanceId`); + + const level = this.options.logLevel || "info"; + this.log = LoggerProvider.getOrCreate({ level, label: this.className }); + + this.instanceId = options.instanceId; + + this.defaultConfig = options.defaultConfig; + // Remove proto in case we use merge method vulnerable to proto pollution + if (this.defaultConfig instanceof Object) { + Object.setPrototypeOf(this.defaultConfig, null); + } + } + + /** + * Iroha V2 ledger consensus family + */ + async getConsensusAlgorithmFamily(): Promise { + return ConsensusAlgorithmFamily.Authority; + } + + /** + * Iroha V2 ledger transaction finality + */ + public async hasTransactionFinality(): Promise { + const currentConsensusAlgorithmFamily = await this.getConsensusAlgorithmFamily(); + return consensusHasTransactionFinality(currentConsensusAlgorithmFamily); + } + + /** + * + * @returns Open API JSON specification. + */ + public getOpenApiSpec(): unknown { + return OAS; + } + + /** + * @warning Method not implemented - do not use! + */ + public deployContract(): Promise { + throw new Error("Method not implemented."); + } + + public getInstanceId(): string { + return this.instanceId; + } + + /** + * Callback that should be called during plugin initialization. + * @returns Void + */ + public async onPluginInit(): Promise { + // Nothing to do... + return; + } + + /** + * Callback that must be called during shutdown. + * Will cleanup allocated resources, stop the connections. + */ + public async shutdown(): Promise { + this.log.info(`Shutting down ${this.className}...`); + this.runningWatchBlocksMonitors.forEach((m) => m.close()); + this.runningWatchBlocksMonitors.clear(); + } + + /** + * Register all supported WebSocket endpoints on specific socket connected to the client. + * + * @param socket Connected socket + * @returns `socket` from input arg. + */ + private registerWatchBlocksSocketIOEndpoint( + socket: SocketIoSocket, + ): SocketIoSocket { + this.log.debug("Register WatchBlocks.Subscribe handler."); + socket.on( + WatchBlocksV1.Subscribe, + async (options: WatchBlocksOptionsV1) => { + // Get client + const cactusIrohaClient = await this.getClient(options.baseConfig); + + // Start monitoring + const monitor = new WatchBlocksV1Endpoint({ + socket, + logLevel: this.options.logLevel, + client: cactusIrohaClient.irohaClient, + }); + this.runningWatchBlocksMonitors.add(monitor); + await monitor.subscribe(options); + this.log.debug( + "Running monitors count:", + this.runningWatchBlocksMonitors.size, + ); + + socket.on("disconnect", async () => { + cactusIrohaClient.clear(); + this.runningWatchBlocksMonitors.delete(monitor); + this.log.debug( + "Running monitors count:", + this.runningWatchBlocksMonitors.size, + ); + }); + }, + ); + + return socket; + } + + /** + * Register Rest and WebSocket services on servers supplied in argument. + * Should be called by cactus cmd server. + * + * @param app ExpressJS app object. + * @param wsApi SocketIO server object. + * @returns registered endpoints list. + */ + async registerWebServices( + app: Express, + wsApi: SocketIoServer, + ): Promise { + // Add custom replacer to handle bigint responses correctly + app.set("json replacer", stringifyBigIntReplacer); + + const webServices = await this.getOrCreateWebServices(); + await Promise.all(webServices.map((ws) => ws.registerExpress(app))); + + if (wsApi) { + wsApi.on("connection", (socket: SocketIoSocket) => { + this.log.debug(`New Socket connected. ID=${socket.id}`); + this.registerWatchBlocksSocketIOEndpoint(socket); + }); + } + + return webServices; + } + + /** + * Get list of rest endpoints supported by this connector plugin. + * The list is initialized once and reused on subsequent calls. + * + * @returns List of web service endpoints. + */ + public async getOrCreateWebServices(): Promise { + if (Array.isArray(this.endpoints)) { + return this.endpoints; + } + + const endpoints: IWebServiceEndpoint[] = [ + new TransactEndpoint({ + connector: this, + logLevel: this.options.logLevel, + }), + new QueryEndpoint({ + connector: this, + logLevel: this.options.logLevel, + }), + ]; + + this.endpoints = endpoints; + return endpoints; + } + + public getPackageName(): string { + return `@hyperledger/cactus-plugin-ledger-connector-iroha2`; + } + + /** + * Read entry with `keychainRef` from keychain with id `keychainId`. + * Assume it's stored in JSON-compatible format. + * + * @param keychainId keychain plugin ID. + * @param keychainRef entry key. + * @returns parsed entry value. + */ + private async getFromKeychain(keychainId: string, keychainRef: string) { + const keychain = this.options.pluginRegistry.findOneByKeychainId( + keychainId, + ); + return JSON.parse(await keychain.get(keychainRef)); + } + + /** + * Return Iroha V2 SDK client compatible key pair object. + * + * @param signingCredentials Credentials received from the client in the request. + * @returns Iroha V2 SDK `KeyPair` object. + */ + private async getSigningKeyPair( + signingCredentials: Iroha2KeyPair | KeychainReference, + ): Promise { + Checks.truthy( + signingCredentials, + "getSigningKeyPair() signingCredentials arg", + ); + + let publicKeyString: string; + let privateKeyJson: Iroha2KeyJson; + if ("keychainId" in signingCredentials) { + this.log.debug("getSigningKeyPair() read from keychain plugin"); + const keychainStoredKey = await this.getFromKeychain( + signingCredentials.keychainId, + signingCredentials.keychainRef, + ); + publicKeyString = keychainStoredKey.publicKey; + privateKeyJson = keychainStoredKey.privateKey; + } else { + this.log.debug( + "getSigningKeyPair() read directly from signingCredentials", + ); + publicKeyString = signingCredentials.publicKey; + privateKeyJson = signingCredentials.privateKey; + } + + Checks.truthy(publicKeyString, "getSigningKeyPair raw public key"); + Checks.truthy(privateKeyJson, "getSigningKeyPair raw private key json"); + + return generateIrohaV2KeyPair(publicKeyString, privateKeyJson); + } + + /** + * Create Cactus IrohaV2 client using both defaultConfig (defined during class creation) + * and config specified in arg. + * + * @param baseConfig Iroha V2 base connection configuration. + * @returns `CactusIrohaV2Client` + */ + public async getClient( + baseConfig?: Iroha2BaseConfig, + ): Promise { + if (!baseConfig && !this.defaultConfig) { + throw new Error("getClient() called without valid Iroha config - fail"); + } + + // Merge default config with config passed to this function + const mergedConfig = { ...this.defaultConfig, ...baseConfig }; + + if (!mergedConfig.torii) { + throw new Error("torii is missing in combined configuration"); + } + + // Parse signing key pair + let keyPair: KeyPair | undefined; + if (mergedConfig.signingCredential) { + keyPair = await this.getSigningKeyPair(mergedConfig.signingCredential); + } + + // Parse account ID + let accountId: AccountId | undefined; + if (mergedConfig.accountId) { + accountId = AccountId({ + name: mergedConfig.accountId.name, + domain_id: DomainId({ + name: mergedConfig.accountId.domainId, + }), + }); + } + + // TODO - confirm which args are optional and remove type casts accordingly + return new CactusIrohaV2Client( + { + apiURL: mergedConfig.torii.apiURL as string, + telemetryURL: mergedConfig.torii.telemetryURL as string, + }, + [accountId as AccountId, keyPair as KeyPair], + this.options.logLevel, + ); + } + + /** + * Helper function used to safely check that required number of parameters were supplied in the request. + * + * @param params Parameter list from the request. + * @param expectedCount Expected parameter count + * @param functionName Function that needs specified number of args (used for error logging only) + * + * @returns List of checked parameters (of `expectedCount` length). + */ + private checkArgsCount( + params: unknown[] | undefined, + expectedCount: number, + functionName: string, + ): unknown[] { + if (!params) { + throw new Error( + `Error [${functionName}] - Missing required parameters in request.`, + ); + } + + const requiredParams = params.slice(0, expectedCount); + + if (requiredParams.length < expectedCount) { + throw new Error( + `Error [${functionName}] - No enough parameters. Expected: ${expectedCount}, got: ${requiredParams.length}`, + ); + } + + return requiredParams; + } + + /** + * Validate required parameters and call transact method + * (will add instruction to the list of operations to be executed) + * + * @note `expectedCount` must be equal to number of args required by `transactFunction`, + * otherwise the code will not compile (this is intended safety-check) + * + * @param client `CactusIrohaV2Client` object. + * @param transactFunction Transact function to be executed + * @param params Parameter list from the request. + * @param expectedCount Expected parameter count + */ + private addTransactionWithCheckedParams< + T extends (...args: any[]) => unknown + >( + client: CactusIrohaV2Client, + transactFunction: T, + params: unknown[] | undefined, + expectedCount: LengthOf>, + ): void { + transactFunction.apply( + client, + this.checkArgsCount(params, expectedCount, transactFunction.name), + ); + } + + /** + * Transact endpoint logic. + * + * @param req Request with a list of instructions to be executed from the client. + * @returns Status of the operation. + */ + public async transact(req: TransactRequestV1): Promise { + const client = await this.getClient(req.baseConfig); + + try { + // Convert single instruction scenario to list with one element + // (both single and multiple instructions are supported) + let instructions: IrohaInstructionRequestV1[]; + if (Array.isArray(req.instruction)) { + instructions = req.instruction; + } else { + instructions = [req.instruction]; + } + + // Each command adds transaction to the list that will be sent to Iroha V2 + instructions.forEach((cmd) => { + switch (cmd.name) { + case IrohaInstruction.RegisterDomain: + this.addTransactionWithCheckedParams( + client, + CactusIrohaV2Client.prototype.registerDomain, + cmd.params, + 1, + ); + break; + case IrohaInstruction.RegisterAssetDefinition: + this.addTransactionWithCheckedParams( + client, + CactusIrohaV2Client.prototype.registerAssetDefinition, + cmd.params, + 4, + ); + break; + case IrohaInstruction.RegisterAsset: + this.addTransactionWithCheckedParams( + client, + CactusIrohaV2Client.prototype.registerAsset, + cmd.params, + 5, + ); + break; + case IrohaInstruction.MintAsset: + this.addTransactionWithCheckedParams( + client, + CactusIrohaV2Client.prototype.mintAsset, + cmd.params, + 5, + ); + break; + case IrohaInstruction.BurnAsset: + this.addTransactionWithCheckedParams( + client, + CactusIrohaV2Client.prototype.burnAsset, + cmd.params, + 5, + ); + break; + case IrohaInstruction.TransferAsset: + this.addTransactionWithCheckedParams( + client, + CactusIrohaV2Client.prototype.transferAsset, + cmd.params, + 7, + ); + break; + case IrohaInstruction.RegisterAccount: + this.addTransactionWithCheckedParams( + client, + CactusIrohaV2Client.prototype.registerAccount, + cmd.params, + 4, + ); + break; + default: + const unknownType: never = cmd.name; + throw new Error( + `Unknown IrohaV2 instruction - '${unknownType}'. Check name and connector version.`, + ); + } + }); + + await client.send(); + + return { + status: "OK", + }; + } finally { + client.free(); + } + } + + /** + * Validate required parameters and call the query method. + * Do not use for queries that does not require any parameters. + * + * @note `expectedCount` must be equal to number of args required by `transactFunction`, + * otherwise the code will not compile (this is intended safety-check) + * + * @param client `CactusIrohaV2Client` object. + * @param queryFunction Query function to be executed + * @param params Parameter list from the request. + * @param expectedCount Expected parameter count + * @returns Query result. + */ + private async runQueryWithCheckedParams< + T extends (...args: any[]) => Promise + >( + client: CactusIrohaV2Client, + queryFunction: T, + params: unknown[] | undefined, + expectedCount: LengthOf>, + ): Promise { + return { + response: await queryFunction.apply( + client.query, + this.checkArgsCount(params, expectedCount, queryFunction.name), + ), + }; + } + + /** + * Query endpoint logic. + * + * @param req Request with a query name. + * @returns Response from the query. + */ + public async query(req: QueryRequestV1): Promise { + const client = await this.getClient(req.baseConfig); + + try { + switch (req.queryName) { + case IrohaQuery.FindAllDomains: + return { + response: await client.query.findAllDomains(), + }; + case IrohaQuery.FindDomainById: + return await this.runQueryWithCheckedParams( + client, + CactusIrohaV2QueryClient.prototype.findDomainById, + req.params, + 1, + ); + case IrohaQuery.FindAssetDefinitionById: + return await this.runQueryWithCheckedParams( + client, + CactusIrohaV2QueryClient.prototype.findAssetDefinitionById, + req.params, + 2, + ); + case IrohaQuery.FindAllAssetsDefinitions: + return { + response: await client.query.findAllAssetsDefinitions(), + }; + case IrohaQuery.FindAssetById: + return await this.runQueryWithCheckedParams( + client, + CactusIrohaV2QueryClient.prototype.findAssetById, + req.params, + 4, + ); + case IrohaQuery.FindAllAssets: + return { + response: await client.query.findAllAssets(), + }; + case IrohaQuery.FindAllPeers: + return { + response: await client.query.findAllPeers(), + }; + case IrohaQuery.FindAccountById: + return await this.runQueryWithCheckedParams( + client, + CactusIrohaV2QueryClient.prototype.findAccountById, + req.params, + 2, + ); + case IrohaQuery.FindAllAccounts: + return { + response: await client.query.findAllAccounts(), + }; + case IrohaQuery.FindAllTransactions: + return { + response: await client.query.findAllTransactions(), + }; + case IrohaQuery.FindTransactionByHash: + return await this.runQueryWithCheckedParams( + client, + CactusIrohaV2QueryClient.prototype.findTransactionByHash, + req.params, + 1, + ); + case IrohaQuery.FindAllBlocks: + return { + response: await client.query.findAllBlocks(), + }; + default: + const unknownType: never = req.queryName; + throw new Error( + `Unknown IrohaV2 query - '${unknownType}'. Check name and connector version.`, + ); + } + } finally { + client.free(); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/public-api.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/public-api.ts new file mode 100755 index 00000000000..15dc22d7257 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/public-api.ts @@ -0,0 +1,22 @@ +export { + IPluginLedgerConnectorIroha2Options, + PluginLedgerConnectorIroha2, +} from "./plugin-ledger-connector-iroha2"; + +export { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector"; + +import { IPluginFactoryOptions } from "@hyperledger/cactus-core-api"; +import { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector"; + +export * from "./generated/openapi/typescript-axios/api"; + +export async function createPluginFactory( + pluginFactoryOptions: IPluginFactoryOptions, +): Promise { + return new PluginFactoryLedgerConnector(pluginFactoryOptions); +} + +export { + Iroha2ApiClient, + Iroha2ApiClientOptions, +} from "./api-client/iroha2-api-client"; diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/utils.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/utils.ts new file mode 100644 index 00000000000..82df30b6333 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/utils.ts @@ -0,0 +1,39 @@ +/** + * Small utility functions reused throughout the connector. + */ + +import sanitizeHtml from "sanitize-html"; +import safeStringify from "fast-safe-stringify"; + +/** + * Abstract type that corresponds to length of supplied iterable ('1', '2', etc...) + */ +export type LengthOf> = T["length"]; + +/** + * Return secure string representation of error from the input. + * Handles circular structures and removes HTML.` + * + * @param error Any object to return as an error, preferable `Error` + * @returns Safe string representation of an error. + * + * @todo use one from cactus-common after #2089 is merged. + */ +export function safeStringifyException(error: unknown): string { + if (error instanceof Error) { + return sanitizeHtml(error.stack || error.message); + } + + return sanitizeHtml(safeStringify(error)); +} + +/** + * `JSON.stringify` replacer function to handle BigInt. + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json + */ +export function stringifyBigIntReplacer(key: string, value: bigint): string { + if (typeof value === "bigint") { + return value.toString(); + } + return value; +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/query-endpoint.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/query-endpoint.ts new file mode 100644 index 00000000000..6d9c7058027 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/query-endpoint.ts @@ -0,0 +1,106 @@ +/** + * ExpressJS `query` endpoint + */ + +import type { Express, Request, Response } from "express"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginLedgerConnectorIroha2 } from "../plugin-ledger-connector-iroha2"; +import { safeStringifyException } from "../utils"; + +import OAS from "../../json/openapi.json"; + +export interface IQueryEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorIroha2; +} + +export class QueryEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "QueryEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return QueryEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IQueryEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getOasPath(): any { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/query" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + const reqBody = req.body; + try { + const resBody = await this.options.connector.query(reqBody); + res.json(resBody); + } catch (ex) { + this.log.warn(`Crash while serving ${reqTag}`, ex); + res.status(500).json({ + message: "Internal Server Error", + error: safeStringifyException(ex), + }); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/transact-endpoint.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/transact-endpoint.ts new file mode 100644 index 00000000000..05e20e167af --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/transact-endpoint.ts @@ -0,0 +1,106 @@ +/** + * ExpressJS `transact` endpoint + */ + +import type { Express, Request, Response } from "express"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginLedgerConnectorIroha2 } from "../plugin-ledger-connector-iroha2"; +import { safeStringifyException } from "../utils"; + +import OAS from "../../json/openapi.json"; + +export interface ITransactEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorIroha2; +} + +export class TransactEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "TransactEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return TransactEndpoint.CLASS_NAME; + } + + constructor(public readonly options: ITransactEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getOasPath(): any { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/transact" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + const reqBody = req.body; + try { + const resBody = await this.options.connector.transact(reqBody); + res.json(resBody); + } catch (ex) { + this.log.warn(`Crash while serving ${reqTag}`, ex); + res.status(500).json({ + message: "Internal Server Error", + error: safeStringifyException(ex), + }); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts new file mode 100644 index 00000000000..41f78efac7b --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts @@ -0,0 +1,160 @@ +/** + * SocketIO `WatchBlocks` endpoint + */ + +import { Socket as SocketIoSocket } from "socket.io"; + +import { + Logger, + LogLevelDesc, + LoggerProvider, + Checks, +} from "@hyperledger/cactus-common"; + +import { + WatchBlocksV1, + WatchBlocksOptionsV1, + WatchBlocksResponseV1, + BlockTypeV1, +} from "../generated/openapi/typescript-axios"; + +import { safeStringifyException, stringifyBigIntReplacer } from "../utils"; + +import { Client as IrohaClient } from "@iroha2/client"; + +import safeStringify from "fast-safe-stringify"; +import { VersionedCommittedBlock } from "@iroha2/data-model"; + +/** + * WatchBlocksV1Endpoint configuration. + */ +export interface IWatchBlocksV1EndpointConfiguration { + logLevel?: LogLevelDesc; + socket: SocketIoSocket; + client: IrohaClient; +} + +/** + * Endpoint to watch for new blocks on Iroha V2 ledger and report them + * to the client using socketio. + */ +export class WatchBlocksV1Endpoint { + public readonly className = "WatchBlocksV1Endpoint"; + private readonly log: Logger; + private readonly client: IrohaClient; + private readonly socket: SocketIoSocket< + Record void> + >; + + constructor(public readonly config: IWatchBlocksV1EndpointConfiguration) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(config, `${fnTag} arg options`); + Checks.truthy(config.socket, `${fnTag} arg options.socket`); + Checks.truthy(config.client, `${fnTag} arg options.client`); + + this.socket = config.socket; + this.client = config.client; + + const level = this.config.logLevel || "info"; + this.log = LoggerProvider.getOrCreate({ level, label: this.className }); + } + + /** + * Subscribe to new blocks on Iroha V2 ledger, push them to the client via SocketIO. + * + * @param options Block monitoring options. + */ + public async subscribe(options: WatchBlocksOptionsV1): Promise { + const { client, socket, log } = this; + const clientId = socket.id; + log.info( + `${WatchBlocksV1.Subscribe} => clientId: ${clientId}, startBlock: ${options.startBlock}`, + ); + + try { + const height = options.startBlock ?? "0"; + const blockType = options.type ?? BlockTypeV1.Raw; + const blockMonitor = await client.torii.listenForBlocksStream({ + height: BigInt(height), + }); + + // Handle events + blockMonitor.ee.on("open", (openEvent) => { + log.debug("listenForBlocksStream open:", safeStringify(openEvent)); + }); + + blockMonitor.ee.on("close", (closeEvent) => { + log.debug("listenForBlocksStream close:", safeStringify(closeEvent)); + }); + + blockMonitor.ee.on("error", (error) => { + const errorMessage = safeStringify(error); + log.warn("listenForBlocksStream error:", errorMessage); + socket.emit(WatchBlocksV1.Error, { + message: "listenForBlocksStream error event", + error: errorMessage, + }); + }); + + blockMonitor.ee.on("block", (block) => { + try { + switch (blockType) { + case BlockTypeV1.Raw: + socket.emit(WatchBlocksV1.Next, { + blockData: JSON.stringify(block, stringifyBigIntReplacer), + }); + break; + case BlockTypeV1.Binary: + socket.emit(WatchBlocksV1.Next, { + binaryBlock: VersionedCommittedBlock.toBuffer(block), + }); + break; + default: + const unknownType: never = blockType; + throw new Error( + `Unknown block listen type - '${unknownType}'. Check name and connector version.`, + ); + } + } catch (error) { + const errorMessage = safeStringifyException(error); + log.warn( + "listenForBlocksStream block serialization error:", + errorMessage, + ); + socket.emit(WatchBlocksV1.Error, { + message: "listenForBlocksStream onBlock event error", + error: errorMessage, + }); + } + }); + + socket.on("disconnect", async (reason: string) => { + log.info( + "WebSocket:disconnect => reason=%o clientId=%s", + reason, + clientId, + ); + blockMonitor.ee.clearListeners(); + await blockMonitor.stop(); + }); + + socket.on(WatchBlocksV1.Unsubscribe, () => { + log.info(`${WatchBlocksV1.Unsubscribe} => clientId: ${clientId}`); + this.close(); + }); + } catch (error) { + const errorMessage = safeStringifyException(error); + log.error(errorMessage); + socket.emit(WatchBlocksV1.Error, { + message: "WatchBlocksV1 Exception", + error: errorMessage, + }); + } + } + + close(): void { + if (this.socket.connected) { + this.socket.disconnect(true); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/api-surface.test.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/api-surface.test.ts new file mode 100644 index 00000000000..34aba3a0aea --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/api-surface.test.ts @@ -0,0 +1,6 @@ +import * as apiSurface from "../../../main/typescript/public-api"; +import "jest-extended"; + +test("Library can be loaded", async () => { + expect(apiSurface).toBeTruthy(); +}); diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha-instructions-and-queries.test.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha-instructions-and-queries.test.ts new file mode 100644 index 00000000000..befa9a8ed26 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha-instructions-and-queries.test.ts @@ -0,0 +1,591 @@ +/** + * Tests for executing Iroha instructions and queries through the cactus connector. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Log settings +const testLogLevel: LogLevelDesc = "info"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, +} from "@hyperledger/cactus-common"; + +import { + IrohaInstruction, + IrohaQuery, + Iroha2KeyPair, +} from "../../../main/typescript/public-api"; +import { + IrohaV2TestEnv, + generateTestIrohaCredentials, + waitForCommit, +} from "../test-helpers/iroha2-env-setup"; +import { addRandomSuffix } from "../test-helpers/utils"; + +import { bytesToHex } from "hada"; +import "jest-extended"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "iroha-instructions-and-queries.test", + level: testLogLevel, +}); + +/** + * Main test suite + */ +describe("Instructions and Queries test", () => { + let env: IrohaV2TestEnv; + + beforeAll(async () => { + env = new IrohaV2TestEnv(log); + await env.start(); + }); + + afterAll(async () => { + if (env) { + await env.stop(); + } + }); + + describe("Domain tests", () => { + let domainName: string; + + // Create domain common test + beforeAll(async () => { + // Generate random domain and assets name + domainName = addRandomSuffix("funcTestDomain"); + expect(domainName).toBeTruthy(); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.status).toBeTruthy(); + expect(transactionResponse.data.status).toEqual("OK"); + + // Sleep + await waitForCommit(); + }); + + test("Query single domain (FindDomainById)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindDomainById, + baseConfig: env.defaultBaseConfig, + params: [domainName], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(queryResponse.data.response.id).toBeTruthy(); + expect(queryResponse.data.response.id.name).toEqual(domainName); + }); + + test("Query all domains (FindAllDomains)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAllDomains, + baseConfig: env.defaultBaseConfig, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(JSON.stringify(queryResponse.data.response)).toContain(domainName); + }); + }); + + describe("Account tests", () => { + let newAccountName: string; + let newAccountDomainName: string; + let newAccountCredentials: Iroha2KeyPair; + + // Register new account (RegisterAccount) + beforeAll(async () => { + newAccountName = addRandomSuffix("fooAcc"); + expect(newAccountName).toBeTruthy(); + newAccountDomainName = addRandomSuffix("newAccDomain"); + expect(newAccountDomainName).toBeTruthy(); + + // Create new domain for our new account + const registerDomainResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [newAccountDomainName], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(registerDomainResponse).toBeTruthy(); + expect(registerDomainResponse.status).toEqual(200); + expect(registerDomainResponse.data.status).toEqual("OK"); + await waitForCommit(); + + // Generate new account credentials + newAccountCredentials = generateTestIrohaCredentials(); + + // Register new account + const registerAccountResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterAccount, + params: [ + newAccountName, + newAccountDomainName, + newAccountCredentials.publicKey, + newAccountCredentials.privateKey.digestFunction, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAccountResponse).toBeTruthy(); + expect(registerAccountResponse.status).toEqual(200); + expect(registerAccountResponse.data.status).toEqual("OK"); + await waitForCommit(); + }); + + test("Query single account (FindAccountById)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAccountById, + baseConfig: env.defaultBaseConfig, + params: [newAccountName, newAccountDomainName], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + const responseData = queryResponse.data.response; + expect(responseData).toBeTruthy(); + expect(responseData.id.name).toEqual(newAccountName); + expect(responseData.id.domain_id.name).toEqual(newAccountDomainName); + expect(responseData.signatories.length).toBeGreaterThan(0); + const receivedPubKey = responseData.signatories.pop().payload; + expect(bytesToHex(Object.values(receivedPubKey))).toEqual( + newAccountCredentials.publicKey, + ); + }); + + test("Query all accounts (FindAllAccounts)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAllAccounts, + baseConfig: env.defaultBaseConfig, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(JSON.stringify(queryResponse.data.response)).toContain( + newAccountName, + ); + }); + }); + + describe("Asset tests", () => { + let assetName: string; + let domainName: string; + const valueType = "Quantity"; + const value = 42; + const mintable = "Infinitely"; + + // Create asset definition and asset itself common test + beforeAll(async () => { + // Generate random domain and assets name + assetName = addRandomSuffix("testAsset"); + expect(assetName).toBeTruthy(); + domainName = addRandomSuffix("testAssetDomain"); + expect(domainName).toBeTruthy(); + + // Create new domain for our new asset + const registerDomainResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(registerDomainResponse).toBeTruthy(); + expect(registerDomainResponse.status).toEqual(200); + expect(registerDomainResponse.data.status).toEqual("OK"); + await waitForCommit(); + + // Create new asset definition + const registerAssetDefResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterAssetDefinition, + params: [assetName, domainName, valueType, mintable], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAssetDefResponse).toBeTruthy(); + expect(registerAssetDefResponse.status).toEqual(200); + expect(registerAssetDefResponse.data.status).toEqual("OK"); + await waitForCommit(); + + // Create new asset + const registerAssetResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterAsset, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + value, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAssetResponse).toBeTruthy(); + expect(registerAssetResponse.status).toEqual(200); + expect(registerAssetResponse.data.status).toEqual("OK"); + await waitForCommit(); + }); + + test("Query single asset definition (FindAssetDefinitionById)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetDefinitionById, + baseConfig: env.defaultBaseConfig, + params: [assetName, domainName], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + const responseData = queryResponse.data.response; + expect(responseData).toBeTruthy(); + expect(responseData.id.name).toEqual(assetName); + expect(responseData.id.domain_id.name).toEqual(domainName); + expect(responseData.value_type.tag).toEqual(valueType); + expect(responseData.mintable.tag).toEqual(mintable); + }); + + test("Query all asset definitions (FindAllAssetsDefinitions)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAllAssetsDefinitions, + baseConfig: env.defaultBaseConfig, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(JSON.stringify(queryResponse.data.response)).toContain(domainName); + }); + + test("Query single asset (FindAssetById)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetById, + baseConfig: env.defaultBaseConfig, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + const responseData = queryResponse.data.response; + expect(responseData).toBeTruthy(); + expect(responseData.id.definition_id.name).toEqual(assetName); + expect(responseData.id.definition_id.domain_id.name).toEqual(domainName); + expect(responseData.id.account_id.name).toEqual( + env.defaultBaseConfig.accountId?.name, + ); + expect(responseData.id.account_id.domain_id.name).toEqual( + env.defaultBaseConfig.accountId?.domainId, + ); + expect(responseData.value.tag).toEqual(valueType); + }); + + test("Query all assets (FindAllAssets)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAllAssets, + baseConfig: env.defaultBaseConfig, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(JSON.stringify(queryResponse.data.response)).toContain(assetName); + }); + + test("Mint asset integer value (MintAsset)", async () => { + const mintValue = 100; + + // Get initial asset value + const initQueryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetById, + baseConfig: env.defaultBaseConfig, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }); + expect(initQueryResponse).toBeTruthy(); + expect(initQueryResponse.data).toBeTruthy(); + const initValue = initQueryResponse.data.response.value.value; + log.info("Initial asset value (before mint):", initValue); + + // Mint additional asset value + const mintResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.MintAsset, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + mintValue, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(mintResponse).toBeTruthy(); + expect(mintResponse.status).toEqual(200); + expect(mintResponse.data.status).toEqual("OK"); + await waitForCommit(); + + // Get final asset value (after mint) + const finalQueryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetById, + baseConfig: env.defaultBaseConfig, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }); + expect(finalQueryResponse).toBeTruthy(); + expect(finalQueryResponse.data).toBeTruthy(); + const finalValue = finalQueryResponse.data.response.value.value; + log.info("Final asset value (after mint):", finalValue); + + expect(finalValue).toEqual(initValue + mintValue); + }); + + test("Burn asset integer value (BurnAsset)", async () => { + const burnValue = 5; + + // Get initial asset value + const initQueryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetById, + baseConfig: env.defaultBaseConfig, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }); + expect(initQueryResponse).toBeTruthy(); + expect(initQueryResponse.data).toBeTruthy(); + const initValue = initQueryResponse.data.response.value.value; + log.info("Initial asset value (before burn):", initValue); + expect(burnValue).toBeLessThan(initValue); + + // Burn asset value + const burnResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.BurnAsset, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + burnValue, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(burnResponse).toBeTruthy(); + expect(burnResponse.status).toEqual(200); + expect(burnResponse.data.status).toEqual("OK"); + await waitForCommit(); + + // Get final asset value (after burn) + const finalQueryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetById, + baseConfig: env.defaultBaseConfig, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }); + expect(finalQueryResponse).toBeTruthy(); + expect(finalQueryResponse.data).toBeTruthy(); + const finalValue = finalQueryResponse.data.response.value.value; + log.info("Final asset value (after burn):", finalValue); + + expect(finalValue).toEqual(initValue - burnValue); + }); + + test("Transfer asset between accounts (TransferAsset)", async () => { + const transferValue = 3; + const sourceAccountName = env.defaultBaseConfig.accountId?.name; + const sourceAccountDomain = env.defaultBaseConfig.accountId?.domainId; + const targetAccountName = addRandomSuffix("transferTargetAcc"); + const targetAccountDomain = sourceAccountDomain; + + // Get initial asset value + const initQueryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetById, + baseConfig: env.defaultBaseConfig, + params: [assetName, domainName, sourceAccountName, sourceAccountDomain], + }); + expect(initQueryResponse).toBeTruthy(); + expect(initQueryResponse.data).toBeTruthy(); + const initValue = initQueryResponse.data.response.value.value; + log.info("Initial asset value (before transfer):", initValue); + expect(transferValue).toBeLessThan(initValue); + + // Register new account to receive the assets + const accountCredentials = generateTestIrohaCredentials(); + const registerAccountResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterAccount, + params: [ + targetAccountName, + targetAccountDomain, + accountCredentials.publicKey, + accountCredentials.privateKey.digestFunction, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAccountResponse).toBeTruthy(); + expect(registerAccountResponse.status).toEqual(200); + expect(registerAccountResponse.data.status).toEqual("OK"); + await waitForCommit(); + + // Transfer asset to the newly created account + const transferResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.TransferAsset, + params: [ + assetName, + domainName, + sourceAccountName, + sourceAccountDomain, + targetAccountName, + targetAccountDomain, + transferValue, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(transferResponse).toBeTruthy(); + expect(transferResponse.status).toEqual(200); + expect(transferResponse.data.status).toEqual("OK"); + await waitForCommit(); + + // Get final asset value on source account (after transfer) + const finalSourceQueryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetById, + baseConfig: env.defaultBaseConfig, + params: [assetName, domainName, sourceAccountName, sourceAccountDomain], + }); + expect(finalSourceQueryResponse).toBeTruthy(); + expect(finalSourceQueryResponse.data).toBeTruthy(); + const finalSrcValue = finalSourceQueryResponse.data.response.value.value; + log.info( + "Final asset value on source account (after transfer):", + finalSrcValue, + ); + expect(finalSrcValue).toEqual(initValue - transferValue); + + // Get final asset value on target account (after transfer) + const finalTargetQueryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAssetById, + baseConfig: env.defaultBaseConfig, + params: [assetName, domainName, targetAccountName, targetAccountDomain], + }); + expect(finalTargetQueryResponse).toBeTruthy(); + expect(finalTargetQueryResponse.data).toBeTruthy(); + const finalTargetValue = + finalTargetQueryResponse.data.response.value.value; + log.info( + "Final asset value on target account (after transfer):", + finalTargetValue, + ); + expect(finalTargetValue).toEqual(transferValue); + }); + }); + + describe("Transaction queries tests", () => { + test("Query all transactions (FindAllTransactions)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAllTransactions, + baseConfig: env.defaultBaseConfig, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response.length).toBeGreaterThan(0); + const singleTx = queryResponse.data.response.pop().value.value; + expect(singleTx.signatures).toBeTruthy(); + expect(singleTx.payload).toBeTruthy(); + expect(singleTx.payload.account_id).toBeTruthy(); + expect(singleTx.payload.instructions).toBeTruthy(); + }); + + /** + * @todo Find a way to calculate / retrieve hash of some transaction. + * Right now it's hardcoded for manual testing. + */ + test.skip("Query single transaction (FindAssetById)", async () => { + const hash = + "f1076e718309d9b54a9fc74f110b0d16111f7d1c4f9c470f18c56b9309ad873d"; + + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindTransactionByHash, + baseConfig: env.defaultBaseConfig, + params: [hash], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + const responseData = queryResponse.data.response.value.value; + expect(responseData).toBeTruthy(); + expect(responseData.signatures).toBeTruthy(); + expect(responseData.payload).toBeTruthy(); + expect(responseData.payload.account_id).toBeTruthy(); + expect(responseData.payload.instructions).toBeTruthy(); + }); + }); + + describe("Miscellaneous tests", () => { + test("Query all peers (FindAllPeers)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAllPeers, + baseConfig: env.defaultBaseConfig, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(queryResponse.data.response.length).toBeGreaterThan(0); + const singlePeer = queryResponse.data.response.pop(); + expect(singlePeer.id).toBeTruthy(); + expect(singlePeer.id.address).toBeTruthy(); + expect(singlePeer.id.public_key).toBeTruthy(); + }); + + test("Query all blocks (FindAllBlocks)", async () => { + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAllBlocks, + baseConfig: env.defaultBaseConfig, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(queryResponse.data.response.length).toBeGreaterThan(0); + const singleBlock = queryResponse.data.response.pop(); + expect(singleBlock.header).toBeTruthy(); + expect(singleBlock.transactions).toBeDefined(); + expect(singleBlock.rejected_transactions).toBeDefined(); + expect(singleBlock.event_recommendations).toBeDefined(); + }); + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/monitoring-endpoints.test.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/monitoring-endpoints.test.ts new file mode 100644 index 00000000000..9a15c0092ff --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/monitoring-endpoints.test.ts @@ -0,0 +1,159 @@ +/** + * SocketIO monitoring endpoints test. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Log settings +const testLogLevel: LogLevelDesc = "info"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, + Checks, +} from "@hyperledger/cactus-common"; + +import { + IrohaInstruction, + BlockTypeV1, + WatchBlocksOptionsV1, + WatchBlocksResponseV1, +} from "../../../main/typescript/public-api"; +import { + IrohaV2TestEnv, + waitForCommit, +} from "../test-helpers/iroha2-env-setup"; +import { addRandomSuffix } from "../test-helpers/utils"; + +import { VersionedCommittedBlock } from "@iroha2/data-model"; +import "jest-extended"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "monitoring-endpoints.test", + level: testLogLevel, +}); + +/** + * WatchBlocks test + */ +describe("Block monitoring tests", () => { + let env: IrohaV2TestEnv; + + beforeAll(async () => { + env = new IrohaV2TestEnv(log); + await env.start(); + }); + + afterAll(async () => { + if (env) { + await env.stop(); + } + }); + + /** + * Common test template for checking watchBlocksV1 endpoint. + * Sends watchBlocks request, creates new dummy domain to trigger block creation, calls onEvent + * when block is received. Caller should define all assertion in onEvent and throw new exception + * if test should fail. + * + * @param monitorOptions `apiClient.watchBlocksV1` argument. + * @param onEvent callback with received block data to be checked. + */ + async function testWatchBlocks( + monitorOptions: WatchBlocksOptionsV1, + onEvent: (event: WatchBlocksResponseV1) => void, + ) { + // Start monitoring + const monitorPromise = new Promise((resolve, reject) => { + const watchObservable = env.apiClient.watchBlocksV1(monitorOptions); + const subscription = watchObservable.subscribe({ + next(event) { + try { + onEvent(event); + resolve(); + } catch (err) { + log.error("watchBlocksV1() event check error:", err); + reject(err); + } finally { + subscription.unsubscribe(); + } + }, + error(err) { + log.error("watchBlocksV1() error:", err); + subscription.unsubscribe(); + reject(err); + }, + }); + }); + + // Wait for monitor setup just to be sure + await waitForCommit(); + + // Create new domain to trigger new block creation + const domainName = addRandomSuffix("watchBlocksTest"); + const transactionResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + log.info("Watch block trigger tx sent to create domain", domainName); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.status).toEqual("OK"); + + await expect(monitorPromise).toResolve(); + } + + test("watchBlocksV1 reports new blocks in raw json (default) format", async () => { + const monitorOptions = { + type: BlockTypeV1.Raw, + baseConfig: env.defaultBaseConfig, + }; + + const testPromise = testWatchBlocks(monitorOptions, (event) => { + log.info("Received block event from the connector"); + if (!("blockData" in event)) { + throw new Error("Unknown response type, wanted raw JSON data"); + } + Checks.truthy(event.blockData); + log.debug("block:", event.blockData); + expect(event.blockData).toBeTruthy(); + const parsedBlock = JSON.parse(event.blockData).value; + expect(parsedBlock).toBeTruthy(); + expect(parsedBlock.header).toBeTruthy(); + expect(parsedBlock.transactions).toBeDefined(); + expect(parsedBlock.rejected_transactions).toBeDefined(); + expect(parsedBlock.event_recommendations).toBeDefined(); + }); + + await expect(testPromise).toResolve(); + }); + + test("watchBlocksV1 reports new blocks in binary format", async () => { + const monitorOptions = { + type: BlockTypeV1.Binary, + baseConfig: env.defaultBaseConfig, + }; + + const testPromise = testWatchBlocks(monitorOptions, (event) => { + log.info("Received block event from the connector"); + if (!("binaryBlock" in event)) { + throw new Error("Unknown response type, wanted binary data"); + } + Checks.truthy(event.binaryBlock); + const decodedBlock = VersionedCommittedBlock.fromBuffer( + Buffer.from(event.binaryBlock), + ); + log.debug("decodedBlock:", decodedBlock); + expect(decodedBlock.as("V1").header).toBeTruthy(); + }); + + await expect(testPromise).toResolve(); + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/setup-and-basic-operations.test.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/setup-and-basic-operations.test.ts new file mode 100644 index 00000000000..cdf2801cdd4 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/setup-and-basic-operations.test.ts @@ -0,0 +1,317 @@ +/** + * Tests for connector setup and basic operation tests for endpoints. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Log settings +const testLogLevel: LogLevelDesc = "info"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, +} from "@hyperledger/cactus-common"; + +import { v4 as uuidv4 } from "uuid"; +import "jest-extended"; + +import { + Iroha2BaseConfig, + IrohaInstruction, + IrohaQuery, + PluginLedgerConnectorIroha2, +} from "../../../main/typescript/public-api"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { crypto } from "@iroha2/crypto-target-node"; +import { setCrypto } from "@iroha2/client"; + +import { + IrohaV2TestEnv, + generateTestIrohaCredentials, + waitForCommit, +} from "../test-helpers/iroha2-env-setup"; +import { addRandomSuffix } from "../test-helpers/utils"; + +setCrypto(crypto); + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "setup-and-basic-operations.test", + level: testLogLevel, +}); + +/** + * Test suite for helper functions used by the test suites + */ +describe("Helper functions test", () => { + test("Adding random suffix to test strings works (addRandomSuffix)", async () => { + const a = addRandomSuffix("foo"); + expect(a).toBeTruthy(); + const b = addRandomSuffix("foo"); + expect(b).toBeTruthy(); + expect(a).not.toEqual(b); + }); + + test("Test key generation works (generateTestIrohaCredentials)", async () => { + const credentials = generateTestIrohaCredentials(); + expect(credentials).toBeTruthy(); + expect(credentials.publicKey).toBeTruthy(); + expect(credentials.privateKey.payload).toBeTruthy(); + expect(credentials.privateKey.digestFunction).toBeTruthy(); + }); +}); + +/** + * Main test suite + */ +describe("Setup and basic endpoint tests", () => { + let env: IrohaV2TestEnv; + + beforeAll(async () => { + env = new IrohaV2TestEnv(log); + await env.start(); + }); + + afterAll(async () => { + if (env) { + await env.stop(); + } + }); + + test("Connector and request config merge works", async () => { + const defaultConfig = { + ...env.defaultBaseConfig, + signingCredential: env.keyPairCredential, + }; + const defaultConfigConnector = new PluginLedgerConnectorIroha2({ + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins: [] }), + defaultConfig, + }); + + // Default config + const allDefault = (await defaultConfigConnector.getClient()).toriiOptions; + expect(allDefault).toEqual(defaultConfig.torii); + + // Overwrite by request + const requestConfig: Iroha2BaseConfig = { + torii: { + apiURL: "http://example.com", + telemetryURL: "http://telemetry.com", + }, + }; + const overwrittenConfig = ( + await defaultConfigConnector.getClient(requestConfig) + ).toriiOptions; + expect(overwrittenConfig).toEqual(requestConfig.torii); + }); + + test("Simple transaction and query endpoints works", async () => { + const domainName = addRandomSuffix("singleTxTest"); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.status).toBeTruthy(); + expect(transactionResponse.data.status).toEqual("OK"); + + // Sleep + await waitForCommit(); + + // Query it + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindDomainById, + baseConfig: env.defaultBaseConfig, + params: [domainName], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(queryResponse.data.response.id).toBeTruthy(); + expect(queryResponse.data.response.id.name).toEqual(domainName); + }); + + test("Sending transaction with keychain signatory works", async () => { + const domainName = addRandomSuffix("keychainSignatoryDomain"); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + baseConfig: { + ...env.defaultBaseConfig, + signingCredential: env.keychainCredentials, + }, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.status).toBeTruthy(); + expect(transactionResponse.data.status).toEqual("OK"); + + // Sleep + await waitForCommit(); + + // Query it + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindDomainById, + baseConfig: env.defaultBaseConfig, + params: [domainName], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(queryResponse.data.response.id).toBeTruthy(); + expect(queryResponse.data.response.id.name).toEqual(domainName); + }); + + test("Sending transaction with keypair signatory works", async () => { + const domainName = addRandomSuffix("keypairSignatoryDomain"); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + baseConfig: { + ...env.defaultBaseConfig, + signingCredential: env.keyPairCredential, + }, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.status).toBeTruthy(); + expect(transactionResponse.data.status).toEqual("OK"); + + // Sleep + await waitForCommit(); + + // Query it + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindDomainById, + baseConfig: env.defaultBaseConfig, + params: [domainName], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(queryResponse.data.response.id).toBeTruthy(); + expect(queryResponse.data.response.id.name).toEqual(domainName); + }); + + test("Multiple instructions in single transaction works", async () => { + // Create two new domains + const firstDomainName = addRandomSuffix("multiTxFirstDomain"); + const secondDomainName = addRandomSuffix("multiTxSecondDomain"); + const transactionResponse = await env.apiClient.transactV1({ + instruction: [ + { + name: IrohaInstruction.RegisterDomain, + params: [firstDomainName], + }, + { + name: IrohaInstruction.RegisterDomain, + params: [secondDomainName], + }, + ], + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.status).toBeTruthy(); + expect(transactionResponse.data.status).toEqual("OK"); + + // Sleep + await waitForCommit(); + + // Query domains + const queryResponse = await env.apiClient.queryV1({ + queryName: IrohaQuery.FindAllDomains, + baseConfig: env.defaultBaseConfig, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.data.response).toBeTruthy(); + expect(JSON.stringify(queryResponse.data.response)).toContain( + firstDomainName, + ); + expect(JSON.stringify(queryResponse.data.response)).toContain( + secondDomainName, + ); + }); + + test("Unknown transaction instruction name reports error", () => { + // Send invalid command + return expect( + env.apiClient.transactV1({ + instruction: { + name: "foo" as IrohaInstruction, + params: [], + }, + baseConfig: env.defaultBaseConfig, + }), + ).toReject(); + }); + + test("Sending transaction with incomplete config reports error", async () => { + const domainName = "wrongConfigDomain"; + + // Use config without account and keypair (only torii) + await expect( + env.apiClient?.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + baseConfig: { + torii: env.defaultBaseConfig.torii, + }, + }), + ).toReject(); + + // Use config without keypair + await expect( + env.apiClient.transactV1({ + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + baseConfig: { + torii: env.defaultBaseConfig.torii, + accountId: env.defaultBaseConfig.accountId, + }, + }), + ).toReject(); + + // Assert it was not created + await expect( + env.apiClient.queryV1({ + queryName: IrohaQuery.FindDomainById, + baseConfig: env.defaultBaseConfig, + params: [domainName], + }), + ).toReject(); + }); + + test("Unknown query name reports error", () => { + // Send invalid query + return expect( + env.apiClient.queryV1({ + queryName: "foo" as IrohaQuery, + baseConfig: env.defaultBaseConfig, + }), + ).toReject(); + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/iroha2-env-setup.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/iroha2-env-setup.ts new file mode 100644 index 00000000000..0ba63a854de --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/iroha2-env-setup.ts @@ -0,0 +1,274 @@ +/** + * Test Iroha V2 environment setup functions. + */ + +// Ledger settings +const containerImageName = "ghcr.io/hyperledger/cactus-iroha2-all-in-one"; +const containerImageVersion = "2022-08-24-b4d59707b"; +const useRunningLedger = false; + +// Log settings +const testLogLevel: LogLevelDesc = "info"; +const sutLogLevel: LogLevelDesc = "info"; + +import { + Iroha2ClientConfig, + Iroha2TestLedger, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; +import { + LogLevelDesc, + Logger, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; + +import { + Iroha2BaseConfig, + KeychainReference, + PluginLedgerConnectorIroha2, + Iroha2KeyPair, + Iroha2ApiClient, +} from "../../../main/typescript/public-api"; +import { addRandomSuffix } from "./utils"; + +import { crypto } from "@iroha2/crypto-target-node"; +import { setCrypto } from "@iroha2/client"; + +import { v4 as uuidv4 } from "uuid"; +import { Server as SocketIoServer } from "socket.io"; +import { AddressInfo } from "net"; +import { bytesToHex } from "hada"; +import http from "http"; +import express from "express"; +import bodyParser from "body-parser"; +import "jest-extended"; + +setCrypto(crypto); + +/** + * Wait for transaction commit on the ledger. + * Currently there's no better way than just wait for a while, so we sleep for 3 seconds. + */ +export async function waitForCommit(): Promise { + const timeout = 3 * 1000; // 3 seconds + await new Promise((resolve) => setTimeout(resolve, timeout)); +} + +/** + * Generates Iroha V2 ledger compatible key pair that can be used as account credentials. + * Pubic key is encoded in a multihash format. + * + * @returns Ed25519 keypair + */ +export function generateTestIrohaCredentials(): Iroha2KeyPair { + const seedBytes = Buffer.from(addRandomSuffix("seed")); + const config = crypto + .createKeyGenConfiguration() + .useSeed(Uint8Array.from(seedBytes)) + .withAlgorithm(crypto.AlgorithmEd25519()); + + const freeableKeys: { free(): void }[] = []; + try { + const keyPair = crypto.generateKeyPairWithConfiguration(config); + freeableKeys.push(keyPair); + + const multiHashPubKey = crypto.createMultihashFromPublicKey( + keyPair.publicKey(), + ); + freeableKeys.push(multiHashPubKey); + + return { + publicKey: bytesToHex(Array.from(multiHashPubKey.toBytes())), + privateKey: { + digestFunction: keyPair.privateKey().digestFunction(), + payload: bytesToHex(Array.from(keyPair.privateKey().payload())), + }, + }; + } finally { + freeableKeys.forEach((x) => x.free()); + } +} + +/** + * Test Iroha V2 environment. + * Starts dockerized ledger, cactus connector and apiClient. + */ +export class IrohaV2TestEnv { + constructor(private log: Logger) { + this.log.info("Creating IrohaV2TestEnv..."); + } + + // Private fields + private ledger?: Iroha2TestLedger; + private connectorServer?: http.Server; + private socketioServer?: SocketIoServer; + private iroha2ConnectorPlugin?: PluginLedgerConnectorIroha2; + private clientConfig?: Iroha2ClientConfig; + + /** + * If value is not falsy throw error informing that environment is not running yet. + * + * @param value any value. + * @returns the value or an error. + */ + private checkedGet(value?: T): T { + if (value) { + return value; + } + throw new Error("IrohaV2TestEnv not started yet."); + } + + // Public fields + private _keyPairCredential?: Iroha2KeyPair; + get keyPairCredential(): Iroha2KeyPair { + return this.checkedGet(this._keyPairCredential); + } + + private _keychainCredentials?: KeychainReference; + get keychainCredentials(): KeychainReference { + return this.checkedGet(this._keychainCredentials); + } + + private _defaultBaseConfig?: Iroha2BaseConfig; + get defaultBaseConfig(): Iroha2BaseConfig { + return this.checkedGet(this._defaultBaseConfig); + } + + private _apiClient?: Iroha2ApiClient; + get apiClient(): Iroha2ApiClient { + return this.checkedGet(this._apiClient); + } + + /** + * Start entire test Iroha V2 environment. + * Runs the ledger, cactus connector, apiClient, handles all intermediate steps. + * @note Remember to call `stop()` after test is done to cleanup allocated resources and stop the docker containers. + */ + async start(): Promise { + this.log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + this.log.info("Start Iroha2TestLedger..."); + this.ledger = new Iroha2TestLedger({ + containerImageName, + containerImageVersion, + useRunningLedger, + emitContainerLogs: true, + logLevel: testLogLevel, + }); + this.log.debug("IrohaV2 image:", this.ledger.fullContainerImageName); + expect(this.ledger).toBeTruthy(); + await this.ledger.start(); + + // Get client config + this.clientConfig = await this.ledger.getClientConfig(); + + // Get signingCredential + this._keyPairCredential = { + publicKey: this.clientConfig.PUBLIC_KEY, + privateKey: { + digestFunction: this.clientConfig.PRIVATE_KEY.digest_function, + payload: this.clientConfig.PRIVATE_KEY.payload, + }, + }; + + // Create Keychain Plugin + const keychainInstanceId = uuidv4(); + const keychainId = uuidv4(); + const keychainEntryKey = "aliceKey"; + const keychainEntryValue = JSON.stringify(this.keyPairCredential); + + const keychainPlugin = new PluginKeychainMemory({ + instanceId: keychainInstanceId, + keychainId, + logLevel: sutLogLevel, + backend: new Map([[keychainEntryKey, keychainEntryValue]]), + }); + + this._keychainCredentials = { + keychainId, + keychainRef: keychainEntryKey, + }; + + this.iroha2ConnectorPlugin = new PluginLedgerConnectorIroha2({ + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + logLevel: sutLogLevel, + }); + + this._defaultBaseConfig = { + torii: { + apiURL: this.clientConfig.TORII_API_URL, + telemetryURL: this.clientConfig.TORII_TELEMETRY_URL, + }, + accountId: { + name: this.clientConfig.ACCOUNT_ID.name, + domainId: this.clientConfig.ACCOUNT_ID.domain_id.name, + }, + signingCredential: this.keychainCredentials, + }; + + // Run http server + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + this.connectorServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 0, + server: this.connectorServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const apiHost = `http://${addressInfo.address}:${addressInfo.port}`; + this.log.debug("Iroha V2 connector URL:", apiHost); + + // Run socketio server + this.socketioServer = new SocketIoServer(this.connectorServer, { + path: Constants.SocketIoConnectionPathV1, + }); + + // Register services + await this.iroha2ConnectorPlugin.getOrCreateWebServices(); + await this.iroha2ConnectorPlugin.registerWebServices( + expressApp, + this.socketioServer, + ); + + // Create ApiClient + const apiConfig = new Configuration({ basePath: apiHost }); + this._apiClient = new Iroha2ApiClient(apiConfig); + } + + /** + * Stop the entire test environment (if it was started in the first place). + */ + async stop(): Promise { + this.log.info("FINISHING THE TESTS"); + + if (this.ledger) { + this.log.info("Stop the fabric ledger..."); + await this.ledger.stop(); + await this.ledger.destroy(); + } + + if (this.socketioServer) { + this.log.info("Stop the SocketIO server connector..."); + await new Promise((resolve) => + this.socketioServer?.close(() => resolve()), + ); + } + + if (this.connectorServer) { + this.log.info("Stop the fabric connector..."); + await new Promise((resolve) => + this.connectorServer?.close(() => resolve()), + ); + } + + this.log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/utils.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/utils.ts new file mode 100644 index 00000000000..d9dc048b542 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/utils.ts @@ -0,0 +1,18 @@ +/** + * Utility functions used throughout the tests. + */ + +import crypto from "crypto"; + +/** + * Adds random suffix to given string. + * Can be used to generate unique names for testing. + * + * @param name + * @returns unique string + */ +export function addRandomSuffix(name: string): string { + const buf = Buffer.alloc(4); + crypto.randomFillSync(buf, 0, 4); + return name + buf.toString("hex"); +} diff --git a/packages/cactus-plugin-ledger-connector-iroha2/tsconfig.json b/packages/cactus-plugin-ledger-connector-iroha2/tsconfig.json new file mode 100644 index 00000000000..334a4bcb297 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/tsconfig.json @@ -0,0 +1,32 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist/lib/", + "declarationDir": "dist/types", + "resolveJsonModule": true, + "rootDir": "./src", + "tsBuildInfoFile": "../../.build-cache/cactus-plugin-ledger-connector-iroha2.tsbuildinfo" + }, + "include": [ + "./src", + "src/**/*.json" + ], + "references": [ + { + "path": "../cactus-common/tsconfig.json" + }, + { + "path": "../cactus-core/tsconfig.json" + }, + { + "path": "../cactus-core-api/tsconfig.json" + }, + { + "path": "../cactus-plugin-keychain-memory/tsconfig.json" + }, + { + "path": "../cactus-test-tooling/tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/packages/cactus-test-tooling/src/main/typescript/iroha/iroha2-test-ledger.ts b/packages/cactus-test-tooling/src/main/typescript/iroha/iroha2-test-ledger.ts index ab33751f080..96bd5bf505c 100644 --- a/packages/cactus-test-tooling/src/main/typescript/iroha/iroha2-test-ledger.ts +++ b/packages/cactus-test-tooling/src/main/typescript/iroha/iroha2-test-ledger.ts @@ -33,8 +33,8 @@ export interface IIroha2TestLedgerOptions { * Default values used by Iroha2TestLedger constructor. */ export const IROHA2_TEST_LEDGER_DEFAULT_OPTIONS = Object.freeze({ - containerImageName: "ghcr.io/outsh/cactus_iroha2_all_in_one", // @todo - update - containerImageVersion: "0.2", // @todo - update + containerImageName: "ghcr.io/hyperledger/cactus-iroha2-all-in-one", + containerImageVersion: "2022-08-24-b4d59707b", logLevel: "info" as LogLevelDesc, emitContainerLogs: true, envVars: [], diff --git a/packages/cactus-test-tooling/src/test/typescript/integration/iroha/iroha2-test-ledger/iroha2-test-ledger.test.ts b/packages/cactus-test-tooling/src/test/typescript/integration/iroha/iroha2-test-ledger/iroha2-test-ledger.test.ts index aa4c392a485..b42c31793c3 100644 --- a/packages/cactus-test-tooling/src/test/typescript/integration/iroha/iroha2-test-ledger/iroha2-test-ledger.test.ts +++ b/packages/cactus-test-tooling/src/test/typescript/integration/iroha/iroha2-test-ledger/iroha2-test-ledger.test.ts @@ -7,8 +7,8 @@ ////////////////////////////////// // Ledger settings -const containerImageName = "ghcr.io/outsh/cactus_iroha2_all_in_one"; -const containerImageVersion = "0.2"; +const containerImageName = "ghcr.io/hyperledger/cactus-iroha2-all-in-one"; +const containerImageVersion = "2022-08-24-b4d59707b"; const useRunningLedger = false; // Log settings diff --git a/packages/cactus-verifier-client/README.md b/packages/cactus-verifier-client/README.md index a4d8d3f40b9..f76625f263d 100644 --- a/packages/cactus-verifier-client/README.md +++ b/packages/cactus-verifier-client/README.md @@ -10,6 +10,7 @@ This package provides `Verifier` and `VerifierFactory` components that can be us | BESU_1X
BESU_2X | cactus-plugin-ledger-connector-besu | | QUORUM_2X | cactus-test-plugin-ledger-connector-quorum | | CORDA_4X | cactus-plugin-ledger-connector-corda | +| IROHA_2X | cactus-plugin-ledger-connector-iroha2 | | legacy-socketio | cactus-plugin-ledger-connector-fabric-socketio
cactus-plugin-ledger-connector-go-ethereum-socketio
cactus-plugin-ledger-connector-sawtooth-socketio | ## VerifierFactory diff --git a/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts b/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts index e4ea053f22a..16293ace51c 100644 --- a/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts +++ b/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts @@ -25,6 +25,11 @@ import { CordaApiClientOptions, } from "@hyperledger/cactus-plugin-ledger-connector-corda"; +import { + Iroha2ApiClient, + Iroha2ApiClientOptions, +} from "@hyperledger/cactus-plugin-ledger-connector-iroha2"; + /** * Configuration of ApiClients currently supported by Verifier and VerifierFactory * Each entry key defines the name of the connection type that has to be specified in VerifierFactory config. @@ -52,6 +57,10 @@ export type ClientApiConfig = { in: CordaApiClientOptions; out: CordaApiClient; }; + IROHA_2X: { + in: Iroha2ApiClientOptions; + out: Iroha2ApiClient; + }; }; /** @@ -77,6 +86,8 @@ export function getValidatorApiClient( return new QuorumApiClient(options as QuorumApiClientOptions); case "CORDA_4X": return new CordaApiClient(options as CordaApiClientOptions); + case "IROHA_2X": + return new Iroha2ApiClient(options as CordaApiClientOptions); default: // Will not compile if any ClientApiConfig key was not handled by this switch const _: never = validatorType; diff --git a/packages/cactus-verifier-client/tsconfig.json b/packages/cactus-verifier-client/tsconfig.json index 9171a7bf611..32354c431c0 100644 --- a/packages/cactus-verifier-client/tsconfig.json +++ b/packages/cactus-verifier-client/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../cactus-plugin-ledger-connector-besu/tsconfig.json" + }, + { + "path": "../cactus-plugin-ledger-connector-iroha2/tsconfig.json" } ] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e0e9b42b6ca..94e866efc82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2920,6 +2920,46 @@ dependencies: multiformats "^9.5.4" +"@iroha2/client@3.0.0": + version "3.0.0" + resolved "https://nexus.iroha.tech/repository/npm-group/@iroha2/client/-/client-3.0.0.tgz#6c4fce7eb491d4b71e94e73a7b5993b386923d58" + integrity sha512-fh5B+ET/mjSQUI5rovzyD4GL8z/J3NQEjeA8q3/ACS4Wv/Llcd32IjqSeNAUE/irBvVQirx192xQyS/+tSqy6A== + dependencies: + "@iroha2/crypto-core" "^0.1.1" + "@iroha2/data-model" "3.0.0" + "@types/ws" "^8.2.2" + debug "^4.3.4" + emittery "^0.10.1" + json-bigint "^1.0.0" + ws "^8.4.0" + +"@iroha2/crypto-core@0.1.1", "@iroha2/crypto-core@^0.1.1": + version "0.1.1" + resolved "https://nexus.iroha.tech/repository/npm-group/@iroha2/crypto-core/-/crypto-core-0.1.1.tgz#1a5ebe686946219cfea80e088c29d8b010c3820f" + integrity sha512-BdZsIhHNwwLyMz8V8MQwPJ/Lce2FHyMkD3+Hy/BGBLScpiXVm/PRajb6oFmciXZgXmgveprTTFwQw/RU6FEahg== + +"@iroha2/crypto-target-node@0.4.0": + version "0.4.0" + resolved "https://nexus.iroha.tech/repository/npm-group/@iroha2/crypto-target-node/-/crypto-target-node-0.4.0.tgz#cbd4a8ce39d4ad7539ee6e868b1e70493d2787c0" + integrity sha512-f01Y6HBqqpZj9AqD0EjakD09RlR7aTRx/T2OF4ttLPSkWwVKqXPdSr1HwRceNMgLRbT1ZDFr4t8IK4Q2c3FRlw== + dependencies: + "@iroha2/crypto-core" "^0.1.1" + +"@iroha2/data-model@3.0.0": + version "3.0.0" + resolved "https://nexus.iroha.tech/repository/npm-group/@iroha2/data-model/-/data-model-3.0.0.tgz#ac5d99da8dc507717c0fb93acfb23a2cc2c4c298" + integrity sha512-no3oB0AwN8Ti+Dpm2zBxorRhRXKSBolGCSB20p/GNTg94sdJCacAzihisICyHNkEtDFsZnDOIvQzZ2sMaEHCSQ== + dependencies: + "@iroha2/i64-fixnum" "^0.4.2" + "@scale-codec/definition-runtime" "^2.2.2" + +"@iroha2/i64-fixnum@^0.4.2": + version "0.4.2" + resolved "https://nexus.iroha.tech/repository/npm-group/@iroha2/i64-fixnum/-/i64-fixnum-0.4.2.tgz#db7f9f35ce786b9eb0184b853a708c3b5cdd5e68" + integrity sha512-T+SRb9qzHtHeyybyTWqhPlrc6DARkiZO3eB580pIHlJsRUgcb8hrZtG6Rs6OY6KCk1prIIPOPCqPNqB6Y1kLww== + dependencies: + "@scale-codec/util" "^1.1.1" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -3823,6 +3863,34 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@scale-codec/core@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scale-codec/core/-/core-1.1.1.tgz#7ac2914ab8faccb786ac538e59517f593be8eac3" + integrity sha512-feiLqQYopj3VXOCFa7xvRexFB8Ik7NJd8L9wGoPzeux+83cnwMjBPQu2TdcKDcchOMtXZ6RyFfF3MNuun75Shg== + dependencies: + "@scale-codec/enum" "^1.1.1" + "@scale-codec/util" "^1.1.1" + +"@scale-codec/definition-runtime@^2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@scale-codec/definition-runtime/-/definition-runtime-2.2.2.tgz#502c8a752da4f31ce25091b8cc0c61a5b58b1ac9" + integrity sha512-dZ/0T5LynQRqGMU+XMmM68Hom+mvvI3xMxz09ZBgJEmTQrXkdnvpu5l8mFVWrzBNqjNCi+d6qZ2UMQFAk1NfVg== + dependencies: + "@scale-codec/core" "^1.1.1" + "@scale-codec/util" "^1.1.1" + fmt-subs "^1.1.1" + type-fest "^2.13.0" + +"@scale-codec/enum@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scale-codec/enum/-/enum-1.1.1.tgz#b50578d5c3aa8c62056e6f37682b8f5c37db94b1" + integrity sha512-GTB5LPcqjZOoWSPn90q97yMUZP6PD1Y3Lk+WnvvqkW1nOgz9vXEqTchm+eYtV23tTeh594Dg5plElA/mVHEUfQ== + +"@scale-codec/util@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scale-codec/util/-/util-1.1.1.tgz#eb76902e043a2c2b30e78ee7c34a468fe1c26579" + integrity sha512-MXKLSZHjoMOyJvHZo+AnOo6URY2cK77KS9gnEMRJRVFE9IW2Gv3lj+H+BBN5hY57dbboeNGZGfUEcQqFUKAxTw== + "@schematics/angular@13.3.2": version "13.3.2" resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-13.3.2.tgz#c8a2c49ca115e41abaa040b3ee5ee4826128471a" @@ -8707,7 +8775,7 @@ debug@4.1.1: dependencies: ms "^2.1.1" -debug@4.3.4, debug@^4.3.2: +debug@4.3.4, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -9421,7 +9489,7 @@ elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.4.1, elliptic@^6.5.2, elliptic@^6.5 minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -emittery@^0.10.2: +emittery@^0.10.1, emittery@^0.10.2: version "0.10.2" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933" integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== @@ -11123,6 +11191,11 @@ flatted@^3.1.0, flatted@^3.2.4: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +fmt-subs@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fmt-subs/-/fmt-subs-1.1.1.tgz#1c31eeaeef7c45c4c7ce0ad5a7e8aa0b090d3a60" + integrity sha512-11qUMl76/5d/gYAtvoBZacQ94DPRKtip989wKVbSb0JD1LWzNg16u47+hLVxmzDqPH74ljlxIyQlbPCAP4XEGQ== + fn.name@1.x.x: version "1.1.0" resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" @@ -12010,6 +12083,11 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" +hada@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/hada/-/hada-0.0.8.tgz#eeb69d44995fd3eca1c0dca20f6d20a26cda0c53" + integrity sha512-CLzCvrb53E6Lh1rO/NFty1PG7VJV3r4pDCU5nR/0yaUCT05lk5ayLBd/0QX7z2QH4t6xu7jflYBa9Dwoo7giZA== + handle-thing@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" @@ -21897,6 +21975,11 @@ type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== +type-fest@^2.13.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@^1.6.4, type-is@~1.6.15, type-is@~1.6.16, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -22019,6 +22102,11 @@ underscore@1.12.1, underscore@1.13.2: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.2.tgz#276cea1e8b9722a8dbed0100a407dda572125881" integrity sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g== +undici@5.10.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.10.0.tgz#dd9391087a90ccfbd007568db458674232ebf014" + integrity sha512-c8HsD3IbwmjjbLvoZuRI26TZic+TSEe8FPMLLOkN1AfYRhdjnKBU6yL+IwcSCbdZiX4e5t0lfMDLDCqj4Sq70g== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -23679,6 +23767,11 @@ ws@^8.1.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== +ws@^8.4.0: + version "8.8.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" + integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== + ws@~7.4.2: version "7.4.6" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"