From 9dd6c7171068d0ee674f478d1ac43597a4a1607b Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Tue, 29 Nov 2022 15:51:04 +0000 Subject: [PATCH] feat(connector-iroha2): add support for Iroha V2 - Add new Iroha V2 cactus connector. - Add endpoints `transact` and `query`. Both endpoints support critical subset of instructions and queries supported by the upstream javascript iroha sdk. Transaction can be awaited or can return immediately. - Add new endpoint `generate-transaction`, to create unsigned transactions that can be signed on the client side. - Add a function to iroha2-connector package to help signing iroha transactions on the client (BLP) side. - One SocketIO endpoint can be used to monitor new blocks from the ledger. - Add new helper method for signing query payload on the client side. - New connector can be used through a verifier-client interface. - All added functions are tested in functional test suites and documented. - Add test for complex scenario that involves creating new account and asset, and then transfering assets between two accounts. - Add test for parsing retrieved block data to find specific transaction hashes. - Added execution of Iroha2 tests to the CI. Additional notes: - Iroha V2 javascript packages are not available on official npm yet, had to include `.npmrc` with private npm address. I'm not sure if there's ETA of delivering these through NPM, so it might be necessary to commit it after all. Closes #2138 Depends on #2140 Signed-off-by: Michal Bajer --- .cspell.json | 5 +- .github/workflows/ci.yml | 27 + package.json | 7 +- .../README.md | 345 +++++++ .../openapitools.json | 7 + .../package.json | 79 ++ .../src/main/json/openapi.json | 727 +++++++++++++++ .../api-client/iroha2-api-client.ts | 126 +++ .../cactus-iroha-sdk-wrapper/client.ts | 879 ++++++++++++++++++ .../data-factories.ts | 121 +++ .../cactus-iroha-sdk-wrapper/query.ts | 575 ++++++++++++ .../.openapi-generator-ignore | 27 + .../typescript-axios/.openapi-generator/FILES | 5 + .../.openapi-generator/VERSION | 1 + .../generated/openapi/typescript-axios/api.ts | 821 ++++++++++++++++ .../openapi/typescript-axios/base.ts | 71 ++ .../openapi/typescript-axios/common.ts | 138 +++ .../openapi/typescript-axios/configuration.ts | 101 ++ .../openapi/typescript-axios/index.ts | 18 + .../src/main/typescript/index.ts | 1 + .../src/main/typescript/index.web.ts | 1 + .../src/main/typescript/iroha-sign-utils.ts | 86 ++ .../plugin-factory-ledger-connector.ts | 20 + .../plugin-ledger-connector-iroha2.ts | 709 ++++++++++++++ .../src/main/typescript/public-api.ts | 24 + .../src/main/typescript/utils.ts | 39 + .../generate-transaction-v1-endpoint.ts | 109 +++ .../web-services/query-v1-endpoint.ts | 106 +++ .../web-services/transact-v1-endpoint.ts | 106 +++ .../web-services/watch-blocks-v1-endpoint.ts | 162 ++++ .../integration/api-surface.test.ts | 6 + ...nerate-and-send-signed-transaction.test.ts | 606 ++++++++++++ .../iroha2-instructions-and-queries.test.ts | 713 ++++++++++++++ .../iroha2-monitoring-endpoints.test.ts | 246 +++++ .../iroha2-setup-and-basic-operations.test.ts | 437 +++++++++ .../test-helpers/iroha2-env-setup.ts | 280 ++++++ .../src/test/typescript/test-helpers/utils.ts | 18 + .../tsconfig.json | 32 + packages/cactus-verifier-client/README.md | 1 + .../typescript/get-validator-api-client.ts | 11 + packages/cactus-verifier-client/tsconfig.json | 3 + tools/docker/iroha2-all-in-one/Dockerfile | 17 +- .../docker/iroha2-all-in-one/freeze-images.sh | 19 + tools/docker/iroha2-all-in-one/healthcheck.sh | 15 +- .../iroha2-all-in-one/iroha_client_cli.sh | 0 .../iroha2-all-in-one/run-iroha-ledger.sh | 18 + .../docker/iroha2-all-in-one/supervisord.conf | 3 +- yarn.lock | 100 +- 48 files changed, 7948 insertions(+), 20 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/README.md create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/openapitools.json create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/package.json create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/json/openapi.json create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/api-client/iroha2-api-client.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/client.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/data-factories.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/query.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator-ignore create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/FILES create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/api.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/base.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/common.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/configuration.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/index.ts create mode 100755 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/index.ts create mode 100755 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/index.web.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/iroha-sign-utils.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-factory-ledger-connector.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-ledger-connector-iroha2.ts create mode 100755 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/public-api.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/utils.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/generate-transaction-v1-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/query-v1-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/transact-v1-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/api-surface.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-generate-and-send-signed-transaction.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-instructions-and-queries.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-monitoring-endpoints.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-setup-and-basic-operations.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/iroha2-env-setup.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/utils.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha2/tsconfig.json create mode 100755 tools/docker/iroha2-all-in-one/freeze-images.sh mode change 100644 => 100755 tools/docker/iroha2-all-in-one/healthcheck.sh mode change 100644 => 100755 tools/docker/iroha2-all-in-one/iroha_client_cli.sh create mode 100755 tools/docker/iroha2-all-in-one/run-iroha-ledger.sh diff --git a/.cspell.json b/.cspell.json index ea9bf98ec54..dc4f8295c4d 100644 --- a/.cspell.json +++ b/.cspell.json @@ -133,7 +133,10 @@ "vscc", "wasm", "Xdai", - "goquorum" + "goquorum", + "hada", + "undici", + "outsh" ], "dictionaries": [ "typescript,node,npm,go,rust" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fce51cde23d..184c758fcb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -820,6 +820,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/package.json b/package.json index 951f1a13c9b..98b040875da 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "run-ci": "./tools/ci.sh", "reset:node-modules": "del-cli '**/node_modules'", "reset:git": "git clean -f -X", - "reset:yarn-lock": "yarn install --update-checksums --force", + "reset:yarn-lock": "yarn run init-registries && yarn install --update-checksums --force", "reset": "run-s reset:git reset:node-modules reset:yarn-lock configure", - "configure": "yarn install --frozen-lockfile --non-interactive && yarn run build:dev:backend", + "configure": "yarn run init-registries && yarn install --frozen-lockfile --non-interactive && yarn run build:dev:backend", "install-yarn": "npm install --global yarn@1.22.17", "custom-checks": "TS_NODE_PROJECT=./tools/tsconfig.json node --trace-deprecation --experimental-modules --abort-on-uncaught-exception --loader ts-node/esm --experimental-specifier-resolution=node ./tools/custom-checks/run-custom-checks.ts", "tools:validate-bundle-names": "TS_NODE_PROJECT=./tools/tsconfig.json node --trace-deprecation --experimental-modules --abort-on-uncaught-exception --loader ts-node/esm --experimental-specifier-resolution=node ./tools/validate-bundle-names.js", @@ -68,7 +68,8 @@ "version": "npm ci && npm run build:dev && npm run build:prod && npm run test:unit", "lerna-publish-canary": "npm run run-ci && lerna publish --canary --force-publish --dist-tag $(git branch --show-current) --preid $(git branch --show-current).$(git rev-parse --short HEAD)", "lerna-publish": "lerna publish --conventional-commits --sign-git-commit --sign-git-tag", - "prepare": "husky install" + "prepare": "husky install", + "init-registries": "npm config set @iroha2:registry=https://nexus.iroha.tech/repository/npm-group/" }, "devDependencies": { "@commitlint/cli": "13.1.0", 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..601ac1d61f9 --- /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.1" + } +} 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..e8b4fef12b0 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/package.json @@ -0,0 +1,79 @@ +{ + "name": "@hyperledger/cactus-plugin-ledger-connector-iroha2", + "version": "1.1.3", + "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.1.3", + "@hyperledger/cactus-core": "1.1.3", + "@hyperledger/cactus-core-api": "1.1.3", + "@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", + "fast-safe-stringify": "2.1.1", + "sanitize-html": "2.7.0", + "undici": "5.10.0" + }, + "devDependencies": { + "@hyperledger/cactus-test-tooling": "1.1.3", + "@hyperledger/cactus-plugin-keychain-memory": "1.1.3", + "@types/express": "4.17.8", + "@types/sanitize-html": "2.6.2", + "socket.io": "4.4.1", + "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..959ad0774c2 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/json/openapi.json @@ -0,0 +1,727 @@ +{ + "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" + ] + }, + "TransactionStatusV1": { + "type": "string", + "description": "Status of Iroha V2 transaction.", + "enum": [ + "submitted", + "committed", + "rejected" + ], + "x-enum-descriptions": [ + "Transaction was submitted to the ledger - use other tools to check if it was accepted and committed.", + "Transaction was committed to the ledger.", + "Transaction was rejected." + ], + "x-enum-varnames": [ + "Submitted", + "Committed", + "Rejected" + ] + }, + "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": {} + } + } + }, + "IrohaTransactionParametersV1": { + "type": "object", + "description": "Iroha V2 transaction payload parameters", + "additionalProperties": true, + "properties": { + "ttl": { + "type": "string", + "description": "BigInt time to live.", + "nullable": false + }, + "creationTime": { + "type": "string", + "description": "BigInt creation time", + "nullable": false + }, + "nonce": { + "type": "number", + "description": "Transaction nonce", + "nullable": false + } + } + }, + "IrohaTransactionDefinitionV1": { + "type": "object", + "description": "Iroha V2 transaction definition", + "required": [ + "instruction" + ], + "additionalProperties": false, + "properties": { + "instruction": { + "oneOf": [ + { + "$ref": "#/components/schemas/IrohaInstructionRequestV1", + "nullable": false + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/IrohaInstructionRequestV1" + } + } + ] + }, + "params": { + "$ref": "#/components/schemas/IrohaTransactionParametersV1", + "description": "Transaction parameters", + "nullable": false + } + } + }, + "IrohaQueryDefinitionV1": { + "type": "object", + "description": "Iroha V2 query definition.", + "required": [ + "query" + ], + "additionalProperties": false, + "properties": { + "query": { + "type": "IrohaQuery", + "description": "Name of the query to be executed.", + "nullable": false + }, + "params": { + "description": "The list of arguments to pass with the query.", + "type": "array", + "items": {} + } + } + }, + "IrohaSignedQueryDefinitionV1": { + "type": "object", + "description": "Iroha V2 signed query definition", + "required": [ + "query", + "payload" + ], + "additionalProperties": false, + "properties": { + "query": { + "type": "IrohaQuery", + "description": "Name of the query to be executed.", + "nullable": false + }, + "payload": { + "description": "Signed query transaction binary data received from generate-transaction endpoint.", + "type": "string", + "format": "binary", + "nullable": false + } + } + }, + "TransactRequestV1": { + "type": "object", + "description": "Request to transact endpoint.", + "additionalProperties": false, + "properties": { + "signedTransaction": { + "description": "Signed transaction binary data received from generate-transaction endpoint.", + "type": "string", + "format": "binary", + "nullable": false + }, + "transaction": { + "$ref": "#/components/schemas/IrohaTransactionDefinitionV1", + "description": "New transaction definition. Caller must provide signing credential in `baseConfig`.", + "nullable": false + }, + "waitForCommit": { + "description": "Wait unitl transaction is sent and return the final status (committed / rejected)", + "type": "boolean", + "default": false, + "nullable": false + }, + "baseConfig": { + "$ref": "#/components/schemas/Iroha2BaseConfig", + "description": "Iroha V2 connection configuration.", + "nullable": false + } + } + }, + "TransactResponseV1": { + "type": "object", + "description": "Response from transaction endpoint with operation status.", + "required": [ + "hash", + "status" + ], + "properties": { + "hash": { + "type": "string", + "description": "Hexadecimal hash of the transaction sent to the ledger.", + "nullable": false + }, + "status": { + "$ref": "#/components/schemas/TransactionStatusV1", + "description": "Status of the sent transaction.", + "nullable": false + }, + "rejectReason": { + "description": "When waitForCommit was suplied and the transaction was rejected, contains the reason of the rejection.", + "type": "string", + "nullable": false + } + } + }, + "QueryRequestV1": { + "type": "object", + "description": "Request to query endpoint.", + "additionalProperties": false, + "properties": { + "query": { + "$ref": "#/components/schemas/IrohaQueryDefinitionV1", + "description": "Query definition. Caller must provide signing credential in `baseConfig`", + "nullable": false + }, + "signedQuery": { + "$ref": "#/components/schemas/IrohaSignedQueryDefinitionV1", + "description": "Query payload signed on the client side.", + "nullable": false + }, + "baseConfig": { + "$ref": "#/components/schemas/Iroha2BaseConfig", + "description": "Iroha V2 connection configuration.", + "nullable": false + } + } + }, + "QueryResponseV1": { + "type": "object", + "description": "Response with the query results.", + "required": [ + "response" + ], + "properties": { + "response": { + "description": "Query response data that varies between different queries.", + "nullable": false + } + } + }, + "GenerateTransactionRequestV1": { + "type": "object", + "description": "Request for generating transaction or query payload that can be signed on the client side.", + "additionalProperties": false, + "required": [ + "request" + ], + "properties": { + "request": { + "oneOf": [ + { + "$ref": "#/components/schemas/IrohaTransactionDefinitionV1", + "description": "New transaction definition. Caller must provide signing credential in `baseConfig`.", + "nullable": false + }, + { + "$ref": "#/components/schemas/IrohaQueryDefinitionV1", + "description": "Query definition. Caller must provide signing credential in `baseConfig`.", + "nullable": false + } + ] + }, + "baseConfig": { + "$ref": "#/components/schemas/Iroha2BaseConfig", + "description": "Iroha V2 connection configuration.", + "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/generate-transaction": { + "post": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/generate-transaction" + } + }, + "operationId": "generateTransactionV1", + "summary": "Generate transaction that can be signed locally.", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateTransactionRequestV1" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "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" + } + } + } + } + } + } + } + } +} \ No newline at end of file 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..baacebbfaa1 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/client.ts @@ -0,0 +1,879 @@ +/** + * Cactus wrapper around IrohaV2 Client and some related functions. + */ + +import { crypto } from "@iroha2/crypto-target-node"; +import { + Signer, + Torii, + setCrypto, + CreateToriiProps, + makeTransactionPayload, + executableIntoSignedTransaction, + computeTransactionHash, + makeSignedTransaction, +} 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, + TransactionPayload, + AccountId, + VersionedTransaction, + RejectionReason, + FilterBox, + PipelineEventFilter, + OptionPipelineEntityKind, + PipelineEntityKind, + OptionPipelineStatusKind, + OptionHash, +} from "@iroha2/data-model"; +import { Key, KeyPair } from "@iroha2/crypto-core"; + +// This module can't be imported unless we use `nodenext` moduleResolution +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { adapter: irohaWSAdapter } = require("@iroha2/client/web-socket/node"); + +import { + Checks, + Logger, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; + +import { bytesToHex, hexToBytes } from "hada"; +import { fetch as undiciFetch } from "undici"; + +import { CactusIrohaV2QueryClient } from "./query"; +import { + createAccountId, + createAssetId, + createAssetValue, + createIrohaValue, +} from "./data-factories"; +import { + TransactResponseV1, + TransactionStatusV1, +} from "../generated/openapi/typescript-axios"; + +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; +} + +/** + * Raw type of executableIntoSignedTransaction payloadParams parameter. + */ +type IrohaInPayloadParams = Parameters< + typeof executableIntoSignedTransaction +>[0]["payloadParams"]; + +/** + * Transaction parameters type to be send in payload. + * Comes from Iroha SDK. + */ +export type TransactionPayloadParameters = Exclude< + IrohaInPayloadParams, + undefined +>; + +/** + * 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 = []; + + /** + * Iroha Torii client used to send transactions to the ledger. + */ + public readonly irohaToriiClient: Torii; + + /** + * Iroha signer used to sign transaction with user private key and account. + */ + public readonly irohaSigner?: Signer; + + /** + * Separate interface for sending IrohaV2 queries. + */ + public readonly query: CactusIrohaV2QueryClient; + + constructor( + public readonly toriiOptions: Omit, + public readonly accountId: AccountId, + private readonly keyPair?: KeyPair, + private readonly logLevel: LogLevelDesc = "info", + ) { + Checks.truthy(toriiOptions.apiURL, "toriiOptions apiURL"); + Checks.truthy(toriiOptions.telemetryURL, "toriiOptions telemetryURL"); + Checks.truthy(accountId, "signerOptions accountId"); + + this.irohaToriiClient = new Torii({ + ...toriiOptions, + ws: irohaWSAdapter, + fetch: undiciFetch as any, + }); + + const label = this.constructor.name; + this.log = LoggerProvider.getOrCreate({ level: this.logLevel, label }); + this.log.debug(`${label} created`); + + if (keyPair) { + this.log.debug("KeyPair present, add Signer and Query function."); + this.irohaSigner = new Signer(accountId, keyPair); + } + + this.query = new CactusIrohaV2QueryClient( + this.irohaToriiClient, + this.irohaSigner ?? this.accountId, + 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); + } + + /** + * Create Iroha SDK compatible `Executable` from instructions saved in current client session. + * + * @returns Iroha `Executable` + */ + private createIrohaExecutable(): Executable { + const irohaInstructions = this.transactions.map( + (entry) => entry.instruction, + ); + this.log.info( + `Created executable with ${irohaInstructions.length} instructions.`, + ); + + return Executable("Instructions", VecInstruction(irohaInstructions)); + } + + /** + * Throw if there are no instructions in current client session. + */ + private assertTransactionsNotEmpty() { + if (this.transactions.length === 0) { + throw new Error( + "assertTransactionsNotEmpty() failed - no instructions defined!", + ); + } + } + + /** + * Get transaction payload buffer that can be signed and then sent to the ledger. + * + * @param txParams Transaction parameters. + * + * @returns Buffer of encoded `TransactionPayload` + */ + public getTransactionPayloadBuffer( + txParams?: TransactionPayloadParameters, + ): Uint8Array { + this.assertTransactionsNotEmpty(); + + const payload = makeTransactionPayload({ + accountId: this.accountId, + executable: this.createIrohaExecutable(), + ...txParams, + }); + + return TransactionPayload.toBuffer(payload); + } + + /** + * Parse IrohaV2 `RejectionReason` and return plain string rejection description. + * + * @param reason Transaction rejection from the Iroha V2 ledger. + * @returns rejection description. + */ + private getRejectionDescription(reason: RejectionReason): string { + Checks.truthy(reason, "getRejectionDescription arg reason"); + + return reason.as("Transaction").match({ + NotPermitted: (error) => { + return `NotPermitted: ${error.reason}`; + }, + UnsatisfiedSignatureCondition: (error) => { + return `UnsatisfiedSignatureCondition: ${error.reason}`; + }, + LimitCheck: (error) => { + return `LimitCheck: ${error}`; + }, + InstructionExecution: (error) => { + return `InstructionExecution: ${error.reason}`; + }, + WasmExecution: (error) => { + return `WasmExecution: ${error.reason}`; + }, + UnexpectedGenesisAccountSignature: () => { + return `UnexpectedGenesisAccountSignature`; + }, + }); + } + + /** + * Wait until transaction with given hash is validated on the ledger and return it's final status. + * + * @param txHash transaction hash in bytes format. + * @returns transaction status (`TransactResponseV1` format). + */ + private async waitForTransactionStatus( + txHash: Uint8Array, + ): Promise { + Checks.truthy(txHash, "waitForTransactionStatus arg txHash"); + const txHashHex = bytesToHex([...txHash]); + this.log.debug("waitForTransactionStatus() - hash:", txHashHex); + + const monitor = await this.irohaToriiClient.listenForEvents({ + filter: FilterBox( + "Pipeline", + PipelineEventFilter({ + entity_kind: OptionPipelineEntityKind( + "Some", + PipelineEntityKind("Transaction"), + ), + status_kind: OptionPipelineStatusKind("None"), + hash: OptionHash("Some", txHash), + }), + ), + }); + this.log.debug("waitForTransactionStatus() - monitoring started."); + + const txStatusPromise = new Promise( + (resolve, reject) => { + monitor.ee.on("error", (error) => { + this.log.warn("waitForTransactionStatus() - Received error", error); + reject(error); + }); + + monitor.ee.on("event", (event) => { + try { + const { hash, status } = event.as("Pipeline"); + const hashHex = bytesToHex([...hash]); + + status.match({ + Validating: () => { + this.log.info( + `waitForTransactionStatus() - Transaction '${hashHex}' [Validating]`, + ); + }, + Committed: () => { + const txStatus = TransactionStatusV1.Committed; + this.log.info( + `waitForTransactionStatus() - Transaction '${hashHex}' [${txStatus}]`, + ); + resolve({ + hash: hashHex, + status: txStatus, + }); + }, + Rejected: (reason) => { + const txStatus = TransactionStatusV1.Rejected; + this.log.info( + `waitForTransactionStatus() - Transaction '${hashHex}' [${txStatus}]`, + ); + resolve({ + hash: hashHex, + status: txStatus, + rejectReason: this.getRejectionDescription(reason), + }); + }, + }); + } catch (error) { + this.log.warn( + "waitForTransactionStatus() - Event handling error:", + error, + ); + reject(error); + } + }); + }, + ); + + return txStatusPromise.finally(async () => { + this.log.debug( + `Transaction ${txHashHex} status received, stop the monitoring...`, + ); + monitor.ee.clearListeners(); + await monitor.stop(); + }); + } + + /** + * Send all the stored instructions as single Iroha transaction. + * + * @param txParams Transaction parameters. + * @param waitForCommit If `true` - block and return the final transaction status. Otherwise - return immediately. + * + * @returns `TransactResponseV1` + */ + public async send( + txParams?: TransactionPayloadParameters, + waitForCommit = false, + ): Promise { + this.assertTransactionsNotEmpty(); + if (!this.irohaSigner) { + throw new Error("send() failed - no Iroha Signer, keyPair was missing"); + } + this.log.debug(this.getTransactionSummary()); + + const txPayload = makeTransactionPayload({ + accountId: this.irohaSigner.accountId, + executable: this.createIrohaExecutable(), + ...txParams, + }); + const hash = computeTransactionHash(txPayload); + let statusPromise; + if (waitForCommit) { + statusPromise = this.waitForTransactionStatus(hash); + } + + const signedTx = makeSignedTransaction(txPayload, this.irohaSigner); + await this.irohaToriiClient.submit(signedTx); + this.clear(); + + if (statusPromise) { + return await statusPromise; + } else { + return { + hash: bytesToHex([...hash]), + status: TransactionStatusV1.Submitted, + }; + } + } + + /** + * Send signed transaction payload to the ledger. + * + * @param signedPayload Encoded or plain `VersionedTransaction` + * @param waitForCommit If `true` - block and return the final transaction status. Otherwise - return immediately. + * + * @returns `TransactResponseV1` + */ + public async sendSignedPayload( + signedPayload: VersionedTransaction | ArrayBufferView, + waitForCommit = false, + ): Promise { + Checks.truthy(signedPayload, "sendSigned arg signedPayload"); + + if (ArrayBuffer.isView(signedPayload)) { + signedPayload = VersionedTransaction.fromBuffer(signedPayload); + } + + const hash = computeTransactionHash(signedPayload.as("V1").payload); + if (waitForCommit) { + const statusPromise = this.waitForTransactionStatus(hash); + await this.irohaToriiClient.submit(signedPayload); + return await statusPromise; + } else { + await this.irohaToriiClient.submit(signedPayload); + return { + hash: bytesToHex([...hash]), + status: TransactionStatusV1.Submitted, + }; + } + } + + /** + * Free all allocated resources. + * Should be called before the shutdown. + */ + public free(): void { + this.log.debug("Free CactusIrohaV2Client key pair"); + this.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..970680ad39e --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/cactus-iroha-sdk-wrapper/query.ts @@ -0,0 +1,575 @@ +/** + * Cactus wrapper around IrohaV2 Client query utilities. + * Intended to be used through `CactusIrohaV2Client` interface but can be instantiated separately if needed. + */ + +import { + Torii, + Signer, + ToriiQueryResult, + makeQueryPayload, + makeSignedQuery, +} from "@iroha2/client"; +import { + DomainId, + Expression, + QueryBox, + Value, + FindDomainById, + EvaluatesToDomainId, + IdBox, + Name as IrohaName, + FindAssetById, + EvaluatesToAssetId, + FindAssetDefinitionById, + EvaluatesToAssetDefinitionId, + FindAccountById, + EvaluatesToAccountId, + FindTransactionByHash, + EvaluatesToHash, + VecValue, + VersionedSignedQueryRequest, + QueryPayload, + AccountId, +} 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"; + +/** + * Action context for specific query. + * Contains methods for sending request or generating payloads. + */ +interface QueryContext< + QueryBoxFactory extends (...args: any[]) => QueryBox, + QueryResponseType +> { + /** + * Request query response from the ledger. + * You must provide a signer to the client (to sign the query transaction), or this method will fail. + */ + request: ( + ...params: Parameters + ) => Promise; + + /** + * Generate unsigned query request payload using provided parameters. + * Payload must be signed, and then sent to the ledger with `QueryContext` method `requestSigned`. + */ + payload: (...params: Parameters) => Promise; + + /** + * Send signed request payload to the ledger. + */ + requestSigned: ( + signedPayload: VersionedSignedQueryRequest | ArrayBufferView, + ) => Promise; +} + +/** + * 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( + public readonly irohaToriiClient: Torii, + public readonly irohaSigner: Signer | AccountId, + private readonly log: Logger, + ) { + Checks.truthy( + irohaToriiClient, + "CactusIrohaV2QueryClient irohaToriiClient", + ); + Checks.truthy(irohaSigner, "CactusIrohaV2QueryClient irohaSigner"); + Checks.truthy(log, "CactusIrohaV2QueryClient log"); + + 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)}`); + }, + }); + } + + /** + * Get signer account either from directly supplied `accountId` or `irohaSigner`. + */ + public get signerAccountId(): AccountId { + if ("accountId" in this.irohaSigner) { + return this.irohaSigner.accountId; + } else { + return this.irohaSigner; + } + } + + /** + * Factory method for query context. + * + * @param args.getQueryBox Method for creating QueryBox for specific query. + * @param args.parseQueryResponse Method for parsing `ToriiQueryResult` for specific query. + * + * @returns `QueryContext` + */ + private createQueryContext< + QueryBoxFactory extends (...args: any[]) => QueryBox, + QueryResponseType + >(args: { + getQueryBox: QueryBoxFactory; + parseQueryResponse: (result: ToriiQueryResult) => QueryResponseType; + }): QueryContext { + // Request method + const request = async (...params: Parameters) => { + if (!("accountId" in this.irohaSigner)) { + throw new Error( + "query request() failed - no irohaSigner, provide signing credentials or use different method", + ); + } + + const queryPayload = makeQueryPayload({ + accountId: this.signerAccountId, + query: args.getQueryBox(...params), + }); + const signedQuery = makeSignedQuery(queryPayload, this.irohaSigner); + + const result = await this.irohaToriiClient.request(signedQuery); + + return args.parseQueryResponse(result); + }; + + // Payload method + const payload = async (...params: Parameters) => { + const queryBox = args.getQueryBox(...params); + const queryPayload = makeQueryPayload({ + accountId: this.signerAccountId, + query: queryBox, + }); + return QueryPayload.toBuffer(queryPayload); + }; + + // RequestSigned method + const requestSigned = async ( + signedPayload: VersionedSignedQueryRequest | ArrayBufferView, + ) => { + if (ArrayBuffer.isView(signedPayload)) { + signedPayload = VersionedSignedQueryRequest.fromBuffer(signedPayload); + } + + const result = await this.irohaToriiClient.request(signedPayload); + return args.parseQueryResponse(result); + }; + + return { + request, + payload, + requestSigned, + }; + } + + // Domains + + /** + * Query all the domains details in the ledger. + * Can return a lot of data. + * + * @returns domain list + */ + public findAllDomains = this.createQueryContext({ + getQueryBox: () => { + return QueryBox("FindAllDomains", null); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findDomainById = this.createQueryContext({ + getQueryBox: (domainName: IrohaName) => { + Checks.truthy(domainName, "findDomainById arg domainName"); + return QueryBox( + "FindDomainById", + FindDomainById({ + id: EvaluatesToDomainId({ + expression: Expression( + "Raw", + Value( + "Id", + IdBox( + "DomainId", + DomainId({ + name: domainName, + }), + ), + ), + ), + }), + }), + ); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAssetDefinitionById = this.createQueryContext({ + getQueryBox: (name: IrohaName, domainName: IrohaName) => { + Checks.truthy(name, "findAssetDefinitionById arg name"); + Checks.truthy(domainName, "findAssetDefinitionById arg domainName"); + + return QueryBox( + "FindAssetDefinitionById", + FindAssetDefinitionById({ + id: EvaluatesToAssetDefinitionId({ + expression: Expression( + "Raw", + Value( + "Id", + IdBox( + "AssetDefinitionId", + createAssetDefinitionId(name, domainName), + ), + ), + ), + }), + }), + ); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAllAssetsDefinitions = this.createQueryContext({ + getQueryBox: () => { + return QueryBox("FindAllAssetsDefinitions", null); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAssetById = this.createQueryContext({ + getQueryBox: ( + assetName: IrohaName, + assetDomainName: IrohaName, + accountName: IrohaName, + accountDomainName: IrohaName, + ) => { + Checks.truthy(assetName, "findAssetById arg assetName"); + Checks.truthy(assetDomainName, "findAssetById arg assetDomainName"); + Checks.truthy(accountName, "findAssetById arg accountName"); + Checks.truthy(accountDomainName, "findAssetById arg accountDomainName"); + + return QueryBox( + "FindAssetById", + FindAssetById({ + id: EvaluatesToAssetId({ + expression: Expression( + "Raw", + Value( + "Id", + IdBox( + "AssetId", + createAssetId( + assetName, + assetDomainName, + accountName, + accountDomainName, + ), + ), + ), + ), + }), + }), + ); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAllAssets = this.createQueryContext({ + getQueryBox: () => { + return QueryBox("FindAllAssets", null); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAccountById = this.createQueryContext({ + getQueryBox: (name: IrohaName, domainName: IrohaName) => { + Checks.truthy(name, "findAccountById arg name"); + Checks.truthy(domainName, "findAccountById arg domainName"); + + return QueryBox( + "FindAccountById", + FindAccountById({ + id: EvaluatesToAccountId({ + expression: Expression( + "Raw", + Value( + "Id", + IdBox("AccountId", createAccountId(name, domainName)), + ), + ), + }), + }), + ); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAllAccounts = this.createQueryContext({ + getQueryBox: () => { + return QueryBox("FindAllAccounts", null); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAllTransactions = this.createQueryContext({ + getQueryBox: () => { + return QueryBox("FindAllTransactions", null); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findTransactionByHash = this.createQueryContext({ + getQueryBox: (hash: string | Uint8Array) => { + 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; + } + + return QueryBox( + "FindTransactionByHash", + FindTransactionByHash({ + hash: EvaluatesToHash({ + expression: Expression("Raw", Value("Hash", hashBytes)), + }), + }), + ); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAllPeers = this.createQueryContext({ + getQueryBox: () => { + return QueryBox("FindAllPeers", null); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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 findAllBlocks = this.createQueryContext({ + getQueryBox: () => { + return QueryBox("FindAllBlocks", null); + }, + parseQueryResponse: (result: ToriiQueryResult) => { + 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..804440660c7 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION @@ -0,0 +1 @@ +5.2.1 \ 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..7fa12b09818 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -0,0 +1,821 @@ +/* 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; +} +/** + * Request for generating transaction or query payload that can be signed on the client side. + * @export + * @interface GenerateTransactionRequestV1 + */ +export interface GenerateTransactionRequestV1 { + /** + * + * @type {IrohaTransactionDefinitionV1 | IrohaQueryDefinitionV1} + * @memberof GenerateTransactionRequestV1 + */ + request: IrohaTransactionDefinitionV1 | IrohaQueryDefinitionV1; + /** + * + * @type {Iroha2BaseConfig} + * @memberof GenerateTransactionRequestV1 + */ + baseConfig?: Iroha2BaseConfig; +} +/** + * 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' +} + +/** + * Iroha V2 query definition. + * @export + * @interface IrohaQueryDefinitionV1 + */ +export interface IrohaQueryDefinitionV1 { + /** + * Name of the query to be executed. + * @type {IrohaQuery} + * @memberof IrohaQueryDefinitionV1 + */ + query: IrohaQuery; + /** + * The list of arguments to pass with the query. + * @type {Array} + * @memberof IrohaQueryDefinitionV1 + */ + params?: Array; +} +/** + * Iroha V2 signed query definition + * @export + * @interface IrohaSignedQueryDefinitionV1 + */ +export interface IrohaSignedQueryDefinitionV1 { + /** + * Name of the query to be executed. + * @type {IrohaQuery} + * @memberof IrohaSignedQueryDefinitionV1 + */ + query: IrohaQuery; + /** + * Signed query transaction binary data received from generate-transaction endpoint. + * @type {any} + * @memberof IrohaSignedQueryDefinitionV1 + */ + payload: any; +} +/** + * Iroha V2 transaction definition + * @export + * @interface IrohaTransactionDefinitionV1 + */ +export interface IrohaTransactionDefinitionV1 { + /** + * + * @type {IrohaInstructionRequestV1 | Array} + * @memberof IrohaTransactionDefinitionV1 + */ + instruction: IrohaInstructionRequestV1 | Array; + /** + * + * @type {IrohaTransactionParametersV1} + * @memberof IrohaTransactionDefinitionV1 + */ + params?: IrohaTransactionParametersV1; +} +/** + * Iroha V2 transaction payload parameters + * @export + * @interface IrohaTransactionParametersV1 + */ +export interface IrohaTransactionParametersV1 { + [key: string]: object | any; + + /** + * BigInt time to live. + * @type {string} + * @memberof IrohaTransactionParametersV1 + */ + ttl?: string; + /** + * BigInt creation time + * @type {string} + * @memberof IrohaTransactionParametersV1 + */ + creationTime?: string; + /** + * Transaction nonce + * @type {number} + * @memberof IrohaTransactionParametersV1 + */ + nonce?: number; +} +/** + * 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 { + /** + * + * @type {IrohaQueryDefinitionV1} + * @memberof QueryRequestV1 + */ + query?: IrohaQueryDefinitionV1; + /** + * + * @type {IrohaSignedQueryDefinitionV1} + * @memberof QueryRequestV1 + */ + signedQuery?: IrohaSignedQueryDefinitionV1; + /** + * + * @type {Iroha2BaseConfig} + * @memberof QueryRequestV1 + */ + baseConfig?: Iroha2BaseConfig; +} +/** + * Response with the 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. + * @export + * @interface TransactRequestV1 + */ +export interface TransactRequestV1 { + /** + * Signed transaction binary data received from generate-transaction endpoint. + * @type {any} + * @memberof TransactRequestV1 + */ + signedTransaction?: any; + /** + * + * @type {IrohaTransactionDefinitionV1} + * @memberof TransactRequestV1 + */ + transaction?: IrohaTransactionDefinitionV1; + /** + * Wait unitl transaction is sent and return the final status (committed / rejected) + * @type {boolean} + * @memberof TransactRequestV1 + */ + waitForCommit?: boolean; + /** + * + * @type {Iroha2BaseConfig} + * @memberof TransactRequestV1 + */ + baseConfig?: Iroha2BaseConfig; +} +/** + * Response from transaction endpoint with operation status. + * @export + * @interface TransactResponseV1 + */ +export interface TransactResponseV1 { + /** + * Hexadecimal hash of the transaction sent to the ledger. + * @type {string} + * @memberof TransactResponseV1 + */ + hash: string; + /** + * + * @type {TransactionStatusV1} + * @memberof TransactResponseV1 + */ + status: TransactionStatusV1; + /** + * When waitForCommit was suplied and the transaction was rejected, contains the reason of the rejection. + * @type {string} + * @memberof TransactResponseV1 + */ + rejectReason?: string; +} +/** + * Status of Iroha V2 transaction. + * @export + * @enum {string} + */ + +export enum TransactionStatusV1 { + /** + * Transaction was submitted to the ledger - use other tools to check if it was accepted and committed. + */ + Submitted = 'submitted', + /** + * Transaction was committed to the ledger. + */ + Committed = 'committed', + /** + * Transaction was rejected. + */ + Rejected = 'rejected' +} + +/** + * 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 Generate transaction that can be signed locally. + * @param {GenerateTransactionRequestV1} [generateTransactionRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + generateTransactionV1: async (generateTransactionRequestV1?: GenerateTransactionRequestV1, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/generate-transaction`; + // 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(generateTransactionRequestV1, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @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 Generate transaction that can be signed locally. + * @param {GenerateTransactionRequestV1} [generateTransactionRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async generateTransactionV1(generateTransactionRequestV1?: GenerateTransactionRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.generateTransactionV1(generateTransactionRequestV1, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @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 Generate transaction that can be signed locally. + * @param {GenerateTransactionRequestV1} [generateTransactionRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + generateTransactionV1(generateTransactionRequestV1?: GenerateTransactionRequestV1, options?: any): AxiosPromise { + return localVarFp.generateTransactionV1(generateTransactionRequestV1, options).then((request) => request(axios, basePath)); + }, + /** + * + * @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 Generate transaction that can be signed locally. + * @param {GenerateTransactionRequestV1} [generateTransactionRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public generateTransactionV1(generateTransactionRequestV1?: GenerateTransactionRequestV1, options?: any) { + return DefaultApiFp(this.configuration).generateTransactionV1(generateTransactionRequestV1, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @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/iroha-sign-utils.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/iroha-sign-utils.ts new file mode 100644 index 00000000000..3f99533e270 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/iroha-sign-utils.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Signing utility functions for HL Iroha V2 ledger. + * Remember to free key objects supplied to the signing methods. + */ + +import { Signer, makeSignedTransaction, makeSignedQuery } from "@iroha2/client"; +import { + QueryPayload, + TransactionPayload, + VersionedSignedQueryRequest, + VersionedTransaction, +} from "@iroha2/data-model"; + +import { generateIrohaV2KeyPair } from "./cactus-iroha-sdk-wrapper/client"; +import { createAccountId } from "./cactus-iroha-sdk-wrapper/data-factories"; +import { Iroha2KeyPair } from "./public-api"; + +/** + * Create IrohaV2 SDK Signer object. + * + * @param accountName signer account name. + * @param domainName signer account domain. + * @param keyPair public and private keys to sign with. + * + * @returns Signer for supplied credentials. + */ +function getSigner( + accountName: string, + domainName: string, + keyPair: Iroha2KeyPair, +): Signer { + const account = createAccountId(accountName, domainName); + const irohaKeyPair = generateIrohaV2KeyPair( + keyPair.publicKey, + keyPair.privateKey, + ); + + return new Signer(account, irohaKeyPair); +} + +/** + * Sign transaction binary received from `GenerateTransactionV1` endpoint. + * + * @param serializedTx serialized unsigned transaction from the connector (`Uint8Array`). + * @param accountName signer account name. + * @param domainName signer account domain. + * @param keyPair public and private keys to sign with. + * + * @returns serialied signed transaction ready to be sent (`Uint8Array`) + */ +export function signIrohaV2Transaction( + serializedTx: Uint8Array, + accountName: string, + domainName: string, + keyPair: Iroha2KeyPair, +): Uint8Array { + const unsignedTx = TransactionPayload.fromBuffer(serializedTx); + const signer = getSigner(accountName, domainName, keyPair); + const signedTx = makeSignedTransaction(unsignedTx, signer); + return VersionedTransaction.toBuffer(signedTx); +} + +/** + * Sign query payload received from `GenerateTransactionV1` endpoint. + * + * @param serializedQuery serialized unsigned query from the connector (`Uint8Array`). + * @param accountName signer account name. + * @param domainName signer account domain. + * @param keyPair public and private keys to sign with. + * + * @returns serialied signed transaction ready to be sent (`Uint8Array`) + */ +export function signIrohaV2Query( + serializedQuery: Uint8Array, + accountName: string, + domainName: string, + keyPair: Iroha2KeyPair, +): Uint8Array { + const unsignedQueryReq = QueryPayload.fromBuffer(serializedQuery); + const signer = getSigner(accountName, domainName, keyPair); + const queryReq = makeSignedQuery(unsignedQueryReq, signer); + return VersionedSignedQueryRequest.toBuffer(queryReq); +} 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..2efdb923d03 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/plugin-ledger-connector-iroha2.ts @@ -0,0 +1,709 @@ +/** + * 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, + GenerateTransactionRequestV1, + IrohaTransactionParametersV1, +} from "./generated/openapi/typescript-axios"; + +import { Iroha2TransactEndpointV1 } from "./web-services/transact-v1-endpoint"; +import { Iroha2QueryEndpointV1 } from "./web-services/query-v1-endpoint"; +import { Iroha2WatchBlocksEndpointV1 } from "./web-services/watch-blocks-v1-endpoint"; +import { Iroha2GenerateTransactionEndpointV1 } from "./web-services/generate-transaction-v1-endpoint"; + +import { KeyPair } from "@iroha2/crypto-core"; +import { + CactusIrohaV2Client, + generateIrohaV2KeyPair, + TransactionPayloadParameters, +} from "./cactus-iroha-sdk-wrapper/client"; +import { CactusIrohaV2QueryClient } from "./cactus-iroha-sdk-wrapper/query"; +import { LengthOf, stringifyBigIntReplacer } from "./utils"; +import { createAccountId } from "./cactus-iroha-sdk-wrapper/data-factories"; + +/** + * 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.createClient(options.baseConfig); + + // Start monitoring + const monitor = new Iroha2WatchBlocksEndpointV1({ + socket, + logLevel: this.options.logLevel, + torii: cactusIrohaClient.irohaToriiClient, + }); + 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 Iroha2TransactEndpointV1({ + connector: this, + logLevel: this.options.logLevel, + }), + new Iroha2QueryEndpointV1({ + connector: this, + logLevel: this.options.logLevel, + }), + new Iroha2GenerateTransactionEndpointV1({ + 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 createClient( + baseConfig?: Iroha2BaseConfig, + ): Promise { + if (!baseConfig && !this.defaultConfig) { + throw new Error( + "createClient() 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 + if (!mergedConfig.accountId) { + throw new Error("accountId is missing in combined configuration"); + } + const accountId = createAccountId( + mergedConfig.accountId.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, + keyPair, + this.options.logLevel, + ); + } + + /** + * Get context for specific query (e.g. it `request` or `payload` methods.). + * + * @param client `CactusIrohaV2QueryClient` instance. + * @param query Name of the query (`IrohaQuery`). + * + * @returns Query Context + */ + public getQueryContext( + client: CactusIrohaV2QueryClient, + query: IrohaQuery, + ): { + request: (...params: any[]) => Promise; + payload: (...params: any[]) => Promise; + requestSigned: (signedPayload: ArrayBufferView) => Promise; + } { + switch (query) { + case IrohaQuery.FindAllDomains: + return client.findAllDomains; + case IrohaQuery.FindDomainById: + return client.findDomainById; + case IrohaQuery.FindAssetDefinitionById: + return client.findAssetDefinitionById; + case IrohaQuery.FindAllAssetsDefinitions: + return client.findAllAssetsDefinitions; + case IrohaQuery.FindAssetById: + return client.findAssetById; + case IrohaQuery.FindAllAssets: + return client.findAllAssets; + case IrohaQuery.FindAllPeers: + return client.findAllPeers; + case IrohaQuery.FindAccountById: + return client.findAccountById; + case IrohaQuery.FindAllAccounts: + return client.findAllAccounts; + case IrohaQuery.FindAllTransactions: + return client.findAllTransactions; + case IrohaQuery.FindTransactionByHash: + return client.findTransactionByHash; + case IrohaQuery.FindAllBlocks: + return client.findAllBlocks; + default: + const unknownType: never = query; + throw new Error( + `Unknown IrohaV2 query - '${unknownType}'. Check name and connector version.`, + ); + } + } + + /** + * 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 { + const validatedArgs = this.checkArgsCount( + params, + expectedCount, + transactFunction.name, + ); + transactFunction.apply(client, validatedArgs); + } + + /** + * Loop through each instruction in `reqInstruction` and add them to current transaction session in `client`. + * + * @param client `CactusIrohaV2Client` instance. + * @param reqInstruction Single or list of Iroha instructions to be added. + */ + private processInstructionsRequests( + client: CactusIrohaV2Client, + reqInstruction: IrohaInstructionRequestV1 | IrohaInstructionRequestV1[], + ): void { + Checks.truthy(client, "processInstructionsRequests client"); + Checks.truthy( + reqInstruction, + "processInstructionsRequests instructions in request", + ); + + // Convert single instruction scenario to list with one element + // (both single and multiple instructions are supported) + let instructions: IrohaInstructionRequestV1[]; + if (Array.isArray(reqInstruction)) { + instructions = reqInstruction; + } else { + instructions = [reqInstruction]; + } + + // Each command adds an instruction to a list included in the final transaction. + 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.`, + ); + } + }); + } + + /** + * Try parsing transaction parameter from request to IrohaV2 SDK compatible format. + * If input is empty, undefined is returned. + * + * @param reqParams transaction parameters from connector request. + * @returns `TransactionPayloadParameters` or `undefined` + */ + private tryParseTransactionParams( + reqParams?: IrohaTransactionParametersV1, + ): TransactionPayloadParameters | undefined { + if (!reqParams) { + return undefined; + } + + return { + ttl: reqParams.ttl ? BigInt(reqParams.ttl) : undefined, + creationTime: reqParams.creationTime + ? BigInt(reqParams.creationTime) + : undefined, + nonce: reqParams.nonce, + }; + } + + /** + * Transact endpoint logic. + * To submit transaction you must provide either signed transaction payload or list of instructions with signingCredential. + * + * @param req Request object. + * + * @returns Status of the operation. + */ + public async transact(req: TransactRequestV1): Promise { + const client = await this.createClient(req.baseConfig); + + try { + if (req.transaction) { + this.processInstructionsRequests(client, req.transaction.instruction); + return await client.send( + this.tryParseTransactionParams(req.transaction.params), + req.waitForCommit, + ); + } else if (req.signedTransaction) { + const transactionBinary = Uint8Array.from( + Object.values(req.signedTransaction), + ); + return await client.sendSignedPayload( + transactionBinary, + req.waitForCommit, + ); + } else { + throw new Error( + "To submit transaction you must provide either signed transaction payload or list of instructions with signingCredential", + ); + } + } finally { + client.free(); + } + } + + /** + * Query endpoint logic. + * To send query request you must provide either signed query payload or query definition with signingCredential. + * + * @param req Request object. + * + * @returns Response from the query. + */ + public async query(req: QueryRequestV1): Promise { + const client = await this.createClient(req.baseConfig); + + try { + if (req.query) { + const queryContext = this.getQueryContext( + client.query, + req.query.query, + ); + + const params = req.query.params ?? []; + return { + response: await queryContext.request(...params), + }; + } else if (req.signedQuery) { + const queryContext = this.getQueryContext( + client.query, + req.signedQuery.query, + ); + + const queryBinary = Uint8Array.from( + Object.values(req.signedQuery.payload), + ); + + return { + response: await queryContext.requestSigned(queryBinary), + }; + } else { + throw new Error( + "To submit transaction you must provide either signed transaction payload or query description with signingCredential", + ); + } + } finally { + client.free(); + } + } + + /** + * Query endpoint logic. + * + * @param req Request object. + * @returns Binary unsigned transaction. + */ + public async generateTransaction( + req: GenerateTransactionRequestV1, + ): Promise { + const client = await this.createClient(req.baseConfig); + + try { + if ("instruction" in req.request) { + this.processInstructionsRequests(client, req.request.instruction); + return client.getTransactionPayloadBuffer( + this.tryParseTransactionParams(req.request.params), + ); + } else if ("query" in req.request) { + const queryContext = this.getQueryContext( + client.query, + req.request.query, + ); + + const params = req.request.params ?? []; + return await queryContext.payload(...params); + } else { + throw new Error("Missing required transaction or query definition"); + } + } 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..b80cc9725da --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/public-api.ts @@ -0,0 +1,24 @@ +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"; + +export { signIrohaV2Transaction } from "./iroha-sign-utils"; 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/generate-transaction-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/generate-transaction-v1-endpoint.ts new file mode 100644 index 00000000000..78e842fc197 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/generate-transaction-v1-endpoint.ts @@ -0,0 +1,109 @@ +/** + * ExpressJS `generateTransaction` 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 IGenerateTransactionOptions { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorIroha2; +} + +export class Iroha2GenerateTransactionEndpointV1 + implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "GenerateTransaction"; + + private readonly log: Logger; + + public get className(): string { + return Iroha2GenerateTransactionEndpointV1.CLASS_NAME; + } + + constructor(public readonly options: IGenerateTransactionOptions) { + 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/generate-transaction" + ]; + } + + 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); + + try { + const txPayload = await this.options.connector.generateTransaction( + req.body, + ); + res.send(txPayload); + } 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/query-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/query-v1-endpoint.ts new file mode 100644 index 00000000000..7324115177a --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/query-v1-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 IIroha2QueryEndpointV1Options { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorIroha2; +} + +export class Iroha2QueryEndpointV1 implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "Iroha2QueryEndpointV1"; + + private readonly log: Logger; + + public get className(): string { + return Iroha2QueryEndpointV1.CLASS_NAME; + } + + constructor(public readonly options: IIroha2QueryEndpointV1Options) { + 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-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/transact-v1-endpoint.ts new file mode 100644 index 00000000000..4985546d3bd --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/transact-v1-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 IIroha2TransactEndpointV1Options { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorIroha2; +} + +export class Iroha2TransactEndpointV1 implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "Iroha2TransactEndpointV1"; + + private readonly log: Logger; + + public get className(): string { + return Iroha2TransactEndpointV1.CLASS_NAME; + } + + constructor(public readonly options: IIroha2TransactEndpointV1Options) { + 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..474a3efc6bb --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts @@ -0,0 +1,162 @@ +/** + * SocketIO `WatchBlocks` endpoint + */ + +import type { 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 { Torii as ToriiClient } from "@iroha2/client"; + +import safeStringify from "fast-safe-stringify"; +import { VersionedCommittedBlock } from "@iroha2/data-model"; + +/** + * Iroha2WatchBlocksEndpointV1 configuration. + */ +export interface IIroha2WatchBlocksEndpointV1Configuration { + logLevel?: LogLevelDesc; + socket: SocketIoSocket; + torii: ToriiClient; +} + +/** + * Endpoint to watch for new blocks on Iroha V2 ledger and report them + * to the client using socketio. + */ +export class Iroha2WatchBlocksEndpointV1 { + public readonly className = "Iroha2WatchBlocksEndpointV1"; + private readonly log: Logger; + private readonly torii: ToriiClient; + private readonly socket: SocketIoSocket< + Record void> + >; + + constructor( + public readonly config: IIroha2WatchBlocksEndpointV1Configuration, + ) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(config, `${fnTag} arg options`); + Checks.truthy(config.socket, `${fnTag} arg options.socket`); + Checks.truthy(config.torii, `${fnTag} arg options.client`); + + this.socket = config.socket; + this.torii = config.torii; + + 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 { torii, 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 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/iroha2-generate-and-send-signed-transaction.test.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-generate-and-send-signed-transaction.test.ts new file mode 100644 index 00000000000..547e49c4b1f --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-generate-and-send-signed-transaction.test.ts @@ -0,0 +1,606 @@ +/** + * Tests for sending transactions to Iroha V2 without sharing private key with the connector. + * Transactions are signed on the client side. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Log settings +const testLogLevel: LogLevelDesc = "info"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, +} from "@hyperledger/cactus-common"; + +import { + IrohaInstruction, + IrohaQuery, + signIrohaV2Transaction, + TransactionStatusV1, +} from "../../../main/typescript/public-api"; +import { + generateTestIrohaCredentials, + IrohaV2TestEnv, + waitForCommit, +} from "../test-helpers/iroha2-env-setup"; +import { addRandomSuffix } from "../test-helpers/utils"; + +import "jest-extended"; +import { TransactionPayload } from "@iroha2/data-model"; +import { signIrohaV2Query } from "../../../main/typescript/iroha-sign-utils"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "generate-and-send-signed-transaction.test", + level: testLogLevel, +}); + +/** + * Main test suite + */ +describe("Generate and send signed transaction tests", () => { + let env: IrohaV2TestEnv; + + beforeAll(async () => { + env = new IrohaV2TestEnv(log); + await env.start(); + }); + + afterAll(async () => { + if (env) { + await env.stop(); + } + }); + + ////////////////////////////////// + // Test Helpers + ////////////////////////////////// + + /** + * Helper function to check if domain with specified `domainName` was correctly created. + * Asserts the response from the connector query. + * + * @param domainName Name of the domain existing on test ledger. + */ + async function assertDomainExistence(domainName: string): Promise { + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindDomainById, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + 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); + } + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + /** + * Create new domain with regular `transact` request to assert the ledger is working correctly. + */ + test("Sanity check if regular create domain transaction works", async () => { + const domainName = addRandomSuffix("sanityTestDomain"); + expect(domainName).toBeTruthy(); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + + // Check if domain was created + await assertDomainExistence(domainName); + }); + + /** + * Create new domain with transaction signed on the client side. + */ + test("Sign transaction on the client (BLP) side", async () => { + const domainName = addRandomSuffix("genNewDomainTx"); + expect(domainName).toBeTruthy(); + + // 1. Generate transaction + const genTxResponse = await env.apiClient.generateTransactionV1({ + request: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + baseConfig: env.defaultBaseConfig, + }); + expect(genTxResponse).toBeTruthy(); + expect(genTxResponse.data).toBeTruthy(); + expect(genTxResponse.status).toEqual(200); + const unsignedTransaction = Uint8Array.from( + Object.values(genTxResponse.data), + ); + expect(unsignedTransaction).toBeTruthy(); + log.info("Received unsigned transcation"); + log.debug("unsignedTransaction:", unsignedTransaction); + + // 2. Sign + const signerAccountId = env.defaultBaseConfig.accountId; + if (!signerAccountId) { + throw new Error("No signer account ID in test environment"); + } + + const signedTransaction = signIrohaV2Transaction( + unsignedTransaction, + signerAccountId.name, + signerAccountId.domainId, + env.keyPairCredential, + ); + expect(signedTransaction).toBeTruthy(); + log.info("Transaction signed with local private key"); + log.debug("signedTransaction:", signedTransaction); + + // 3. Send + const transactionResponse = await env.apiClient.transactV1({ + signedTransaction, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Submitted, + ); + + // Sleep + await waitForCommit(); + + // Check if domain was created + await assertDomainExistence(domainName); + }); + + /** + * Test passing transaction parameters. + */ + test("Transaction parameters sent in request are included in generated payload", async () => { + const domainName = addRandomSuffix("txParamsDomain"); + expect(domainName).toBeTruthy(); + const inputParams = { + ttl: "300000", + creationTime: Date.now().toString(), + nonce: 123, + }; + + // Generate transaction with tx params + const genTxResponse = await env.apiClient.generateTransactionV1({ + request: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + params: inputParams, + }, + baseConfig: env.defaultBaseConfig, + }); + + // Assert generateTransactionV1 response + expect(genTxResponse).toBeTruthy(); + expect(genTxResponse.data).toBeTruthy(); + expect(genTxResponse.status).toEqual(200); + const unsignedTransaction = Uint8Array.from( + Object.values(genTxResponse.data), + ); + expect(unsignedTransaction).toBeTruthy(); + log.info("Received unsigned transcation"); + + // Assert generated transaction structure + const decodedTx = TransactionPayload.fromBuffer(unsignedTransaction); + expect(decodedTx).toBeTruthy(); + expect(BigInt(decodedTx.creation_time).toString()).toEqual( + inputParams.creationTime, + ); + expect(BigInt(decodedTx.time_to_live_ms).toString()).toEqual( + inputParams.ttl, + ); + expect(decodedTx.nonce.value).toEqual(inputParams.nonce); + }); + + /** + * Create new domain and query it with query request signed on the client side. + */ + test("Sign query on the client (BLP) side", async () => { + const domainName = addRandomSuffix("querySignDomain"); + expect(domainName).toBeTruthy(); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + + // 1. Generate query request payload + const genQueryResponse = await env.apiClient.generateTransactionV1({ + request: { + query: IrohaQuery.FindDomainById, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(genQueryResponse).toBeTruthy(); + expect(genQueryResponse.data).toBeTruthy(); + expect(genQueryResponse.status).toEqual(200); + const unsignedQueryReq = Uint8Array.from( + Object.values(genQueryResponse.data), + ); + expect(unsignedQueryReq).toBeTruthy(); + log.info("Received unsigned query request"); + log.debug("unsignedQueryReq:", unsignedQueryReq); + + // 2. Sign + const signerAccountId = env.defaultBaseConfig.accountId; + if (!signerAccountId) { + throw new Error("No signer account ID in test environment"); + } + + const signedQueryReq = signIrohaV2Query( + unsignedQueryReq, + signerAccountId.name, + signerAccountId.domainId, + env.keyPairCredential, + ); + expect(signedQueryReq).toBeTruthy(); + log.info("Query request signed with a local private key"); + log.debug("signedQueryReq:", signedQueryReq); + + // 3. Send + const queryResponse = await env.apiClient.queryV1({ + signedQuery: { + query: IrohaQuery.FindDomainById, + payload: signedQueryReq, + }, + baseConfig: env.defaultBaseConfig, + }); + 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 generateTransactionV1 transaction error handling + */ + test("generateTransactionV1 returns error for invalid transaction content", async () => { + const domainName = addRandomSuffix("errorCheckDomain"); + expect(domainName).toBeTruthy(); + + // Generate transaction with wrong instruction + try { + await env.apiClient.generateTransactionV1({ + request: { + instruction: { + name: "foo" as IrohaInstruction, + params: [domainName], + }, + }, + baseConfig: env.defaultBaseConfig, + }); + expect(false).toBe(true); // should always throw by now + } catch (err: any) { + expect(err.response.status).toBe(500); + expect(err.response.data.message).toEqual("Internal Server Error"); + expect(err.response.data.error).toBeTruthy(); + } + }); + + /** + * Test generateTransactionV1 query error handling + */ + test("generateTransactionV1 returns error for invalid query parameter number", async () => { + // Query domain without it's ID + try { + await env.apiClient.generateTransactionV1({ + request: { + query: IrohaQuery.FindDomainById, + params: [], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(false).toBe(true); // should always throw by now + } catch (err: any) { + expect(err.response.status).toBe(500); + expect(err.response.data.message).toEqual("Internal Server Error"); + expect(err.response.data.error).toBeTruthy(); + } + }); + + /** + * Complex test for account creation, asset definition and finally an asset transfer. + */ + test("Complex asset transfer between accounts", async () => { + // 1. Register new account - bob + const bobDomain = "wonderland"; + const bobName = addRandomSuffix("bob"); + expect(bobName).toBeTruthy(); + + const bobCredentials = generateTestIrohaCredentials(); + const registerAccountResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterAccount, + params: [ + bobName, + bobDomain, + bobCredentials.publicKey, + bobCredentials.privateKey.digestFunction, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAccountResponse).toBeTruthy(); + expect(registerAccountResponse.status).toEqual(200); + expect(registerAccountResponse.data.rejectReason).toBeUndefined(); + expect(registerAccountResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + log.info(`User '${bobName}' registered.`); + + // 2. Confirm account bob was created + const accountQueryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAccountById, + params: [bobName, bobDomain], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(accountQueryResponse.data).toBeTruthy(); + const responseData = accountQueryResponse.data.response; + expect(responseData).toBeTruthy(); + expect(responseData.id.name).toEqual(bobName); + expect(responseData.id.domain_id.name).toEqual(bobDomain); + + // 3. Create new asset + const assetOwnerName = env.defaultBaseConfig.accountId?.name; + expect(assetOwnerName).toBeTruthy(); + const assetOwnerDomainName = env.defaultBaseConfig.accountId?.domainId; + expect(assetOwnerDomainName).toBeTruthy(); + const assetName = addRandomSuffix("aliceGold"); + expect(assetName).toBeTruthy(); + const assetDomain = assetOwnerDomainName; + expect(assetDomain).toBeTruthy(); + const valueType = "Quantity"; + const initAssetValue = 100; + const mintable = "Infinitely"; + + const registerAssetDefResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterAssetDefinition, + params: [assetName, assetDomain, valueType, mintable], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAssetDefResponse).toBeTruthy(); + expect(registerAssetDefResponse.status).toEqual(200); + expect(registerAssetDefResponse.data.rejectReason).toBeUndefined(); + expect(registerAssetDefResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + + const registerAssetResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterAsset, + params: [ + assetName, + assetDomain, + assetOwnerName, + assetOwnerDomainName, + initAssetValue, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAssetResponse).toBeTruthy(); + expect(registerAssetResponse.status).toEqual(200); + expect(registerAssetResponse.data.rejectReason).toBeUndefined(); + expect(registerAssetResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + log.info( + `Asset '${assetName}#${assetDomain}' registered. Initial value: ${initAssetValue}`, + ); + + // 4. Initial transfer of asset from alice to bob + const transferValue = 10; + + const initTransferResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.TransferAsset, + params: [ + assetName, + assetDomain, + assetOwnerName, + assetOwnerDomainName, + bobName, + bobDomain, + transferValue, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(initTransferResponse).toBeTruthy(); + expect(initTransferResponse.status).toEqual(200); + expect(initTransferResponse.data.rejectReason).toBeUndefined(); + expect(initTransferResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + log.info("Initial transfer of asset done."); + + // 5. Confirm asset balance on both accounts + const initAliceBalanceQueryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAssetById, + params: [assetName, assetDomain, assetOwnerName, assetOwnerDomainName], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(initAliceBalanceQueryResponse).toBeTruthy(); + expect(initAliceBalanceQueryResponse.data).toBeTruthy(); + const initAliceBalance = + initAliceBalanceQueryResponse.data.response.value.value; + log.info( + "Alice (source) balance after initial transfer:", + initAliceBalance, + ); + expect(initAliceBalance).toEqual(initAssetValue - transferValue); + + const initBobBalanceQueryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAssetById, + params: [assetName, assetDomain, bobName, bobDomain], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(initBobBalanceQueryResponse).toBeTruthy(); + expect(initBobBalanceQueryResponse.data).toBeTruthy(); + const initBobBalance = + initBobBalanceQueryResponse.data.response.value.value; + log.info("Bob (target) balance after initial transfer:", initBobBalance); + expect(initBobBalance).toEqual(transferValue); + + // 6. Generate and sign transaction to transfer half of bob assets back to alice + const transferBackValue = transferValue / 2; + log.info("Transfer back to alice asset amount:", transferBackValue); + + const bobConfig = { + ...env.defaultBaseConfig, + accountId: { + name: bobName, + domainId: bobDomain, + }, + }; + + const genTxResponse = await env.apiClient.generateTransactionV1({ + request: { + instruction: { + name: IrohaInstruction.TransferAsset, + params: [ + assetName, + assetDomain, + bobName, + bobDomain, + assetOwnerName, + assetOwnerDomainName, + transferBackValue, + ], + }, + }, + baseConfig: bobConfig, + }); + expect(genTxResponse).toBeTruthy(); + expect(genTxResponse.data).toBeTruthy(); + expect(genTxResponse.status).toEqual(200); + const unsignedTransaction = Uint8Array.from( + Object.values(genTxResponse.data), + ); + expect(unsignedTransaction).toBeTruthy(); + log.info("Received unsigned transcation"); + log.debug("unsignedTransaction:", unsignedTransaction); + + const signedTransaction = signIrohaV2Transaction( + unsignedTransaction, + bobName, + bobDomain, + { + publicKey: bobCredentials.publicKeyMultihash, + privateKey: bobCredentials.privateKey, + }, + ); + expect(signedTransaction).toBeTruthy(); + log.info("Transaction signed with bob private key"); + log.debug("signedTransaction:", signedTransaction); + + // 7. Send signed transfer transaction + const transactionResponse = await env.apiClient.transactV1({ + signedTransaction, + waitForCommit: true, + baseConfig: bobConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + + // 8. Confirm final asset balance on both accounts + const finalAliceBalanceQueryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAssetById, + params: [assetName, assetDomain, assetOwnerName, assetOwnerDomainName], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(finalAliceBalanceQueryResponse).toBeTruthy(); + expect(finalAliceBalanceQueryResponse.data).toBeTruthy(); + const finalAliceBalance = + finalAliceBalanceQueryResponse.data.response.value.value; + log.info("Alice (target) balance after final transfer:", finalAliceBalance); + expect(finalAliceBalance).toEqual( + initAssetValue - transferValue + transferBackValue, + ); + + const finalBobBalanceQueryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAssetById, + params: [assetName, assetDomain, bobName, bobDomain], + }, + baseConfig: env.defaultBaseConfig, + }); + expect(finalBobBalanceQueryResponse).toBeTruthy(); + expect(finalBobBalanceQueryResponse.data).toBeTruthy(); + const finalBobBalance = + finalBobBalanceQueryResponse.data.response.value.value; + log.info("Bob (source) balance after final transfer:", finalBobBalance); + expect(finalBobBalance).toEqual(transferValue - transferBackValue); + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-instructions-and-queries.test.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-instructions-and-queries.test.ts new file mode 100644 index 00000000000..ba016005595 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-instructions-and-queries.test.ts @@ -0,0 +1,713 @@ +/** + * Tests for executing Iroha instructions and queries on Iroha V2 through the cactus connector. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Log settings +const testLogLevel: LogLevelDesc = "info"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, +} from "@hyperledger/cactus-common"; + +import { + IrohaInstruction, + IrohaQuery, + Iroha2KeyPair, + TransactionStatusV1, +} from "../../../main/typescript/public-api"; +import { + IrohaV2TestEnv, + generateTestIrohaCredentials, +} 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({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(transactionResponse.data.hash).toBeTruthy(); + }); + + test("Query single domain (FindDomainById)", async () => { + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindDomainById, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: 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({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [newAccountDomainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerDomainResponse).toBeTruthy(); + expect(registerDomainResponse.status).toEqual(200); + expect(registerDomainResponse.data.rejectReason).toBeUndefined(); + expect(registerDomainResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(registerDomainResponse.data.hash).toBeTruthy(); + + // Generate new account credentials + newAccountCredentials = generateTestIrohaCredentials(); + + // Register new account + const registerAccountResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterAccount, + params: [ + newAccountName, + newAccountDomainName, + newAccountCredentials.publicKey, + newAccountCredentials.privateKey.digestFunction, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAccountResponse).toBeTruthy(); + expect(registerAccountResponse.status).toEqual(200); + expect(registerAccountResponse.data.rejectReason).toBeUndefined(); + expect(registerAccountResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(registerAccountResponse.data.hash).toBeTruthy(); + }); + + test("Query single account (FindAccountById)", async () => { + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAccountById, + params: [newAccountName, newAccountDomainName], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: 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({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerDomainResponse).toBeTruthy(); + expect(registerDomainResponse.status).toEqual(200); + expect(registerDomainResponse.data.rejectReason).toBeUndefined(); + expect(registerDomainResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(registerDomainResponse.data.hash).toBeTruthy(); + + // Create new asset definition + const registerAssetDefResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterAssetDefinition, + params: [assetName, domainName, valueType, mintable], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAssetDefResponse).toBeTruthy(); + expect(registerAssetDefResponse.status).toEqual(200); + expect(registerAssetDefResponse.data.rejectReason).toBeUndefined(); + expect(registerAssetDefResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(registerAssetDefResponse.data.hash).toBeTruthy(); + + // Create new asset + const registerAssetResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterAsset, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + value, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAssetResponse).toBeTruthy(); + expect(registerAssetResponse.status).toEqual(200); + expect(registerAssetResponse.data.rejectReason).toBeUndefined(); + expect(registerAssetResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(registerAssetResponse.data.hash).toBeTruthy(); + }); + + test("Query single asset definition (FindAssetDefinitionById)", async () => { + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAssetDefinitionById, + params: [assetName, domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: 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({ + query: { + query: IrohaQuery.FindAssetById, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: 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({ + query: { + query: IrohaQuery.FindAssetById, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + transaction: { + instruction: { + name: IrohaInstruction.MintAsset, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + mintValue, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(mintResponse).toBeTruthy(); + expect(mintResponse.status).toEqual(200); + expect(mintResponse.data.rejectReason).toBeUndefined(); + expect(mintResponse.data.status).toEqual(TransactionStatusV1.Committed); + expect(mintResponse.data.hash).toBeTruthy(); + + // Get final asset value (after mint) + const finalQueryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAssetById, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: IrohaQuery.FindAssetById, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + transaction: { + instruction: { + name: IrohaInstruction.BurnAsset, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + burnValue, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(burnResponse).toBeTruthy(); + expect(burnResponse.status).toEqual(200); + expect(burnResponse.data.rejectReason).toBeUndefined(); + expect(burnResponse.data.status).toEqual(TransactionStatusV1.Committed); + expect(burnResponse.data.hash).toBeTruthy(); + + // Get final asset value (after burn) + const finalQueryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAssetById, + params: [ + assetName, + domainName, + env.defaultBaseConfig.accountId?.name, + env.defaultBaseConfig.accountId?.domainId, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: IrohaQuery.FindAssetById, + params: [ + assetName, + domainName, + sourceAccountName, + sourceAccountDomain, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterAccount, + params: [ + targetAccountName, + targetAccountDomain, + accountCredentials.publicKey, + accountCredentials.privateKey.digestFunction, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(registerAccountResponse).toBeTruthy(); + expect(registerAccountResponse.status).toEqual(200); + expect(registerAccountResponse.data.rejectReason).toBeUndefined(); + expect(registerAccountResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(registerAccountResponse.data.hash).toBeTruthy(); + + // Transfer asset to the newly created account + const transferResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.TransferAsset, + params: [ + assetName, + domainName, + sourceAccountName, + sourceAccountDomain, + targetAccountName, + targetAccountDomain, + transferValue, + ], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(transferResponse).toBeTruthy(); + expect(transferResponse.status).toEqual(200); + expect(transferResponse.data.rejectReason).toBeUndefined(); + expect(transferResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(transferResponse.data.hash).toBeTruthy(); + + // Get final asset value on source account (after transfer) + const finalSourceQueryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindAssetById, + params: [ + assetName, + domainName, + sourceAccountName, + sourceAccountDomain, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: IrohaQuery.FindAssetById, + params: [ + assetName, + domainName, + targetAccountName, + targetAccountDomain, + ], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: 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(); + }); + + test("Query single transaction (FindAssetById)", async () => { + const domainName = addRandomSuffix("querySingleTx"); + expect(domainName).toBeTruthy(); + + // Create new domain to get a valid transaction hash + const transactionResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + const txHash = transactionResponse.data.hash; + expect(txHash).toBeTruthy(); + + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindTransactionByHash, + params: [txHash], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + query: { + query: 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({ + query: { + query: 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/iroha2-monitoring-endpoints.test.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-monitoring-endpoints.test.ts new file mode 100644 index 00000000000..1f5c36c2038 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-monitoring-endpoints.test.ts @@ -0,0 +1,246 @@ +/** + * Tests for monitoring endpoints in Iroha V2 connector. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Log settings +const testLogLevel: LogLevelDesc = "info"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, + Checks, +} from "@hyperledger/cactus-common"; + +import { + IrohaInstruction, + BlockTypeV1, + WatchBlocksOptionsV1, + WatchBlocksResponseV1, + TransactionStatusV1, +} 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"; +import { computeTransactionHash } from "@iroha2/client"; +import { bytesToHex } from "hada"; + +// 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({ + transaction: { + 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( + TransactionStatusV1.Submitted, + ); + + 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(); + }); + + /** + * Test shows parsing of block data to retrieve committed transactions that can be used to monitor submitted transaction status. + */ + test("Watching for transaction hash in block content", async () => { + // Will be filled later when the TX is sent. + let searchedHash: string | undefined = undefined; + + // Start monitoring + const monitorOptions = { + type: BlockTypeV1.Binary, + baseConfig: env.defaultBaseConfig, + }; + + const monitorPromise = new Promise((resolve, reject) => { + const watchObservable = env.apiClient.watchBlocksV1(monitorOptions); + const subscription = watchObservable.subscribe({ + next(event) { + try { + // Process block data + 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), + ); + decodedBlock.as("V1").transactions.forEach((tx) => { + const txPayload = tx.as("V1").payload; + const hashByes = computeTransactionHash(txPayload); + const hashHex = bytesToHex([...hashByes]); + log.debug("Received transaction", hashHex); + if (hashHex === searchedHash) { + log.info("Matching transaction found in block - resolve"); + subscription.unsubscribe(); + resolve(); + } + }); + } catch (err) { + log.error("watchBlocksV1() event check error:", err); + subscription.unsubscribe(); + reject(err); + } + }, + 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("watchBlockContent"); + const transactionResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Submitted, + ); + searchedHash = transactionResponse.data.hash; + expect(searchedHash).toBeTruthy(); + log.info( + `Search for transaction '${searchedHash}' in incoming block content...`, + ); + + await expect(monitorPromise).toResolve(); + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-setup-and-basic-operations.test.ts b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-setup-and-basic-operations.test.ts new file mode 100644 index 00000000000..5f67b63b014 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/integration/iroha2-setup-and-basic-operations.test.ts @@ -0,0 +1,437 @@ +/** + * Tests for Iroha V2 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, + TransactionStatusV1, +} 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.publicKeyMultihash).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.createClient()) + .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.createClient(requestConfig) + ).toriiOptions; + expect(overwrittenConfig).toEqual(requestConfig.torii); + }); + + test("Simple transaction without waiting and query endpoints works", async () => { + const domainName = addRandomSuffix("singleTxTest"); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + transaction: { + 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( + TransactionStatusV1.Submitted, + ); + expect(transactionResponse.data.hash).toBeTruthy(); + expect(transactionResponse.data.hash.length).toEqual(64); + + // Sleep + await waitForCommit(); + + // Query it + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindDomainById, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + 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("Waiting for transaction commit returns it's status", async () => { + const domainName = addRandomSuffix("waitForTx"); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data).toBeTruthy(); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(transactionResponse.data.hash).toBeTruthy(); + expect(transactionResponse.data.hash.length).toEqual(64); + + // Query it + // Transaction should be committed so no waiting is needed. + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindDomainById, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + 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("Rejected transaction returns a reason", async () => { + const domainName = addRandomSuffix("waitForRejectTx"); + + // Create new domain - first one is committed + const transactionResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data).toBeTruthy(); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + + // Create existing domain again - should be rejected + const rejectResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(rejectResponse).toBeTruthy(); + expect(rejectResponse.status).toEqual(200); + expect(rejectResponse.data).toBeTruthy(); + expect(rejectResponse.data.status).toEqual(TransactionStatusV1.Rejected); + expect(rejectResponse.data.rejectReason).toBeTruthy(); + log.debug( + "OK - transaction rejected with reason:", + rejectResponse.data.rejectReason, + ); + }); + + test("Sending transaction with keychain signatory works", async () => { + const domainName = addRandomSuffix("keychainSignatoryDomain"); + + // Create new domain + const transactionResponse = await env.apiClient.transactV1({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: { + ...env.defaultBaseConfig, + signingCredential: env.keychainCredentials, + }, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(transactionResponse.data.hash).toBeTruthy(); + + // Query it + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindDomainById, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + waitForCommit: true, + baseConfig: { + ...env.defaultBaseConfig, + signingCredential: env.keyPairCredential, + }, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(transactionResponse.data.hash).toBeTruthy(); + + // Query it + const queryResponse = await env.apiClient.queryV1({ + query: { + query: IrohaQuery.FindDomainById, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }); + 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({ + transaction: { + instruction: [ + { + name: IrohaInstruction.RegisterDomain, + params: [firstDomainName], + }, + { + name: IrohaInstruction.RegisterDomain, + params: [secondDomainName], + }, + ], + }, + waitForCommit: true, + baseConfig: env.defaultBaseConfig, + }); + expect(transactionResponse).toBeTruthy(); + expect(transactionResponse.status).toEqual(200); + expect(transactionResponse.data.rejectReason).toBeUndefined(); + expect(transactionResponse.data.status).toEqual( + TransactionStatusV1.Committed, + ); + expect(transactionResponse.data.hash).toBeTruthy(); + + // Query domains + const queryResponse = await env.apiClient.queryV1({ + query: { + query: 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({ + transaction: { + 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({ + transaction: { + instruction: { + name: IrohaInstruction.RegisterDomain, + params: [domainName], + }, + }, + baseConfig: { + torii: env.defaultBaseConfig.torii, + }, + }), + ).toReject(); + + // Use config without keypair + await expect( + env.apiClient.transactV1({ + transaction: { + 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({ + query: { + query: IrohaQuery.FindDomainById, + params: [domainName], + }, + baseConfig: env.defaultBaseConfig, + }), + ).toReject(); + }); + + test("Unknown query name reports error", () => { + // Send invalid query + return expect( + env.apiClient.queryV1({ + query: { + query: "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..b7f12e246fe --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha2/src/test/typescript/test-helpers/iroha2-env-setup.ts @@ -0,0 +1,280 @@ +/** + * Test Iroha V2 environment setup functions. + */ + +// Ledger settings +const containerImageName = "ghcr.io/outsh/cactus-iroha2-all-in-one"; +const containerImageVersion = "0.5"; +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 & { + publicKeyMultihash: string; +} { + 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(keyPair.publicKey().payload())), + publicKeyMultihash: 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 }); + + // Fix flaky tests when running on local (fast) machine + await new Promise((resolve) => setTimeout(resolve, 5000)); + } +} 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-verifier-client/README.md b/packages/cactus-verifier-client/README.md index c50037c5d20..484cbaadb2b 100644 --- a/packages/cactus-verifier-client/README.md +++ b/packages/cactus-verifier-client/README.md @@ -11,6 +11,7 @@ This package provides `Verifier` and `VerifierFactory` components that can be us | QUORUM_2X | cactus-test-plugin-ledger-connector-quorum | | CORDA_4X | cactus-plugin-ledger-connector-corda | | IROHA_1X | cactus-plugin-ledger-connector-iroha | +| IROHA_2X | cactus-plugin-ledger-connector-iroha2 | | FABRIC_2X | cactus-plugin-ledger-connector-fabric | | legacy-socketio | cactus-plugin-ledger-connector-fabric-socketio
cactus-plugin-ledger-connector-go-ethereum-socketio
cactus-plugin-ledger-connector-sawtooth-socketio | 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 e48bb4ef7d0..dcf6b62e08c 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 @@ -30,6 +30,11 @@ import { IrohaApiClientOptions, } from "@hyperledger/cactus-plugin-ledger-connector-iroha"; +import { + Iroha2ApiClient, + Iroha2ApiClientOptions, +} from "@hyperledger/cactus-plugin-ledger-connector-iroha2"; + import { FabricApiClient, FabricApiClientOptions, @@ -66,6 +71,10 @@ export type ClientApiConfig = { in: IrohaApiClientOptions; out: IrohaApiClient; }; + IROHA_2X: { + in: Iroha2ApiClientOptions; + out: Iroha2ApiClient; + }; FABRIC_2X: { in: FabricApiClientOptions; out: FabricApiClient; @@ -97,6 +106,8 @@ export function getValidatorApiClient( return new CordaApiClient(options as CordaApiClientOptions); case "IROHA_1X": return new IrohaApiClient(options as IrohaApiClientOptions); + case "IROHA_2X": + return new Iroha2ApiClient(options as CordaApiClientOptions); case "FABRIC_2X": return new FabricApiClient(options as FabricApiClientOptions); default: diff --git a/packages/cactus-verifier-client/tsconfig.json b/packages/cactus-verifier-client/tsconfig.json index 2b95c15fda3..5a5de1963db 100644 --- a/packages/cactus-verifier-client/tsconfig.json +++ b/packages/cactus-verifier-client/tsconfig.json @@ -32,6 +32,9 @@ { "path": "../cactus-plugin-ledger-connector-iroha/tsconfig.json" }, + { + "path": "../cactus-plugin-ledger-connector-iroha2/tsconfig.json" + }, { "path": "../cactus-plugin-ledger-connector-fabric/tsconfig.json" } diff --git a/tools/docker/iroha2-all-in-one/Dockerfile b/tools/docker/iroha2-all-in-one/Dockerfile index cefa2c9b5b4..0e6b69f9efb 100644 --- a/tools/docker/iroha2-all-in-one/Dockerfile +++ b/tools/docker/iroha2-all-in-one/Dockerfile @@ -1,6 +1,9 @@ -FROM docker:20.10.17-dind +FROM docker:20.10.21-dind ENV APP_ROOT="/app" +ENV FREEZE_TMP_DIR="/opt/docker-freeze" +ENV IROHA_VERSION="dev-nightly-75da907f66d5270f407a50e06bc76cec41d3d409" +ENV IROHA_CLI_VERSION="dev-nightly-75da907f66d5270f407a50e06bc76cec41d3d409" # Install docker-compose RUN apk update \ @@ -13,6 +16,8 @@ RUN apk update \ # Other dependencies supervisor \ jq \ + curl \ + bash \ && pip install wheel \ && pip install docker-compose @@ -23,7 +28,11 @@ RUN chmod +x /bin/iroha_client_cli # Setup healtcheck COPY ./healthcheck.sh /bin/healthcheck RUN chmod +x /bin/healthcheck -HEALTHCHECK --interval=5s --timeout=5s --start-period=30s --retries=60 CMD /bin/healthcheck +HEALTHCHECK --interval=5s --timeout=10s --start-period=45s --retries=60 CMD /bin/healthcheck + +# Freeze docker images +COPY ./freeze-images.sh /usr/bin/freeze-images.sh +RUN bash /usr/bin/freeze-images.sh WORKDIR ${APP_ROOT} @@ -35,10 +44,8 @@ EXPOSE 8080 # Peer0 telemetry EXPOSE 8180 -ENV IROHA_VERSION="dev-nightly-75da907f66d5270f407a50e06bc76cec41d3d409" -ENV IROHA_CLI_VERSION="dev-nightly-75da907f66d5270f407a50e06bc76cec41d3d409" - # Setup supervisor entrypoint +COPY ./run-iroha-ledger.sh ./run-iroha-ledger.sh COPY supervisord.conf /etc/supervisord.conf ENTRYPOINT ["/usr/bin/supervisord"] CMD ["--configuration", "/etc/supervisord.conf", "--nodaemon"] diff --git a/tools/docker/iroha2-all-in-one/freeze-images.sh b/tools/docker/iroha2-all-in-one/freeze-images.sh new file mode 100755 index 00000000000..b78181bd560 --- /dev/null +++ b/tools/docker/iroha2-all-in-one/freeze-images.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +FREEZE_SCRIPT_NAME="download-frozen-image-v2.sh" +FREEZE_SCRIPT_PATH="/usr/bin/${FREEZE_SCRIPT_NAME}" + +echo "Download freeze script..." +curl -sSL https://raw.githubusercontent.com/moby/moby/dedf8528a51c6db40686ed6676e9486d1ed5f9c0/contrib/download-frozen-image-v2.sh > "${FREEZE_SCRIPT_PATH}" +chmod +x "${FREEZE_SCRIPT_PATH}" + +# Get default iroha image +img_name="hyperledger/iroha2:${IROHA_VERSION}" +img_path="${FREEZE_TMP_DIR}/iroha2_${IROHA_VERSION}" +echo "Freeze image '${img_name}' in '${img_path}" +mkdir -p "${img_path}" +bash "${FREEZE_SCRIPT_PATH}" "${img_path}" "${img_name}" + +echo "Image freeze done." diff --git a/tools/docker/iroha2-all-in-one/healthcheck.sh b/tools/docker/iroha2-all-in-one/healthcheck.sh old mode 100644 new mode 100755 index f2fec563206..826443caed7 --- a/tools/docker/iroha2-all-in-one/healthcheck.sh +++ b/tools/docker/iroha2-all-in-one/healthcheck.sh @@ -7,13 +7,20 @@ set -e API_URL="http://0.0.0.0:8080" TELEMETRY_URL="http://0.0.0.0:8180" +echo "Iroha2 Ledger Healtcheck..." + # Check health -wget -O- "${API_URL}/health" | grep -Fi 'Healthy' -echo "Status: Healthy" +healthStatus=$(wget -O- "${API_URL}/health" 2>/dev/null) +if echo $healthStatus | grep -F '"Healthy"'; then + echo "Status healthy" +else + echo "Wrong health status check: ${healthStatus}" + exit 1 +fi # Get blocks -blocks=$(wget -O- "${TELEMETRY_URL}/status" | jq -r '.blocks') -if [ blocks -lt 1]; then +blocks=$(wget -O- "${TELEMETRY_URL}/status" 2>/dev/null | jq -r '.blocks') +if [ $blocks -lt 1 ]; then echo "No genesis block yet..." exit 1 fi diff --git a/tools/docker/iroha2-all-in-one/iroha_client_cli.sh b/tools/docker/iroha2-all-in-one/iroha_client_cli.sh old mode 100644 new mode 100755 diff --git a/tools/docker/iroha2-all-in-one/run-iroha-ledger.sh b/tools/docker/iroha2-all-in-one/run-iroha-ledger.sh new file mode 100755 index 00000000000..61fc4ed932d --- /dev/null +++ b/tools/docker/iroha2-all-in-one/run-iroha-ledger.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +while ! docker ps &> /dev/null +do + echo "Wait for dockerd to start..." + sleep 3 +done + +# Get list of images from docker-compose +for img in `ls ${FREEZE_TMP_DIR}` +do + echo "Load frozen image '${img}'" + tar -cC "${FREEZE_TMP_DIR}/${img}" . | docker load +done + +echo "Frozen images loaded" + +docker compose -f "${APP_ROOT}/docker-compose.yml" up diff --git a/tools/docker/iroha2-all-in-one/supervisord.conf b/tools/docker/iroha2-all-in-one/supervisord.conf index b00705be017..f4beec301ca 100644 --- a/tools/docker/iroha2-all-in-one/supervisord.conf +++ b/tools/docker/iroha2-all-in-one/supervisord.conf @@ -12,9 +12,10 @@ stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 +priority=1 [program:iroha2-test-ledger] -command=docker-compose -f %(ENV_APP_ROOT)s/docker-compose.yml up +command=%(ENV_APP_ROOT)s/run-iroha-ledger.sh autostart=true autorestart=false stderr_logfile=/dev/stderr diff --git a/yarn.lock b/yarn.lock index 10a0a21a679..79fc5f41ca6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3119,6 +3119,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" @@ -4022,6 +4062,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" @@ -8946,7 +9014,7 @@ debug@3.2.6: dependencies: ms "^2.1.1" -debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -9679,7 +9747,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== @@ -11407,6 +11475,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" @@ -12330,6 +12403,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" @@ -22299,10 +22377,10 @@ 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.3.3: - version "2.13.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.13.1.tgz#621c84220df0e01a8469002594fc005714f0cfba" - integrity sha512-hXYyrPFwETT2swFLHeoKtJrvSF/ftG/sA15/8nGaLuaDGfVAaq8DYFpu4yOyV4tzp082WqnTEoMsm3flKMI2FQ== +type-fest@^2.13.0, type-fest@^2.3.3: + 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" @@ -22426,6 +22504,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" @@ -24077,6 +24160,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"