From c037ec55bc07e5314cd2104579b1a882d79f1488 Mon Sep 17 00:00:00 2001 From: Hana Awad Date: Mon, 21 Jun 2021 16:38:57 -0400 Subject: [PATCH] feat(connector-besu): add getPastLogs web service Fixes #1067 Signed-off-by: Hana Awad --- .../src/main/json/openapi.json | 36 +++- .../generated/openapi/typescript-axios/api.ts | 67 +++++++ .../plugin-ledger-connector-besu.ts | 19 +- .../web-services/get-past-logs-endpoint.ts | 101 +++++++++++ .../get-past-logs-endpoint.test.ts | 171 ++++++++++++++++++ 5 files changed, 382 insertions(+), 12 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-past-logs-endpoint.ts create mode 100644 packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/get-past-logs-endpoint.test.ts diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json index 197ca0362c..6919eb643b 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json @@ -990,7 +990,6 @@ } } }, - "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-transaction": { "post": { "x-hyperledger-cactus": { @@ -1025,6 +1024,41 @@ } } }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-past-logs": { + "post": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-past-logs" + } + }, + "operationId": "getPastLogsV1", + "summary": "Gets past logs, matching the given options.", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPastLogsV1Request" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + + "$ref": "#/components/schemas/GetPastLogsV1Response" + } + } + } + } + } + } + }, "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/invoke-contract": { "post": { "x-hyperledger-cactus": { diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts index 1d4541f554..2e4630a093 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -1056,6 +1056,40 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Gets past logs, matching the given options. + * @param {GetPastLogsV1Request} [getPastLogsV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPastLogsV1: async (getPastLogsV1Request?: GetPastLogsV1Request, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-past-logs`; + // 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(getPastLogsV1Request, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Get the Prometheus Metrics @@ -1233,6 +1267,17 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1BesuRunTransaction(runTransactionRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Gets past logs, matching the given options. + * @param {GetPastLogsV1Request} [getPastLogsV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPastLogsV1(getPastLogsV1Request?: GetPastLogsV1Request, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPastLogsV1(getPastLogsV1Request, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Get the Prometheus Metrics @@ -1316,6 +1361,16 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa apiV1BesuRunTransaction(runTransactionRequest?: RunTransactionRequest, options?: any): AxiosPromise { return localVarFp.apiV1BesuRunTransaction(runTransactionRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Gets past logs, matching the given options. + * @param {GetPastLogsV1Request} [getPastLogsV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPastLogsV1(getPastLogsV1Request?: GetPastLogsV1Request, options?: any): AxiosPromise { + return localVarFp.getPastLogsV1(getPastLogsV1Request, options).then((request) => request(axios, basePath)); + }, /** * * @summary Get the Prometheus Metrics @@ -1401,6 +1456,18 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).apiV1BesuRunTransaction(runTransactionRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Gets past logs, matching the given options. + * @param {GetPastLogsV1Request} [getPastLogsV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getPastLogsV1(getPastLogsV1Request?: GetPastLogsV1Request, options?: any) { + return DefaultApiFp(this.configuration).getPastLogsV1(getPastLogsV1Request, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Get the Prometheus Metrics diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts index 5575641370..a2c8f8c60d 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts @@ -80,6 +80,7 @@ import { import { WatchBlocksV1Endpoint } from "./web-services/watch-blocks-v1-endpoint"; import { GetBalanceEndpoint } from "./web-services/get-balance-endpoint"; import { GetTransactionEndpoint } from "./web-services/get-transaction-endpoint"; +import { GetPastLogsEndpoint } from "./web-services/get-past-logs-endpoint"; export const E_KEYCHAIN_NOT_FOUND = "cactus.connector.besu.keychain_not_found"; @@ -92,17 +93,6 @@ export interface IPluginLedgerConnectorBesuOptions logLevel?: LogLevelDesc; } -// export interface Log{ -// address: string; -// data: string; -// blockHash: string; -// transactionHash: string; -// topics: Array; -// logIndex: number; -// transactionIndex: number; -// blockNumber: number; - -// } export class PluginLedgerConnectorBesu implements IPluginLedgerConnector< @@ -236,6 +226,13 @@ export class PluginLedgerConnectorBesu }); endpoints.push(endpoint); } + { + const endpoint = new GetPastLogsEndpoint({ + connector: this, + logLevel: this.options.logLevel, + }); + endpoints.push(endpoint); + } { const endpoint = new RunTransactionEndpoint({ connector: this, diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-past-logs-endpoint.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-past-logs-endpoint.ts new file mode 100644 index 0000000000..509e6a866c --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-past-logs-endpoint.ts @@ -0,0 +1,101 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginLedgerConnectorBesu } from "../plugin-ledger-connector-besu"; + +import OAS from "../../json/openapi.json"; + +export interface IGetPastLogsEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorBesu; +} + +export class GetPastLogsEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "GetPastLogsEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return GetPastLogsEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IGetPastLogsEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getOasPath() { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-past-logs" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + const reqBody = req.body; + try { + const resBody = await this.options.connector.getPastLogs(reqBody); + res.json(resBody); + } catch (ex) { + this.log.error(`Crash while serving ${reqTag}`, ex); + res.status(500).json({ + message: "Internal Server Error", + error: ex?.stack || ex?.message, + }); + } + } +} diff --git a/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/get-past-logs-endpoint.test.ts b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/get-past-logs-endpoint.test.ts new file mode 100644 index 0000000000..cd080ee85a --- /dev/null +++ b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/get-past-logs-endpoint.test.ts @@ -0,0 +1,171 @@ +import test, { Test } from "tape-promise/tape"; + +import { v4 as uuidv4 } from "uuid"; +import { createServer } from "http"; +import KeyEncoder from "key-encoder"; +import { AddressInfo } from "net"; +import Web3 from "web3"; +import EEAClient, { IWeb3InstanceExtended } from "web3-eea"; + +import { + ApiServer, + AuthorizationProtocol, + ConfigService, +} from "@hyperledger/cactus-cmd-api-server"; +import { + Secp256k1Keys, + KeyFormat, + LogLevelDesc, +} from "@hyperledger/cactus-common"; + +import { + BesuTestLedger, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; + +import { + BesuApiClientOptions, + BesuApiClient, + IPluginLedgerConnectorBesuOptions, + PluginLedgerConnectorBesu, + GetPastLogsV1Request, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; + +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; + +const testCase = "API client can call getPastLogs via network"; +const logLevel: LogLevelDesc = "TRACE"; + +test("BEFORE " + testCase, async (t: Test) => { + const pruning = pruneDockerAllIfGithubAction({ logLevel }); + await t.doesNotReject(pruning, "Pruning didn't throw OK"); + t.end(); +}); + +test(testCase, async (t: Test) => { + const keyEncoder: KeyEncoder = new KeyEncoder("secp256k1"); + const keychainId = uuidv4(); + const keychainRef = uuidv4(); + + const { privateKey } = Secp256k1Keys.generateKeyPairsBuffer(); + const keyHex = privateKey.toString("hex"); + const pem = keyEncoder.encodePrivate(keyHex, KeyFormat.Raw, KeyFormat.PEM); + + const keychain = new PluginKeychainMemory({ + backend: new Map([[keychainRef, pem]]), + keychainId, + logLevel, + instanceId: uuidv4(), + }); + + const httpServer1 = createServer(); + await new Promise((resolve, reject) => { + httpServer1.once("error", reject); + httpServer1.once("listening", resolve); + httpServer1.listen(0, "127.0.0.1"); + }); + const addressInfo1 = httpServer1.address() as AddressInfo; + t.comment(`HttpServer1 AddressInfo: ${JSON.stringify(addressInfo1)}`); + const node1Host = `http://${addressInfo1.address}:${addressInfo1.port}`; + t.comment(`Cactus Node 1 Host: ${node1Host}`); + + const besuTestLedger = new BesuTestLedger(); + await besuTestLedger.start(); + + const tearDown = async () => { + await besuTestLedger.stop(); + await besuTestLedger.destroy(); + }; + + test.onFinish(tearDown); + const testAccount = await besuTestLedger.createEthTestAccount(); + const rpcApiHttpHost = await besuTestLedger.getRpcApiHttpHost(); + const rpcApiWsHost = await besuTestLedger.getRpcApiWsHost(); + + // 2. Instantiate plugin registry which will provide the web service plugin with the key value storage plugin + const pluginRegistry = new PluginRegistry({ plugins: [keychain] }); + + // 3. Instantiate the web service consortium plugin + const options: IPluginLedgerConnectorBesuOptions = { + instanceId: uuidv4(), + rpcApiHttpHost, + rpcApiWsHost, + pluginRegistry, + logLevel, + }; + const pluginValidatorBesu = new PluginLedgerConnectorBesu(options); + + // 4. Create the API Server object that we embed in this test + const configService = new ConfigService(); + const apiServerOptions = configService.newExampleConfig(); + apiServerOptions.authorizationProtocol = AuthorizationProtocol.NONE; + apiServerOptions.configFile = ""; + apiServerOptions.apiCorsDomainCsv = "*"; + apiServerOptions.apiPort = addressInfo1.port; + apiServerOptions.cockpitPort = 0; + apiServerOptions.apiTlsEnabled = false; + const config = configService.newExampleConfigConvict(apiServerOptions); + + pluginRegistry.add(pluginValidatorBesu); + + const apiServer = new ApiServer({ + httpServerApi: httpServer1, + config: config.getProperties(), + pluginRegistry, + }); + + // 5. make sure the API server is shut down when the testing if finished. + test.onFinish(() => apiServer.shutdown()); + + // 6. Start the API server which is now listening on port A and it's healthcheck works through the main SDK + await apiServer.start(); + + // 7. Instantiate the main SDK dynamically with whatever port the API server ended up bound to (port 0) + t.comment(`AddressInfo: ${JSON.stringify(addressInfo1)}`); + + const web3Provider = new Web3.providers.HttpProvider(rpcApiHttpHost); + const web3 = new Web3(web3Provider); + const web3Eea: IWeb3InstanceExtended = EEAClient(web3, 2018); + + const orionKeyPair = await besuTestLedger.getOrionKeyPair(); + const besuKeyPair = await besuTestLedger.getBesuKeyPair(); + + const besuPrivateKey = besuKeyPair.privateKey.toLowerCase().startsWith("0x") + ? besuKeyPair.privateKey.substring(2) + : besuKeyPair.privateKey; // besu node's private key + + const contractOptions = { + data: `0x123`, + // privateFrom : Orion public key of the sender. + privateFrom: orionKeyPair.publicKey, + // privateFor : Orion public keys of recipients or privacyGroupId: Privacy group to receive the transaction + privateFor: [orionKeyPair.publicKey], + // privateKey: Ethereum private key with which to sign the transaction. + privateKey: besuPrivateKey, + }; + + const transactionHash = await web3Eea.eea.sendRawTransaction(contractOptions); + + await web3.eth.getTransaction(transactionHash); + + const request: GetPastLogsV1Request = { + address: testAccount.address, + }; + + const configuration = new BesuApiClientOptions({ basePath: node1Host }); + const api = new BesuApiClient(configuration); + + const res = await api.getPastLogsV1(request); + // const { } = res; + t.ok(res, "API response object is truthy"); + t.ok(res.data.logs, "Response.logs is truthy ok"); + t.true(Array.isArray(res.data.logs), "Response.logs is Array ok"); +}); + +test("AFTER " + testCase, async (t: Test) => { + const pruning = pruneDockerAllIfGithubAction({ logLevel }); + await t.doesNotReject(pruning, "Pruning didn't throw OK"); + t.end(); +});