From 50107f62cda1e3be688576b9074b9408757e9b49 Mon Sep 17 00:00:00 2001 From: Youngone Lee Date: Mon, 28 Jun 2021 11:16:10 -0500 Subject: [PATCH] feat(connector-besu): add getBalance web service Fixes #1066 Signed-off-by: Youngone Lee --- .../src/main/json/openapi.json | 34 ++++ .../generated/openapi/typescript-axios/api.ts | 67 +++++++ .../plugin-ledger-connector-besu.ts | 8 + .../web-services/get-balance-endpoint.ts | 101 ++++++++++ .../deploy-contract/get-balance.test.ts | 8 - .../get-balance-endpoint.test.ts | 176 ++++++++++++++++++ 6 files changed, 386 insertions(+), 8 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-balance-endpoint.ts create mode 100644 packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/get-balance-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 f988ee6053..63d4084f08 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 @@ -846,6 +846,40 @@ } } }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-balance": { + "post": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-balance" + } + }, + "operationId": "getbalanceV1", + "summary": "Return balance of an address of a given block", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBalanceV1Request" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBalanceV1Response" + } + } + } + } + } + } + }, "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/run-transaction": { "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 23dacedf60..dd0e88cc22 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 @@ -987,6 +987,40 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Return balance of an address of a given block + * @param {GetBalanceV1Request} [getBalanceV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getbalanceV1: async (getBalanceV1Request?: GetBalanceV1Request, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-balance`; + // 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(getBalanceV1Request, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Obtain signatures of ledger from the corresponding transaction hash. * @summary Obtain signatures of ledger from the corresponding transaction hash. @@ -1076,6 +1110,17 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getPrometheusExporterMetricsV1(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Return balance of an address of a given block + * @param {GetBalanceV1Request} [getBalanceV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getbalanceV1(getBalanceV1Request?: GetBalanceV1Request, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getbalanceV1(getBalanceV1Request, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * Obtain signatures of ledger from the corresponding transaction hash. * @summary Obtain signatures of ledger from the corresponding transaction hash. @@ -1136,6 +1181,16 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa getPrometheusExporterMetricsV1(options?: any): AxiosPromise { return localVarFp.getPrometheusExporterMetricsV1(options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Return balance of an address of a given block + * @param {GetBalanceV1Request} [getBalanceV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getbalanceV1(getBalanceV1Request?: GetBalanceV1Request, options?: any): AxiosPromise { + return localVarFp.getbalanceV1(getBalanceV1Request, options).then((request) => request(axios, basePath)); + }, /** * Obtain signatures of ledger from the corresponding transaction hash. * @summary Obtain signatures of ledger from the corresponding transaction hash. @@ -1203,6 +1258,18 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).getPrometheusExporterMetricsV1(options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Return balance of an address of a given block + * @param {GetBalanceV1Request} [getBalanceV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getbalanceV1(getBalanceV1Request?: GetBalanceV1Request, options?: any) { + return DefaultApiFp(this.configuration).getbalanceV1(getBalanceV1Request, options).then((request) => request(this.axios, this.basePath)); + } + /** * Obtain signatures of ledger from the corresponding transaction hash. * @summary Obtain signatures of ledger from the corresponding transaction hash. 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 9f42fd2056..6cb30f4b28 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 @@ -74,6 +74,7 @@ import { IGetPrometheusExporterMetricsEndpointV1Options, } from "./web-services/get-prometheus-exporter-metrics-endpoint-v1"; import { WatchBlocksV1Endpoint } from "./web-services/watch-blocks-v1-endpoint"; +import { GetBalanceEndpoint } from "./web-services/get-balance-endpoint"; export const E_KEYCHAIN_NOT_FOUND = "cactus.connector.besu.keychain_not_found"; @@ -205,6 +206,13 @@ export class PluginLedgerConnectorBesu }); endpoints.push(endpoint); } + { + const endpoint = new GetBalanceEndpoint({ + 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-balance-endpoint.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-balance-endpoint.ts new file mode 100644 index 0000000000..c7515d1c6a --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-balance-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 IGetBalanceEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorBesu; +} + +export class GetBalanceEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "GetBalanceEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return GetBalanceEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IGetBalanceEndpointOptions) { + 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-balance" + ]; + } + + 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.getBalance(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-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/get-balance.test.ts b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/get-balance.test.ts index 6920d3680f..c0838724ad 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/get-balance.test.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/get-balance.test.ts @@ -32,14 +32,6 @@ test("can get balance of an account", async (t: Test) => { * @see https://github.com/hyperledger/besu/blob/1.5.1/config/src/main/resources/dev.json */ const firstHighNetWorthAccount = "627306090abaB3A6e1400e9345bC60c78a8BEf57"; - /* - const besuKeyPair = { - privateKey: - "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3", - }; - const contractName = "HelloWorld"; - - */ const web3 = new Web3(rpcApiHttpHost); const testEthAccount = web3.eth.accounts.create(uuidv4()); diff --git a/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/get-balance-endpoint.test.ts b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/get-balance-endpoint.test.ts new file mode 100644 index 0000000000..89eed39f37 --- /dev/null +++ b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/get-balance-endpoint.test.ts @@ -0,0 +1,176 @@ +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, + GetBalanceV1Request, +} 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 getBalance 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 transaction = await web3.eth.getTransaction(transactionHash); + const singData = jsObjectSigner.sign(transaction.input); + const signDataHex = Buffer.from(singData).toString("hex"); + */ + + const request: GetBalanceV1Request = { + //is it suppose to be GetBalanceV1Request? + address: testAccount.address, + }; + + const configuration = new BesuApiClientOptions({ basePath: node1Host }); + const api = new BesuApiClient(configuration); + + // Test for 200 valid response test case + const res = await api.getbalanceV1(request); + t.ok(res, "API response object is truthy"); + t.true(typeof res.data.balance === "string", "Response is String ok"); +}); + +test("AFTER " + testCase, async (t: Test) => { + const pruning = pruneDockerAllIfGithubAction({ logLevel }); + await t.doesNotReject(pruning, "Pruning didn't throw OK"); + t.end(); +});