diff --git a/packages/cactus-plugin-ledger-connector-fabric-socketio/src/main/typescript/connector/ServerPlugin.ts b/packages/cactus-plugin-ledger-connector-fabric-socketio/src/main/typescript/connector/ServerPlugin.ts index 254607667e..bd7d51e3d0 100644 --- a/packages/cactus-plugin-ledger-connector-fabric-socketio/src/main/typescript/connector/ServerPlugin.ts +++ b/packages/cactus-plugin-ledger-connector-fabric-socketio/src/main/typescript/connector/ServerPlugin.ts @@ -21,12 +21,11 @@ import { ValidatorAuthentication } from "./ValidatorAuthentication"; // Read the library, SDK, etc. according to EC specifications as needed import { getClientAndChannel, getSubmitterAndEnroll } from "./fabricaccess"; -import Client, { ProposalRequest } from "fabric-client"; +import Client, { ProposalRequest, Block } from "fabric-client"; import safeStringify from "fast-safe-stringify"; const path = require("path"); const { FileSystemWallet, Gateway } = require("fabric-network"); -const fs = require("fs"); const connUserName = config.read("fabric.connUserName"); // Cryptographic for fabric @@ -304,6 +303,105 @@ export class ServerPlugin { }); }); } + + /** + * Get fabric block specified in args. + * + * @param args + * ``` javascript + * { + * "args": { + * "contract": {"channelName": string}, // Fabric channel to execute the request on + * "args": { + * // OneOf following fields is required. First one found will be used. + * "blockNumber"?: number, + * "blockHash"?: Array, + * "txId"?: string, + * // Optional. If true, this function returns an encoded block. + * "skipDecode"?: boolean, + * } + * }, + * "reqID": string // optional requestID from verifier + * } + * ``` + */ + async getBlock(args: any) { + logger.info("getBlock start"); + + const channelName = args.contract.channelName; + const blockNumber = args.args.blockNumber; + const blockHash = args.args.blockHash; + const txId = args.args.txId; + const skipDecode = args.args.skipDecode ?? false; + + const reqID = args.reqID ?? null; + logger.info(`##getBlock: reqID: ${reqID}`); + + let { client, channel } = await getClientAndChannel(channelName); + await getSubmitterAndEnroll(client); + + let block: Block; + if (typeof blockNumber === "number") { + block = await channel.queryBlock( + blockNumber, + undefined, + undefined, + skipDecode, + ); + } else if (blockHash) { + block = await channel.queryBlockByHash( + blockHash, + undefined, + undefined, + skipDecode, + ); + } else if (txId) { + block = await channel.queryBlockByTxID( + txId, + undefined, + undefined, + skipDecode, + ); + } else { + const errObj = { + resObj: { + status: 400, + errorDetail: + "getBlock: Provide either blockNumber, blockHash, or txId", + }, + id: reqID, + }; + logger.error(errObj); + throw errObj; + } + + if (!block) { + const errObj = { + resObj: { + status: 504, + errorDetail: "getBlock: Could not retrieve block", + }, + id: reqID, + }; + logger.error(errObj); + throw errObj; + } + + const signedBlock = ValidatorAuthentication.sign({ + result: block, + }); + + const retObj = { + resObj: { + status: 200, + data: signedBlock, + }, + id: reqID, + }; + logger.debug("##getBlock: response:", retObj); + + return retObj; + } } /* class */ /* diff --git a/packages/cactus-plugin-ledger-connector-fabric-socketio/src/test/typescript/integration/fabric-socketio-connector.test.ts b/packages/cactus-plugin-ledger-connector-fabric-socketio/src/test/typescript/integration/fabric-socketio-connector.test.ts index 5ab4906bee..9b4d48df97 100644 --- a/packages/cactus-plugin-ledger-connector-fabric-socketio/src/test/typescript/integration/fabric-socketio-connector.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric-socketio/src/test/typescript/integration/fabric-socketio-connector.test.ts @@ -387,6 +387,47 @@ describe("Fabric-SocketIO connector tests", () => { expect(readSingleAsset).toEqual(firstAsset); }); + /** + * Get block by number. + */ + test("Get block by it's number works (both decoded and encoded)", async () => { + const contract = { channelName: ledgerChannelName }; + const method = { type: "function", command: "getBlock" }; + const argsParam = { + blockNumber: 0, + }; + + // Get decoded block + const response = await apiClient.sendSyncRequest( + contract, + method, + argsParam, + ); + expect(response).toBeTruthy(); + expect(response.status).toEqual(200); + expect(response.data).toBeTruthy(); + expect(response.data.header).toBeTruthy(); + expect(response.data.data).toBeTruthy(); + expect(response.data.metadata).toBeTruthy(); + + // Get encoded block + const argsParamEncoded = { + ...argsParam, + skipDecode: true, + }; + + const responseEncoded = await apiClient.sendSyncRequest( + contract, + method, + argsParamEncoded, + ); + expect(responseEncoded).toBeTruthy(); + expect(responseEncoded.status).toEqual(200); + expect(responseEncoded.data).toBeTruthy(); + expect(responseEncoded.data.type).toEqual("Buffer"); + expect(responseEncoded.data.data).toBeTruthy(); + }); + /** * Send transaction proposal to be signed with keys attached to the request (managed by BLP), * and then send signed transaction to the ledger. diff --git a/packages/cactus-plugin-ledger-connector-fabric/package.json b/packages/cactus-plugin-ledger-connector-fabric/package.json index d1c50feeba..9675e63819 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package.json @@ -72,6 +72,8 @@ "ngo": "2.7.0", "node-ssh": "12.0.0", "node-vault": "0.9.22", + "fast-safe-stringify": "2.1.1", + "sanitize-html": "2.7.0", "openapi-types": "9.1.0", "prom-client": "13.2.0", "sanitize-filename": "1.6.3", @@ -85,6 +87,7 @@ "@hyperledger/cactus-plugin-keychain-memory": "1.0.0", "@hyperledger/cactus-test-tooling": "1.0.0", "@types/express": "4.17.13", + "@types/sanitize-html": "2.6.2", "@types/fs-extra": "9.0.12", "@types/jsrsasign": "8.0.13", "@types/multer": "1.4.7", @@ -110,4 +113,4 @@ ] } } -} +} \ No newline at end of file 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 9b74b37b09..0bc2a39b53 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 @@ -11,46 +11,46 @@ }, "components": { "schemas": { - "TransactReceiptTransactionEndorsement":{ - "type":"object", + "TransactReceiptTransactionEndorsement": { + "type": "object", "properties": { - "mspid":{ - "type":"string" + "mspid": { + "type": "string" }, - "endorserID":{ - "type":"string" + "endorserID": { + "type": "string" }, - "signature":{ - "type":"string" + "signature": { + "type": "string" } } }, - "TransactReceiptTransactionCreator":{ - "type":"object", + "TransactReceiptTransactionCreator": { + "type": "object", "properties": { - "mspid":{ - "type":"string" + "mspid": { + "type": "string" }, - "creatorID":{ - "type":"string" + "creatorID": { + "type": "string" } } }, - "TransactReceiptBlockMetaData":{ - "type":"object", + "TransactReceiptBlockMetaData": { + "type": "object", "properties": { - "mspid":{ - "type":"string" + "mspid": { + "type": "string" }, - "blockCreatorID":{ - "type":"string" + "blockCreatorID": { + "type": "string" }, - "signature":{ - "type":"string" + "signature": { + "type": "string" } } }, - "VaultTransitKey" : { + "VaultTransitKey": { "type": "object", "nullable": false, "required": [ @@ -75,7 +75,7 @@ }, "description": "vault key details for signing fabric message with private key stored with transit engine." }, - "WebSocketKey" : { + "WebSocketKey": { "type": "object", "nullable": false, "required": [ @@ -100,7 +100,7 @@ }, "description": "web-socket key details for signing fabric message with private key stored with external client" }, - "FabricSigningCredentialType" : { + "FabricSigningCredentialType": { "type": "string", "enum": [ "X.509", @@ -129,17 +129,17 @@ "maxLength": 100, "nullable": false }, - "type" : { - "$ref" : "#/components/schemas/FabricSigningCredentialType", - "description" : "singing identity type to be used for signing fabric message , by by default default is supported" + "type": { + "$ref": "#/components/schemas/FabricSigningCredentialType", + "description": "singing identity type to be used for signing fabric message , by by default default is supported" }, - "vaultTransitKey" : { - "$ref" : "#/components/schemas/VaultTransitKey", - "properties" : "vault key details , if Vault-X.509 identity provider to be used for singing fabric messages" + "vaultTransitKey": { + "$ref": "#/components/schemas/VaultTransitKey", + "properties": "vault key details , if Vault-X.509 identity provider to be used for singing fabric messages" }, - "webSocketKey" : { - "$ref" : "#/components/schemas/WebSocketKey", - "properties" : "web-socket key details , if WS-X.509 identity provider to be used for singing fabric messages" + "webSocketKey": { + "$ref": "#/components/schemas/WebSocketKey", + "properties": "web-socket key details , if WS-X.509 identity provider to be used for singing fabric messages" } } }, @@ -278,7 +278,10 @@ }, "GatewayOptions": { "type": "object", - "required": ["identity", "wallet"], + "required": [ + "identity", + "wallet" + ], "properties": { "connectionProfile": { "$ref": "#/components/schemas/ConnectionProfile" @@ -503,7 +506,7 @@ "type": "boolean", "nullable": false }, - "transactionId":{ + "transactionId": { "type": "string", "nullable": false } @@ -512,39 +515,39 @@ "GetTransactionReceiptResponse": { "type": "object", "properties": { - "blockNumber":{ - "type":"string" - }, - "channelID":{ - "type": "string" - }, - "transactionCreator":{ - "$ref": "#/components/schemas/TransactReceiptTransactionCreator" - }, - "transactionEndorsement":{ - "type":"array", - "items":{ - "$ref": "#/components/schemas/TransactReceiptTransactionEndorsement" + "blockNumber": { + "type": "string" + }, + "channelID": { + "type": "string" + }, + "transactionCreator": { + "$ref": "#/components/schemas/TransactReceiptTransactionCreator" + }, + "transactionEndorsement": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactReceiptTransactionEndorsement" } - }, - "blockMetaData":{ - "$ref": "#/components/schemas/TransactReceiptBlockMetaData" - }, - "chainCodeName":{ - "type":"string" - }, - "chainCodeVersion":{ - "type":"string" - }, - "responseStatus":{ - "type":"string" - }, - "rwsetKey":{ - "type":"string" - }, - "rwsetWriteData":{ - "type": "string" - } + }, + "blockMetaData": { + "$ref": "#/components/schemas/TransactReceiptBlockMetaData" + }, + "chainCodeName": { + "type": "string" + }, + "chainCodeVersion": { + "type": "string" + }, + "responseStatus": { + "type": "string" + }, + "rwsetKey": { + "type": "string" + }, + "rwsetWriteData": { + "type": "string" + } } }, "DeploymentTargetOrganization": { @@ -949,6 +952,136 @@ } } }, + "GetBlockRequestV1": { + "description": "Request for GetBlock endpoint.", + "type": "object", + "required": [ + "channelName", + "gatewayOptions", + "query" + ], + "properties": { + "channelName": { + "type": "string", + "description": "Fabric channel which we want to query.", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "connectionChannelName": { + "type": "string", + "description": "Fabric channel we want to connect to. If not provided, then one from channelName parameter will be used", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "gatewayOptions": { + "$ref": "#/components/schemas/GatewayOptions", + "description": "Fabric SDK gateway options.", + "nullable": false + }, + "query": { + "description": "Query selector, caller must provide at least one of them. First found will be used, rest will be ignored, so it's recommended to pass single selector.", + "type": "object", + "properties": { + "blockNumber": { + "type": "string", + "description": "Select block by it's number.", + "nullable": false + }, + "blockHash": { + "type": "object", + "description": "Select block by it's hash.", + "required": [ + "buffer" + ], + "properties": { + "encoding": { + "type": "string", + "description": "NodeJS Buffer encoding (utf-8, hex, binary, base64, etc...). Passed directly to `Buffer.from()` call on hashBuffer. If not provided then JSON buffer format is assumed.", + "nullable": false + }, + "buffer": { + "type": "string", + "format": "binary", + "description": "Buffer of blockHash. It's encoding should be described in `encoding` parameter.", + "nullable": false + } + }, + "nullable": false + }, + "transactionId": { + "type": "string", + "description": "Select block by id of transaction that it contains.", + "nullable": false + } + } + }, + "skipDecode": { + "type": "boolean", + "description": "If true, encoded buffer will be returned. Otherwise, entire block object is returned.", + "default": false, + "nullable": false + } + } + }, + "FabricFullBlockDataV1": { + "description": "Full hyperledger fabric block data." + }, + "GetBlockResponseDecodedV1": { + "type": "object", + "description": "When skipDecode is false (default) then decoded block object is returned.", + "required": [ + "decodedBlock" + ], + "properties": { + "decodedBlock": { + "$ref": "#/components/schemas/FabricFullBlockDataV1", + "nullable": false + } + } + }, + "GetBlockResponseEncodedV1": { + "type": "object", + "description": "When skipDecode is true then encoded block Buffer is returned.", + "required": [ + "encodedBlock" + ], + "properties": { + "encodedBlock": { + "type": "string", + "format": "binary" + } + } + }, + "GetBlockResponseV1": { + "description": "Response from GetBlock endpoint.", + "oneOf": [ + { + "$ref": "#/components/schemas/GetBlockResponseDecodedV1" + }, + { + "$ref": "#/components/schemas/GetBlockResponseEncodedV1" + } + ] + }, + "ErrorExceptionResponseV1": { + "type": "object", + "required": [ + "message", + "error" + ], + "properties": { + "message": { + "type": "string", + "nullable": false + }, + "error": { + "type": "string", + "nullable": false + } + } + }, "PrometheusExporterMetricsResponse": { "type": "string", "nullable": false @@ -1138,6 +1271,50 @@ } } }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-block": { + "post": { + "operationId": "getBlockV1", + "summary": "Get block from the channel using one of selectors from the input. Works only on Fabric 2.x.", + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-block" + } + }, + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBlockRequestV1" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBlockResponseV1" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionResponseV1" + } + } + } + } + } + } + }, "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-prometheus-exporter-metrics": { "get": { "x-hyperledger-cactus": { diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/get-transaction-receipt-by-tx-id.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/get-transaction-receipt-by-tx-id.ts index 09a4025794..54dd871931 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/get-transaction-receipt-by-tx-id.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/get-transaction-receipt-by-tx-id.ts @@ -7,13 +7,15 @@ import { TransactReceiptBlockMetaData, } from "../generated/openapi/typescript-axios"; import { common } from "fabric-protos"; -const { BlockDecoder } = require("fabric-common"); +import { querySystemChainCode } from "./query-system-chain-code"; + export interface IGetTransactionReceiptByTxIDOptions { readonly logLevel?: LogLevelDesc; readonly gateway: Gateway; readonly channelName: string; readonly params: string[]; } + export async function getTransactionReceiptByTxID( req: IGetTransactionReceiptByTxIDOptions, ): Promise { @@ -23,22 +25,32 @@ export async function getTransactionReceiptByTxID( level: req.logLevel || "INFO", }); log.info(`${fnTag}, start getting fabric transact receipt`); - const { gateway } = req; - const contractName = "qscc"; - const methodName = "GetBlockByTxID"; if (req.params.length != 2) { throw new Error(`${fnTag}, should have 2 params`); } - const network = await gateway.getNetwork(req.channelName); - const contract = network.getContract(contractName); - const out: Buffer = await contract.evaluateTransaction( - methodName, - ...req.params, - ); + const { gateway } = req; + const paramChannelName = req.params[0]; const reqTxID = req.params[1]; - const block: common.Block = BlockDecoder.decode(out); + + const queryConfig = { + gateway, + connectionChannelName: req.channelName, + }; + const block: common.Block = await querySystemChainCode( + queryConfig, + "GetBlockByTxID", + paramChannelName, + reqTxID, + ); + + if (block instanceof Buffer) { + throw new Error( + "Unexpected encoded response from querySystemChainCode::GetBlockByTxID()", + ); + } + const blockJson = JSON.parse(JSON.stringify(block)); const transactReceipt: GetTransactionReceiptResponse = {}; diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/query-system-chain-code.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/query-system-chain-code.ts new file mode 100644 index 0000000000..da84cd81f0 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/common/query-system-chain-code.ts @@ -0,0 +1,46 @@ +import { Gateway } from "fabric-network"; +// BlockDecoder is not exported in ts definition so we need to use legacy import. +const { BlockDecoder } = require("fabric-common"); + +const QSCC_ContractName = "qscc"; + +/** + * Configuration parameter type for `querySystemChainCode` function. + */ +export interface QuerySystemChainCodeConfig { + gateway: Gateway; + connectionChannelName: string; // used to connect to the network + skipDecode?: boolean; +} + +/** + * Call method on fabric system contract `qscc` and return decoded or encoded response. + * + * @param config Configuration of `querySystemChainCode` method itself. + * @param functionName Method on `qscc` to call. + * @param args Args to method from `functionName` + * @returns Encoded `Buffer` or decoded `JSON string` response from the ledger. + */ +export async function querySystemChainCode( + config: QuerySystemChainCodeConfig, + functionName: string, + ...args: (string | Buffer)[] +): Promise { + const { gateway, connectionChannelName, skipDecode } = config; + const network = await gateway.getNetwork(connectionChannelName); + const contract = network.getContract(QSCC_ContractName); + + const resultBuffer = await contract.evaluateTransaction( + functionName, + ...(args as string[]), // contract expects byte[], node Buffer fits well here as well + ); + if (!resultBuffer) { + throw new Error(`Received empty response from qscc call ${functionName}`); + } + + if (skipDecode) { + return resultBuffer; + } + + return BlockDecoder.decode(resultBuffer); +} 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 f26a1541a9..14485e5ed1 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 @@ -509,6 +509,25 @@ export interface DeploymentTargetOrganization { */ ORDERER_TLS_ROOTCERT_FILE: string; } +/** + * + * @export + * @interface ErrorExceptionResponseV1 + */ +export interface ErrorExceptionResponseV1 { + /** + * + * @type {string} + * @memberof ErrorExceptionResponseV1 + */ + message: string; + /** + * + * @type {string} + * @memberof ErrorExceptionResponseV1 + */ + error: string; +} /** * * @export @@ -695,6 +714,120 @@ export interface GatewayOptionsWallet { */ json?: string; } +/** + * Request for GetBlock endpoint. + * @export + * @interface GetBlockRequestV1 + */ +export interface GetBlockRequestV1 { + /** + * Fabric channel which we want to query. + * @type {string} + * @memberof GetBlockRequestV1 + */ + channelName: string; + /** + * Fabric channel we want to connect to. If not provided, then one from channelName parameter will be used + * @type {string} + * @memberof GetBlockRequestV1 + */ + connectionChannelName?: string; + /** + * + * @type {GatewayOptions} + * @memberof GetBlockRequestV1 + */ + gatewayOptions: GatewayOptions; + /** + * + * @type {GetBlockRequestV1Query} + * @memberof GetBlockRequestV1 + */ + query: GetBlockRequestV1Query; + /** + * If true, encoded buffer will be returned. Otherwise, entire block object is returned. + * @type {boolean} + * @memberof GetBlockRequestV1 + */ + skipDecode?: boolean; +} +/** + * Query selector, caller must provide at least one of them. First found will be used, rest will be ignored, so it\'s recommended to pass single selector. + * @export + * @interface GetBlockRequestV1Query + */ +export interface GetBlockRequestV1Query { + /** + * Select block by it\'s number. + * @type {string} + * @memberof GetBlockRequestV1Query + */ + blockNumber?: string; + /** + * + * @type {GetBlockRequestV1QueryBlockHash} + * @memberof GetBlockRequestV1Query + */ + blockHash?: GetBlockRequestV1QueryBlockHash; + /** + * Select block by id of transaction that it contains. + * @type {string} + * @memberof GetBlockRequestV1Query + */ + transactionId?: string; +} +/** + * Select block by it\'s hash. + * @export + * @interface GetBlockRequestV1QueryBlockHash + */ +export interface GetBlockRequestV1QueryBlockHash { + /** + * NodeJS Buffer encoding (utf-8, hex, binary, base64, etc...). Passed directly to `Buffer.from()` call on hashBuffer. If not provided then JSON buffer format is assumed. + * @type {string} + * @memberof GetBlockRequestV1QueryBlockHash + */ + encoding?: string; + /** + * Buffer of blockHash. It\'s encoding should be described in `encoding` parameter. + * @type {any} + * @memberof GetBlockRequestV1QueryBlockHash + */ + buffer: any; +} +/** + * When skipDecode is false (default) then decoded block object is returned. + * @export + * @interface GetBlockResponseDecodedV1 + */ +export interface GetBlockResponseDecodedV1 { + /** + * Full hyperledger fabric block data. + * @type {any} + * @memberof GetBlockResponseDecodedV1 + */ + decodedBlock: any | null; +} +/** + * When skipDecode is true then encoded block Buffer is returned. + * @export + * @interface GetBlockResponseEncodedV1 + */ +export interface GetBlockResponseEncodedV1 { + /** + * + * @type {any} + * @memberof GetBlockResponseEncodedV1 + */ + encodedBlock: any; +} +/** + * @type GetBlockResponseV1 + * Response from GetBlock endpoint. + * @export + */ +export type GetBlockResponseV1 = GetBlockResponseDecodedV1 | GetBlockResponseEncodedV1; + /** * * @export @@ -1080,6 +1213,40 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Get block from the channel using one of selectors from the input. Works only on Fabric 2.x. + * @param {GetBlockRequestV1} [getBlockRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getBlockV1: async (getBlockRequestV1?: GetBlockRequestV1, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-block`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getBlockRequestV1, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Get the Prometheus Metrics @@ -1214,6 +1381,17 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.deployContractV1(deployContractV1Request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Get block from the channel using one of selectors from the input. Works only on Fabric 2.x. + * @param {GetBlockRequestV1} [getBlockRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getBlockV1(getBlockRequestV1?: GetBlockRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getBlockV1(getBlockRequestV1, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Get the Prometheus Metrics @@ -1276,6 +1454,16 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa deployContractV1(deployContractV1Request?: DeployContractV1Request, options?: any): AxiosPromise { return localVarFp.deployContractV1(deployContractV1Request, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Get block from the channel using one of selectors from the input. Works only on Fabric 2.x. + * @param {GetBlockRequestV1} [getBlockRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getBlockV1(getBlockRequestV1?: GetBlockRequestV1, options?: any): AxiosPromise { + return localVarFp.getBlockV1(getBlockRequestV1, options).then((request) => request(axios, basePath)); + }, /** * * @summary Get the Prometheus Metrics @@ -1339,6 +1527,18 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).deployContractV1(deployContractV1Request, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Get block from the channel using one of selectors from the input. Works only on Fabric 2.x. + * @param {GetBlockRequestV1} [getBlockRequestV1] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getBlockV1(getBlockRequestV1?: GetBlockRequestV1, options?: any) { + return DefaultApiFp(this.configuration).getBlockV1(getBlockRequestV1, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Get the Prometheus Metrics 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 new file mode 100644 index 0000000000..9c1629b6b2 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/get-block-endpoint-v1.ts @@ -0,0 +1,118 @@ +import { Express, Request, Response } from "express"; +import safeStringify from "fast-safe-stringify"; +import sanitizeHtml from "sanitize-html"; + +import { + Logger, + LoggerProvider, + LogLevelDesc, + Checks, + IAsyncProvider, +} 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 IGetBlockEndpointV1Options { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorFabric; +} + +export class GetBlockEndpointV1 implements IWebServiceEndpoint { + private readonly log: Logger; + + constructor(public readonly opts: IGetBlockEndpointV1Options) { + const fnTag = "GetBlockEndpointV1#constructor()"; + + Checks.truthy(opts, `${fnTag} options`); + Checks.truthy(opts.connector, `${fnTag} options.connector`); + + this.log = LoggerProvider.getOrCreate({ + label: "get-block-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 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" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().post.operationId; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = "GetBlockEndpointV1#handleRequest()"; + this.log.debug(`POST ${this.getPath()}`); + + try { + const resBody = await this.opts.connector.getBlock(req.body); + res.status(200).send(resBody); + } 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: {}, + }), + }); + } + } + } +} 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 bfea6d566d..95b9175af0 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 @@ -15,7 +15,7 @@ import { DefaultEventHandlerOptions, DefaultEventHandlerStrategies, Gateway, - GatewayOptions, + GatewayOptions as FabricGatewayOptions, Wallets, X509Identity, TransientMap, @@ -72,6 +72,9 @@ import { DefaultEventHandlerStrategy, FabricSigningCredentialType, GetTransactionReceiptResponse, + GatewayOptions, + GetBlockRequestV1, + GetBlockResponseV1, } from "./generated/openapi/typescript-axios/index"; import { @@ -93,6 +96,7 @@ import FabricCAServices, { } from "fabric-ca-client"; import { createGateway } from "./common/create-gateway"; import { Endorser, ICryptoKey } from "fabric-common"; + import { IVaultConfig, IWebSocketConfig, @@ -108,6 +112,9 @@ import { getTransactionReceiptByTxID, IGetTransactionReceiptByTxIDOptions, } from "./common/get-transaction-receipt-by-tx-id"; +import { GetBlockEndpointV1 } from "./get-block/get-block-endpoint-v1"; +import { querySystemChainCode } from "./common/query-system-chain-code"; + /** * Constant value holding the default $GOPATH in the Fabric CLI container as * observed on fabric deployments that are produced by the official examples @@ -831,6 +838,13 @@ export class PluginLedgerConnectorFabric const endpoint = new GetTransactionReceiptByTxIDEndpointV1(opts); endpoints.push(endpoint); } + { + const endpoint = new GetBlockEndpointV1({ + connector: this, + logLevel: this.opts.logLevel, + }); + endpoints.push(endpoint); + } { const opts: IGetPrometheusExporterMetricsEndpointV1Options = { @@ -847,30 +861,62 @@ export class PluginLedgerConnectorFabric return endpoints; } + /** + * Create gateway from request (will choose logic based on request) + * + * @node It seems that Gateway is not supposed to be created and destroyed rapidly, but + * rather kept around for longer. Possible issues: + * - Disconnect is async and takes a while until all internal services are closed. + * - Possible memory and connection pool leak (see https://github.com/hyperledger/fabric-sdk-node/issues/529). + * - Performance: there's a setup overhead that might be significant after scaling up. Hence... + * @todo Cache and reuse gateways (destroy only ones not used for a while). + * Or maybe add separate methods "start/stopSession" that would leave session management to the client? + * + * @param req must contain either gatewayOptions or signingCredential. + * @returns Fabric SDK Gateway + */ protected async createGateway(req: RunTransactionRequest): Promise { if (req.gatewayOptions) { - return createGateway({ - logLevel: this.opts.logLevel, - pluginRegistry: this.opts.pluginRegistry, - defaultConnectionProfile: this.opts.connectionProfile, - defaultDiscoveryOptions: this.opts.discoveryOptions || { - enabled: true, - asLocalhost: true, - }, - defaultEventHandlerOptions: this.opts.eventHandlerOptions || { - endorseTimeout: 300, - commitTimeout: 300, - strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, - }, - gatewayOptions: req.gatewayOptions, - secureIdentity: this.secureIdentity, - certStore: this.certStore, - }); + return this.createGatewayWithOptions(req.gatewayOptions); } else { return this.createGatewayLegacy(req.signingCredential); } } + /** + * Create Gateway from dedicated gateway options. + * + * @param options gateway options + * @returns Fabric SDK Gateway + */ + protected async createGatewayWithOptions( + options: GatewayOptions, + ): Promise { + return createGateway({ + logLevel: this.opts.logLevel, + pluginRegistry: this.opts.pluginRegistry, + defaultConnectionProfile: this.opts.connectionProfile, + defaultDiscoveryOptions: this.opts.discoveryOptions || { + enabled: true, + asLocalhost: true, + }, + defaultEventHandlerOptions: this.opts.eventHandlerOptions || { + endorseTimeout: 300, + commitTimeout: 300, + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + }, + gatewayOptions: options, + secureIdentity: this.secureIdentity, + certStore: this.certStore, + }); + } + + /** + * Create Gateway from signing credential (legacy, can be done with gateway options) + * + * @param signingCredential sign data. + * @returns Fabric SDK Gateway + */ protected async createGatewayLegacy( signingCredential: FabricSigningCredential, ): Promise { @@ -933,7 +979,7 @@ export class PluginLedgerConnectorFabric DefaultEventHandlerStrategies[eho.strategy]; } - const gatewayOptions: GatewayOptions = { + const gatewayOptions: FabricGatewayOptions = { discovery: this.opts.discoveryOptions, eventHandlerOptions, identity: identity, @@ -1347,4 +1393,83 @@ export class PluginLedgerConnectorFabric mspId: certData.mspId, }); } + + /** + * Get fabric block from a channel, using one of selectors. + * + * @param req input parameters + * @returns Entire block object or encoded buffer (if req.skipDecode is true) + */ + public async getBlock(req: GetBlockRequestV1): Promise { + this.log.debug( + "getBlock() called, channelName:", + req.channelName, + "query:", + JSON.stringify(req.query), + ); + + const gateway = await this.createGatewayWithOptions(req.gatewayOptions); + const { channelName, skipDecode } = req; + const connectionChannelName = req.connectionChannelName ?? channelName; + const queryConfig = { + gateway, + connectionChannelName, + skipDecode, + }; + + let responseData: unknown; + if (req.query.blockNumber) { + this.log.debug("getBlock by it's blockNumber:", req.query.blockNumber); + responseData = await querySystemChainCode( + queryConfig, + "GetBlockByNumber", + channelName, + req.query.blockNumber, + ); + } else if (req.query.blockHash) { + const { buffer, encoding } = req.query.blockHash; + this.log.debug("getBlock by it's hash:", buffer); + + if (encoding && !Buffer.isEncoding(encoding)) { + throw new Error(`Unknown buffer encoding provided: ${encoding}`); + } + + responseData = await querySystemChainCode( + queryConfig, + "GetBlockByHash", + channelName, + Buffer.from(buffer, encoding as BufferEncoding), + ); + } else if (req.query.transactionId) { + this.log.debug( + "getBlock by transactionId it contains:", + req.query.transactionId, + ); + responseData = await querySystemChainCode( + queryConfig, + "GetBlockByTxID", + channelName, + req.query.transactionId, + ); + } else { + throw new Error( + "Unsupported block query type - you must provide either number, hash or txId", + ); + } + + if (!responseData) { + throw new Error("Could not retrieve block data (got empty response)"); + } + this.log.debug("responseData:", responseData); + + if (skipDecode) { + return { + encodedBlock: responseData, + }; + } + + return { + decodedBlock: responseData, + }; + } } 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 new file mode 100644 index 0000000000..e2668aa8fc --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/get-block.test.ts @@ -0,0 +1,419 @@ +/** + * Functional test of GetBlockEndpointV1 on connector-fabric (packages/cactus-plugin-ledger-connector-fabric) + * Assumes sample CC was already deployed on the test ledger. + */ + +////////////////////////////////// +// 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 { DiscoveryOptions } from "fabric-network"; + +import { + FabricTestLedgerV1, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; + +import { Configuration } from "@hyperledger/cactus-core-api"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; + +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; + +import { + PluginLedgerConnectorFabric, + DefaultEventHandlerStrategy, + DefaultApi as FabricApi, + GatewayOptions, + FabricContractInvocationType, + FabricSigningCredential, +} from "../../../../main/typescript/public-api"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "get-block.test", + level: testLogLevel, +}); + +/** + * Main test suite + */ +describe("Get Block endpoint tests", () => { + let ledger: FabricTestLedgerV1; + let gatewayOptions: GatewayOptions; + let fabricConnectorPlugin: PluginLedgerConnectorFabric; + let connectorServer: http.Server; + let apiClient: FabricApi; + + ////////////////////////////////// + // 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(); + 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)]]), + }); + + gatewayOptions = { + identity: keychainEntryKey, + wallet: { + keychain: { + 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, + }, + }); + + // 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}`; + + // Register services + await fabricConnectorPlugin.getOrCreateWebServices(); + await fabricConnectorPlugin.registerWebServices(expressApp); + + // Create ApiClient + const apiConfig = new Configuration({ basePath: apiHost }); + apiClient = new FabricApi(apiConfig); + }); + + afterAll(async () => { + log.info("FINISHING THE TESTS"); + + if (fabricConnectorPlugin) { + log.info("Close ApiClient connections..."); + fabricConnectorPlugin.shutdown(); + } + + if (connectorServer) { + log.info("Stop the HTTP server connector..."); + await new Promise((resolve) => + connectorServer.close(() => resolve()), + ); + } + + if (ledger) { + log.info("Stop the fabric ledger..."); + await ledger.stop(); + await ledger.destroy(); + } + + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + ////////////////////////////////// + // Helpers + ////////////////////////////////// + + /** + * Run get block endpoint using block number, do basic response checks. + * Can be reused throughout the tests. + * + * @param blockNumber string number of the block + * @param skipDecode true to return encoded, false to return decoded + * @returns block object / block buffer + */ + async function getBlockByNumber( + blockNumber = "0", + skipDecode = false, + ): Promise { + const getBlockReq = { + channelName: ledgerChannelName, + gatewayOptions, + query: { + blockNumber, + }, + skipDecode, + }; + + const getBlockResponse = await apiClient.getBlockV1(getBlockReq); + + expect(getBlockResponse).toBeTruthy(); + expect(getBlockResponse.status).toEqual(200); + expect(getBlockResponse.data).toBeTruthy(); + + if (!skipDecode) { + // Decoded check + if (!("decodedBlock" in getBlockResponse.data)) { + throw new Error( + "Wrong response received - expected decoded, received encoded.", + ); + } + expect(getBlockResponse.data.decodedBlock).toBeTruthy(); + return getBlockResponse.data.decodedBlock; + } else { + // Encoded check + if (!("encodedBlock" in getBlockResponse.data)) { + throw new Error( + "Wrong response received - expected encoded, received decoded.", + ); + } + expect(getBlockResponse.data.encodedBlock).toBeTruthy(); + return getBlockResponse.data.encodedBlock; + } + } + + /** + * Create new asset on the ledger to trigger new transaction creation. + * + * @param assetName unique asset name to create + * @returns committed transaction id. + */ + async function sendTransactionOnFabric(assetName: string) { + const createAssetResponse = await apiClient.runTransactionV1({ + signingCredential: gatewayOptions.wallet + .keychain as FabricSigningCredential, + channelName: ledgerChannelName, + invocationType: FabricContractInvocationType.Send, + contractName: ledgerContractName, + methodName: "CreateAsset", + params: [assetName, "green", "111", "someOwner", "299"], + }); + 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(); + + log.debug("Crated new transaction, txId:", txId); + return txId; + } + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + /** + * GetBlock endpoint using block number + */ + test("Get first block by it's number, both decoded and encoded.", async () => { + // Check decoded + const decodedFirstBlock = await getBlockByNumber("0", false); + log.debug("Received decodedFirstBlock:", decodedFirstBlock); + expect(decodedFirstBlock.header).toBeTruthy(); + expect(decodedFirstBlock.header.number.low).toBe(0); + expect(decodedFirstBlock.header.number.high).toBe(0); + expect(decodedFirstBlock.data).toBeTruthy(); + expect(decodedFirstBlock.metadata).toBeTruthy(); + + // Check encoded + const encodedFirstBlock = await getBlockByNumber("0", true); + log.debug("Received encodedFirstBlock:", encodedFirstBlock); + const blockBuffer = Buffer.from(encodedFirstBlock); + expect(blockBuffer).toBeTruthy(); + }); + + /** + * GetBlock endpoint using transactionId + */ + test("Get a block by transactionId it contains", async () => { + // Run some transaction + const txId = await sendTransactionOnFabric("getBlockTx"); + + // Get block using transactionId we've just sent + const getBlockByTxId = { + channelName: ledgerChannelName, + gatewayOptions, + query: { + transactionId: txId, + }, + }; + + const getBlockResponse = await apiClient.getBlockV1(getBlockByTxId); + if (!("decodedBlock" in getBlockResponse.data)) { + // narrow the type + throw new Error( + "Wrong response received - expected decoded, received encoded.", + ); + } + const { decodedBlock } = getBlockResponse.data; + expect(decodedBlock).toBeTruthy(); + expect(decodedBlock.header).toBeTruthy(); + expect(decodedBlock.data).toBeTruthy(); + expect(decodedBlock.metadata).toBeTruthy(); + }); + + /** + * GetBlock endpoint using block hash + */ + test("Get block by it's hash.", async () => { + // Run transaction to ensure more than one block is present + await sendTransactionOnFabric("txForNewBlock"); + + // Get second block by it's number + const decodedSecondBlock = await getBlockByNumber("1", false); + expect(decodedSecondBlock.header).toBeTruthy(); + const firstBlockHashJSON = decodedSecondBlock.header.previous_hash; + expect(firstBlockHashJSON).toBeTruthy(); + + // Get using default JSON hash representation + log.info("Get by JSON hash:", firstBlockHashJSON); + const getBlockByJsonHashReq = { + channelName: ledgerChannelName, + gatewayOptions, + query: { + blockHash: { + buffer: firstBlockHashJSON, + }, + }, + }; + const getBlockByJsonHashResponse = await apiClient.getBlockV1( + getBlockByJsonHashReq, + ); + if (!("decodedBlock" in getBlockByJsonHashResponse.data)) { + // narrow the type + throw new Error( + "Wrong response received - expected decoded, received encoded.", + ); + } + const { decodedBlock } = getBlockByJsonHashResponse.data; + expect(decodedBlock).toBeTruthy(); + expect(decodedBlock.header).toBeTruthy(); + expect(decodedBlock.header.number.low).toBe(0); + expect(decodedBlock.header.number.high).toBe(0); + expect(decodedBlock.data).toBeTruthy(); + expect(decodedBlock.metadata).toBeTruthy(); + + // Get using HEX encoded hash representation + const firstBlockHashHex = Buffer.from(firstBlockHashJSON).toString("hex"); + log.info("Get by HEX hash:", firstBlockHashHex); + const getBlockByHexHashReq = { + channelName: ledgerChannelName, + gatewayOptions, + query: { + blockHash: { + encoding: "hex", + buffer: firstBlockHashHex, + }, + }, + }; + const getBlockByHexHashResponse = await apiClient.getBlockV1( + getBlockByHexHashReq, + ); + if (!("decodedBlock" in getBlockByHexHashResponse.data)) { + // narrow the type + throw new Error( + "Wrong response received - expected decoded, received encoded.", + ); + } + const decodedBlockHex = getBlockByHexHashResponse.data.decodedBlock; + expect(decodedBlockHex).toBeTruthy(); + expect(decodedBlockHex.header).toBeTruthy(); + expect(decodedBlockHex.header.number.low).toBe(0); + expect(decodedBlockHex.header.number.high).toBe(0); + expect(decodedBlockHex.data).toBeTruthy(); + expect(decodedBlockHex.metadata).toBeTruthy(); + }); + + /** + * Check error handling + */ + test("Reading block with invalid number returns an error.", async () => { + const getBlockReq = { + channelName: ledgerChannelName, + gatewayOptions, + query: { + blockNumber: "foo", // non existent block + }, + }; + + try { + await apiClient.getBlockV1(getBlockReq); + expect(true).toBe(false); // above call should always throw + } catch (err) { + expect(err).toBeTruthy(); + } + }); +});