From 4ee177edb89d7c009ff652fa629a151c07deadff Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Mon, 18 Sep 2023 11:01:48 +0000 Subject: [PATCH] feat(cactus-plugin-ledger-connector-fabric): support delegated (offline) signatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add new `RunDelegatedSignTransactionEndpointV1` endpoint for delegated / offline signing. Takes `signerCertificate` and `signerMspID`, uses `signCallback` on connector to sign messages. Sign must be implemented by a user, can contain any logic (contacting 3'rd party services, reading from secure sources, etc…). Interface is similar to transact. Supports private transactions. - Refactor transact endpoint: Use common logic for handling response format. with delegated transact - Refactor logic of choosing ednorsers in transact endpoint. Previously both `endorsingPeers` and `endorsingParties` were selecting organizations in sligly different way under different circumstances. Now `endorsingPeers` selectes peers and `endorsingOrgs` selects orgs for all cases (query, send, privatesend) in both transact and transact with delegated sign. This is more consistent and predictable. - Add new socketio endpoint `SubscribeDelegatedSign` for monitoring new blocks with delegated sign. - Use common error handling in getblock, transact and transact delgated endpoints. - Add functional tests for delegated signing feature. Depends on: #2598 Signed-off-by: Michal Bajer --- .cspell.json | 4 + .../README.md | 43 ++ .../package.json | 1 + .../src/main/json/openapi.json | 218 ++++++- .../kotlin-client/.openapi-generator/FILES | 3 + .../generated/openapi/kotlin-client/README.md | 4 + .../openapitools/client/apis/DefaultApi.kt | 73 +++ .../RunDelegatedSignTransactionRequest.kt | 84 +++ .../client/models/RunTransactionRequest.kt | 16 +- .../client/models/RunTransactionResponse.kt | 4 - .../models/RunTransactionResponseType.kt | 63 ++ .../WatchBlocksDelegatedSignOptionsV1.kt | 58 ++ .../client/models/WatchBlocksV1.kt | 5 +- .../api-client/fabric-api-client.ts | 115 +++- .../src/main/typescript/common/sign-utils.ts | 86 +++ .../src/main/typescript/common/utils.ts | 31 + .../deploy-contract-go-source-endpoint-v1.ts | 2 +- .../deploy-contract-endpoint-v1.ts | 2 +- .../generated/openapi/typescript-axios/api.ts | 234 ++++++- .../get-block/get-block-endpoint-v1.ts | 36 +- ...prometheus-exporter-metrics-endpoint-v1.ts | 5 +- ...transaction-receipt-by-txid-endpoint-v1.ts | 5 +- .../plugin-ledger-connector-fabric.ts | 583 +++++++++++++++--- .../src/main/typescript/public-api.ts | 5 +- ...-delegated-sign-transaction-endpoint-v1.ts | 102 +++ .../run-transaction-endpoint-v1.ts | 20 +- .../watch-blocks/watch-blocks-v1-endpoint.ts | 223 +++++-- .../delegate-signing-methods.test.ts | 501 +++++++++++++++ ...-blocks-delegated-sign-v1-endpoint.test.ts | 431 +++++++++++++ .../fabric-watch-blocks-v1-endpoint.test.ts | 1 - .../fabric-v2-2-x/get-block.test.ts | 1 - .../run-transaction-endpoint-v1.test.ts | 2 +- .../run-transaction-with-identities.test.ts | 7 +- .../run-transaction-with-ws-ids.test.ts | 7 +- .../openapi/openapi-validation-go.test.ts | 4 +- .../openapi/openapi-validation.test.ts | 8 +- .../typescript/plugin-persistence-fabric.ts | 51 +- .../persistence-fabric-functional.test.ts | 25 +- yarn.lock | 1 + 39 files changed, 2787 insertions(+), 277 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunDelegatedSignTransactionRequest.kt create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionResponseType.kt create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/WatchBlocksDelegatedSignOptionsV1.kt create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/sign-utils.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/utils.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/run-transaction/run-delegated-sign-transaction-endpoint-v1.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/delegate-signing-methods.test.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-delegated-sign-v1-endpoint.test.ts diff --git a/.cspell.json b/.cspell.json index 8b5900e5a09..70220fd4364 100644 --- a/.cspell.json +++ b/.cspell.json @@ -21,6 +21,8 @@ "cafile", "caio", "cccs", + "ccep", + "cccg", "cbdc", "Cbdc", "ccid", @@ -64,6 +66,7 @@ "HTLC", "Hursley", "HyperLedger", + "immalleable", "ipaddress", "ipfs", "Iroha", @@ -86,6 +89,7 @@ "miekg", "mitchellh", "MSPCONFIGPATH", + "Mspids", "MSPID", "MSPIDSCOPEALLFORTX", "MSPIDSCOPEANYFORTX", diff --git a/packages/cactus-plugin-ledger-connector-fabric/README.md b/packages/cactus-plugin-ledger-connector-fabric/README.md index dbb0c6385da..90f14a0986c 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/README.md +++ b/packages/cactus-plugin-ledger-connector-fabric/README.md @@ -13,6 +13,8 @@ - [1.5 Monitoring new blocks (WatchBlocks)](#15-monitoring-new-blocks-watchblocks) - [1.5.1 Example](#151-example) - [1.5.2 Listener Type](#152-listener-type) + - [1.6 Delegated Signature](#16-delegated-signature) + - [1.6.1 Example](#161-example) - [2. Architecture](#2-architecture) - [2.1. run-transaction-endpoint](#21-run-transaction-endpoint) - [3. Containerization](#3-containerization) @@ -329,6 +331,47 @@ Corresponds directly to `BlockType` from `fabric-common`: - `WatchBlocksListenerTypeV1.Full`, - `WatchBlocksListenerTypeV1.Private`, +### 1.6 Delegated Signature +- Custom signature callback can be used when increased security is needed or currently available options are not sufficient. +- Signature callback is used whenever fabric request must be signed. +- To use delegate signature instead of identity supplied directly / through keychain use `transactDelegatedSign` (for transact) or `watchBlocksDelegatedSignV1` for block monitoring. +- `uniqueTransactionData` can be passed to each delegate sign method on connector. This data is passed to signCallback to identify and verify the request. It can be used to pass signing tokens or any other data needed for performing the signing (e.g. user, scopes, etc...). +- `signProposal` method from this package can be used to sign the requests in offline location. +- For more complex examples see tests: `delegate-signing-methods.test` and `fabric-watch-blocks-delegated-sign-v1-endpoint.test`. + +#### 1.6.1 Example +```typescript +// Setup - supply callback when instantiating the connector plugin +fabricConnectorPlugin = new PluginLedgerConnectorFabric({ + instanceId: uuidv4(), + // ... + signCallback: async (payload, txData) => { + log.debug("signCallback called with txData (token):", txData); + return signProposal(adminIdentity.credentials.privateKey, payload); + }, +}); + +// Run transactions +await apiClient.runDelegatedSignTransactionV1({ + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + channelName: ledgerChannelName, + contractName: assetTradeContractName, + invocationType: FabricContractInvocationType.Call, + methodName: "ReadAsset", + params: ["asset1"], + uniqueTransactionData: myJwtToken, +}); + +// Monitor for transactions: +apiClient.watchBlocksDelegatedSignV1({ + type: WatchBlocksListenerTypeV1.CactusTransactions, + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + channelName: ledgerChannelName, +}) +``` + ##### Cactus (custom) Parses the data and returns custom formatted block. - `WatchBlocksListenerTypeV1.CactusTransactions`: Returns transactions summary. Compatible with legacy `fabric-socketio` monitoring operation. diff --git a/packages/cactus-plugin-ledger-connector-fabric/package.json b/packages/cactus-plugin-ledger-connector-fabric/package.json index f794b964929..a2c22565fc8 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package.json @@ -77,6 +77,7 @@ "node-vault": "0.9.22", "openapi-types": "9.1.0", "prom-client": "13.2.0", + "run-time-error": "1.4.0", "rxjs": "7.8.1", "sanitize-filename": "1.6.3", "sanitize-html": "2.7.0", diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json index 736b8154500..10101d1fd2a 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json @@ -374,6 +374,15 @@ } } }, + "RunTransactionResponseType": { + "type": "string", + "description": "Response format from transaction / query execution", + "enum": [ + "org.hyperledger.cacti.api.hlfabric.RunTransactionResponseType.JSON", + "org.hyperledger.cacti.api.hlfabric.RunTransactionResponseType.UTF8" + ], + "x-enum-varnames": ["JSON", "UTF8"] + }, "RunTransactionRequest": { "type": "object", "required": [ @@ -387,7 +396,17 @@ "additionalProperties": false, "properties": { "endorsingPeers": { - "description": "An array of MSP IDs to set as the list of endorsing peers for the transaction.", + "description": "An array of endorsing peers (name or url) for the transaction.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "nullable": false + } + }, + "endorsingOrgs": { + "description": "An array of endorsing organizations (by mspID or issuer org name on certificate) for the transaction.", "type": "array", "items": { "type": "string", @@ -440,35 +459,108 @@ "nullable": true } }, - "endorsingParties": { - "type": "array", - "nullable": false, - "default": [], - "items": { - "type": "string", - "nullable": true - } - }, "responseType": { - "type": "string" + "$ref": "#/components/schemas/RunTransactionResponseType" } } }, "RunTransactionResponse": { "type": "object", - "required": ["functionOutput", "success", "transactionId"], + "required": ["functionOutput", "transactionId"], "properties": { "functionOutput": { "type": "string", "nullable": false }, - "success": { - "type": "boolean", + "transactionId": { + "type": "string", + "nullable": false + } + } + }, + "RunDelegatedSignTransactionRequest": { + "type": "object", + "required": [ + "signerCertificate", + "signerMspID", + "channelName", + "contractName", + "invocationType", + "methodName", + "params" + ], + "additionalProperties": false, + "properties": { + "endorsingPeers": { + "description": "An array of endorsing peers (name or url) for the transaction.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "nullable": false + } + }, + "endorsingOrgs": { + "description": "An array of endorsing organizations (by mspID or issuer org name on certificate) for the transaction.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "nullable": false + } + }, + "transientData": { + "type": "object", + "nullable": true + }, + "signerCertificate": { + "type": "string", "nullable": false }, - "transactionId": { + "signerMspID": { "type": "string", "nullable": false + }, + "uniqueTransactionData": { + "description": "Can be used to uniquely identify and authorize signing request", + "nullable": false + }, + "channelName": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "contractName": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "invocationType": { + "$ref": "#/components/schemas/FabricContractInvocationType", + "nullable": false, + "description": "Indicates if it is a CALL or a SEND type of invocation where only SEND ends up creating an actual transaction on the ledger." + }, + "methodName": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "params": { + "type": "array", + "nullable": false, + "default": [], + "items": { + "type": "string", + "nullable": true + } + }, + "responseType": { + "$ref": "#/components/schemas/RunTransactionResponseType" } } }, @@ -1031,6 +1123,7 @@ "description": "Websocket requests for monitoring new blocks.", "enum": [ "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Subscribe", + "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.SubscribeDelegatedSign", "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Next", "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Unsubscribe", "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Error", @@ -1038,6 +1131,7 @@ ], "x-enum-varnames": [ "Subscribe", + "SubscribeDelegatedSign", "Next", "Unsubscribe", "Error", @@ -1081,6 +1175,43 @@ } } }, + "WatchBlocksDelegatedSignOptionsV1": { + "type": "object", + "description": "Options passed when subscribing to block monitoring with delegated signing.", + "required": ["type", "channelName", "signerCertificate", "signerMspID"], + "properties": { + "type": { + "$ref": "#/components/schemas/WatchBlocksListenerTypeV1", + "description": "Type of response block to return.", + "nullable": false + }, + "startBlock": { + "type": "string", + "description": "From which block start monitoring. Defaults to latest.", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "channelName": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "signerCertificate": { + "type": "string", + "nullable": false + }, + "signerMspID": { + "type": "string", + "nullable": false + }, + "uniqueTransactionData": { + "description": "Can be used to uniquely identify and authorize signing request", + "nullable": false + } + } + }, "WatchBlocksCactusTransactionsEventV1": { "type": "object", "description": "Transaction summary from commited block.", @@ -1241,8 +1372,61 @@ } } }, - "404": { - "description": "" + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionResponseV1" + } + } + } + } + } + } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-delegated-sign-transaction": { + "post": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-delegated-sign-transaction" + } + }, + "operationId": "runDelegatedSignTransactionV1", + "summary": "Runs a transaction on a Fabric ledger using user-provided signing callback.", + "description": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunDelegatedSignTransactionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunTransactionResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionResponseV1" + } + } + } } } } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/.openapi-generator/FILES b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/.openapi-generator/FILES index 13fe3a3422c..60dc2bf86b3 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/.openapi-generator/FILES +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/.openapi-generator/FILES @@ -51,8 +51,10 @@ src/main/kotlin/org/openapitools/client/models/GetBlockResponseDecodedV1.kt src/main/kotlin/org/openapitools/client/models/GetBlockResponseEncodedV1.kt src/main/kotlin/org/openapitools/client/models/GetBlockResponseV1.kt src/main/kotlin/org/openapitools/client/models/GetTransactionReceiptResponse.kt +src/main/kotlin/org/openapitools/client/models/RunDelegatedSignTransactionRequest.kt src/main/kotlin/org/openapitools/client/models/RunTransactionRequest.kt src/main/kotlin/org/openapitools/client/models/RunTransactionResponse.kt +src/main/kotlin/org/openapitools/client/models/RunTransactionResponseType.kt src/main/kotlin/org/openapitools/client/models/SSHExecCommandResponse.kt src/main/kotlin/org/openapitools/client/models/TransactReceiptBlockMetaData.kt src/main/kotlin/org/openapitools/client/models/TransactReceiptTransactionCreator.kt @@ -61,6 +63,7 @@ src/main/kotlin/org/openapitools/client/models/VaultTransitKey.kt src/main/kotlin/org/openapitools/client/models/WatchBlocksCactusErrorResponseV1.kt src/main/kotlin/org/openapitools/client/models/WatchBlocksCactusTransactionsEventV1.kt src/main/kotlin/org/openapitools/client/models/WatchBlocksCactusTransactionsResponseV1.kt +src/main/kotlin/org/openapitools/client/models/WatchBlocksDelegatedSignOptionsV1.kt src/main/kotlin/org/openapitools/client/models/WatchBlocksFilteredResponseV1.kt src/main/kotlin/org/openapitools/client/models/WatchBlocksFullResponseV1.kt src/main/kotlin/org/openapitools/client/models/WatchBlocksListenerTypeV1.kt diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/README.md b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/README.md index c074b2c8267..35e73891a67 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/README.md +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/README.md @@ -49,6 +49,7 @@ Class | Method | HTTP request | Description *DefaultApi* | [**getBlockV1**](docs/DefaultApi.md#getblockv1) | **POST** /api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-block | Get block from the channel using one of selectors from the input. Works only on Fabric 2.x. *DefaultApi* | [**getPrometheusMetricsV1**](docs/DefaultApi.md#getprometheusmetricsv1) | **GET** /api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-prometheus-exporter-metrics | Get the Prometheus Metrics *DefaultApi* | [**getTransactionReceiptByTxIDV1**](docs/DefaultApi.md#gettransactionreceiptbytxidv1) | **POST** /api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-transaction-receipt-by-txid | get a transaction receipt by tx id on a Fabric ledger. +*DefaultApi* | [**runDelegatedSignTransactionV1**](docs/DefaultApi.md#rundelegatedsigntransactionv1) | **POST** /api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-delegated-sign-transaction | Runs a transaction on a Fabric ledger using user-provided signing callback. *DefaultApi* | [**runTransactionV1**](docs/DefaultApi.md#runtransactionv1) | **POST** /api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-transaction | Runs a transaction on a Fabric ledger. @@ -85,8 +86,10 @@ Class | Method | HTTP request | Description - [org.openapitools.client.models.GetBlockResponseEncodedV1](docs/GetBlockResponseEncodedV1.md) - [org.openapitools.client.models.GetBlockResponseV1](docs/GetBlockResponseV1.md) - [org.openapitools.client.models.GetTransactionReceiptResponse](docs/GetTransactionReceiptResponse.md) + - [org.openapitools.client.models.RunDelegatedSignTransactionRequest](docs/RunDelegatedSignTransactionRequest.md) - [org.openapitools.client.models.RunTransactionRequest](docs/RunTransactionRequest.md) - [org.openapitools.client.models.RunTransactionResponse](docs/RunTransactionResponse.md) + - [org.openapitools.client.models.RunTransactionResponseType](docs/RunTransactionResponseType.md) - [org.openapitools.client.models.SSHExecCommandResponse](docs/SSHExecCommandResponse.md) - [org.openapitools.client.models.TransactReceiptBlockMetaData](docs/TransactReceiptBlockMetaData.md) - [org.openapitools.client.models.TransactReceiptTransactionCreator](docs/TransactReceiptTransactionCreator.md) @@ -95,6 +98,7 @@ Class | Method | HTTP request | Description - [org.openapitools.client.models.WatchBlocksCactusErrorResponseV1](docs/WatchBlocksCactusErrorResponseV1.md) - [org.openapitools.client.models.WatchBlocksCactusTransactionsEventV1](docs/WatchBlocksCactusTransactionsEventV1.md) - [org.openapitools.client.models.WatchBlocksCactusTransactionsResponseV1](docs/WatchBlocksCactusTransactionsResponseV1.md) + - [org.openapitools.client.models.WatchBlocksDelegatedSignOptionsV1](docs/WatchBlocksDelegatedSignOptionsV1.md) - [org.openapitools.client.models.WatchBlocksFilteredResponseV1](docs/WatchBlocksFilteredResponseV1.md) - [org.openapitools.client.models.WatchBlocksFullResponseV1](docs/WatchBlocksFullResponseV1.md) - [org.openapitools.client.models.WatchBlocksListenerTypeV1](docs/WatchBlocksListenerTypeV1.md) diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt index 8206459ff02..7f3d897bb57 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt @@ -28,6 +28,7 @@ import org.openapitools.client.models.ErrorExceptionResponseV1 import org.openapitools.client.models.GetBlockRequestV1 import org.openapitools.client.models.GetBlockResponseV1 import org.openapitools.client.models.GetTransactionReceiptResponse +import org.openapitools.client.models.RunDelegatedSignTransactionRequest import org.openapitools.client.models.RunTransactionRequest import org.openapitools.client.models.RunTransactionResponse @@ -410,6 +411,78 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient ) } + /** + * Runs a transaction on a Fabric ledger using user-provided signing callback. + * + * @param runDelegatedSignTransactionRequest + * @return RunTransactionResponse + * @throws IllegalStateException If the request is not correctly configured + * @throws IOException Rethrows the OkHttp execute method exception + * @throws UnsupportedOperationException If the API returns an informational or redirection response + * @throws ClientException If the API returns a client error response + * @throws ServerException If the API returns a server error response + */ + @Suppress("UNCHECKED_CAST") + @Throws(IllegalStateException::class, IOException::class, UnsupportedOperationException::class, ClientException::class, ServerException::class) + fun runDelegatedSignTransactionV1(runDelegatedSignTransactionRequest: RunDelegatedSignTransactionRequest) : RunTransactionResponse { + val localVarResponse = runDelegatedSignTransactionV1WithHttpInfo(runDelegatedSignTransactionRequest = runDelegatedSignTransactionRequest) + + return when (localVarResponse.responseType) { + ResponseType.Success -> (localVarResponse as Success<*>).data as RunTransactionResponse + ResponseType.Informational -> throw UnsupportedOperationException("Client does not support Informational responses.") + ResponseType.Redirection -> throw UnsupportedOperationException("Client does not support Redirection responses.") + ResponseType.ClientError -> { + val localVarError = localVarResponse as ClientError<*> + throw ClientException("Client error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse) + } + ResponseType.ServerError -> { + val localVarError = localVarResponse as ServerError<*> + throw ServerException("Server error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse) + } + } + } + + /** + * Runs a transaction on a Fabric ledger using user-provided signing callback. + * + * @param runDelegatedSignTransactionRequest + * @return ApiResponse + * @throws IllegalStateException If the request is not correctly configured + * @throws IOException Rethrows the OkHttp execute method exception + */ + @Suppress("UNCHECKED_CAST") + @Throws(IllegalStateException::class, IOException::class) + fun runDelegatedSignTransactionV1WithHttpInfo(runDelegatedSignTransactionRequest: RunDelegatedSignTransactionRequest) : ApiResponse { + val localVariableConfig = runDelegatedSignTransactionV1RequestConfig(runDelegatedSignTransactionRequest = runDelegatedSignTransactionRequest) + + return request( + localVariableConfig + ) + } + + /** + * To obtain the request config of the operation runDelegatedSignTransactionV1 + * + * @param runDelegatedSignTransactionRequest + * @return RequestConfig + */ + fun runDelegatedSignTransactionV1RequestConfig(runDelegatedSignTransactionRequest: RunDelegatedSignTransactionRequest) : RequestConfig { + val localVariableBody = runDelegatedSignTransactionRequest + val localVariableQuery: MultiValueMap = mutableMapOf() + val localVariableHeaders: MutableMap = mutableMapOf() + localVariableHeaders["Content-Type"] = "application/json" + localVariableHeaders["Accept"] = "application/json" + + return RequestConfig( + method = RequestMethod.POST, + path = "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-delegated-sign-transaction", + query = localVariableQuery, + headers = localVariableHeaders, + requiresAuthentication = false, + body = localVariableBody + ) + } + /** * Runs a transaction on a Fabric ledger. * diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunDelegatedSignTransactionRequest.kt b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunDelegatedSignTransactionRequest.kt new file mode 100644 index 00000000000..a95ddc11181 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunDelegatedSignTransactionRequest.kt @@ -0,0 +1,84 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.openapitools.client.models + +import org.openapitools.client.models.FabricContractInvocationType +import org.openapitools.client.models.RunTransactionResponseType + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * + * @param signerCertificate + * @param signerMspID + * @param channelName + * @param contractName + * @param invocationType + * @param methodName + * @param params + * @param endorsingPeers An array of endorsing peers (name or url) for the transaction. + * @param endorsingOrgs An array of endorsing organizations (by mspID or issuer org name on certificate) for the transaction. + * @param transientData + * @param uniqueTransactionData Can be used to uniquely identify and authorize signing request + * @param responseType + */ + + +data class RunDelegatedSignTransactionRequest ( + + @Json(name = "signerCertificate") + val signerCertificate: kotlin.String, + + @Json(name = "signerMspID") + val signerMspID: kotlin.String, + + @Json(name = "channelName") + val channelName: kotlin.String, + + @Json(name = "contractName") + val contractName: kotlin.String, + + @Json(name = "invocationType") + val invocationType: FabricContractInvocationType, + + @Json(name = "methodName") + val methodName: kotlin.String, + + @Json(name = "params") + val params: kotlin.collections.List = arrayListOf(), + + /* An array of endorsing peers (name or url) for the transaction. */ + @Json(name = "endorsingPeers") + val endorsingPeers: kotlin.collections.List? = null, + + /* An array of endorsing organizations (by mspID or issuer org name on certificate) for the transaction. */ + @Json(name = "endorsingOrgs") + val endorsingOrgs: kotlin.collections.List? = null, + + @Json(name = "transientData") + val transientData: kotlin.Any? = null, + + /* Can be used to uniquely identify and authorize signing request */ + @Json(name = "uniqueTransactionData") + val uniqueTransactionData: kotlin.Any? = null, + + @Json(name = "responseType") + val responseType: RunTransactionResponseType? = null + +) + diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionRequest.kt b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionRequest.kt index b9abd976237..ef4de914af6 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionRequest.kt +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionRequest.kt @@ -18,6 +18,7 @@ package org.openapitools.client.models import org.openapitools.client.models.FabricContractInvocationType import org.openapitools.client.models.FabricSigningCredential import org.openapitools.client.models.GatewayOptions +import org.openapitools.client.models.RunTransactionResponseType import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -31,10 +32,10 @@ import com.squareup.moshi.JsonClass * @param invocationType * @param methodName * @param params - * @param endorsingPeers An array of MSP IDs to set as the list of endorsing peers for the transaction. + * @param endorsingPeers An array of endorsing peers (name or url) for the transaction. + * @param endorsingOrgs An array of endorsing organizations (by mspID or issuer org name on certificate) for the transaction. * @param transientData * @param gatewayOptions - * @param endorsingParties * @param responseType */ @@ -59,21 +60,22 @@ data class RunTransactionRequest ( @Json(name = "params") val params: kotlin.collections.List = arrayListOf(), - /* An array of MSP IDs to set as the list of endorsing peers for the transaction. */ + /* An array of endorsing peers (name or url) for the transaction. */ @Json(name = "endorsingPeers") val endorsingPeers: kotlin.collections.List? = null, + /* An array of endorsing organizations (by mspID or issuer org name on certificate) for the transaction. */ + @Json(name = "endorsingOrgs") + val endorsingOrgs: kotlin.collections.List? = null, + @Json(name = "transientData") val transientData: kotlin.Any? = null, @Json(name = "gatewayOptions") val gatewayOptions: GatewayOptions? = null, - @Json(name = "endorsingParties") - val endorsingParties: kotlin.collections.List? = arrayListOf(), - @Json(name = "responseType") - val responseType: kotlin.String? = null + val responseType: RunTransactionResponseType? = null ) diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionResponse.kt b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionResponse.kt index 4ed7d3516f4..4119a5706f4 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionResponse.kt +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionResponse.kt @@ -23,7 +23,6 @@ import com.squareup.moshi.JsonClass * * * @param functionOutput - * @param success * @param transactionId */ @@ -33,9 +32,6 @@ data class RunTransactionResponse ( @Json(name = "functionOutput") val functionOutput: kotlin.String, - @Json(name = "success") - val success: kotlin.Boolean, - @Json(name = "transactionId") val transactionId: kotlin.String diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionResponseType.kt b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionResponseType.kt new file mode 100644 index 00000000000..96a4be44ca2 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/RunTransactionResponseType.kt @@ -0,0 +1,63 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.openapitools.client.models + + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Response format from transaction / query execution + * + * Values: JSON,UTF8 + */ + +@JsonClass(generateAdapter = false) +enum class RunTransactionResponseType(val value: kotlin.String) { + + @Json(name = "org.hyperledger.cacti.api.hlfabric.RunTransactionResponseType.JSON") + JSON("org.hyperledger.cacti.api.hlfabric.RunTransactionResponseType.JSON"), + + @Json(name = "org.hyperledger.cacti.api.hlfabric.RunTransactionResponseType.UTF8") + UTF8("org.hyperledger.cacti.api.hlfabric.RunTransactionResponseType.UTF8"); + + /** + * Override [toString()] to avoid using the enum variable name as the value, and instead use + * the actual value defined in the API spec file. + * + * This solves a problem when the variable name and its value are different, and ensures that + * the client sends the correct enum values to the server always. + */ + override fun toString(): String = value + + companion object { + /** + * Converts the provided [data] to a [String] on success, null otherwise. + */ + fun encode(data: kotlin.Any?): kotlin.String? = if (data is RunTransactionResponseType) "$data" else null + + /** + * Returns a valid [RunTransactionResponseType] for [data], null otherwise. + */ + fun decode(data: kotlin.Any?): RunTransactionResponseType? = data?.let { + val normalizedData = "$it".lowercase() + values().firstOrNull { value -> + it == value || normalizedData == "$value".lowercase() + } + } + } +} + diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/WatchBlocksDelegatedSignOptionsV1.kt b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/WatchBlocksDelegatedSignOptionsV1.kt new file mode 100644 index 00000000000..62a2bcacab2 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/WatchBlocksDelegatedSignOptionsV1.kt @@ -0,0 +1,58 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.openapitools.client.models + +import org.openapitools.client.models.WatchBlocksListenerTypeV1 + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Options passed when subscribing to block monitoring with delegated signing. + * + * @param type + * @param channelName + * @param signerCertificate + * @param signerMspID + * @param startBlock From which block start monitoring. Defaults to latest. + * @param uniqueTransactionData Can be used to uniquely identify and authorize signing request + */ + + +data class WatchBlocksDelegatedSignOptionsV1 ( + + @Json(name = "type") + val type: WatchBlocksListenerTypeV1, + + @Json(name = "channelName") + val channelName: kotlin.String, + + @Json(name = "signerCertificate") + val signerCertificate: kotlin.String, + + @Json(name = "signerMspID") + val signerMspID: kotlin.String, + + /* From which block start monitoring. Defaults to latest. */ + @Json(name = "startBlock") + val startBlock: kotlin.String? = null, + + /* Can be used to uniquely identify and authorize signing request */ + @Json(name = "uniqueTransactionData") + val uniqueTransactionData: kotlin.Any? = null + +) + diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/WatchBlocksV1.kt b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/WatchBlocksV1.kt index de97ec704a7..99f33851cac 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/WatchBlocksV1.kt +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/models/WatchBlocksV1.kt @@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass /** * Websocket requests for monitoring new blocks. * - * Values: Subscribe,Next,Unsubscribe,Error,Complete + * Values: Subscribe,SubscribeDelegatedSign,Next,Unsubscribe,Error,Complete */ @JsonClass(generateAdapter = false) @@ -31,6 +31,9 @@ enum class WatchBlocksV1(val value: kotlin.String) { @Json(name = "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Subscribe") Subscribe("org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Subscribe"), + @Json(name = "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.SubscribeDelegatedSign") + SubscribeDelegatedSign("org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.SubscribeDelegatedSign"), + @Json(name = "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Next") Next("org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Next"), diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/api-client/fabric-api-client.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/api-client/fabric-api-client.ts index 24e951167ef..1311dc57f9f 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/api-client/fabric-api-client.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/api-client/fabric-api-client.ts @@ -14,6 +14,9 @@ import { WatchBlocksV1, WatchBlocksOptionsV1, WatchBlocksResponseV1, + WatchBlocksDelegatedSignOptionsV1, + WatchBlocksCactusTransactionsResponseV1, + WatchBlocksCactusTransactionsEventV1, } from "../generated/openapi/typescript-axios"; import { Configuration } from "../generated/openapi/typescript-axios/configuration"; @@ -31,7 +34,8 @@ export class FabricApiClientOptions extends Configuration { */ export class FabricApiClient extends DefaultApi - implements ISocketApiClient { + implements ISocketApiClient +{ public static readonly CLASS_NAME = "FabricApiClient"; private readonly log: Logger; @@ -120,6 +124,115 @@ export class FabricApiClient ); } + /** + * Watch for new blocks on Fabric ledger. Type of response must be configured in monitorOptions. + * Works with delegated signing function (no need to supply identity - requests are signing in a connector callback) + * + * @param monitorOptions Monitoring configuration. + * + * @returns Observable that will receive new blocks once they appear. + */ + public watchBlocksDelegatedSignV1( + monitorOptions: WatchBlocksDelegatedSignOptionsV1, + ): 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.SubscribeDelegatedSign, 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); + }), + ); + } + + /** + * Wait for transaction with specified ID to be committed. + * Must be started before sending the transaction (uses realtime monitoring). + * @warning: Remember to use timeout mechanism on production + * + * @param txId transactionId to wait for. + * @param monitorObservable Block observable in CactusTransactions mode (important - other mode will not work!) + * @returns `WatchBlocksCactusTransactionsEventV1` of specified transaction. + */ + public async waitForTransactionCommit( + txId: string, + monitorObservable: Observable, + ) { + this.log.info("waitForTransactionCommit()", txId); + + return new Promise( + (resolve, reject) => { + const subscription = monitorObservable.subscribe({ + next: (event) => { + try { + this.log.debug( + "waitForTransactionCommit() Received event:", + JSON.stringify(event), + ); + if (!("cactusTransactionsEvents" in event)) { + throw new Error("Invalid event type received!"); + } + + const foundTransaction = event.cactusTransactionsEvents.find( + (tx) => tx.transactionId === txId, + ); + if (foundTransaction) { + this.log.info( + "waitForTransactionCommit() Found transaction with txId", + txId, + ); + subscription.unsubscribe(); + resolve(foundTransaction); + } + } catch (err) { + this.log.error( + "waitForTransactionCommit() event check error:", + err, + ); + subscription.unsubscribe(); + reject(err); + } + }, + error: (err) => { + this.log.error("waitForTransactionCommit() error:", err); + subscription.unsubscribe(); + reject(err); + }, + }); + }, + ); + } + /** * Stop all ongoing monitors, terminate connections. * diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/sign-utils.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/sign-utils.ts new file mode 100644 index 00000000000..82f7c666866 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/sign-utils.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2023 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Util tools used for cryptography related to hyperledger fabric (e.g. signing proposals) + */ + +import crypto from "crypto"; +import jsrsa from "jsrsasign"; +import elliptic from "elliptic"; + +const ellipticCurves = elliptic.curves as any; + +/** + * This function comes from `CryptoSuite_ECDSA_AES.js` and will be part of the + * stand alone fabric-sig package in future. + */ +const ordersForCurve: Record = { + secp256r1: { + halfOrder: ellipticCurves.p256.n.shrn(1), + order: ellipticCurves.p256.n, + }, + secp384r1: { + halfOrder: ellipticCurves.p384.n.shrn(1), + order: ellipticCurves.p384.n, + }, +}; + +/** + * This function comes from `CryptoSuite_ECDSA_AES.js` and will be part of the + * stand alone fabric-sig package in future. + * + * @param sig EC signature + * @param curveParams EC key params. + * @returns Signature + */ +function preventMalleability(sig: any, curveParams: { name: string }) { + const halfOrder = ordersForCurve[curveParams.name].halfOrder; + if (!halfOrder) { + throw new Error( + 'Can not find the half order needed to calculate "s" value for immalleable signatures. Unsupported curve name: ' + + curveParams.name, + ); + } + + // in order to guarantee 's' falls in the lower range of the order, as explained in the above link, + // first see if 's' is larger than half of the order, if so, it needs to be specially treated + if (sig.s.cmp(halfOrder) === 1) { + // module 'bn.js', file lib/bn.js, method cmp() + // convert from BigInteger used by jsrsasign Key objects and bn.js used by elliptic Signature objects + const bigNum = ordersForCurve[curveParams.name].order; + sig.s = bigNum.sub(sig.s); + } + + return sig; +} + +/** + * Internal function to sign input buffer with private key. + * + * @param privateKeyPEM private key in PEM format. + * @param proposalBytes Buffer of the proposal to sign. + * @param algorithm hash algorithm (input for `crypto.createHash`) + * @param ecdsaCurveName private key curve name + * @returns + */ +export function signProposal( + privateKeyPEM: string, + proposalBytes: Buffer, + algorithm = "sha256", + ecdsaCurveName = "p256", +) { + const ecdsaCurve = ellipticCurves[ecdsaCurveName]; + const ecdsa = new elliptic.ec(ecdsaCurve); + const key = jsrsa.KEYUTIL.getKey(privateKeyPEM) as any; + + const signKey = ecdsa.keyFromPrivate(key.prvKeyHex, "hex"); + const digest = crypto + .createHash(algorithm) + .update(proposalBytes) + .digest("hex"); + + let sig = ecdsa.sign(Buffer.from(digest, "hex"), signKey); + sig = preventMalleability(sig, key.ecparams); + return Buffer.from(sig.toDER()); +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/utils.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/utils.ts new file mode 100644 index 00000000000..ac71ac1cedd --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/utils.ts @@ -0,0 +1,31 @@ +/** + * Check if provided variable is a function. Throws otherwise. + * To be used with unsafe `require()` imports from fabric SDK packages. + * + * @param functionVariable function imported from fabric SDK + * @param functionName name of the imported function (for logging purposes) + */ +export function assertFabricFunctionIsAvailable( + functionVariable: unknown, + functionName: string, +) { + if (typeof functionVariable !== "function") { + throw new Error(`${functionName} could not be imported from fabric SDK`); + } +} + +/** + * Convert input bytes into Buffer. Handle cases where input is undefined or null. + * + * @note method comes from Fabric Node SDK. + * + * @param bytes input byte array + * @returns `Buffer` object + */ +export function asBuffer(bytes: Uint8Array | null | undefined): Buffer { + if (!bytes) { + return Buffer.alloc(0); + } + + return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength); // Create a Buffer view to avoid copying +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-v1.ts index 36477a3e2a5..802561f876c 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-v1.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract-go-source/deploy-contract-go-source-endpoint-v1.ts @@ -50,7 +50,7 @@ export class DeployContractGoSourceEndpointV1 implements IWebServiceEndpoint { return this.handleRequest.bind(this); } - public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source"] { + public get oasPath(): (typeof OAS.paths)["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source"] { return OAS.paths[ "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source" ]; diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract/deploy-contract-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract/deploy-contract-endpoint-v1.ts index 7d2f554bf3a..3093c72bbbb 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract/deploy-contract-endpoint-v1.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/deploy-contract/deploy-contract-endpoint-v1.ts @@ -49,7 +49,7 @@ export class DeployContractEndpointV1 implements IWebServiceEndpoint { return this.handleRequest.bind(this); } - public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source"] { + public get oasPath(): (typeof OAS.paths)["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract-go-source"] { return OAS.paths[ "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/deploy-contract" ]; diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts index 815ad8222c0..e6aa44b37f4 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -931,6 +931,87 @@ export interface GetTransactionReceiptResponse { */ 'rwsetWriteData'?: string; } +/** + * + * @export + * @interface RunDelegatedSignTransactionRequest + */ +export interface RunDelegatedSignTransactionRequest { + /** + * An array of endorsing peers (name or url) for the transaction. + * @type {Array} + * @memberof RunDelegatedSignTransactionRequest + */ + 'endorsingPeers'?: Array; + /** + * An array of endorsing organizations (by mspID or issuer org name on certificate) for the transaction. + * @type {Array} + * @memberof RunDelegatedSignTransactionRequest + */ + 'endorsingOrgs'?: Array; + /** + * + * @type {object} + * @memberof RunDelegatedSignTransactionRequest + */ + 'transientData'?: object | null; + /** + * + * @type {string} + * @memberof RunDelegatedSignTransactionRequest + */ + 'signerCertificate': string; + /** + * + * @type {string} + * @memberof RunDelegatedSignTransactionRequest + */ + 'signerMspID': string; + /** + * Can be used to uniquely identify and authorize signing request + * @type {any} + * @memberof RunDelegatedSignTransactionRequest + */ + 'uniqueTransactionData'?: any; + /** + * + * @type {string} + * @memberof RunDelegatedSignTransactionRequest + */ + 'channelName': string; + /** + * + * @type {string} + * @memberof RunDelegatedSignTransactionRequest + */ + 'contractName': string; + /** + * + * @type {FabricContractInvocationType} + * @memberof RunDelegatedSignTransactionRequest + */ + 'invocationType': FabricContractInvocationType; + /** + * + * @type {string} + * @memberof RunDelegatedSignTransactionRequest + */ + 'methodName': string; + /** + * + * @type {Array} + * @memberof RunDelegatedSignTransactionRequest + */ + 'params': Array; + /** + * + * @type {RunTransactionResponseType} + * @memberof RunDelegatedSignTransactionRequest + */ + 'responseType'?: RunTransactionResponseType; +} + + /** * * @export @@ -938,11 +1019,17 @@ export interface GetTransactionReceiptResponse { */ export interface RunTransactionRequest { /** - * An array of MSP IDs to set as the list of endorsing peers for the transaction. + * An array of endorsing peers (name or url) for the transaction. * @type {Array} * @memberof RunTransactionRequest */ 'endorsingPeers'?: Array; + /** + * An array of endorsing organizations (by mspID or issuer org name on certificate) for the transaction. + * @type {Array} + * @memberof RunTransactionRequest + */ + 'endorsingOrgs'?: Array; /** * * @type {object} @@ -993,16 +1080,10 @@ export interface RunTransactionRequest { 'params': Array; /** * - * @type {Array} + * @type {RunTransactionResponseType} * @memberof RunTransactionRequest */ - 'endorsingParties'?: Array; - /** - * - * @type {string} - * @memberof RunTransactionRequest - */ - 'responseType'?: string; + 'responseType'?: RunTransactionResponseType; } @@ -1018,12 +1099,6 @@ export interface RunTransactionResponse { * @memberof RunTransactionResponse */ 'functionOutput': string; - /** - * - * @type {boolean} - * @memberof RunTransactionResponse - */ - 'success': boolean; /** * * @type {string} @@ -1031,6 +1106,20 @@ export interface RunTransactionResponse { */ 'transactionId': string; } +/** + * Response format from transaction / query execution + * @export + * @enum {string} + */ + +export const RunTransactionResponseType = { + JSON: 'org.hyperledger.cacti.api.hlfabric.RunTransactionResponseType.JSON', + UTF8: 'org.hyperledger.cacti.api.hlfabric.RunTransactionResponseType.UTF8' +} as const; + +export type RunTransactionResponseType = typeof RunTransactionResponseType[keyof typeof RunTransactionResponseType]; + + /** * * @export @@ -1213,6 +1302,51 @@ export interface WatchBlocksCactusTransactionsResponseV1 { */ 'cactusTransactionsEvents': Array; } +/** + * Options passed when subscribing to block monitoring with delegated signing. + * @export + * @interface WatchBlocksDelegatedSignOptionsV1 + */ +export interface WatchBlocksDelegatedSignOptionsV1 { + /** + * + * @type {WatchBlocksListenerTypeV1} + * @memberof WatchBlocksDelegatedSignOptionsV1 + */ + 'type': WatchBlocksListenerTypeV1; + /** + * From which block start monitoring. Defaults to latest. + * @type {string} + * @memberof WatchBlocksDelegatedSignOptionsV1 + */ + 'startBlock'?: string; + /** + * + * @type {string} + * @memberof WatchBlocksDelegatedSignOptionsV1 + */ + 'channelName': string; + /** + * + * @type {string} + * @memberof WatchBlocksDelegatedSignOptionsV1 + */ + 'signerCertificate': string; + /** + * + * @type {string} + * @memberof WatchBlocksDelegatedSignOptionsV1 + */ + 'signerMspID': string; + /** + * Can be used to uniquely identify and authorize signing request + * @type {any} + * @memberof WatchBlocksDelegatedSignOptionsV1 + */ + 'uniqueTransactionData'?: any; +} + + /** * Response that corresponds to Fabric SDK \'filtered\' EventType. * @export @@ -1316,6 +1450,7 @@ export type WatchBlocksResponseV1 = WatchBlocksCactusErrorResponseV1 | WatchBloc export const WatchBlocksV1 = { Subscribe: 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Subscribe', + SubscribeDelegatedSign: 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.SubscribeDelegatedSign', Next: 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Next', Unsubscribe: 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Unsubscribe', Error: 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Error', @@ -1519,6 +1654,42 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Runs a transaction on a Fabric ledger using user-provided signing callback. + * @param {RunDelegatedSignTransactionRequest} runDelegatedSignTransactionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + runDelegatedSignTransactionV1: async (runDelegatedSignTransactionRequest: RunDelegatedSignTransactionRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'runDelegatedSignTransactionRequest' is not null or undefined + assertParamExists('runDelegatedSignTransactionV1', 'runDelegatedSignTransactionRequest', runDelegatedSignTransactionRequest) + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-delegated-sign-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); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(runDelegatedSignTransactionRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Runs a transaction on a Fabric ledger. @@ -1619,6 +1790,17 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getTransactionReceiptByTxIDV1(runTransactionRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Runs a transaction on a Fabric ledger using user-provided signing callback. + * @param {RunDelegatedSignTransactionRequest} runDelegatedSignTransactionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async runDelegatedSignTransactionV1(runDelegatedSignTransactionRequest: RunDelegatedSignTransactionRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.runDelegatedSignTransactionV1(runDelegatedSignTransactionRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Runs a transaction on a Fabric ledger. @@ -1689,6 +1871,16 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa getTransactionReceiptByTxIDV1(runTransactionRequest: RunTransactionRequest, options?: any): AxiosPromise { return localVarFp.getTransactionReceiptByTxIDV1(runTransactionRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Runs a transaction on a Fabric ledger using user-provided signing callback. + * @param {RunDelegatedSignTransactionRequest} runDelegatedSignTransactionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + runDelegatedSignTransactionV1(runDelegatedSignTransactionRequest: RunDelegatedSignTransactionRequest, options?: any): AxiosPromise { + return localVarFp.runDelegatedSignTransactionV1(runDelegatedSignTransactionRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary Runs a transaction on a Fabric ledger. @@ -1768,6 +1960,18 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).getTransactionReceiptByTxIDV1(runTransactionRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Runs a transaction on a Fabric ledger using user-provided signing callback. + * @param {RunDelegatedSignTransactionRequest} runDelegatedSignTransactionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public runDelegatedSignTransactionV1(runDelegatedSignTransactionRequest: RunDelegatedSignTransactionRequest, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).runDelegatedSignTransactionV1(runDelegatedSignTransactionRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Runs a transaction on a Fabric ledger. diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/get-block-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/get-block-endpoint-v1.ts index 9c1629b6b2b..b44e8c5605d 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/get-block-endpoint-v1.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/get-block-endpoint-v1.ts @@ -1,6 +1,4 @@ import { Express, Request, Response } from "express"; -import safeStringify from "fast-safe-stringify"; -import sanitizeHtml from "sanitize-html"; import { Logger, @@ -8,6 +6,7 @@ import { LogLevelDesc, Checks, IAsyncProvider, + safeStringifyException, } from "@hyperledger/cactus-common"; import { @@ -55,7 +54,7 @@ export class GetBlockEndpointV1 implements IWebServiceEndpoint { return this.handleRequest.bind(this); } - public getOasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-block"] { + public getOasPath(): (typeof OAS.paths)["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-block"] { return OAS.paths[ "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-block" ]; @@ -87,32 +86,13 @@ export class GetBlockEndpointV1 implements IWebServiceEndpoint { this.log.debug(`POST ${this.getPath()}`); try { - const resBody = await this.opts.connector.getBlock(req.body); - res.status(200).send(resBody); + res.status(200).send(await this.opts.connector.getBlock(req.body)); } catch (error) { - this.log.error(`Crash while serving ${fnTag}:`, error); - const status = 500; - - if (error instanceof Error) { - const message = "Internal Server Error"; - this.log.info(`${message} [${status}]`); - res.status(status).json({ - message, - error: sanitizeHtml(error.stack || error.message, { - allowedTags: [], - allowedAttributes: {}, - }), - }); - } else { - this.log.warn("Unexpected exception that is not instance of Error!"); - res.status(status).json({ - message: "Unexpected Error", - error: sanitizeHtml(safeStringify(error), { - allowedTags: [], - allowedAttributes: {}, - }), - }); - } + this.log.error(`Crash while serving ${fnTag}`, error); + res.status(500).json({ + message: "Internal Server Error", + error: safeStringifyException(error), + }); } } } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-endpoint-v1.ts index 4e6522d293a..c75b1b72ebd 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-endpoint-v1.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-endpoint-v1.ts @@ -26,7 +26,8 @@ export interface IGetPrometheusExporterMetricsEndpointV1Options { } export class GetPrometheusExporterMetricsEndpointV1 - implements IWebServiceEndpoint { + implements IWebServiceEndpoint +{ private readonly log: Logger; constructor( @@ -57,7 +58,7 @@ export class GetPrometheusExporterMetricsEndpointV1 return this.handleRequest.bind(this); } - public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-prometheus-exporter-metrics"] { + public get oasPath(): (typeof OAS.paths)["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-prometheus-exporter-metrics"] { return OAS.paths[ "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-prometheus-exporter-metrics" ]; diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-transaction-receipt/get-transaction-receipt-by-txid-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-transaction-receipt/get-transaction-receipt-by-txid-endpoint-v1.ts index f6315ad3bdb..e0a0bef50cd 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-transaction-receipt/get-transaction-receipt-by-txid-endpoint-v1.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-transaction-receipt/get-transaction-receipt-by-txid-endpoint-v1.ts @@ -26,7 +26,8 @@ export interface IRunTransactionEndpointV1Options { } export class GetTransactionReceiptByTxIDEndpointV1 - implements IWebServiceEndpoint { + implements IWebServiceEndpoint +{ private readonly log: Logger; constructor(public readonly opts: IRunTransactionEndpointV1Options) { @@ -55,7 +56,7 @@ export class GetTransactionReceiptByTxIDEndpointV1 return this.handleRequest.bind(this); } - public getOasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-transaction-receipt-by-txid"] { + public getOasPath(): (typeof OAS.paths)["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-transaction-receipt-by-txid"] { return OAS.paths[ "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-transaction-receipt-by-txid" ]; diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts index 257e35abd2e..b5ed3f47c28 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; - +import { v4 as uuidv4 } from "uuid"; import { Certificate } from "@fidm/x509"; import { Express } from "express"; import { RuntimeError } from "run-time-error"; @@ -12,6 +12,10 @@ import { SSHExecCommandOptions, SSHExecCommandResponse, } from "node-ssh"; +import type { + Server as SocketIoServer, + Socket as SocketIoSocket, +} from "socket.io"; import { DefaultEventHandlerOptions, DefaultEventHandlerStrategies, @@ -22,13 +26,15 @@ import { TransientMap, Wallet, } from "fabric-network"; - -import type { - Server as SocketIoServer, - Socket as SocketIoSocket, -} from "socket.io"; - -import OAS from "../json/openapi.json"; +import { + BuildProposalRequest, + Channel, + Client, + IdentityContext, + User, + Endorser, + ICryptoKey, +} from "fabric-common"; import { ConsensusAlgorithmFamily, @@ -51,11 +57,18 @@ import { LoggerProvider, } from "@hyperledger/cactus-common"; +import OAS from "../json/openapi.json"; + import { IRunTransactionEndpointV1Options, RunTransactionEndpointV1, } from "./run-transaction/run-transaction-endpoint-v1"; +import { + IRunDelegatedSignTransactionEndpointV1Options, + RunDelegatedSignTransactionEndpointV1, +} from "./run-transaction/run-delegated-sign-transaction-endpoint-v1"; + import { IGetPrometheusExporterMetricsEndpointV1Options, GetPrometheusExporterMetricsEndpointV1, @@ -85,6 +98,9 @@ import { GetBlockResponseV1, WatchBlocksV1, WatchBlocksOptionsV1, + RunDelegatedSignTransactionRequest, + RunTransactionResponseType, + WatchBlocksDelegatedSignOptionsV1, } from "./generated/openapi/typescript-axios/index"; import { @@ -105,7 +121,6 @@ import FabricCAServices, { IRegisterRequest, } from "fabric-ca-client"; import { createGateway } from "./common/create-gateway"; -import { Endorser, ICryptoKey } from "fabric-common"; import { IVaultConfig, @@ -125,6 +140,10 @@ import { import { GetBlockEndpointV1 } from "./get-block/get-block-endpoint-v1"; import { querySystemChainCode } from "./common/query-system-chain-code"; import { isSshExecOk } from "./common/is-ssh-exec-ok"; +import { asBuffer, assertFabricFunctionIsAvailable } from "./common/utils"; + +const { loadFromConfig } = require("fabric-network/lib/impl/ccp/networkconfig"); +assertFabricFunctionIsAvailable(loadFromConfig, "loadFromConfig"); /** * Constant value holding the default $GOPATH in the Fabric CLI container as @@ -132,7 +151,6 @@ import { isSshExecOk } from "./common/is-ssh-exec-ok"; * found in the https://github.com/hyperledger/fabric-samples repository. */ export const K_DEFAULT_CLI_CONTAINER_GO_PATH = "/opt/gopath/"; -export const JSONstringResponseType = "JSONstring"; /** * The command that will be used to issue docker commands while controlling @@ -140,6 +158,11 @@ export const JSONstringResponseType = "JSONstring"; */ export const K_DEFAULT_DOCKER_BINARY = "docker"; +export type SignPayloadCallback = ( + payload: Buffer, + txData: unknown, +) => Promise; + export interface IPluginLedgerConnectorFabricOptions extends ICactusPluginOptions { logLevel?: LogLevelDesc; @@ -158,6 +181,7 @@ export interface IPluginLedgerConnectorFabricOptions supportedIdentity?: FabricSigningCredentialType[]; vaultConfig?: IVaultConfig; webSocketConfig?: IWebSocketConfig; + signCallback?: SignPayloadCallback; } export class PluginLedgerConnectorFabric @@ -169,7 +193,8 @@ export class PluginLedgerConnectorFabric RunTransactionResponse >, ICactusPlugin, - IPluginWebService { + IPluginWebService +{ public static readonly CLASS_NAME = "PluginLedgerConnectorFabric"; private readonly instanceId: string; private readonly log: Logger; @@ -188,6 +213,11 @@ export class PluginLedgerConnectorFabric return PluginLedgerConnectorFabric.CLASS_NAME; } + /** + * Callback used to sign fabric requests in methods that use delegated sign. + */ + public signCallback: SignPayloadCallback | undefined; + constructor(public readonly opts: IPluginLedgerConnectorFabricOptions) { const fnTag = `${this.className}#constructor()`; Checks.truthy(opts, `${fnTag} arg options`); @@ -234,6 +264,8 @@ export class PluginLedgerConnectorFabric if (this.sshDebugOn) { this.opts.sshConfig = this.enableSshDebugLogs(this.opts.sshConfig); } + + this.signCallback = opts.signCallback; } public getOpenApiSpec(): unknown { @@ -267,13 +299,12 @@ export class PluginLedgerConnectorFabric return; } - public async getConsensusAlgorithmFamily(): Promise< - ConsensusAlgorithmFamily - > { + public async getConsensusAlgorithmFamily(): Promise { return ConsensusAlgorithmFamily.Authority; } public async hasTransactionFinality(): Promise { - const currentConsensusAlgorithmFamily = await this.getConsensusAlgorithmFamily(); + const currentConsensusAlgorithmFamily = + await this.getConsensusAlgorithmFamily(); return consensusHasTransactionFinality(currentConsensusAlgorithmFamily); } @@ -847,10 +878,59 @@ export class PluginLedgerConnectorFabric const monitor = new WatchBlocksV1Endpoint({ socket, logLevel: this.opts.logLevel, - gateway: await this.createGatewayWithOptions(options.gatewayOptions), }); this.runningWatchBlocksMonitors.add(monitor); - await monitor.subscribe(options); + await monitor.subscribe( + options, + await this.createGatewayWithOptions(options.gatewayOptions), + ); + this.log.debug( + "Running monitors count:", + this.runningWatchBlocksMonitors.size, + ); + + socket.on("disconnect", () => { + this.runningWatchBlocksMonitors.delete(monitor); + this.log.debug( + "Running monitors count:", + this.runningWatchBlocksMonitors.size, + ); + }); + }, + ); + + socket.on( + WatchBlocksV1.SubscribeDelegatedSign, + async (options: WatchBlocksDelegatedSignOptionsV1) => { + if (!this.signCallback) { + socket.emit(WatchBlocksV1.Error, { + code: 500, + errorMessage: + "WatchBlocksDelegatedSignOptionsV1 called but signCallback is missing!", + }); + return; + } + + // Start monitoring + const monitor = new WatchBlocksV1Endpoint({ + socket, + logLevel: this.opts.logLevel, + }); + this.runningWatchBlocksMonitors.add(monitor); + + const { channel, userIdCtx } = await this.getFabricClientWithoutSigner( + options.channelName, + options.signerCertificate, + options.signerMspID, + options.uniqueTransactionData, + ); + + await monitor.SubscribeDelegatedSign( + options, + channel, + userIdCtx, + this.signCallback.bind(this), + ); this.log.debug( "Running monitors count:", this.runningWatchBlocksMonitors.size, @@ -931,6 +1011,15 @@ export class PluginLedgerConnectorFabric endpoints.push(endpoint); } + { + const opts: IRunDelegatedSignTransactionEndpointV1Options = { + connector: this, + logLevel: this.opts.logLevel, + }; + const endpoint = new RunDelegatedSignTransactionEndpointV1(opts); + endpoints.push(endpoint); + } + { const opts: IRunTransactionEndpointV1Options = { connector: this, @@ -939,6 +1028,7 @@ export class PluginLedgerConnectorFabric const endpoint = new GetTransactionReceiptByTxIDEndpointV1(opts); endpoints.push(endpoint); } + { const endpoint = new GetBlockEndpointV1({ connector: this, @@ -1104,6 +1194,134 @@ export class PluginLedgerConnectorFabric return gateway; } + /** + * Common method for converting `Buffer` response from running transaction + * into type specified in input `RunTransactionResponseType` field. + * + * @param data transaction response + * @param responseType target type format + * @returns converted data (string) + */ + private convertToTransactionResponseType( + data: Buffer, + responseType?: RunTransactionResponseType, + ): string { + switch (responseType) { + case RunTransactionResponseType.JSON: + return JSON.stringify(data); + case RunTransactionResponseType.UTF8: + default: + return data.toString("utf-8"); + } + } + + /** + * Filter endorsers by peers + * + * @param endorsingPeers list of endorsers to use (name or url). + * @param allEndorsers list of all endorsing peers detected. + * @returns filtered list of endorser objects. + */ + private filterEndorsingPeers( + endorsingPeers: string[], + allEndorsers: Endorser[], + ) { + return allEndorsers.filter((e) => { + const looseEndpoint = e.endpoint as any; + return ( + endorsingPeers.includes(e.name) || + endorsingPeers.includes(looseEndpoint.url) || + endorsingPeers.includes(looseEndpoint.addr) + ); + }); + } + + /** + * Filter endorsers by organization. + * + * @param endorsingOrgs list of endorser organizations to use (mspid or org name on certificate). + * @param allEndorsers list of all endorsing peers detected. + * @returns filtered list of endorser objects. + */ + private filterEndorsingOrgs( + endorsingOrgs: string[], + allEndorsers: Endorser[], + ) { + const allEndorsersLoose = allEndorsers as unknown as Array< + Endorser & { options: { pem: string } } + >; + + return allEndorsersLoose + .map((endorser) => { + const certificate = Certificate.fromPEM( + endorser.options.pem as unknown as Buffer, + ); + return { certificate, endorser }; + }) + .filter( + ({ endorser, certificate }) => + endorsingOrgs.includes(endorser.mspid) || + endorsingOrgs.includes(certificate.issuer.organizationName), + ) + .map((it) => it.endorser); + } + + /** + * Filter endorsers by both peers and organizations + * @param allEndorsers list of all endorsing peers detected. + * @param endorsingPeers list of endorsers to use (name or url). + * @param endorsingOrgs list of endorser organizations to use (mspid or org name on certificate). + * @returns filtered list of endorser objects. + */ + private filterEndorsers( + allEndorsers: Endorser[], + endorsingPeers?: string[], + endorsingOrgs?: string[], + ) { + const toEndorserNames = (e: Endorser[]) => e.map((v) => v.name); + this.log.debug("Endorsing targets:", toEndorserNames(allEndorsers)); + + if (endorsingPeers) { + allEndorsers = this.filterEndorsingPeers(endorsingPeers, allEndorsers); + this.log.debug( + "Endorsing targets after peer filtering:", + toEndorserNames(allEndorsers), + ); + } + + if (endorsingOrgs) { + allEndorsers = this.filterEndorsingOrgs(endorsingOrgs, allEndorsers); + this.log.debug( + "Endorsing targets after org filtering:", + toEndorserNames(allEndorsers), + ); + } + + return allEndorsers; + } + + /** + * Convert transient data from input into transient map (used in private transactions) + * + * @param transientData transient data from request + * @returns correct TransientMap + */ + private toTransientMap(transientData?: unknown): TransientMap { + const transientMap = transientData as TransientMap; + + try { + //Obtains and parses each component of transient data + for (const key in transientMap) { + transientMap[key] = Buffer.from(JSON.stringify(transientMap[key])); + } + } catch (ex) { + this.log.error(`Building transient map crashed: `, ex); + throw new Error(`Unable to build the transient map: ${ex.message}`); + } + + return transientMap; + } + public async transact( req: RunTransactionRequest, ): Promise { @@ -1116,20 +1334,23 @@ export class PluginLedgerConnectorFabric methodName: fnName, params, transientData, - endorsingParties, responseType: responseType, } = req; try { this.log.debug("%s Creating Fabric Gateway instance...", fnTag); const gateway = await this.createGateway(req); - // const gateway = await this.createGatewayLegacy(req.signingCredential); this.log.debug("%s Obtaining Fabric gateway network instance...", fnTag); const network = await gateway.getNetwork(channelName); this.log.debug("%s Obtaining Fabric contract instance...", fnTag); const contract = network.getContract(contractName); - const channel = network.getChannel(); + const endorsingTargets = this.filterEndorsers( + channel.getEndorsers(), + req.endorsingPeers, + req.endorsingOrgs, + ); + const endorsers = channel.getEndorsers(); const endorsersMetadata = endorsers.map((x) => ({ @@ -1143,54 +1364,25 @@ export class PluginLedgerConnectorFabric this.log.debug("%s Endorsers metadata: %o", fnTag, endorsersMetadata); let out: Buffer; - let success: boolean; let transactionId = ""; switch (invocationType) { case FabricContractInvocationType.Call: { - out = await contract.evaluateTransaction(fnName, ...params); - success = true; + out = await contract + .createTransaction(fnName) + .setEndorsingPeers(endorsingTargets) + .evaluate(...params); break; } case FabricContractInvocationType.Send: { this.log.debug("%s Creating tx instance on %s", fnTag, contractName); this.log.debug("%s Endorsing peers: %o", fnTag, req.endorsingPeers); const tx = contract.createTransaction(fnName); - this.log.debug("%s Created TX OK %o", fnTag, tx); - if (req.endorsingPeers) { - const { endorsingPeers } = req; - const channel = network.getChannel(); - - const allChannelEndorsers = (channel.getEndorsers() as unknown) as Array< - Endorser & { options: { pem: string } } - >; - - const endorsers = allChannelEndorsers - .map((endorser) => { - const certificate = Certificate.fromPEM( - (endorser.options.pem as unknown) as Buffer, - ); - return { certificate, endorser }; - }) - .filter( - ({ endorser, certificate }) => - endorsingPeers.includes(endorser.mspid) || - endorsingPeers.includes(certificate.issuer.organizationName), - ) - .map((it) => it.endorser); - - this.log.debug( - "%o endorsers: %o", - endorsers.length, - endorsers.map((it) => `${it.mspid}:${it.name}`), - ); - tx.setEndorsingPeers(endorsers); - } + tx.setEndorsingPeers(endorsingTargets); this.log.debug("%s Submitting TX... (%o)", fnTag, params); out = await tx.submit(...params); this.log.debug("%s Submitted TX OK (%o)", fnTag, params); transactionId = tx.getTransactionId(); this.log.debug("%s Obtained TX ID OK (%s)", fnTag, transactionId); - success = true; break; } case FabricContractInvocationType.Sendprivate: { @@ -1200,32 +1392,10 @@ export class PluginLedgerConnectorFabric throw new Error(`${fnTag} ${message}`); } - const transientMap: TransientMap = transientData as TransientMap; - - try { - //Obtains and parses each component of transient data - for (const key in transientMap) { - transientMap[key] = Buffer.from( - JSON.stringify(transientMap[key]), - ); - } - } catch (ex) { - this.log.error(`Building transient map crashed: `, ex); - throw new Error( - `${fnTag} Unable to build the transient map: ${ex.message}`, - ); - } - + const transientMap = this.toTransientMap(req.transientData); const transactionProposal = await contract.createTransaction(fnName); - - if (endorsingParties) { - endorsingParties.forEach((org) => { - transactionProposal.setEndorsingOrganizations(org); - }); - } - + transactionProposal.setEndorsingPeers(endorsingTargets); out = await transactionProposal.setTransient(transientMap).submit(); - success = true; break; } default: { @@ -1233,19 +1403,12 @@ export class PluginLedgerConnectorFabric throw new Error(`${fnTag} unknown ${message}`); } } - let outResp = ""; - - switch (responseType) { - case JSONstringResponseType: - outResp = JSON.stringify(out); - break; - default: - outResp = out.toString("utf-8"); - } const res: RunTransactionResponse = { - functionOutput: outResp, - success, + functionOutput: this.convertToTransactionResponseType( + out, + responseType, + ), transactionId: transactionId, }; gateway.disconnect(); @@ -1258,6 +1421,7 @@ export class PluginLedgerConnectorFabric throw new Error(`${fnTag} Unable to run transaction: ${ex.message}`); } } + public async getTransactionReceiptByTxID( req: RunTransactionRequest, ): Promise { @@ -1612,4 +1776,245 @@ export class PluginLedgerConnectorFabric decodedBlock: responseData, }; } + + /** + * Get plain Fabric Client, Channel and IdentityContext without a signer attached (like in gateway). + * These low-level entities can be used to manually sign and send requests. + * Node discovery will be done if configured in connector, so signCallback may be used in the process. + * + * @param channelName channel name to connect to + * @param signerCertificate signing user certificate + * @param signerMspID signing user mspid + * @param uniqueTransactionData unique transaction data to be passed to sign callback (on discovery). + * @returns `Client`, `Channel` and `IdentityContext` + */ + private async getFabricClientWithoutSigner( + channelName: string, + signerCertificate: string, + signerMspID: string, + uniqueTransactionData?: unknown, + ): Promise<{ + client: Client; + channel: Channel; + userIdCtx: IdentityContext; + }> { + this.log.debug(`getFabricChannelWithoutSigner() channel ${channelName}`); + // Setup a client without a signer + const clientId = `fcClient-${uuidv4()}`; + this.log.debug("Create Fabric Client without a signer with ID", clientId); + const client = new Client(clientId); + // Use fabric SDK methods for parsing connection profile into Client structure + await loadFromConfig(client, this.opts.connectionProfile); + + // Create user + const user = User.createUser("", "", signerMspID, signerCertificate); + const userIdCtx = client.newIdentityContext(user); + + const channel = client.getChannel(channelName); + + // Discover fabric nodes + if ((this.opts.discoveryOptions?.enabled ?? true) && this.signCallback) { + const discoverers = []; + for (const peer of client.getEndorsers()) { + const discoverer = channel.client.newDiscoverer(peer.name, peer.mspid); + discoverer.setEndpoint(peer.endpoint); + discoverers.push(discoverer); + } + + const discoveryService = channel.newDiscoveryService(channel.name); + const discoveryRequest = discoveryService.build(userIdCtx); + const signature = await this.signCallback( + discoveryRequest, + uniqueTransactionData, + ); + await discoveryService.sign(signature); + await discoveryService.send({ + asLocalhost: this.opts.discoveryOptions?.asLocalhost ?? true, + targets: discoverers, + }); + } + + this.log.info( + `Created channel for ${channelName} with ${ + channel.getMspids().length + } Mspids, ${channel.getCommitters().length} commiters, ${ + channel.getEndorsers().length + } endorsers`, + ); + + return { + client, + channel, + userIdCtx, + }; + } + + /** + * Send fabric query or transaction request using delegated sign with `signCallback`. + * Interface is mostly compatible with regular transact() method. + * + * @param req request specification + * @returns query / transaction response + */ + public async transactDelegatedSign( + req: RunDelegatedSignTransactionRequest, + ): Promise { + this.log.info( + `transactDelegatedSign() ${req.methodName}@${req.contractName} on channel ${req.channelName}`, + ); + if (!this.signCallback) { + throw new Error( + "No signing callback was set for this connector - abort!", + ); + } + + // Connect Client and Channel, discover nodes + const { channel, userIdCtx } = await this.getFabricClientWithoutSigner( + req.channelName, + req.signerCertificate, + req.signerMspID, + req.uniqueTransactionData, + ); + + const endorsingTargets = this.filterEndorsers( + channel.getEndorsers(), + req.endorsingPeers, + req.endorsingOrgs, + ); + + switch (req.invocationType) { + case FabricContractInvocationType.Call: { + const query = channel.newQuery(req.contractName); + const queryRequest = query.build(userIdCtx, { + fcn: req.methodName, + args: req.params, + }); + const signature = await this.signCallback( + queryRequest, + req.uniqueTransactionData, + ); + query.sign(signature); + const queryResponse = await query.send({ + targets: endorsingTargets, + }); + + // Parse query results + // Strategy: first endorsed response is returned + for (const res of queryResponse.responses) { + if (res.response.status === 200 && res.endorsement) { + return { + functionOutput: this.convertToTransactionResponseType( + asBuffer(res.response.payload), + ), + transactionId: "", + }; + } + } + + throw new Error( + `Query failed, errors: ${JSON.stringify( + queryResponse.errors, + )}, responses: ${JSON.stringify( + queryResponse.responses.map((r) => { + return { + status: r.response.status, + message: r.response.message, + }; + }), + )}`, + ); + } + case FabricContractInvocationType.Send: + case FabricContractInvocationType.Sendprivate: { + // Private transactions needs transient data set + if ( + req.invocationType === FabricContractInvocationType.Sendprivate && + !req.transientData + ) { + throw new Error( + "Missing transient data in a private transaction mode", + ); + } + + const endorsement = channel.newEndorsement(req.contractName); + + const buildOptions: BuildProposalRequest = { + fcn: req.methodName, + args: req.params, + }; + if (req.transientData) { + buildOptions.transientMap = this.toTransientMap(req.transientData); + } + + const endorsementRequest = endorsement.build(userIdCtx, buildOptions); + const endorsementSignature = await this.signCallback( + endorsementRequest, + req.uniqueTransactionData, + ); + await endorsement.sign(endorsementSignature); + const endorsementResponse = await endorsement.send({ + targets: endorsingTargets, + }); + + if ( + !endorsementResponse.responses || + endorsementResponse.responses.length === 0 + ) { + throw new Error("No endorsement responses from peers! Abort"); + } + + // We will try to commit if at least one endorsement passed + let endorsedMethodResponse: Buffer | undefined; + + for (const response of endorsementResponse.responses) { + const endorsementStatus = `${response.connection.name}: ${ + response.response.status + } message ${response.response.message}, endorsement: ${Boolean( + response.endorsement, + )}`; + + if (response.response.status !== 200 || !response.endorsement) { + this.log.warn(`Endorsement from peer ERROR: ${endorsementStatus}`); + } else { + this.log.debug(`Endorsement from peer OK: ${endorsementStatus}`); + endorsedMethodResponse = asBuffer(response.payload); + } + } + + if (!endorsedMethodResponse) { + throw new Error("No valid endorsements received!"); + } + + const commit = endorsement.newCommit(); + const commitRequest = commit.build(userIdCtx); + const commitSignature = await this.signCallback( + commitRequest, + req.uniqueTransactionData, + ); + await commit.sign(commitSignature); + const commitResponse = await commit.send({ + targets: channel.getCommitters(), + }); + this.log.debug("Commit response:", commitResponse); + + if (commitResponse.status !== "SUCCESS") { + throw new Error("Transaction commit request failed!"); + } + + this.prometheusExporter.addCurrentTransaction(); + + return { + functionOutput: this.convertToTransactionResponseType( + endorsedMethodResponse, + ), + transactionId: userIdCtx.transactionId, + }; + } + default: { + throw new Error( + `transactDelegatedSign() Unknown invocation type: ${req.invocationType}`, + ); + } + } + } } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts index 155edc3ce28..50172d9cf87 100755 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts @@ -8,10 +8,9 @@ export { export { PluginLedgerConnectorFabric, IPluginLedgerConnectorFabricOptions, + SignPayloadCallback, } from "./plugin-ledger-connector-fabric"; -export { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector"; - import { IPluginFactoryOptions } from "@hyperledger/cactus-core-api"; import { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector"; @@ -23,3 +22,5 @@ export async function createPluginFactory( export { IVaultConfig, IWebSocketConfig } from "./identity/identity-provider"; export { IIdentityData } from "./identity/internal/cert-datastore"; + +export { signProposal } from "./common/sign-utils"; diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/run-transaction/run-delegated-sign-transaction-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/run-transaction/run-delegated-sign-transaction-endpoint-v1.ts new file mode 100644 index 00000000000..e064aba32e7 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/run-transaction/run-delegated-sign-transaction-endpoint-v1.ts @@ -0,0 +1,102 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + LoggerProvider, + LogLevelDesc, + Checks, + IAsyncProvider, + safeStringifyException, +} from "@hyperledger/cactus-common"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, + IEndpointAuthzOptions, +} from "@hyperledger/cactus-core-api"; + +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginLedgerConnectorFabric } from "../plugin-ledger-connector-fabric"; +import OAS from "../../json/openapi.json"; + +export interface IRunDelegatedSignTransactionEndpointV1Options { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorFabric; +} + +export class RunDelegatedSignTransactionEndpointV1 + implements IWebServiceEndpoint +{ + private readonly log: Logger; + + constructor( + public readonly opts: IRunDelegatedSignTransactionEndpointV1Options, + ) { + const fnTag = "RunDelegatedSignTransactionEndpointV1#constructor()"; + + Checks.truthy(opts, `${fnTag} options`); + Checks.truthy(opts.connector, `${fnTag} options.connector`); + + this.log = LoggerProvider.getOrCreate({ + label: "run-delegated-sign-transaction-endpoint-v1", + level: opts.logLevel || "INFO", + }); + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public get oasPath(): (typeof OAS.paths)["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-delegated-sign-transaction"] { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-delegated-sign-transaction" + ]; + } + + public getPath(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.oasPath.post.operationId; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = "RunDelegatedSignTransactionEndpointV1#handleRequest()"; + this.log.debug(`POST ${this.getPath()}`); + + try { + res + .status(200) + .json(await this.opts.connector.transactDelegatedSign(req.body)); + } catch (error) { + this.log.error(`Crash while serving ${fnTag}`, error); + res.status(500).json({ + message: "Internal Server Error", + error: safeStringifyException(error), + }); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/run-transaction/run-transaction-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/run-transaction/run-transaction-endpoint-v1.ts index dcd016a6259..3884cc965b1 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/run-transaction/run-transaction-endpoint-v1.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/run-transaction/run-transaction-endpoint-v1.ts @@ -6,6 +6,7 @@ import { LogLevelDesc, Checks, IAsyncProvider, + safeStringifyException, } from "@hyperledger/cactus-common"; import { @@ -17,7 +18,6 @@ import { import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; import { PluginLedgerConnectorFabric } from "../plugin-ledger-connector-fabric"; -import { RunTransactionRequest } from "../generated/openapi/typescript-axios"; import OAS from "../../json/openapi.json"; export interface IRunTransactionEndpointV1Options { @@ -54,7 +54,7 @@ export class RunTransactionEndpointV1 implements IWebServiceEndpoint { return this.handleRequest.bind(this); } - public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-transaction"] { + public get oasPath(): (typeof OAS.paths)["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-transaction"] { return OAS.paths[ "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/run-transaction" ]; @@ -84,15 +84,13 @@ export class RunTransactionEndpointV1 implements IWebServiceEndpoint { this.log.debug(`POST ${this.getPath()}`); try { - const reqBody = req.body as RunTransactionRequest; - const resBody = await this.opts.connector.transact(reqBody); - res.status(200); - res.json(resBody); - } catch (ex) { - this.log.error(`${fnTag} failed to serve request`, ex); - res.status(500); - res.statusMessage = ex.message; - res.json({ error: ex.stack }); + res.status(200).json(await this.opts.connector.transact(req.body)); + } catch (error) { + this.log.error(`Crash while serving ${fnTag}`, error); + res.status(500).json({ + message: "Internal Server Error", + error: safeStringifyException(error), + }); } } } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/watch-blocks/watch-blocks-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/watch-blocks/watch-blocks-v1-endpoint.ts index 272153df0ce..67867c2be71 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/watch-blocks/watch-blocks-v1-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/watch-blocks/watch-blocks-v1-endpoint.ts @@ -1,25 +1,46 @@ -import { Socket as SocketIoSocket } from "socket.io"; - -import { BlockEvent, BlockListener, EventType, Gateway } from "fabric-network"; - import { Logger, LogLevelDesc, LoggerProvider, Checks, + safeStringifyException, } from "@hyperledger/cactus-common"; +import { + BlockType, + Channel, + EventCallback, + EventInfo, + IdentityContext, +} from "fabric-common"; +import { BlockEvent, BlockListener, EventType, Gateway } from "fabric-network"; +import { Socket as SocketIoSocket } from "socket.io"; +import { v4 as uuidv4 } from "uuid"; +import { RuntimeError } from "run-time-error"; + +import { assertFabricFunctionIsAvailable } from "../common/utils"; +import { SignPayloadCallback } from "../plugin-ledger-connector-fabric"; import { WatchBlocksV1, WatchBlocksResponseV1, WatchBlocksListenerTypeV1, WatchBlocksOptionsV1, WatchBlocksCactusTransactionsEventV1, + WatchBlocksDelegatedSignOptionsV1, } from "../generated/openapi/typescript-axios"; -import safeStringify from "fast-safe-stringify"; -import sanitizeHtml from "sanitize-html"; -import { RuntimeError } from "run-time-error"; +const { + newFilteredBlockEvent, +} = require("fabric-network/lib/impl/event/filteredblockeventfactory"); +assertFabricFunctionIsAvailable(newFilteredBlockEvent, "newFilteredBlockEvent"); +const { + newFullBlockEvent, +} = require("fabric-network/lib/impl/event/fullblockeventfactory"); +assertFabricFunctionIsAvailable(newFullBlockEvent, "newFullBlockEvent"); +const { + newPrivateBlockEvent, +} = require("fabric-network/lib/impl/event/privateblockeventfactory"); +assertFabricFunctionIsAvailable(newPrivateBlockEvent, "newPrivateBlockEvent"); /** * WatchBlocksV1Endpoint configuration. @@ -27,24 +48,6 @@ import { RuntimeError } from "run-time-error"; export interface IWatchBlocksV1EndpointConfiguration { logLevel?: LogLevelDesc; socket: SocketIoSocket; - gateway: Gateway; -} - -/** - * 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)); } /** @@ -129,15 +132,10 @@ export class WatchBlocksV1Endpoint { functionArgs: decodedArgs, }); } catch (error) { - const errorMessage = safeStringifyException(error); - log.error( + log.warn( "Could not retrieve transaction from received block. Error:", - errorMessage, + safeStringifyException(error), ); - socket.emit(WatchBlocksV1.Error, { - code: 512, - errorMessage, - }); } } @@ -265,12 +263,39 @@ export class WatchBlocksV1Endpoint { return { listener, listenerType }; } + /** + * Use Fabric SDK functions to convert raw `EventInfo` to `BlockEvent` of specified `BlockType`. + * + * @param blockType block type (e.g. full, filtered) + * @param event raw block event from EventService + * @returns parsed BlockEvent + */ + private toFabricBlockEvent( + blockType: BlockType, + event: EventInfo, + ): BlockEvent { + if (blockType === "filtered") { + return newFilteredBlockEvent(event); + } else if (blockType === "full") { + return newFullBlockEvent(event); + } else if (blockType === "private") { + return newPrivateBlockEvent(event); + } else { + // Exhaustive check + const unknownBlockType: never = blockType; + throw new Error(`Unsupported event type: ${unknownBlockType}`); + } + } + /** * Subscribe to new blocks on fabric ledger, push them to the client via socketio. * * @param options Block monitoring options. */ - public async subscribe(options: WatchBlocksOptionsV1): Promise { + public async subscribe( + options: WatchBlocksOptionsV1, + gateway: Gateway, + ): Promise { const { socket, log } = this; const clientId = socket.id; log.info(`${WatchBlocksV1.Subscribe} => clientId: ${clientId}`); @@ -285,7 +310,7 @@ export class WatchBlocksV1Endpoint { try { Checks.truthy(options.channelName, "Missing channel name"); - const network = await this.config.gateway.getNetwork(options.channelName); + const network = await gateway.getNetwork(options.channelName); const { listener, listenerType } = this.getBlockListener(options.type); @@ -304,6 +329,131 @@ export class WatchBlocksV1Endpoint { clientId, ); network.removeBlockListener(listener); + gateway.disconnect(); + this.close(); + }); + + socket.on(WatchBlocksV1.Unsubscribe, () => { + log.info(`${WatchBlocksV1.Unsubscribe} => clientId: ${clientId}`); + this.close(); + }); + } catch (error) { + const errorMessage = safeStringifyException(error); + log.warn(errorMessage); + socket.emit(WatchBlocksV1.Error, { + code: 500, + errorMessage, + }); + } + } + + /** + * Subscribe to new blocks on fabric ledger, push them to the client via socketio. + * Uses delegate signing callback from the connector to support custom signing scenarios. + * + * @param options Block monitoring options. + * @param channel Target channel to monitor blocks. + * @param userIdCtx Signer identity context. + * @param signCallback Signing callback to use when sending requests to a network. + */ + public async SubscribeDelegatedSign( + options: WatchBlocksDelegatedSignOptionsV1, + channel: Channel, + userIdCtx: IdentityContext, + signCallback: SignPayloadCallback, + ): Promise { + const { socket, log } = this; + const clientId = socket.id; + log.info( + `${WatchBlocksV1.SubscribeDelegatedSign} => clientId: ${clientId}`, + ); + log.debug( + "WatchBlocksV1.SubscribeDelegatedSign args: channelName:", + options.channelName, + ", startBlock:", + options.startBlock, + ", type: ", + options.type, + ); + + try { + const { listener, listenerType } = this.getBlockListener(options.type); + log.debug("Subscribing to new blocks... listenerType:", listenerType); + + // Eventers + // (prefer peers from same org) + let peers = channel.getEndorsers(options.signerMspID); + peers = peers.length > 0 ? peers : channel.getEndorsers(); + const eventers = peers.map((peer) => { + const eventer = channel.client.newEventer(peer.name); + eventer.setEndpoint(peer.endpoint); + return eventer; + }); + if (eventers.length === 0) { + throw new Error("No peers (eventers) available for monitoring"); + } + + // Event Service + const eventService = channel.newEventService( + `SubscribeDelegatedSign_${uuidv4()}`, + ); + eventService.setTargets(eventers); + + // Event listener + const eventCallback: EventCallback = ( + error?: Error, + event?: EventInfo, + ) => { + try { + if (error) { + throw error; + } + + if (event) { + listener(this.toFabricBlockEvent(listenerType, event)); + } else { + this.log.warn( + "SubscribeDelegatedSign() missing event - without an error.", + ); + } + } catch (error) { + const errorMessage = safeStringifyException(error); + log.warn("SubscribeDelegatedSign callback exception:", errorMessage); + socket.emit(WatchBlocksV1.Error, { + code: 500, + errorMessage, + }); + } + }; + + const eventListener = eventService.registerBlockListener(eventCallback, { + startBlock: options.startBlock, + unregister: false, + }); + + // Start monitoring + const monitorRequest = eventService.build(userIdCtx, { + blockType: listenerType, + startBlock: options.startBlock, + }); + const signature = await signCallback( + monitorRequest, + options.uniqueTransactionData, + ); + eventService.sign(signature); + await eventService.send(); + + socket.on("disconnect", async (reason: string) => { + log.info( + "WebSocket:disconnect => reason=%o clientId=%s", + reason, + clientId, + ); + + eventListener.unregisterEventListener(); + eventService.close(); + channel.close(); + channel.client.close(); this.close(); }); @@ -321,10 +471,13 @@ export class WatchBlocksV1Endpoint { } } + /** + * Disconnect the socket if connected. + * This will trigger cleanups for all started monitoring logics that use this socket. + */ close(): void { if (this.socket.connected) { this.socket.disconnect(true); } - this.config.gateway.disconnect(); } } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/delegate-signing-methods.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/delegate-signing-methods.test.ts new file mode 100644 index 00000000000..56ac8000f74 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/delegate-signing-methods.test.ts @@ -0,0 +1,501 @@ +/** + * Tests of fabric connector methods that use delegated signing instead of identity provided directly / through keychain. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Ledger settings +const imageName = "ghcr.io/hyperledger/cactus-fabric2-all-in-one"; +const imageVersion = "2021-09-02--fix-876-supervisord-retries"; +const fabricEnvVersion = "2.2.0"; +const fabricEnvCAVersion = "1.4.9"; +const ledgerChannelName = "mychannel"; +const assetTradeContractName = "copyAssetTrade"; +const privateAssetTradeContractName = "privateAssetTrade"; +const testTimeout = 1000 * 60 * 10; // 10 minutes per test + +// For development on local sawtooth network +// 1. leaveLedgerRunning = true, useRunningLedger = false to run ledger and leave it running after test finishes. +// 2. leaveLedgerRunning = true, useRunningLedger = true to use that ledger in future runs. +const useRunningLedger = false; +const leaveLedgerRunning = false; + +// Log settings +const testLogLevel: LogLevelDesc = "info"; // default: info +const sutLogLevel: LogLevelDesc = "info"; // default: info + +import "jest-extended"; +import http from "http"; +import express from "express"; +import bodyParser from "body-parser"; +import { AddressInfo } from "net"; +import { v4 as uuidv4 } from "uuid"; +import { X509Identity } from "fabric-network"; +import { Server as SocketIoServer } from "socket.io"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + Containers, + FabricTestLedgerV1, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; + +import { + PluginLedgerConnectorFabric, + GatewayOptions, + FabricContractInvocationType, + RunTransactionRequest, + FabricApiClient, + signProposal, + WatchBlocksListenerTypeV1, + WatchBlocksCactusTransactionsResponseV1, + FabricSigningCredential, +} from "../../../../main/typescript/public-api"; +import { Observable } from "rxjs"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "delegate-signing-methods.test", + level: testLogLevel, +}); + +/** + * Main test suite + */ +describe("Delegated signing tests", () => { + let ledger: FabricTestLedgerV1; + let gatewayOptions: GatewayOptions; + let fabricConnectorPlugin: PluginLedgerConnectorFabric; + let connectorServer: http.Server; + let apiClient: FabricApiClient; + let socketioServer: SocketIoServer; + let adminIdentity: X509Identity; + + const mockSignCallback = jest.fn(async (payload, txData) => { + log.debug("mockSignCallback called with txData (token):", txData); + return signProposal(adminIdentity.credentials.privateKey, payload); + }); + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + // Start Ledger + log.info("Start FabricTestLedgerV1..."); + log.debug("Version:", fabricEnvVersion, "CA Version:", fabricEnvCAVersion); + ledger = new FabricTestLedgerV1({ + emitContainerLogs: false, + publishAllPorts: true, + logLevel: testLogLevel, + imageName, + imageVersion, + envVars: new Map([ + ["FABRIC_VERSION", fabricEnvVersion], + ["CA_VERSION", fabricEnvCAVersion], + ["CACTUS_FABRIC_TEST_LOOSE_MEMBERSHIP", "1"], + ]), + useRunningLedger, + }); + log.debug("Fabric image:", ledger.getContainerImageName()); + await ledger.start(); + + // Get connection profile + log.info("Get fabric connection profile for Org1..."); + const connectionProfile = await ledger.getConnectionProfileOrg1(); + expect(connectionProfile).toBeTruthy(); + + // Enroll admin + const enrollAdminOut = await ledger.enrollAdmin(); + adminIdentity = enrollAdminOut[0]; + log.error("adminIdentity", adminIdentity); + + // Create Keychain Plugin + const keychainId = uuidv4(); + const keychainEntryKey = "admin"; + const keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId, + logLevel: sutLogLevel, + backend: new Map([[keychainEntryKey, JSON.stringify(adminIdentity)]]), + }); + + gatewayOptions = { + identity: keychainEntryKey, + wallet: { + keychain: { + keychainId, + keychainRef: keychainEntryKey, + }, + }, + }; + + // Create Connector Plugin + fabricConnectorPlugin = new PluginLedgerConnectorFabric({ + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + sshConfig: await ledger.getSshConfig(), + cliContainerEnv: {}, + peerBinary: "/fabric-samples/bin/peer", + logLevel: sutLogLevel, + connectionProfile, + discoveryOptions: { + enabled: true, + asLocalhost: true, + }, + signCallback: mockSignCallback, + }); + + // Run http server + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + connectorServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 0, + server: connectorServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const apiHost = `http://${addressInfo.address}:${addressInfo.port}`; + + // Run socketio server + socketioServer = new SocketIoServer(connectorServer, { + path: Constants.SocketIoConnectionPathV1, + }); + + // Register services + await fabricConnectorPlugin.getOrCreateWebServices(); + await fabricConnectorPlugin.registerWebServices(expressApp, socketioServer); + + // Create ApiClient + const apiConfig = new Configuration({ basePath: apiHost }); + apiClient = new FabricApiClient(apiConfig); + + // Deploy contract asset-transfer-basic + if (!useRunningLedger) { + const cmd = [ + "./network.sh", + "deployCC", + "-ccn", + assetTradeContractName, + "-ccp", + "../asset-transfer-basic/chaincode-go", + "-ccl", + "go", + ]; + const out = await Containers.exec( + ledger.getContainer(), + cmd, + 180000, + sutLogLevel, + "/fabric-samples/test-network/", + ); + expect(out).toBeTruthy(); + + const initResponse = await apiClient.runTransactionV1({ + signingCredential: gatewayOptions.wallet + .keychain as FabricSigningCredential, + channelName: ledgerChannelName, + contractName: assetTradeContractName, + invocationType: FabricContractInvocationType.Send, + methodName: "InitLedger", + params: [], + } as RunTransactionRequest); + expect(initResponse).toBeTruthy(); + expect(initResponse.data).toBeTruthy(); + expect(initResponse.status).toEqual(200); + log.info("Asset trade initialized"); + } + + // Deploy contract asset-transfer-private-data + if (!useRunningLedger) { + const cmd = [ + "./network.sh", + "deployCC", + "-ccn", + privateAssetTradeContractName, + "-ccp", + "../asset-transfer-private-data/chaincode-go/", + "-ccl", + "go", + "-ccep", + "OR('Org1MSP.peer','Org2MSP.peer')", + "-cccg", + "../asset-transfer-private-data/chaincode-go/collections_config.json", + ]; + const out = await Containers.exec( + ledger.getContainer(), + cmd, + 180000, + sutLogLevel, + "/fabric-samples/test-network/", + ); + expect(out).toBeTruthy(); + } + }); + + afterAll(async () => { + log.info("FINISHING THE TESTS"); + + if (fabricConnectorPlugin) { + log.info("Close ApiClient connections..."); + fabricConnectorPlugin.shutdown(); + } + + if (socketioServer) { + log.info("Stop the SocketIO server connector..."); + await new Promise((resolve) => + socketioServer.close(() => resolve()), + ); + } + + if (connectorServer) { + log.info("Stop the HTTP server connector..."); + await new Promise((resolve) => + connectorServer.close(() => resolve()), + ); + } + + if (ledger && !leaveLedgerRunning) { + log.info("Stop the fabric ledger..."); + await ledger.stop(); + await ledger.destroy(); + } + + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + afterEach(async () => { + mockSignCallback.mockClear(); + }); + + ////////////////////////////////// + // Helpers + ////////////////////////////////// + async function waitForTxCommit(txId: string) { + const committedTx = await apiClient.waitForTransactionCommit( + txId, + apiClient.watchBlocksDelegatedSignV1({ + type: WatchBlocksListenerTypeV1.CactusTransactions, + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + channelName: ledgerChannelName, + }) as Observable, + ); + mockSignCallback.mockClear(); + return committedTx; + } + + /** + * Check call history on mock signing callback, clear it afterwards. + * @param txData secret token sent to callback + * @param count how many calls to callback we expect. For query use 2 (discovery, query). For transaction use 3 (discovery, endorse, commit) + */ + async function checkMockSigningCallbacksAndClear(txData: string, count = 2) { + expect(mockSignCallback.mock.calls).toHaveLength(count); + expect(mockSignCallback.mock.calls.every((c) => c[1] === txData)); + mockSignCallback.mockClear(); + } + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + test( + "Sanity check with runTransactionV1 endpoint", + async () => { + const newAssetOwner = `sanity-${uuidv4()}`; + + // Check current owner + const initQueryResponse = await apiClient.runTransactionV1({ + signingCredential: gatewayOptions.wallet + .keychain as FabricSigningCredential, + channelName: ledgerChannelName, + contractName: assetTradeContractName, + invocationType: FabricContractInvocationType.Call, + methodName: "ReadAsset", + params: ["asset1"], + }); + expect(initQueryResponse).toBeTruthy(); + expect(initQueryResponse.data).toBeTruthy(); + expect(initQueryResponse.status).toEqual(200); + const initQueryOutput = JSON.parse(initQueryResponse.data.functionOutput); + expect(initQueryOutput["ID"]).toEqual("asset1"); + expect(initQueryOutput["owner"]).not.toEqual(newAssetOwner); + + // Transfer ownership + const sendResponse = await apiClient.runTransactionV1({ + signingCredential: gatewayOptions.wallet + .keychain as FabricSigningCredential, + channelName: ledgerChannelName, + contractName: assetTradeContractName, + invocationType: FabricContractInvocationType.Send, + methodName: "TransferAsset", + params: ["asset1", newAssetOwner], + }); + expect(sendResponse).toBeTruthy(); + expect(sendResponse.data).toBeTruthy(); + expect(sendResponse.status).toEqual(200); + + // Confirm new owner + const queryResponse = await apiClient.runTransactionV1({ + signingCredential: gatewayOptions.wallet + .keychain as FabricSigningCredential, + channelName: ledgerChannelName, + contractName: assetTradeContractName, + invocationType: FabricContractInvocationType.Call, + methodName: "ReadAsset", + params: ["asset1"], + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.status).toEqual(200); + const queryOutput = JSON.parse(queryResponse.data.functionOutput); + expect(queryOutput["ID"]).toEqual("asset1"); + expect(queryOutput["owner"]).toEqual(newAssetOwner); + }, + testTimeout, + ); + + test("Transact and query using delegated sign callback", async () => { + const newAssetOwner = `owner-${uuidv4()}`; + + // Check current owner + const initQueryId = `initQuery-${uuidv4()}`; + const initQueryResponse = await apiClient.runDelegatedSignTransactionV1({ + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + channelName: ledgerChannelName, + contractName: assetTradeContractName, + invocationType: FabricContractInvocationType.Call, + methodName: "ReadAsset", + params: ["asset1"], + uniqueTransactionData: initQueryId, + }); + expect(initQueryResponse).toBeTruthy(); + expect(initQueryResponse.data).toBeTruthy(); + expect(initQueryResponse.status).toEqual(200); + const initQueryOutput = JSON.parse(initQueryResponse.data.functionOutput); + expect(initQueryOutput["ID"]).toEqual("asset1"); + expect(initQueryOutput["owner"]).not.toEqual(newAssetOwner); + checkMockSigningCallbacksAndClear(initQueryId, 2); + + // Transfer ownership + const transferQueryId = `transfer-${uuidv4()}`; + const sendResponse = await apiClient.runDelegatedSignTransactionV1({ + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + channelName: ledgerChannelName, + contractName: assetTradeContractName, + invocationType: FabricContractInvocationType.Send, + methodName: "TransferAsset", + params: ["asset1", newAssetOwner], + uniqueTransactionData: transferQueryId, + }); + expect(sendResponse).toBeTruthy(); + expect(sendResponse.data).toBeTruthy(); + expect(sendResponse.status).toEqual(200); + checkMockSigningCallbacksAndClear(transferQueryId, 3); + const txId = sendResponse.data.transactionId; + expect(txId).toBeTruthy(); + const committedTx = await waitForTxCommit(txId); + log.debug("Committed transaction:", committedTx); + + // Confirm new owner + const finalQueryId = `finalQuery-${uuidv4()}`; + const queryResponse = await apiClient.runDelegatedSignTransactionV1({ + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + channelName: ledgerChannelName, + contractName: assetTradeContractName, + invocationType: FabricContractInvocationType.Call, + methodName: "ReadAsset", + params: ["asset1"], + uniqueTransactionData: finalQueryId, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.status).toEqual(200); + const queryOutput = JSON.parse(queryResponse.data.functionOutput); + expect(queryOutput["ID"]).toEqual("asset1"); + expect(queryOutput["owner"]).toEqual(newAssetOwner); + checkMockSigningCallbacksAndClear(finalQueryId, 2); + }); + + test( + "Private transaction and query using delegated sign callback", + async () => { + // Create private asset + const assetID = uuidv4(); + const assetColor = "gray"; + const transientAssetData = { + asset_properties: { + objectType: "asset", + assetID, + color: assetColor, + size: 3, + appraisedValue: 500, + }, + }; + + const transferQueryId = `transferPriv-${uuidv4()}`; + const sendResponse = await apiClient.runDelegatedSignTransactionV1({ + invocationType: FabricContractInvocationType.Sendprivate, + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + channelName: ledgerChannelName, + contractName: privateAssetTradeContractName, + methodName: "CreateAsset", + params: [], + uniqueTransactionData: transferQueryId, + transientData: transientAssetData, + endorsingOrgs: [adminIdentity.mspId], + }); + expect(sendResponse).toBeTruthy(); + expect(sendResponse.data).toBeTruthy(); + expect(sendResponse.status).toEqual(200); + checkMockSigningCallbacksAndClear(transferQueryId, 3); + const txId = sendResponse.data.transactionId; + expect(txId).toBeTruthy(); + const committedTx = await waitForTxCommit(txId); + log.debug("Committed transaction:", committedTx); + + // Query the new asset + const finalQueryId = `finalQuery-${uuidv4()}`; + const queryResponse = await apiClient.runDelegatedSignTransactionV1({ + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + channelName: ledgerChannelName, + contractName: privateAssetTradeContractName, + invocationType: FabricContractInvocationType.Call, + methodName: "ReadAsset", + params: [assetID], + endorsingOrgs: [adminIdentity.mspId], + uniqueTransactionData: finalQueryId, + }); + expect(queryResponse).toBeTruthy(); + expect(queryResponse.data).toBeTruthy(); + expect(queryResponse.status).toEqual(200); + const queryOutput = JSON.parse(queryResponse.data.functionOutput); + expect(queryOutput["assetID"]).toEqual(assetID); + expect(queryOutput["color"]).toEqual(assetColor); + checkMockSigningCallbacksAndClear(finalQueryId, 2); + }, + testTimeout, + ); +}); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-delegated-sign-v1-endpoint.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-delegated-sign-v1-endpoint.test.ts new file mode 100644 index 00000000000..dd3bc1cae7d --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-delegated-sign-v1-endpoint.test.ts @@ -0,0 +1,431 @@ +/** + * Functional test of watchBlocksDelegatedSignV1 on connector-fabric (packages/cactus-plugin-ledger-connector-fabric) + * Assumes sample CC was already deployed on the test ledger. + * + * @note - this test sometimes hangs infinitely when used with fabric-node-sdk 2.3.0, + * probably due to bug in the underlying dependency grpc-js. Problem does not occur on 2.5.0. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Ledger settings +const imageName = "ghcr.io/hyperledger/cactus-fabric2-all-in-one"; +const imageVersion = "2021-09-02--fix-876-supervisord-retries"; +const fabricEnvVersion = "2.2.0"; +const fabricEnvCAVersion = "1.4.9"; +const ledgerChannelName = "mychannel"; +const ledgerContractName = "basic"; + +// Log settings +const testLogLevel: LogLevelDesc = "info"; // default: info +const sutLogLevel: LogLevelDesc = "info"; // default: info + +import "jest-extended"; +import http from "http"; +import { AddressInfo } from "net"; +import { v4 as uuidv4 } from "uuid"; +import bodyParser from "body-parser"; +import express from "express"; +import { Server as SocketIoServer } from "socket.io"; +import { DiscoveryOptions, X509Identity } from "fabric-network"; + +import { + FabricTestLedgerV1, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; + +import { Constants, Configuration } from "@hyperledger/cactus-core-api"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; + +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; + +import { + PluginLedgerConnectorFabric, + FabricContractInvocationType, + DefaultEventHandlerStrategy, + FabricSigningCredential, + FabricApiClient, + WatchBlocksListenerTypeV1, + WatchBlocksResponseV1, + signProposal, +} from "../../../../main/typescript/public-api"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "fabric-watch-blocks-delegated-sign-v1-endpoint.test", + level: testLogLevel, +}); + +/** + * Main test suite + */ +describe("watchBlocksDelegatedSignV1 of fabric connector tests", () => { + let ledger: FabricTestLedgerV1; + let signingCredential: FabricSigningCredential; + let fabricConnectorPlugin: PluginLedgerConnectorFabric; + let connectorServer: http.Server; + let socketioServer: SocketIoServer; + let apiClient: FabricApiClient; + let adminIdentity: X509Identity; + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + // Start Ledger + log.info("Start FabricTestLedgerV1..."); + log.debug("Version:", fabricEnvVersion, "CA Version:", fabricEnvCAVersion); + ledger = new FabricTestLedgerV1({ + emitContainerLogs: false, + publishAllPorts: true, + logLevel: testLogLevel, + imageName, + imageVersion, + envVars: new Map([ + ["FABRIC_VERSION", fabricEnvVersion], + ["CA_VERSION", fabricEnvCAVersion], + ]), + }); + log.debug("Fabric image:", ledger.getContainerImageName()); + await ledger.start(); + + // Get connection profile + log.info("Get fabric connection profile for Org1..."); + const connectionProfile = await ledger.getConnectionProfileOrg1(); + expect(connectionProfile).toBeTruthy(); + + // Enroll admin and user + const enrollAdminOut = await ledger.enrollAdmin(); + adminIdentity = enrollAdminOut[0]; + const adminWallet = enrollAdminOut[1]; + const [userIdentity] = await ledger.enrollUser(adminWallet); + + // Create Keychain Plugin + const keychainId = uuidv4(); + const keychainEntryKey = "user2"; + const keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId, + logLevel: sutLogLevel, + backend: new Map([[keychainEntryKey, JSON.stringify(userIdentity)]]), + }); + signingCredential = { + keychainId, + keychainRef: keychainEntryKey, + }; + + // Create Connector Plugin + const discoveryOptions: DiscoveryOptions = { + enabled: true, + asLocalhost: true, + }; + fabricConnectorPlugin = new PluginLedgerConnectorFabric({ + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + sshConfig: await ledger.getSshConfig(), + cliContainerEnv: {}, + peerBinary: "/fabric-samples/bin/peer", + logLevel: sutLogLevel, + connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAnyfortx, + commitTimeout: 300, + }, + signCallback: async (payload, txData) => { + log.debug("signCallback called with txData (token):", txData); + return signProposal(adminIdentity.credentials.privateKey, payload); + }, + }); + + // Run http server + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + connectorServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 0, + server: connectorServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const apiHost = `http://${addressInfo.address}:${addressInfo.port}`; + + // Run socketio server + socketioServer = new SocketIoServer(connectorServer, { + path: Constants.SocketIoConnectionPathV1, + }); + + // Register services + await fabricConnectorPlugin.getOrCreateWebServices(); + await fabricConnectorPlugin.registerWebServices(expressApp, socketioServer); + + // Create ApiClient + const apiConfig = new Configuration({ basePath: apiHost }); + apiClient = new FabricApiClient(apiConfig); + }); + + afterAll(async () => { + log.info("FINISHING THE TESTS"); + + if (fabricConnectorPlugin) { + log.info("Close Fabric connector..."); + fabricConnectorPlugin.shutdown(); + } + + if (apiClient) { + log.info("Close ApiClient connections..."); + apiClient.close(); + } + + if (socketioServer) { + log.info("Stop the SocketIO server connector..."); + await new Promise((resolve) => + socketioServer.close(() => resolve()), + ); + } + + if (connectorServer) { + log.info("Stop the HTTP server connector..."); + await new Promise((resolve) => + connectorServer.close(() => resolve()), + ); + } + + // Wait for monitor to be terminated + await new Promise((resolve) => setTimeout(resolve, 8000)); + + if (ledger) { + log.info("Stop the fabric ledger..."); + await ledger.stop(); + await ledger.destroy(); + } + + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + ////////////////////////////////// + // Helpers + ////////////////////////////////// + + /** + * Common logic for executing watchBlock monitoring tests. + * Will subscribe to new blocks and send new transaction, to trigger creation of the new block. + * + * @param monitorName Unique name, will be used for identification and in transaction argument. + * @param type Type of block to receive. + * @param checkEventCallback Callback called when received the event from the connector. + * + * @returns Monitoring promise - will resolve if `checkEventCallback` passes, reject if it throws. + */ + async function testWatchBlock( + monitorName: string, + type: WatchBlocksListenerTypeV1, + checkEventCallback: (event: WatchBlocksResponseV1) => void, + triggerTransactionCreation = true, + ) { + // Start monitoring + const monitorPromise = new Promise((resolve, reject) => { + const watchObservable = apiClient.watchBlocksDelegatedSignV1({ + channelName: ledgerChannelName, + signerCertificate: adminIdentity.credentials.certificate, + signerMspID: adminIdentity.mspId, + type, + }); + + const subscription = watchObservable.subscribe({ + next(event) { + log.debug("Received event:", JSON.stringify(event)); + try { + checkEventCallback(event); + subscription.unsubscribe(); + resolve(); + } catch (err) { + log.error("watchBlocksDelegatedSignV1() event check error:", err); + subscription.unsubscribe(); + reject(err); + } + }, + error(err) { + log.error("watchBlocksDelegatedSignV1() error:", err); + subscription.unsubscribe(); + reject(err); + }, + }); + }); + + // Create new asset to trigger new block creation + if (triggerTransactionCreation) { + const createAssetResponse = await apiClient.runTransactionV1({ + signingCredential, + channelName: ledgerChannelName, + invocationType: FabricContractInvocationType.Send, + contractName: ledgerContractName, + methodName: "CreateAsset", + params: [monitorName, "green", "111", "someOwner", "299"], + }); + expect(createAssetResponse).toBeTruthy(); + expect(createAssetResponse.status).toEqual(200); + expect(createAssetResponse.data).toBeTruthy(); + expect(createAssetResponse.data.transactionId).toBeTruthy(); + log.debug( + "runTransactionV1 response:", + JSON.stringify(createAssetResponse.data), + ); + } + + return monitorPromise; + } + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + /** + * Check full block monitoring + */ + test("Monitoring with type Full returns entire raw block", async () => { + const monitorPromise = testWatchBlock( + "FullBlockTest", + WatchBlocksListenerTypeV1.Full, + (event) => { + expect(event).toBeTruthy(); + + if (!("fullBlock" in event)) { + throw new Error( + `Unexpected response from the connector: ${JSON.stringify(event)}`, + ); + } + + const fullBlock = event.fullBlock; + expect(fullBlock.blockNumber).toBeTruthy(); + expect(fullBlock.blockData).toBeTruthy(); + expect(fullBlock.blockData.header).toBeTruthy(); + expect(fullBlock.blockData.data).toBeTruthy(); + expect(fullBlock.blockData.metadata).toBeTruthy(); + }, + ); + + await monitorPromise; + }); + + /** + * Check filtered block monitoring + */ + test("Monitoring with type Filtered returns filtered block", async () => { + const monitorPromise = testWatchBlock( + "FilteredBlockTest", + WatchBlocksListenerTypeV1.Filtered, + (event) => { + expect(event).toBeTruthy(); + + if (!("filteredBlock" in event)) { + throw new Error( + `Unexpected response from the connector: ${JSON.stringify(event)}`, + ); + } + + const filteredBlock = event.filteredBlock; + expect(filteredBlock.blockNumber).toBeTruthy(); + expect(filteredBlock.blockData).toBeTruthy(); + expect(filteredBlock.blockData.channel_id).toBeTruthy(); + expect(filteredBlock.blockData.number).toBeTruthy(); + expect(filteredBlock.blockData.filtered_transactions).toBeTruthy(); + }, + ); + + await monitorPromise; + }); + + /** + * Check private block monitoring + */ + test("Monitoring with type Private returns private block", async () => { + const monitorPromise = testWatchBlock( + "PrivateBlockTest", + WatchBlocksListenerTypeV1.Private, + (event) => { + expect(event).toBeTruthy(); + + if (!("privateBlock" in event)) { + throw new Error( + `Unexpected response from the connector: ${JSON.stringify(event)}`, + ); + } + + const fullBlock = event.privateBlock; + expect(fullBlock.blockNumber).toBeTruthy(); + expect(fullBlock.blockData).toBeTruthy(); + expect(fullBlock.blockData.header).toBeTruthy(); + expect(fullBlock.blockData.data).toBeTruthy(); + expect(fullBlock.blockData.metadata).toBeTruthy(); + }, + ); + + await monitorPromise; + }); + + /** + * Check Cactus custom transactions summary block monitoring. + */ + test("Monitoring with type CactusTransactions returns transactions summary", async () => { + const monitorPromise = testWatchBlock( + "CactusTransactionsTest", + WatchBlocksListenerTypeV1.CactusTransactions, + (event) => { + expect(event).toBeTruthy(); + + if (!("cactusTransactionsEvents" in event)) { + throw new Error( + `Unexpected response from the connector: ${JSON.stringify(event)}`, + ); + } + + const eventData = event.cactusTransactionsEvents; + expect(eventData.length).toBeGreaterThan(0); + expect(eventData[0].chaincodeId).toBeTruthy(); + expect(eventData[0].transactionId).toBeTruthy(); + expect(eventData[0].functionName).toBeTruthy(); + expect(eventData[0].functionArgs).toBeTruthy(); + }, + ); + + await monitorPromise; + }); + + test("Invalid WatchBlocksListenerTypeV1 value gets knocked down", async () => { + const monitorPromise = testWatchBlock( + "CactusTransactionsTest", + "Some_INVALID_WatchBlocksListenerTypeV1" as WatchBlocksListenerTypeV1, + () => undefined, // will never reach this because it is meant to error out + false, + ); + + try { + await monitorPromise; + } catch (ex: any) { + // Execution never reaches this point - I'm assuming because the + // testWatchBlock method somehow does not fulfil it's obligation of + // either succeeding or throwing (it seems to get stuck idling forever + // when I debug this in VSCode) + expect(ex).toBeTruthy(); + expect(ex.code).toEqual(500); + expect(ex.errorMessage).toBeTruthy(); + } + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-v1-endpoint.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-v1-endpoint.test.ts index 632029c43b6..54ee885d54e 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-v1-endpoint.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-v1-endpoint.test.ts @@ -285,7 +285,6 @@ describe("watchBlocksV1 of fabric connector tests", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); log.debug( "runTransactionV1 response:", diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/get-block.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/get-block.test.ts index c2c2a6d220c..369a19f487b 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/get-block.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/get-block.test.ts @@ -270,7 +270,6 @@ describe("Get Block endpoint tests", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); const txId = createAssetResponse.data.transactionId; expect(txId).toBeTruthy(); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts index f29a890baa5..f9e9337d8e0 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts @@ -265,7 +265,7 @@ describe(testCase, () => { contractName, methodName: "CreateAsset", params: ["asset388", "green", "111", assetOwner, "299"], - endorsingPeers: ["org1.example.com", "Org2MSP"], + endorsingOrgs: ["org1.example.com", "Org2MSP"], }; const res = await apiClient.runTransactionV1(req); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-identities.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-identities.test.ts index f8e54d66928..c1dd83b7777 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-identities.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-identities.test.ts @@ -313,7 +313,7 @@ test("run-transaction-with-identities", async (t: Test) => { t.comment(out); { // make invoke InitLedger using a client1 client - const resp = await plugin.transact({ + await plugin.transact({ signingCredential: { keychainId: keychainId, keychainRef: client1Key, @@ -324,11 +324,10 @@ test("run-transaction-with-identities", async (t: Test) => { methodName: "InitLedger", params: [], }); - t.true(resp.success, "InitLedger tx for Basic2 success===true OK"); } { // make invoke TransferAsset using a client2 client - const resp = await plugin.transact({ + await plugin.transact({ signingCredential: { keychainId: keychainId, keychainRef: client2Key, @@ -344,7 +343,6 @@ test("run-transaction-with-identities", async (t: Test) => { methodName: "TransferAsset", params: ["asset1", "client2"], }); - t.true(resp.success, "TransferAsset asset1 client2 success true OK"); } { // make query ReadAsset using a registrar client @@ -364,7 +362,6 @@ test("run-transaction-with-identities", async (t: Test) => { methodName: "ReadAsset", params: ["asset1"], }); - t.true(resp.success); const asset = JSON.parse(resp.functionOutput); t.equal(asset.Owner, "client2"); } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-ws-ids.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-ws-ids.test.ts index 131d37ee0aa..c0713599ee9 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-ws-ids.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-with-ws-ids.test.ts @@ -270,7 +270,7 @@ test("run-transaction-with-ws-ids", async (t: Test) => { { // make invoke InitLedger using a client1 client - const resp = await plugin.transact({ + await plugin.transact({ signingCredential: { keychainId: keychainId, keychainRef: client2Key, @@ -283,11 +283,10 @@ test("run-transaction-with-ws-ids", async (t: Test) => { methodName: "InitLedger", params: [], }); - t.true(resp.success, "InitLedger tx for Basic2 success===true OK"); } { // make invoke TransferAsset using a client2 client - const resp = await plugin.transact({ + await plugin.transact({ signingCredential: { keychainId: keychainId, keychainRef: client2Key, @@ -300,7 +299,6 @@ test("run-transaction-with-ws-ids", async (t: Test) => { methodName: "TransferAsset", params: ["asset1", "client2"], }); - t.true(resp.success, "TransferAsset asset1 client2 success true OK"); } { const resp = await plugin.transact({ @@ -316,7 +314,6 @@ test("run-transaction-with-ws-ids", async (t: Test) => { methodName: "ReadAsset", params: ["asset1"], }); - t.true(resp.success); const asset = JSON.parse(resp.functionOutput); t.equal(asset.Owner, "client2"); } diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation-go.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation-go.test.ts index 776741a5386..fa705a84cbb 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation-go.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation-go.test.ts @@ -226,7 +226,7 @@ test(testCase, async (t: Test) => { try { await apiClient.deployContractGoSourceV1( - (parameters as any) as DeployContractGoSourceV1Request, + parameters as any as DeployContractGoSourceV1Request, ); } catch (e) { t2.equal( @@ -288,7 +288,7 @@ test(testCase, async (t: Test) => { try { await apiClient.deployContractGoSourceV1( - (parameters as any) as DeployContractGoSourceV1Request, + parameters as any as DeployContractGoSourceV1Request, ); } catch (e) { t2.equal( diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation.test.ts index 1eb7412d2a2..c1e9c9f2562 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/openapi/openapi-validation.test.ts @@ -315,7 +315,7 @@ test(testCase, async (t: Test) => { try { await apiClient.deployContractV1( - (parameters as any) as DeployContractV1Request, + parameters as any as DeployContractV1Request, ); } catch (e) { t2.equal( @@ -354,7 +354,7 @@ test(testCase, async (t: Test) => { try { await apiClient.deployContractV1( - (parameters as any) as DeployContractV1Request, + parameters as any as DeployContractV1Request, ); } catch (e) { t2.equal( @@ -416,7 +416,7 @@ test(testCase, async (t: Test) => { try { await apiClient.runTransactionV1( - (parameters as any) as RunTransactionRequest, + parameters as any as RunTransactionRequest, ); } catch (e) { t2.equal( @@ -455,7 +455,7 @@ test(testCase, async (t: Test) => { try { await apiClient.runTransactionV1( - (parameters as any) as RunTransactionRequest, + parameters as any as RunTransactionRequest, ); } catch (e) { t2.equal( diff --git a/packages/cactus-plugin-persistence-fabric/src/main/typescript/plugin-persistence-fabric.ts b/packages/cactus-plugin-persistence-fabric/src/main/typescript/plugin-persistence-fabric.ts index 38885951ed6..6f88c6b4548 100644 --- a/packages/cactus-plugin-persistence-fabric/src/main/typescript/plugin-persistence-fabric.ts +++ b/packages/cactus-plugin-persistence-fabric/src/main/typescript/plugin-persistence-fabric.ts @@ -22,6 +22,7 @@ import { GatewayOptions, FabricApiClient, GetBlockResponseV1, + RunTransactionResponseType, } from "@hyperledger/cactus-plugin-ledger-connector-fabric"; import PostgresDatabaseClient from "./db-client/db-client"; @@ -46,7 +47,8 @@ export interface IPluginPersistenceFabricOptions extends ICactusPluginOptions { } export class PluginPersistenceFabric - implements ICactusPlugin, IPluginWebService { + implements ICactusPlugin, IPluginWebService +{ private log: Logger; public static readonly CLASS_NAME = "PluginPersistenceFabric"; private dbClient: PostgresDatabaseClient; @@ -201,7 +203,7 @@ export class PluginPersistenceFabric contractName: "qscc", methodName: "GetChainInfo", params: [this.ledgerChannelName.toString()], - responseType: "JSONstring", + responseType: RunTransactionResponseType.JSON, }); this.log.warn("lastBlockInChainTest", lastBlockInChainTest); const parse = JSON.parse(lastBlockInChainTest.data.functionOutput); @@ -450,16 +452,15 @@ export class PluginPersistenceFabric } async getBlockFromLedger(blockNumber: string): Promise { - const block: AxiosResponse = await this.apiClient.getBlockV1( - { + const block: AxiosResponse = + await this.apiClient.getBlockV1({ channelName: this.ledgerChannelName, gatewayOptions: this.gatewayOptions, query: { blockNumber, }, skipDecode: false, - }, - ); + }); const tempBlockParse = block.data; @@ -477,15 +478,14 @@ export class PluginPersistenceFabric public async migrateBlockNrWithTransactions( blockNumber: string, ): Promise { - const block: AxiosResponse = await this.apiClient.getBlockV1( - { + const block: AxiosResponse = + await this.apiClient.getBlockV1({ channelName: this.ledgerChannelName, gatewayOptions: this.gatewayOptions, query: { blockNumber, }, - }, - ); + }); const tempBlockParse: any = JSON.parse(JSON.stringify(block.data)); @@ -560,7 +560,8 @@ export class PluginPersistenceFabric ).toString("hex"); } - const creator_id_bytes = transactionDataObject.payload.header.signature_header.creator.id_bytes.data.toString(); + const creator_id_bytes = + transactionDataObject.payload.header.signature_header.creator.id_bytes.data.toString(); if (transactionDataObject.payload.data.actions !== undefined) { chaincodeID = transactionDataObject.payload.data.actions[0].payload.action @@ -604,13 +605,15 @@ export class PluginPersistenceFabric ).toString("hex"); } // payload_proposal_hash = transactionDataObject.payload.data.actions[0].payload.action.proposal_response_payload.proposal_hash.data.toString( - detailedTxData.payload_proposal_hash = transactionDataObject.payload.data.actions[0].payload.action.proposal_response_payload.proposal_hash.data.toString( - "hex", - ); + detailedTxData.payload_proposal_hash = + transactionDataObject.payload.data.actions[0].payload.action.proposal_response_payload.proposal_hash.data.toString( + "hex", + ); // endorser_id_bytes = transactionDataObject.payload.data.actions[0].payload.action.endorsements[0].endorser.id_bytes.data.toString( - detailedTxData.endorser_id_bytes = transactionDataObject.payload.data.actions[0].payload.action.endorsements[0].endorser.id_bytes.data.toString( - "hex", - ); + detailedTxData.endorser_id_bytes = + transactionDataObject.payload.data.actions[0].payload.action.endorsements[0].endorser.id_bytes.data.toString( + "hex", + ); detailedTxData.endorser_msp_id = tempBlockParse.decodedBlock.data.data[ @@ -674,9 +677,8 @@ export class PluginPersistenceFabric .signature_header.creator.mspid, endorser_msp_id: detailedTxData.endorser_msp_id, chaincode_id: chaincode_id, //tempBlockParse.decodedBlock.data.data[0].payload.data.payload.chaincode_proposal_payload.input.chaincode_spec.chaincode_id, - type: - tempBlockParse.decodedBlock.data.data[txIndex].payload.header - .channel_header.typeString, + type: tempBlockParse.decodedBlock.data.data[txIndex].payload.header + .channel_header.typeString, read_set: detailedTxData.read_set, //tempBlockParse.decodedBlock.data.data[0].payload.data.actions[0].payload.action.proposal_response_payload, write_set: detailedTxData.write_set, //tempBlockParse.decodedBlock.data.data[0].payload.data.actions[0].payload.chaincode_proposal_payload.input.chaincode_spec.input, channel_id: @@ -729,7 +731,7 @@ export class PluginPersistenceFabric return true; } /** - * + * * @param limitLastBlockConsidered this parameter - set the last block in ledger which we consider valid by our party and synchronize only to this point in ledger If some blocks above this number are already in database they will not be removed. * @returns number which is this.lastBlock , artificially set lastBlock in ledger @@ -781,15 +783,14 @@ If some blocks above this number are already in database they will not be remove this.log.info("database start Synchronization"); do { blockNumber = this.missedBlocks[missedIndex]; - const block: AxiosResponse = await this.apiClient.getBlockV1( - { + const block: AxiosResponse = + await this.apiClient.getBlockV1({ channelName: this.ledgerChannelName, gatewayOptions: this.gatewayOptions, query: { blockNumber, }, - }, - ); + }); if (block.status == 200) { // Put scrapped block into database diff --git a/packages/cactus-plugin-persistence-fabric/src/test/typescript/integration/persistence-fabric-functional.test.ts b/packages/cactus-plugin-persistence-fabric/src/test/typescript/integration/persistence-fabric-functional.test.ts index 26e645051ac..f670c70fc9a 100644 --- a/packages/cactus-plugin-persistence-fabric/src/test/typescript/integration/persistence-fabric-functional.test.ts +++ b/packages/cactus-plugin-persistence-fabric/src/test/typescript/integration/persistence-fabric-functional.test.ts @@ -62,7 +62,7 @@ import { import DatabaseClient from "../../../main/typescript/db-client/db-client"; jest.mock("../../../main/typescript/db-client/db-client"); -const DatabaseClientMock = (DatabaseClient as unknown) as jest.Mock; +const DatabaseClientMock = DatabaseClient as unknown as jest.Mock; import { enrollAdmin, @@ -190,10 +190,9 @@ describe("Persistence Fabric", () => { contractName: ledgerContractName, peers: [], // will be filled below orderer: { - name: - connectionProfile.orderers[ordererId].grpcOptions[ - "ssl-target-name-override" - ], + name: connectionProfile.orderers[ordererId].grpcOptions[ + "ssl-target-name-override" + ], url: connectionProfile.orderers[ordererId].url, tlscaValue: connectionProfile.orderers[ordererId].tlsCACerts.pem, }, @@ -527,7 +526,6 @@ describe("Persistence Fabric", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); }); @@ -545,7 +543,6 @@ describe("Persistence Fabric", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); }); @@ -563,7 +560,6 @@ describe("Persistence Fabric", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); }); @@ -581,7 +577,6 @@ describe("Persistence Fabric", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); }); @@ -599,7 +594,6 @@ describe("Persistence Fabric", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); }); @@ -617,7 +611,6 @@ describe("Persistence Fabric", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); }); @@ -635,7 +628,6 @@ describe("Persistence Fabric", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); }); @@ -653,7 +645,6 @@ describe("Persistence Fabric", () => { expect(createAssetResponse).toBeTruthy(); expect(createAssetResponse.status).toEqual(200); expect(createAssetResponse.data).toBeTruthy(); - expect(createAssetResponse.data.success).toBeTrue(); expect(createAssetResponse.data.transactionId).toBeTruthy(); }); // end of helpers @@ -725,9 +716,8 @@ describe("Persistence Fabric", () => { }); test("initialBlocksSynchronization", async () => { - const initialBlocksSynchronization = await persistence.initialBlocksSynchronization( - 10, - ); + const initialBlocksSynchronization = + await persistence.initialBlocksSynchronization(10); expect(initialBlocksSynchronization).toBeTruthy(); expect(initialBlocksSynchronization).toEqual("done"); }); @@ -797,7 +787,8 @@ describe("Persistence Fabric", () => { test("check missing blocks", async () => { missBlock10(10); - const missingBlocksCheck = await persistence.whichBlocksAreMissingInDdSimple(); + const missingBlocksCheck = + await persistence.whichBlocksAreMissingInDdSimple(); log.info( "Getting missing blocks from plugin for analyze", missingBlocksCheck, diff --git a/yarn.lock b/yarn.lock index 3f5ffc95dca..eed78ff35fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7145,6 +7145,7 @@ __metadata: node-vault: 0.9.22 openapi-types: 9.1.0 prom-client: 13.2.0 + run-time-error: 1.4.0 rxjs: 7.8.1 sanitize-filename: 1.6.3 sanitize-html: 2.7.0