From 66778280bc6bba31eaefb98607beb1f8b87d97da Mon Sep 17 00:00:00 2001 From: wangyinglun Date: Tue, 11 Apr 2023 14:17:11 +0800 Subject: [PATCH] feat(connector-tcs-huawei): add initial version Signed-off-by: wangyinglun --- examples/cactus-example-tcs-huawei/.gitignore | 7 + .../BalanceManagement.ts | 65 +++ .../BusinessLogicTcsElectricityTrade.ts | 429 ++++++++++++++++++ examples/cactus-example-tcs-huawei/Dockerfile | 16 + .../cactus-example-tcs-huawei/MeterInfo.ts | 20 + .../MeterManagement.ts | 304 +++++++++++++ examples/cactus-example-tcs-huawei/README.md | 13 + .../cactus-example-tcs-huawei/RequestInfo.ts | 33 ++ .../TransactionEthereum.ts | 163 +++++++ examples/cactus-example-tcs-huawei/balance.ts | 49 ++ .../config/contractInfo.yaml | 1 + .../config/default.yaml | 28 ++ .../config/usersetting.yaml | 30 ++ .../config/validator-registry-config.yaml | 62 +++ .../docker-compose.debug.yml | 9 + .../docker-compose.yml | 69 +++ .../electricity-trade.ts | 75 +++ .../images/electricity-trade-image.png | Bin 0 -> 73935 bytes .../images/electricity-trade-image.pptx | Bin 0 -> 46713 bytes .../cactus-example-tcs-huawei/package.json | 46 ++ .../script-cleanup.sh | 30 ++ .../script-gen-electricity-usage.sh | 27 ++ .../script-get-app.sh | 12 + .../script-post-setup-request.sh | 15 + .../script-post-start-request.sh | 6 + .../script-start-ledgers.sh | 130 ++++++ .../tools/periodicExecuter/package.json | 17 + .../periodicExecuter/periodicExecuter.ts | 75 +++ .../tools/periodicExecuter/tsconfig.json | 24 + .../transferNumericAsset/config/default.js | 17 + .../transferNumericAsset/copyStaticAssets.ts | 8 + .../tools/transferNumericAsset/package.json | 23 + .../transferNumericAsset.ts | 121 +++++ .../tools/transferNumericAsset/tsconfig.json | 27 ++ .../cactus-example-tcs-huawei/tsconfig.json | 20 + .../cactus-example-tcs-huawei/tslint.json | 28 ++ examples/cactus-example-tcs-huawei/www.ts | 7 + .../.gitignore | 1 + .../Dockerfile | 13 + .../README.md | 77 ++++ .../package.json | 33 ++ .../sample-config/default.yaml | 21 + .../src/main/typescript/common/core/app.ts | 53 +++ .../main/typescript/common/core/bin/www.ts | 144 ++++++ .../src/main/typescript/common/core/config.ts | 37 ++ .../main/typescript/connector/PluginUtil.ts | 87 ++++ .../connector/ServerMonitorPlugin.ts | 198 ++++++++ .../connector/ValidatorAuthentication.ts | 33 ++ .../tsconfig.json | 18 + .../agent/example/cross_config/config.yaml | 35 ++ .../agent/example/docker-compose.yml | 37 ++ tsconfig.json | 6 + 52 files changed, 2799 insertions(+) create mode 100644 examples/cactus-example-tcs-huawei/.gitignore create mode 100644 examples/cactus-example-tcs-huawei/BalanceManagement.ts create mode 100644 examples/cactus-example-tcs-huawei/BusinessLogicTcsElectricityTrade.ts create mode 100644 examples/cactus-example-tcs-huawei/Dockerfile create mode 100644 examples/cactus-example-tcs-huawei/MeterInfo.ts create mode 100644 examples/cactus-example-tcs-huawei/MeterManagement.ts create mode 100644 examples/cactus-example-tcs-huawei/README.md create mode 100644 examples/cactus-example-tcs-huawei/RequestInfo.ts create mode 100644 examples/cactus-example-tcs-huawei/TransactionEthereum.ts create mode 100644 examples/cactus-example-tcs-huawei/balance.ts create mode 100644 examples/cactus-example-tcs-huawei/config/contractInfo.yaml create mode 100644 examples/cactus-example-tcs-huawei/config/default.yaml create mode 100644 examples/cactus-example-tcs-huawei/config/usersetting.yaml create mode 100644 examples/cactus-example-tcs-huawei/config/validator-registry-config.yaml create mode 100644 examples/cactus-example-tcs-huawei/docker-compose.debug.yml create mode 100644 examples/cactus-example-tcs-huawei/docker-compose.yml create mode 100644 examples/cactus-example-tcs-huawei/electricity-trade.ts create mode 100644 examples/cactus-example-tcs-huawei/images/electricity-trade-image.png create mode 100644 examples/cactus-example-tcs-huawei/images/electricity-trade-image.pptx create mode 100644 examples/cactus-example-tcs-huawei/package.json create mode 100644 examples/cactus-example-tcs-huawei/script-cleanup.sh create mode 100644 examples/cactus-example-tcs-huawei/script-gen-electricity-usage.sh create mode 100644 examples/cactus-example-tcs-huawei/script-get-app.sh create mode 100644 examples/cactus-example-tcs-huawei/script-post-setup-request.sh create mode 100644 examples/cactus-example-tcs-huawei/script-post-start-request.sh create mode 100644 examples/cactus-example-tcs-huawei/script-start-ledgers.sh create mode 100644 examples/cactus-example-tcs-huawei/tools/periodicExecuter/package.json create mode 100644 examples/cactus-example-tcs-huawei/tools/periodicExecuter/periodicExecuter.ts create mode 100644 examples/cactus-example-tcs-huawei/tools/periodicExecuter/tsconfig.json create mode 100644 examples/cactus-example-tcs-huawei/tools/transferNumericAsset/config/default.js create mode 100644 examples/cactus-example-tcs-huawei/tools/transferNumericAsset/copyStaticAssets.ts create mode 100644 examples/cactus-example-tcs-huawei/tools/transferNumericAsset/package.json create mode 100644 examples/cactus-example-tcs-huawei/tools/transferNumericAsset/transferNumericAsset.ts create mode 100644 examples/cactus-example-tcs-huawei/tools/transferNumericAsset/tsconfig.json create mode 100644 examples/cactus-example-tcs-huawei/tsconfig.json create mode 100644 examples/cactus-example-tcs-huawei/tslint.json create mode 100644 examples/cactus-example-tcs-huawei/www.ts create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/.gitignore create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/Dockerfile create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/README.md create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/package.json create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/default.yaml create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/app.ts create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/bin/www.ts create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/config.ts create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/PluginUtil.ts create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/ServerMonitorPlugin.ts create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/ValidatorAuthentication.ts create mode 100644 packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/tsconfig.json create mode 100644 tools/docker/tcs-huawei-testnet/agent/example/cross_config/config.yaml create mode 100644 tools/docker/tcs-huawei-testnet/agent/example/docker-compose.yml diff --git a/examples/cactus-example-tcs-huawei/.gitignore b/examples/cactus-example-tcs-huawei/.gitignore new file mode 100644 index 0000000000..bc3fec4ce7 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/.gitignore @@ -0,0 +1,7 @@ +MeterInfo.json + +# BLP artifacts +etc/ + +# don't commit package-lock +package-lock.json diff --git a/examples/cactus-example-tcs-huawei/BalanceManagement.ts b/examples/cactus-example-tcs-huawei/BalanceManagement.ts new file mode 100644 index 0000000000..2eaf10e66b --- /dev/null +++ b/examples/cactus-example-tcs-huawei/BalanceManagement.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2020-2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * BalanceManagement.ts + */ + +import { + LPInfoHolder, + ConfigUtil, +} from "@hyperledger/cactus-cmd-socketio-server"; +import { + VerifierFactory, + VerifierFactoryConfig, +} from "@hyperledger/cactus-verifier-client"; + +const config: any = ConfigUtil.getConfig(); +import { getLogger } from "log4js"; +const moduleName = "BalanceManagement"; +const logger = getLogger(`${moduleName}`); +logger.level = config.logLevel; + +export class BalanceManagement { + private connectInfo: LPInfoHolder = null; // connection information + private readonly verifierFactory: VerifierFactory; + + constructor() { + this.connectInfo = new LPInfoHolder(); + this.verifierFactory = new VerifierFactory( + this.connectInfo.ledgerPluginInfo as VerifierFactoryConfig, + config.logLevel, + ); + } + + getBalance(account: string): Promise { + return new Promise((resolve, reject) => { + // for LedgerOperation + // const execData = {"referedAddress": account}; + // const ledgerOperation: LedgerOperation = new LedgerOperation("getNumericBalance", "", execData); + + // for Neo + const contract = {}; // NOTE: Since contract does not need to be specified, specify an empty object. + const method = { type: "web3Eth", command: "getBalance" }; + const template = "default"; + const args = { args: [account] }; + // const method = "default"; + // const args = {"method": {type: "web3Eth", command: "getBalance"}, "args": {"args": [account]}}; + + this.verifierFactory + .getVerifier("84jUisrs") + .sendSyncRequest(contract, method, args) + .then((result) => { + const response = { + status: result.status, + amount: parseFloat(result.data), + }; + resolve(response); + }) + .catch((err) => { + logger.error(err); + reject(err); + }); + }); + } +} diff --git a/examples/cactus-example-tcs-huawei/BusinessLogicTcsElectricityTrade.ts b/examples/cactus-example-tcs-huawei/BusinessLogicTcsElectricityTrade.ts new file mode 100644 index 0000000000..c9cb6ee16d --- /dev/null +++ b/examples/cactus-example-tcs-huawei/BusinessLogicTcsElectricityTrade.ts @@ -0,0 +1,429 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * BusinessLogicElectricityTrade.ts + */ + +import { Request } from "express"; +import { RequestInfo } from "./RequestInfo"; +import { MeterManagement } from "./MeterManagement"; +import { MeterInfo } from "./MeterInfo"; +import { + TradeInfo, + routesTransactionManagement, + BusinessLogicBase, + LedgerEvent, + json2str, + ConfigUtil, + LPInfoHolder, +} from "@hyperledger/cactus-cmd-socketio-server"; +import { makeRawTransaction } from "./TransactionEthereum"; + +import fs from "fs"; +import yaml from "js-yaml"; +//const config: any = JSON.parse(fs.readFileSync("/etc/cactus/default.json", 'utf8')); +const config: any = ConfigUtil.getConfig(); +import { getLogger } from "log4js"; +import { + VerifierFactory, + VerifierFactoryConfig, +} from "@hyperledger/cactus-verifier-client"; +import { NIL } from "uuid"; + +const moduleName = "BusinessLogicElectricityTrade"; +const logger = getLogger(`${moduleName}`); +logger.level = config.logLevel; +const connectInfo = new LPInfoHolder(); +const routesVerifierFactory = new VerifierFactory( + connectInfo.ledgerPluginInfo as VerifierFactoryConfig, + config.logLevel, +); + +export class BusinessLogicElectricityTrade extends BusinessLogicBase { + businessLogicID: string; + meterManagement: MeterManagement; + + constructor(businessLogicID: string) { + super(); + this.businessLogicID = businessLogicID; + this.meterManagement = new MeterManagement(); + } + + startTransaction(req: Request, businessLogicID: string, tradeID: string) { + logger.debug("called startTransaction()"); + + // set RequestInfo + const requestInfo: RequestInfo = new RequestInfo(); + requestInfo.businessLogicID = businessLogicID; + + // set TradeID + requestInfo.setTradeID(tradeID); + + // Create trade information + const tradeInfo: TradeInfo = new TradeInfo( + requestInfo.businessLogicID, + requestInfo.tradeID, + ); + + this.startMonitor(tradeInfo); + } + + startMonitor(tradeInfo: TradeInfo) { + // Get Verifier Instance + logger.debug( + `##startMonitor(): businessLogicID: ${tradeInfo.businessLogicID}`, + ); + const useValidator = JSON.parse( + routesTransactionManagement.getValidatorToUse(tradeInfo.businessLogicID), + ); + logger.debug( + `filterKey: ${config.electricityTradeInfo.tcsHuawei.filterKey}`, + ); + const options = { + filterKey: config.electricityTradeInfo.tcsHuawei.filterKey, + }; + const verifierTcs = routesVerifierFactory.getVerifier( + useValidator["validatorID"][0], + ); + verifierTcs.startMonitor( + "BusinessLogicTcsElectricityTrade", + options, + routesTransactionManagement, + ); + logger.debug("getVerifierTcs"); + } + + remittanceTransaction(transactionSubset: object) { + logger.debug( + `called remittanceTransaction(), accountInfo = ${json2str( + transactionSubset, + )}`, + ); + + const accountInfo = this.getAccountInfo(transactionSubset); + if (Object.keys(accountInfo).length === 0) { + logger.debug(`remittanceTransaction(): skip (No target meter)`); + return; + } + + const txParam: { + fromAddress: string; + fromAddressPkey: string; + toAddress: string; + amount: number; + gas: number; + } = { + fromAddress: accountInfo["fromAddress"], + fromAddressPkey: accountInfo["fromAddressPkey"], + toAddress: accountInfo["toAddress"], + amount: Number(transactionSubset["Value"]), + gas: config.electricityTradeInfo.ethereum.gas, + }; + logger.debug(`####txParam = ${json2str(txParam)}`); + + // Get Verifier Instance + logger.debug( + `##remittanceTransaction(): businessLogicID: ${this.businessLogicID}`, + ); + const useValidator = JSON.parse( + routesTransactionManagement.getValidatorToUse(this.businessLogicID), + ); + // const verifierEthereum = routesTransactionManagement.getVerifier(useValidator['validatorID'][1]); + const verifierEthereum = routesVerifierFactory.getVerifier( + useValidator["validatorID"][1], + ); + verifierEthereum.startMonitor( + "BusinessLogicElectricityTrade", + {}, + routesTransactionManagement, + ); + logger.debug("getVerifierEthereum"); + + // Generate parameters for// sendRawTransaction + logger.debug(`####exec makeRawTransaction!!`); + makeRawTransaction(txParam) + .then((result) => { + logger.info("remittanceTransaction txId : " + result.txId); + + // Set Parameter + logger.debug("remittanceTransaction data : " + json2str(result.data)); + const contract = {}; // NOTE: Since contract does not need to be specified, specify an empty object. + const method = { type: "web3Eth", command: "sendRawTransaction" }; + const args = { args: [result.data["serializedTx"]] }; + + // Run Verifier (Ethereum) + verifierEthereum + .sendAsyncRequest(contract, method, args) + .then(() => { + logger.debug(`##remittanceTransaction sendAsyncRequest finish`); + }) + .catch((err) => { + logger.error(err); + }); + }) + .catch((err) => { + logger.error(err); + }); + } + + onEvent(ledgerEvent: LedgerEvent, targetIndex: number): void { + logger.debug(`##in BLP:onEvent()`); + logger.debug( + `##onEvent(): ${json2str(ledgerEvent["data"]["blockData"][targetIndex])}`, + ); + switch (ledgerEvent.verifierId) { + case config.electricityTradeInfo.tcsHuawei.validatorID: + this.onEventTCS(ledgerEvent.data, targetIndex); + break; + case config.electricityTradeInfo.ethereum.validatorID: + this.onEventEthereum(ledgerEvent.data, targetIndex); + break; + default: + logger.error( + `##onEvent(), invalid verifierId: ${ledgerEvent.verifierId}`, + ); + return; + } + } + + onEventTCS(event: any, targetIndex: number): void { + logger.debug(`##in onEventTCS()`); + logger.debug("event.blockData", event["blockData"]) + logger.debug("event.blockData[0]", event["blockData"][targetIndex]) + logger.debug("event.blockData[0].Payload", event["blockData"][targetIndex]["Payload"]) + logger.debug("event.blockData.Payload.Name", event["blockData"][targetIndex]["Payload"]["Name"]) + + var trxPayload = event["blockData"][targetIndex]["Payload"] + if (trxPayload == undefined || trxPayload == NIL) { + logger.error("transaction payload is empty") + return + } + try { + if (trxPayload["Verb"].Verb !== "set") { + const transactionSubset = { + Name: trxPayload["Name"], + Value: trxPayload["Value"], + Verb: trxPayload["Verb"], + }; + this.remittanceTransaction(transactionSubset); + } + } catch (err) { + logger.error( + `##onEventTCS(): err: ${err}, event: ${json2str(event)}`, + ); + } + } + + getTransactionFromTCSEvent( + event: object, + targetIndex: number, + ): object | null { + try { + const retTransaction = event["blockData"][targetIndex]; + logger.debug( + `##getTransactionFromTCSEvent(), retTransaction: ${retTransaction}`, + ); + return retTransaction; + } catch (err) { + logger.error( + `##getTransactionFromTCSEvent(): invalid even, err:${err}, event:${event}`, + ); + } + } + + onEventEthereum(event: object, targetIndex: number): void { + logger.debug(`##in onEventEthereum()`); + const tx = this.getTransactionFromEthereumEvent(event, targetIndex); + if (tx == null) { + logger.error(`##onEventEthereum(): invalid event: ${json2str(event)}`); + return; + } + + try { + const txId = tx["hash"]; + const status = event["status"]; + logger.debug(`##txId = ${txId}`); + logger.debug(`##status =${status}`); + + if (status !== 200) { + logger.error( + `##onEventEthereum(): error event, status: ${status}, txId: ${txId}`, + ); + return; + } + } catch (err) { + logger.error( + `##onEventEthereum(): err: ${err}, event: ${json2str(event)}`, + ); + } + } + + getTransactionFromEthereumEvent( + event: object, + targetIndex: number, + ): object | null { + try { + const retTransaction = event["blockData"]["transactions"][targetIndex]; + logger.debug( + `##getTransactionFromEthereumEvent(), retTransaction: ${retTransaction}`, + ); + return retTransaction; + } catch (err) { + logger.error( + `##getTransactionFromEthereumEvent(): invalid even, err:${err}, event:${event}`, + ); + } + } + + getOperationStatus(tradeID: string): object { + logger.debug(`##in getOperationStatus()`); + return {}; + } + + getTxIDFromEvent( + ledgerEvent: LedgerEvent, + targetIndex: number, + ): string | null { + logger.debug(`##in getTxIDFromEvent`); + // logger.debug(`##event: ${json2str(ledgerEvent)}`); + + switch (ledgerEvent.verifierId) { + case config.electricityTradeInfo.tcsHuawei.validatorID: + return this.getTxIDFromEventTCS(ledgerEvent.data, targetIndex); + case config.electricityTradeInfo.ethereum.validatorID: + return this.getTxIDFromEventEtherem(ledgerEvent.data, targetIndex); + default: + logger.error( + `##getTxIDFromEvent(): invalid verifierId: ${ledgerEvent.verifierId}`, + ); + } + return null; + } + + getTxIDFromEventTCS(event: object, targetIndex: number): string | null { + logger.debug(`##in getTxIDFromEventTCS()`); + const tx = this.getTransactionFromTCSEvent(event, targetIndex); + if (tx == null) { + logger.warn(`#getTxIDFromEventTCS(): skip(not found tx)`); + return null; + } + + try { + return "txId-test-1" + const txId = tx["header_signature"]; + + if (typeof txId !== "string") { + logger.warn( + `#getTxIDFromEventTCS(): skip(invalid block, not found txId.), event: ${json2str( + event, + )}`, + ); + return null; + } + + logger.debug(`###getTxIDFromEventTCS(): txId: ${txId}`); + return txId; + } catch (err) { + logger.error( + `##getTxIDFromEventTCS(): err: ${err}, event: ${json2str(event)}`, + ); + return null; + } + } + + getTxIDFromEventEtherem(event: object, targetIndex: number): string | null { + logger.debug(`##in getTxIDFromEventEtherem()`); + const tx = this.getTransactionFromEthereumEvent(event, targetIndex); + if (tx == null) { + logger.warn(`#getTxIDFromEventEtherem(): skip(not found tx)`); + return null; + } + + try { + const txId = tx["hash"]; + + if (typeof txId !== "string") { + logger.warn( + `#getTxIDFromEventEtherem(): skip(invalid block, not found txId.), event: ${json2str( + event, + )}`, + ); + return null; + } + + logger.debug(`###getTxIDFromEventEtherem(): txId: ${txId}`); + return txId; + } catch (err) { + logger.error( + `##getTxIDFromEventEtherem(): err: ${err}, event: ${json2str(event)}`, + ); + return null; + } + } + + getEventDataNum(ledgerEvent: LedgerEvent): number { + logger.debug( + `##in BLP:getEventDataNum(), ledgerEvent.verifierId: ${ledgerEvent.verifierId}`, + ); + const event = ledgerEvent.data; + let retEventNum = 0; + + try { + switch (ledgerEvent.verifierId) { + case config.electricityTradeInfo.tcsHuawei.validatorID: + retEventNum = event["blockData"].length; + break; + case config.electricityTradeInfo.ethereum.validatorID: + retEventNum = event["blockData"]["transactions"].length; + break; + default: + logger.error( + `##getEventDataNum(): invalid verifierId: ${ledgerEvent.verifierId}`, + ); + break; + } + logger.debug( + `##getEventDataNum(): retEventNum: ${retEventNum}, verifierId: ${ledgerEvent.verifierId}`, + ); + return retEventNum; + } catch (err) { + logger.error( + `##getEventDataNum(): invalid even, err: ${err}, event: ${event}`, + ); + return 0; + } + } + + getAccountInfo(transactionSubset: object): object { + const transactionInfo = {}; + + // Get Meter Information. + const meterInfo: MeterInfo | null = this.meterManagement.getMeterInfo( + transactionSubset["Name"], + ); + if (meterInfo === null) { + logger.debug(`Not registered. meterID = ${transactionSubset["Name"]}`); + return transactionInfo; + } + + logger.debug(`getAccountInfo(): Verb = ${transactionSubset["Verb"]}`); + if (transactionSubset["Verb"] === "inc") { + logger.debug("getAccountInfo(): Verb = inc"); + transactionInfo["fromAddress"] = "0x" + meterInfo.bankAccount; + transactionInfo["fromAddressPkey"] = meterInfo.bankAccountPKey; + transactionInfo["toAddress"] = "0x" + meterInfo.powerCompanyAccount; + } + + return transactionInfo; + } + + setConfig(meterParams: string[]): object { + logger.debug("called setConfig()"); + + // add MeterInfo + const meterInfo = new MeterInfo(meterParams); + const result: {} = this.meterManagement.addMeterInfo(meterInfo); + return result; + } +} diff --git a/examples/cactus-example-tcs-huawei/Dockerfile b/examples/cactus-example-tcs-huawei/Dockerfile new file mode 100644 index 0000000000..6795b68371 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/Dockerfile @@ -0,0 +1,16 @@ +FROM cactus-cmd-socketio-server:latest + +ARG NPM_PKG_VERSION=latest + +ENV APP_HOME=/root/cactus + +WORKDIR ${APP_HOME} + +COPY ./dist/yarn.lock ./package.json ./ +RUN yarn add "${CACTUS_CMD_SOCKETIO_PATH}" "@hyperledger/cactus-verifier-client@${NPM_PKG_VERSION}" \ + --production --ignore-engines --non-interactive --cache-folder ./.yarnCache && \ + rm -rf ./.yarnCache + +COPY ./dist ./dist/ + +CMD ["node", "./dist/www.js"] diff --git a/examples/cactus-example-tcs-huawei/MeterInfo.ts b/examples/cactus-example-tcs-huawei/MeterInfo.ts new file mode 100644 index 0000000000..e598b81d16 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/MeterInfo.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * MeterInfo.ts + */ + +export class MeterInfo { + meterID: string; + bankAccount: string; + bankAccountPKey: string; + powerCompanyAccount: string; + + constructor(meterParams: string[]) { + this.meterID = meterParams[0]; + this.bankAccount = meterParams[1]; + this.bankAccountPKey = meterParams[2]; + this.powerCompanyAccount = meterParams[3]; + } +} diff --git a/examples/cactus-example-tcs-huawei/MeterManagement.ts b/examples/cactus-example-tcs-huawei/MeterManagement.ts new file mode 100644 index 0000000000..89b4c9bc2c --- /dev/null +++ b/examples/cactus-example-tcs-huawei/MeterManagement.ts @@ -0,0 +1,304 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * MeterManagement.ts + */ + +import { MeterInfo } from "./MeterInfo"; +import { ConfigUtil } from "@hyperledger/cactus-cmd-socketio-server"; +import fs from "fs" +import yaml from "js-yaml" +const config: any = ConfigUtil.getConfig(); +import { getLogger } from "log4js"; +const moduleName = "MeterManagement"; +const logger = getLogger(`${moduleName}`); +logger.level = config.logLevel; + +// Meter Management Class +export class MeterManagement { + fileName = "MeterInfo.json"; + + constructor() {} + + // For debugging + fileDump() { + const confirmData: string = fs.readFileSync(this.fileName, "utf8"); + const arrayData: MeterInfo[] = JSON.parse(confirmData).table as MeterInfo[]; + logger.debug(arrayData); + } + + addMeterInfo(addMeterInfo: MeterInfo): object { + // Existence check of table file + try { + fs.statSync(this.fileName); + } catch (err) { + // Creating an empty table file + const emptyTable = { + table: [], + }; + const emptyTableJson: string = JSON.stringify(emptyTable); + fs.writeFileSync(this.fileName, emptyTableJson, "utf8"); + } + + // Read table file + const meterInfoFileData: string = fs.readFileSync(this.fileName, "utf8"); + const meterInfoTable: string[] = JSON.parse(meterInfoFileData) + .table as string[]; + + // Search target records / replace data + const meterInfoTableNew = { + table: [], + }; + let existFlag = false; + let action = ""; + meterInfoTable.forEach((meterInfoJSON, index) => { + const meterInfo: MeterInfo = JSON.parse(meterInfoJSON) as MeterInfo; + + // Determine if it is a target record + if (meterInfo.meterID === addMeterInfo.meterID) { + // Change Status + meterInfo.bankAccount = addMeterInfo.bankAccount; + meterInfo.bankAccountPKey = addMeterInfo.bankAccountPKey; + meterInfo.powerCompanyAccount = addMeterInfo.powerCompanyAccount; + existFlag = true; + action = "update"; + } + + // Register Record + const meterInfoNewJson: string = JSON.stringify(meterInfo); + // logger.debug(`meter info = ${meterInfoNewJson}`); + meterInfoTableNew.table.push(meterInfoNewJson); + }); + if (existFlag === false) { + const addMeterInfoJson: string = JSON.stringify(addMeterInfo); + logger.debug(`add meter info = ${addMeterInfoJson}`); + meterInfoTableNew.table.push(addMeterInfoJson); + action = "add"; + } + + // Table File Write + const meterInfoTableNewJson: string = JSON.stringify(meterInfoTableNew); + fs.writeFileSync(this.fileName, meterInfoTableNewJson, "utf8"); + + // this.fileDump(); + + const result = { + action: action, + meterID: addMeterInfo.meterID, + }; + return result; + } + + getMeterInfo(meterID: string): MeterInfo { + // Existence check of table file + try { + fs.statSync(this.fileName); + } catch (err) { + throw err; + } + + // Read table file + const meterInfoFileData: string = fs.readFileSync(this.fileName, "utf8"); + const meterInfoTable: string[] = JSON.parse(meterInfoFileData) + .table as string[]; + + // Search target records + let retMeterInfo: MeterInfo | null = null; + meterInfoTable.forEach((meterInfoJSON, index) => { + const meterInfo: MeterInfo = JSON.parse(meterInfoJSON) as MeterInfo; + + // Determine if it is a target record + if (meterInfo.meterID === meterID) { + retMeterInfo = meterInfo; + return; + } + }); + + return retMeterInfo; + } + + /* + setStatus(tradeInfo: TradeInfo, status: CartradeStatus) { + + // Existence check of table file + try { + fs.statSync(this.fileName); + } catch (err) { + throw err; + } + + // Read table file + const fileData: string = fs.readFileSync(this.fileName, 'utf8'); + const transactionInfoTable: string[] = JSON.parse(fileData).table as string[]; + + // Search target records / replace data + const transactionInfoTableNew = { + table: [] + }; + transactionInfoTable.forEach((transactionInfoJSON, index) => { + const transactionInfo: TransactionInfo = JSON.parse(transactionInfoJSON) as TransactionInfo; + + // Determine if it is a target record + if (transactionInfo.businessLogicID === tradeInfo.businessLogicID && transactionInfo.tradeID === tradeInfo.tradeID) { + // Change Status + transactionInfo.status = status; + } + + // Register Record + const transactionInfoJson: string = JSON.stringify(transactionInfo); + transactionInfoTableNew.table.push(transactionInfoJson); + }); + + // Table File Write + const transactionInfoTableNewJson: string = JSON.stringify(transactionInfoTableNew); + fs.writeFileSync(this.fileName, transactionInfoTableNewJson, 'utf8'); + + this.fileDump(); + } + + setTransactionData(tradeInfo: TradeInfo, transactionData: TransactionData) { + + // Existence check of table file + try { + fs.statSync(this.fileName); + } catch (err) { + throw err; + } + + // Read table file + const fileData: string = fs.readFileSync(this.fileName, 'utf8'); + const transactionInfoTable: string[] = JSON.parse(fileData).table as string[]; + + // Search target records / replace data + const transactionInfoTableNew = { + table: [] + }; + transactionInfoTable.forEach((transactionInfoJSON, index) => { + const transactionInfo: TransactionInfo = JSON.parse(transactionInfoJSON) as TransactionInfo; + + // Determine if it is a target record + if (transactionInfo.businessLogicID === tradeInfo.businessLogicID && transactionInfo.tradeID === tradeInfo.tradeID) { + + // Determine if it is the target transaction + if (transactionData.target === "escrow") { + // escrow: dataset + transactionInfo.escrowLedger = transactionData.ledger; + transactionInfo.escrowTxID = transactionData.txID; + } + else if (transactionData.target === "transfer") { + // transfer: dataset + transactionInfo.transferLedger = transactionData.ledger; + transactionInfo.transferTxID = transactionData.txID; + } + else if (transactionData.target === "settlement") { + // settlement: dataset + transactionInfo.settlementLedger = transactionData.ledger; + transactionInfo.settlementTxID = transactionData.txID; + } + + } + + // Register Record + const transactionInfoJson: string = JSON.stringify(transactionInfo); + transactionInfoTableNew.table.push(transactionInfoJson); + }); + + // Table File Write + const transactionInfoTableNewJson: string = JSON.stringify(transactionInfoTableNew); + fs.writeFileSync(this.fileName, transactionInfoTableNewJson, 'utf8'); + + this.fileDump(); + } + + setTxInfo(tradeInfo: TradeInfo, txInfoData: TxInfoData) { + + // Existence check of table file + try { + fs.statSync(this.fileName); + } catch (err) { + throw err; + } + + // Read table file + const fileData: string = fs.readFileSync(this.fileName, 'utf8'); + const transactionInfoTable: string[] = JSON.parse(fileData).table as string[]; + + // Search target records / replace data + const transactionInfoTableNew = { + table: [] + }; + transactionInfoTable.forEach((transactionInfoJSON, index) => { + const transactionInfo: TransactionInfo = JSON.parse(transactionInfoJSON) as TransactionInfo; + + // Determine if it is a target record + if (transactionInfo.businessLogicID === tradeInfo.businessLogicID && transactionInfo.tradeID === tradeInfo.tradeID) { + + // Determine if it is the target transaction + if (txInfoData.target === "escrow") { + // escrow: dataset + transactionInfo.escrowTxInfo = txInfoData.txInfo; + } + else if (txInfoData.target === "transfer") { + // transfer: dataset + transactionInfo.transferTxInfo = txInfoData.txInfo; + } + else if (txInfoData.target === "settlement") { + // settlement: dataset + transactionInfo.settlementTxInfo = txInfoData.txInfo; + } + + } + + // Register Record + const transactionInfoJson: string = JSON.stringify(transactionInfo); + transactionInfoTableNew.table.push(transactionInfoJson); + }); + + // Table File Write + const transactionInfoTableNewJson: string = JSON.stringify(transactionInfoTableNew); + fs.writeFileSync(this.fileName, transactionInfoTableNewJson, 'utf8'); + + this.fileDump(); + } +*/ + /** + * Get transaction data corresponding to the specified txId. + * (*Return if any of escrowTxID, transferTxID, settlementTxID matches txId) + * + * @return Transaction data corresponding to txId. Returns null if it does not exist. + * + */ + /* + getTransactionInfoByTxId(txId: string): TransactionInfo { + + // Existence check of table file + try { + fs.statSync(this.fileName); + } catch (err) { + throw err; + } + + // Read table file + const fileData: string = fs.readFileSync(this.fileName, 'utf8'); + const transactionInfoTable: string[] = JSON.parse(fileData).table as string[]; + + // Search target records + const transactionInfoTableNew = { + table: [] + }; + let retTransactionInfo: TransactionInfo | null = null; + transactionInfoTable.forEach((transactionInfoJSON, index) => { + const transactionInfo: TransactionInfo = JSON.parse(transactionInfoJSON) as TransactionInfo; + + // Determine if it is a target record + if (transactionInfo.escrowTxID === txId || transactionInfo.transferTxID === txId || transactionInfo.settlementTxID === txId) { + retTransactionInfo = transactionInfo; + return; + } + }); + + return retTransactionInfo; + } +*/ +} diff --git a/examples/cactus-example-tcs-huawei/README.md b/examples/cactus-example-tcs-huawei/README.md new file mode 100644 index 0000000000..40b52f8671 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/README.md @@ -0,0 +1,13 @@ +# Cactus example-tcs-huawei + +## Abstract + +Cactus **example-tcs-huawei** is a sample application for exchanging electricity .It starts in a similar way to cactus-example-electricity-trade. + +![electricity-trade image](./images/electricity-trade-image.png) + +## Required software components +- OS: Linux (recommend: Ubuntu18.04 or CentOS7) +- Docker (recommend: v17.06.2-ce or greater) +- Docker-compose (recommend: v1.14.0 or greater) +- node.js v16 (recommend: v16 or greater) diff --git a/examples/cactus-example-tcs-huawei/RequestInfo.ts b/examples/cactus-example-tcs-huawei/RequestInfo.ts new file mode 100644 index 0000000000..8753de81a9 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/RequestInfo.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * RequestInfo.ts + */ + +// transaction information +class TradeInfo { + ethereumAccountA: string; + ethereumAccountB: string; +} + +// authorization information +class AuthInfo { + company: string; +} + +// request information +export class RequestInfo { + businessLogicID: string; + tradeID: string; + tradeInfo: TradeInfo; + authInfo: AuthInfo; + constructor() { + this.tradeInfo = new TradeInfo(); + this.authInfo = new AuthInfo(); + } + + setTradeID(tradeID: string) { + this.tradeID = tradeID; + } +} diff --git a/examples/cactus-example-tcs-huawei/TransactionEthereum.ts b/examples/cactus-example-tcs-huawei/TransactionEthereum.ts new file mode 100644 index 0000000000..8e63a04c5c --- /dev/null +++ b/examples/cactus-example-tcs-huawei/TransactionEthereum.ts @@ -0,0 +1,163 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * TransactionEthereum.ts + */ + +import { + LPInfoHolder, + TransactionSigner, + ConfigUtil, +} from "@hyperledger/cactus-cmd-socketio-server"; + +import { + VerifierFactory, + VerifierFactoryConfig, +} from "@hyperledger/cactus-verifier-client"; + + +import fs from "fs"; +import yaml from "js-yaml"; +//const config: any = JSON.parse(fs.readFileSync("/etc/cactus/default.json", 'utf8')); +const config: any = ConfigUtil.getConfig(); +import { getLogger } from "log4js"; +const moduleName = "TransactionEthereum"; +const logger = getLogger(`${moduleName}`); +logger.level = config.logLevel; + +const mapFromAddressNonce: Map = new Map(); +let xConnectInfo: LPInfoHolder = null; // connection information +let xVerifierFactory: VerifierFactory = null; + +export function makeRawTransaction(txParam: { + fromAddress: string; + fromAddressPkey: string; + toAddress: string; + amount: number; + gas: number; +}): Promise<{ data: {}; txId: string }> { + return new Promise(async (resolve, reject) => { + try { + logger.debug(`makeRawTransaction: txParam: ${JSON.stringify(txParam)}`); + + getNewNonce(txParam.fromAddress).then((result) => { + logger.debug( + `##makeRawTransaction(A): result: ${JSON.stringify(result)}`, + ); + + const txnCountHex: string = result.txnCountHex; + + const rawTx: { nonce: string; to: string; value: number; gas: number } = + { + nonce: txnCountHex, + to: txParam.toAddress, + value: txParam.amount, + gas: txParam.gas, + }; + logger.debug( + `##makeRawTransaction(B), rawTx: ${JSON.stringify(rawTx)}`, + ); + + const signedTx = TransactionSigner.signTxEthereum( + rawTx, + txParam.fromAddressPkey, + ); + const resp: { data: {}; txId: string } = { + data: { serializedTx: signedTx["serializedTx"] }, + txId: signedTx["txId"], + }; + + return resolve(resp); + }); + } catch (err) { + logger.error(err); + return reject(err); + } + }); +} + +function getNewNonce(fromAddress: string): Promise<{ txnCountHex: string }> { + return new Promise(async (resolve, reject) => { + try { + logger.debug(`getNewNonce start: fromAddress: ${fromAddress}`); + + if (xConnectInfo === null) { + xConnectInfo = new LPInfoHolder(); + } + + if (xVerifierFactory === null) { + logger.debug("create verifier factory"); + xVerifierFactory = new VerifierFactory( + xConnectInfo.ledgerPluginInfo as VerifierFactoryConfig, + config.logLevel, + ); + } + + // Get the number of transactions in account + const contract = {}; // NOTE: Since contract does not need to be specified, specify an empty object. + const method = { type: "function", command: "getNonce" }; + const template = "default"; + const args = { args: { args: [fromAddress] } }; + + logger.debug(`##getNewNonce(A): call validator#getNonce()`); + xVerifierFactory + .getVerifier("84jUisrs") + .sendSyncRequest(contract, method, args) + .then((result) => { + // logger.debug(`##getNewNonce(A): result: ${JSON.stringify(result)}`); + + let txnCount: number = result.data.nonce; + let txnCountHex: string = result.data.nonceHex; + + const latestNonce = getLatestNonce(fromAddress); + // logger.debug(`##getNewNonce(B): fromAddress: ${fromAddress}, txnCount: ${txnCount}, latestNonce: ${latestNonce}`); + if (txnCount <= latestNonce) { + // nonce correction + txnCount = latestNonce + 1; + logger.debug( + `##getNewNonce(C): Adjust txnCount, fromAddress: ${fromAddress}, txnCount: ${txnCount}, latestNonce: ${latestNonce}`, + ); + + const method = { type: "function", command: "toHex" }; + const args = { args: { args: [txnCount] } }; + + logger.debug(`##getNewNonce(D): call validator#toHex()`); + xVerifierFactory + .getVerifier("84jUisrs") + .sendSyncRequest(contract, method, args) + .then((result) => { + txnCountHex = result.data.hexStr; + logger.debug(`##getNewNonce(E): txnCountHex: ${txnCountHex}`); + + // logger.debug(`##getNewNonce(F) _nonce: ${txnCount}, latestNonce: ${latestNonce}`); + setLatestNonce(fromAddress, txnCount); + + return resolve({ txnCountHex: txnCountHex }); + }); + } else { + // logger.debug(`##getNewNonce(F) _nonce: ${txnCount}, latestNonce: ${latestNonce}`); + setLatestNonce(fromAddress, txnCount); + + logger.debug(`##getNewNonce(G): txnCountHex: ${txnCountHex}`); + return resolve({ txnCountHex: txnCountHex }); + } + }); + } catch (err) { + logger.error(err); + return reject(err); + } + }); +} + +function getLatestNonce(fromAddress: string): number { + if (mapFromAddressNonce.has(fromAddress)) { + return mapFromAddressNonce.get(fromAddress); + } + //return 0; + return -1; +} + +function setLatestNonce(fromAddress: string, nonce: number): void { + mapFromAddressNonce.set(fromAddress, nonce); +} diff --git a/examples/cactus-example-tcs-huawei/balance.ts b/examples/cactus-example-tcs-huawei/balance.ts new file mode 100644 index 0000000000..79d87dfe66 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/balance.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * balance.ts + */ + +import { Router, NextFunction, Request, Response } from "express"; +import { ConfigUtil, RIFError } from "@hyperledger/cactus-cmd-socketio-server"; +import { BalanceManagement } from "./BalanceManagement"; + +const config: any = ConfigUtil.getConfig(); +import { getLogger } from "log4js"; +const moduleName = "balance"; +const logger = getLogger(`${moduleName}`); +logger.level = config.logLevel; + +const router: Router = Router(); +const balanceManagement: BalanceManagement = new BalanceManagement(); + +/* GET balance. */ +router.get("/:account", (req: Request, res: Response, next: NextFunction) => { + try { + balanceManagement + .getBalance(req.params.account) + .then((result) => { + logger.debug(`#####[sample/balance.ts]`); + logger.debug("result(getBalance) = " + JSON.stringify(result)); + res.status(200).json(result); + }) + .catch((err) => { + logger.error(err); + }); + } catch (err) { + logger.error(`##err name: ${err.constructor.name}`); + + if (err instanceof RIFError) { + logger.debug(`##catch RIFError, ${err.statusCode}, ${err.message}`); + res.status(err.statusCode); + res.send(err.message); + return; + } + + logger.error(`##err in balance: ${err}`); + next(err); + } +}); + +export default router; diff --git a/examples/cactus-example-tcs-huawei/config/contractInfo.yaml b/examples/cactus-example-tcs-huawei/config/contractInfo.yaml new file mode 100644 index 0000000000..5bcb340a4f --- /dev/null +++ b/examples/cactus-example-tcs-huawei/config/contractInfo.yaml @@ -0,0 +1 @@ +contractInfo: [] \ No newline at end of file diff --git a/examples/cactus-example-tcs-huawei/config/default.yaml b/examples/cactus-example-tcs-huawei/config/default.yaml new file mode 100644 index 0000000000..8b79c03caf --- /dev/null +++ b/examples/cactus-example-tcs-huawei/config/default.yaml @@ -0,0 +1,28 @@ +# Default cactus-cmd-socketio-server configuration +# Do not change this, update the settings in usersetting.yaml +# Check examples/cactus-example-discounted-asset-trade/config for working example + +# List of Bussiness Logic Plugin and it's validators +blpRegistry: [] + +# log4js log levels used by cactus-cmd-socketio-server and smaple BLPs +logLevel: info + +# Connection info +applicationHostInfo: + hostName: 0.0.0.0 + hostPort: 5034 + +# Socket.IO Manager options +socketOptions: + rejectUnauthorized: false + reconnection: false + timeout: 20000 + +# Verifier component configuration +verifier: + maxCounterRequestID: 100 + syncFunctionTimeoutMillisecond: 5000 + +# API route path and JS file to handle that route logic +appRouters: [] diff --git a/examples/cactus-example-tcs-huawei/config/usersetting.yaml b/examples/cactus-example-tcs-huawei/config/usersetting.yaml new file mode 100644 index 0000000000..730b4fd332 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/config/usersetting.yaml @@ -0,0 +1,30 @@ +# Overwrite defaults +blpRegistry: + - + businessLogicID: h40Q9eMD + validatorID: [sUr7dZly, 84jUisrs] + +logLevel: debug + +appRouters: + - + path: /api/v1/bl/electricity-trade/ + routerJs: /root/cactus/dist/electricity-trade.js + - + path: /api/v1/bl/balance/ + routerJs: /root/cactus/dist/balance.js + +# BLP Config +electricityTradeInfo: + tcsHuawei: + validatorID: sUr7dZly + filterKey: intkey + ethereum: + validatorID: 84jUisrs + gethURL: http://localhost:8545 + chainName: geth1 + networkID: 10 + chainID: 10 + network: mainnet + hardfork: petersburg + gas: 21000 diff --git a/examples/cactus-example-tcs-huawei/config/validator-registry-config.yaml b/examples/cactus-example-tcs-huawei/config/validator-registry-config.yaml new file mode 100644 index 0000000000..c939a4b6a0 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/config/validator-registry-config.yaml @@ -0,0 +1,62 @@ +ledgerPluginInfo: + - + validatorID: 84jUisrs + validatorType: legacy-socketio + validatorURL: https://ethereum-validator:5050 + validatorKeyPath: /etc/cactus/connector-go-ethereum-socketio/CA/connector.crt + maxCounterRequestID: 100 + syncFunctionTimeoutMillisecond: 5000 + socketOptions: + rejectUnauthorized: false + reconnection: false + timeout: 20000 + ledgerInfo: + ledgerAbstract: Go-Ethereum Ledger + apiInfo: + - + apiType: getNumericBalance + requestedData: + - + dataName: referedAddress + dataType: string + - + apiType: transferNumericAsset + requestedData: + - + dataName: fromAddress + dataType: string + - + dataName: toAddress + dataType: string + - + dataName: amount + dataType: number + - + apiType: sendRawTransaction + requestedData: + - + dataName: serializedTx + dataType: string + + - + validatorID: sUr7dZly + validatorType: legacy-socketio + validatorURL: https://tcs-validator:5140 + validatorKeyPath: /etc/cactus/connector-tcs-huawei/CA/connector.crt + maxCounterRequestID: 100 + syncFunctionTimeoutMillisecond: 5000 + socketOptions: + rejectUnauthorized: false + reconnection: false + timeout: 20000 + ledgerInfo: + ledgerAbstract: Sawtooth Ledger + apiInfo: [] + +signTxInfo: + ethereum: + chainName: geth1 + networkID: 10 + chainID: 10 + network: mainnet + hardfork: petersburg diff --git a/examples/cactus-example-tcs-huawei/docker-compose.debug.yml b/examples/cactus-example-tcs-huawei/docker-compose.debug.yml new file mode 100644 index 0000000000..bd8f1b63a9 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/docker-compose.debug.yml @@ -0,0 +1,9 @@ +version: "3.4" + +# Use: docker-compose -f docker-compose.yml -f docker-compose.debug.yml up + +services: + cactus-example-electricity-trade-blp: + ports: + - "9034:9034" + command: node --inspect=0.0.0.0:9034 ./dist/www.js diff --git a/examples/cactus-example-tcs-huawei/docker-compose.yml b/examples/cactus-example-tcs-huawei/docker-compose.yml new file mode 100644 index 0000000000..8414e7ff26 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/docker-compose.yml @@ -0,0 +1,69 @@ +version: "3.4" + +# Start the ledgers first +# ./etc/cactus should already exist and contain cactus node configs + +services: + ethereum-validator: + container_name: cactus-example-electricity-trade-ethereum-validator + image: cactus-plugin-ledger-connector-go-ethereum-socketio + build: + context: ../../packages/cactus-plugin-ledger-connector-go-ethereum-socketio/ + ports: + - "5050:5050" + networks: + - geth1net + - cactus-example-electricity-trade-net + volumes: + - type: bind + source: ./etc/cactus + target: /etc/cactus + + tcs-validator: + container_name: cactus-example-electricity-trade-tcs-validator + image: cactus-plugin-ledger-connector-tcs + build: + context: ../../packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/ + ports: + - "5140:5140" + networks: + - example_tcs_huawei_testnet_1x + - cactus-example-electricity-trade-net + volumes: + - type: bind + source: ./etc/cactus + target: /etc/cactus + + cmd-socketio-base-image: + # Build base image and immediately exit + container_name: cmd-socketio-base-dummy + image: cactus-cmd-socketio-server + build: + context: ../../packages/cactus-cmd-socketio-server/ + command: ["echo", "OK - Exit"] + + cactus-example-electricity-trade-blp: + container_name: cactus-example-electricity-trade-blp + image: cactus-example-electricity-trade-blp + build: + context: . + ports: + - "5034:5034" + networks: + - cactus-example-electricity-trade-net + depends_on: + - ethereum-validator + - tcs-validator + - cmd-socketio-base-image + volumes: + - type: bind + source: ./etc/cactus + target: /etc/cactus + +networks: + example_tcs_huawei_testnet_1x: + external: true + geth1net: + external: true + cactus-example-electricity-trade-net: + driver: bridge diff --git a/examples/cactus-example-tcs-huawei/electricity-trade.ts b/examples/cactus-example-tcs-huawei/electricity-trade.ts new file mode 100644 index 0000000000..5fc73c4703 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/electricity-trade.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * electricity-trade.ts + */ + +import { Router, NextFunction, Request, Response } from "express"; +import { + TransactionManagement, + RIFError, + ConfigUtil, +} from "@hyperledger/cactus-cmd-socketio-server"; +import escapeHtml from "escape-html"; + +const config: any = ConfigUtil.getConfig(); +import { getLogger } from "log4js"; +const moduleName = "trades"; +const logger = getLogger(`${moduleName}`); +logger.level = config.logLevel; + +const router: Router = Router(); +export const transactionManagement: TransactionManagement = + new TransactionManagement(); + +// Request Execution of Trade +router.post("/", (req: Request, res: Response, next: NextFunction) => { + try { + logger.warn("pre startBusinessLogic ") + const tradeID = transactionManagement.startBusinessLogic(req); + if (!tradeID) { + throw new RIFError(`Could not run BLP, tradeId = ${tradeID}`); + } + const result = { tradeID: tradeID }; + res.status(200).json(result); + } catch (err) { + if (err instanceof RIFError) { + res.status(err.statusCode); + res.send(escapeHtml(err.message)); + return; + } + + next(err); + } +}); + +// Request Execution of Trade +router.post( + "/meter/register/", + (req: Request, res: Response, next: NextFunction) => { + try { + const result = transactionManagement.setBusinessLogicConfig(req); + + if (!result) { + throw new RIFError("Error when running setBusinessLogicConfig"); + } + + let status = 200; + if (result["action"] === "add") { + status = 201; + } + res.status(status).json(result); + } catch (err) { + if (err instanceof RIFError) { + res.status(err.statusCode); + res.send(escapeHtml(err.message)); + return; + } + + next(err); + } + } +); + +export default router; diff --git a/examples/cactus-example-tcs-huawei/images/electricity-trade-image.png b/examples/cactus-example-tcs-huawei/images/electricity-trade-image.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa98815244c2ad020334b1f41e79b76a5786018 GIT binary patch literal 73935 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

q3_8-` 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 literal 0 HcmV?d00001 diff --git a/examples/cactus-example-tcs-huawei/package.json b/examples/cactus-example-tcs-huawei/package.json new file mode 100644 index 0000000000..1f119e97e1 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/package.json @@ -0,0 +1,46 @@ +{ + "name": "@hyperledger/cactus-example-tcs-huawei", + "version": "2.0.0-alpha.1", + "license": "Apache-2.0", + "main": "dist/www.js", + "module": "dist/www.js", + "types": "dist/www.d.ts", + "private": false, + "scripts": { + "start": "docker-compose build && docker-compose up", + "build": "npm run build-ts && npm run build:dev:backend:postbuild", + "build-ts": "tsc", + "build:dev:backend:postbuild": "cp -f ../../yarn.lock ./dist/" + }, + "dependencies": { + "@types/node": "14.18.12", + "body-parser": "1.19.2", + "cookie-parser": "1.4.6", + "debug": "2.6.9", + "escape-html": "1.0.3", + "ethereumjs-common": "1.5.2", + "ethereumjs-tx": "2.1.2", + "express": "4.16.4", + "fabric-ca-client": "2.2.10", + "fabric-network": "2.2.10", + "http-errors": "1.6.3", + "js-yaml": "3.14.1", + "jsonwebtoken": "8.5.1", + "log4js": "6.4.0", + "morgan": "1.9.1", + "shelljs": "0.8.5", + "socket.io": "4.4.1", + "ts-node": "8.9.1", + "web3": "1.7.0", + "xmlhttprequest": "1.8.0" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "4.33.0", + "@typescript-eslint/parser": "4.33.0", + "@types/escape-html": "1.0.1", + "eslint": "7.32.0", + "eslint-config-prettier": "8.4.0", + "eslint-plugin-prettier": "4.0.0", + "prettier": "2.5.1" + } +} diff --git a/examples/cactus-example-tcs-huawei/script-cleanup.sh b/examples/cactus-example-tcs-huawei/script-cleanup.sh new file mode 100644 index 0000000000..e8f02d166f --- /dev/null +++ b/examples/cactus-example-tcs-huawei/script-cleanup.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Copyright 2020-2022 Hyperledger Cactus Contributors +# SPDX-License-Identifier: Apache-2.0 + +ROOT_DIR="../.." # Path to cactus root dir + +echo ">> Remove the config files on your machine" +rm -rf ./etc/cactus/ + +# pushd ${ROOT_DIR}/tools/docker/tcs-huawei-testnet/example +# docker-compose down & +# popd + +echo ">> Stop and remove the docker containers" +docker rm -f geth1 \ + cactus-example-electricity-trade-blp \ + cactus-example-electricity-trade-ethereum-validator \ + cactus-example-electricity-trade-tcs-validator \ + cmd-socketio-base-dummy \ + agent + +echo ">> Remove docker networks" +docker network rm tcs_huawei_testnet_1x \ + cactus-example-tcs_default \ + cactus-example-tcs_cactus-example-electricity-trade-net \ + geth1net \ + geth-testnet_default + + +echo "Cleanup done." diff --git a/examples/cactus-example-tcs-huawei/script-gen-electricity-usage.sh b/examples/cactus-example-tcs-huawei/script-gen-electricity-usage.sh new file mode 100644 index 0000000000..c6427592a1 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/script-gen-electricity-usage.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Copyright 2020-2022 Hyperledger Cactus Contributors +# SPDX-License-Identifier: Apache-2.0 + +# echo "# Create intkey representing electricity usage" +# docker exec -t sawtooth_all_in_one_ledger_1x shell intkey set MI000001 50 --url http://rest-api:8008 + +# echo "# Increase usage" +# docker exec -t sawtooth_all_in_one_ledger_1x shell intkey inc MI000001 24 --url http://rest-api:8008 + +curl --location -k --request POST 'https://127.0.0.1:8001/v1/cross/transaction/query' \ +--header 'Content-Type: text/plain' --data '{ + "to_chain":"B01234567890123456789012345678901234567890123456789", + "from_chaincode_id":"ExcuteChainCode", + "to_chaincode_id":"ExcuteChainCode", + "to_query_func_name":"set", + "args":["MI000001", "50"] + }' --cert ../../packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/cert.pem --key ../../packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/key.pem + +curl --location -k --request POST 'https://127.0.0.1:8001/v1/cross/transaction/query' \ +--header 'Content-Type: text/plain' --data '{ + "to_chain":"B01234567890123456789012345678901234567890123456789", + "from_chaincode_id":"ExcuteChainCode", + "to_chaincode_id":"ExcuteChainCode", + "to_query_func_name":"inc", + "args":["MI000001", "24"] + }' --cert ../../packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/cert.pem --key ../../packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/key.pem \ No newline at end of file diff --git a/examples/cactus-example-tcs-huawei/script-get-app.sh b/examples/cactus-example-tcs-huawei/script-get-app.sh new file mode 100644 index 0000000000..f1873bed1d --- /dev/null +++ b/examples/cactus-example-tcs-huawei/script-get-app.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Copyright 2020-2022 Hyperledger Cactus Contributors +# SPDX-License-Identifier: Apache-2.0 + +echo "# Source Eth balance:" +curl localhost:5034/api/v1/bl/balance/06fc56347d91c6ad2dae0c3ba38eb12ab0d72e97 + +echo -e "\n\n# Destination Eth balance:" +curl localhost:5034/api/v1/bl/balance/9d624f7995e8bd70251f8265f2f9f2b49f169c55 + +echo -e "\n\n# Electricity usage" +# docker exec -t sawtooth_all_in_one_ledger_1x shell intkey list --url http://rest-api:8008 diff --git a/examples/cactus-example-tcs-huawei/script-post-setup-request.sh b/examples/cactus-example-tcs-huawei/script-post-setup-request.sh new file mode 100644 index 0000000000..7851c4be4b --- /dev/null +++ b/examples/cactus-example-tcs-huawei/script-post-setup-request.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Copyright 2020-2022 Hyperledger Cactus Contributors +# SPDX-License-Identifier: Apache-2.0 + +echo "Register electricity-trade account information" +curl localhost:5034/api/v1/bl/electricity-trade/meter/register/ -XPOST \ + -H "Content-Type: application/json" \ + -d '{"businessLogicID":"h40Q9eMD", + "meterParams":[ + "MI000001", + "06fc56347d91c6ad2dae0c3ba38eb12ab0d72e97", + "cb5d48d371916a4ea1627189d8af4f642a5d72746a06b559780c3f5932658207", + "9d624f7995e8bd70251f8265f2f9f2b49f169c55" + ] + }' diff --git a/examples/cactus-example-tcs-huawei/script-post-start-request.sh b/examples/cactus-example-tcs-huawei/script-post-start-request.sh new file mode 100644 index 0000000000..99c084df76 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/script-post-start-request.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Copyright 2020-2022 Hyperledger Cactus Contributors +# SPDX-License-Identifier: Apache-2.0 + +echo "Start electricity-trade" +curl localhost:5034/api/v1/bl/electricity-trade/ -XPOST -H "Content-Type: application/json" -d '{"businessLogicID":"h40Q9eMD"}' diff --git a/examples/cactus-example-tcs-huawei/script-start-ledgers.sh b/examples/cactus-example-tcs-huawei/script-start-ledgers.sh new file mode 100644 index 0000000000..e9fb702ae2 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/script-start-ledgers.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Copyright 2020-2022 Hyperledger Cactus Contributors +# SPDX-License-Identifier: Apache-2.0 + +set -e + +ROOT_DIR="../.." # Path to cactus root dir +CONFIG_VOLUME_PATH="./etc/cactus" # Docker volume with shared configuration +WAIT_TIME=10 # How often to check container status + +# Cert options +CERT_CURVE_NAME="prime256v1" +CERT_COUNTRY="JP" +CERT_STATE="Tokyo" +CERT_LOCALITY="Minato-Ku" +CERT_ORG="CactusSamples" + +# generate_certificate +function generate_certificate() { + # Check OpenSSL command existance + if ! openssl version > /dev/null; then + echo "Could not execute [openssl version], check if OpenSSL tool is available on the system." + exit 1; + fi + + # Check input parameters + ARGS_NUMBER=2 + if [ "$#" -lt "$ARGS_NUMBER" ]; then + echo "generate_certificate called with wrong number of arguments (expected - $ARGS_NUMBER, actual - $#)"; + exit 2 + fi + + common_name=$1 + destination=$2 + subject="/C=$CERT_COUNTRY/ST=$CERT_STATE/L=$CERT_LOCALITY/O=$CERT_ORG/CN=$common_name" + echo "Create new cert in '${destination}' with subject '${subject}'" + + # Crete destination path + if [ ! -d "$destination" ]; then + echo "Re-create destination dir..." + rm -rf "$destination" + mkdir -p "$destination" + fi + + keyPath="${destination}/connector.priv" + csrPath="${destination}/connector.csr" + certPath="${destination}/connector.crt" + + # Generate keys + openssl ecparam -genkey -name "$CERT_CURVE_NAME" -out "$keyPath" + openssl req -new -sha256 -key "$keyPath" -out "$csrPath" -subj "$subject" + openssl req -x509 -sha256 -days 365 -key "$keyPath" -in "$csrPath" -out "$certPath" +} + +function start_ethereum_testnet() { + pushd "${ROOT_DIR}/tools/docker/geth-testnet" + ./script-start-docker.sh + popd +} + +function copy_ethereum_validator_config() { + echo ">> copy_ethereum_validator_config()" + cp -fr ${ROOT_DIR}/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/sample-config/* \ + "${CONFIG_VOLUME_PATH}/connector-go-ethereum-socketio/" + generate_certificate "GoEthereumCactusValidator" "${CONFIG_VOLUME_PATH}/connector-go-ethereum-socketio/CA/" + echo ">> copy_ethereum_validator_config() done." +} + +function copy_tcs_config() { + cp -fr ${ROOT_DIR}/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/* \ + "${CONFIG_VOLUME_PATH}/connector-tcs-huawei/" + generate_certificate "TcsCactusValidator" "${CONFIG_VOLUME_PATH}/connector-tcs-huawei/CA/" +} + +function start_tcs_agent(){ + echo ">> start_tcs_agent()" + docker network create tcs_huawei_testnet_1x + pushd ${ROOT_DIR}/tools/docker/tcs-huawei-testnet/agent/example + docker-compose up > tcs-agent.log & + popd + echo ">> start_tcs_agent() done" +} + +function start_ledgers() { + prepare_certificate + + # Clear ./etc/cactus + mkdir -p "${CONFIG_VOLUME_PATH}/" + rm -fr ${CONFIG_VOLUME_PATH}/* + + # Copy cmd-socketio-config + cp -f ./config/*.yaml "${CONFIG_VOLUME_PATH}/" + + # Start Ethereum + mkdir -p "${CONFIG_VOLUME_PATH}/connector-go-ethereum-socketio" + start_ethereum_testnet + copy_ethereum_validator_config + + # Start tcs-agent + mkdir -p "${CONFIG_VOLUME_PATH}/connector-tcs-huawei" + start_tcs_agent + copy_tcs_config + +} + +function prepare_certificate(){ + mkdir Server_Cert + pushd ./Server_Cert + openssl genrsa -out cakey.pem 2048 + openssl req -new -key cakey.pem -out ca.csr -subj "/C=CN/ST=myprovince/L=mycity/O=myorganization/OU=mygroup/CN=myCA" + openssl x509 -req -days 365 -sha1 -extensions v3_ca -signkey cakey.pem -in ca.csr -out cacert.pem + + openssl genrsa -out key.pem 2048 + openssl req -new -key key.pem -out server.csr -subj "/C=CN/ST=myprovince/L=mycity/O=myorganization/OU=mygroup/CN=myServer" + openssl x509 -req -days 365 -sha1 -extensions v3_req -CA ./cacert.pem -CAkey ./cakey.pem -CAserial ca.srl -CAcreateserial -in server.csr -out cert.pem + + openssl genrsa -out clikey.pem 2048 + openssl req -new -key clikey.pem -out client.csr -subj "/C=CN/ST=myprovince/L=mycity/O=myorganization/OU=mygroup/CN=myClient" + openssl x509 -req -days 365 -sha1 -extensions v3_req -CA ./cacert.pem -CAkey ./cakey.pem -CAserial ./ca.srl -in client.csr -out clicert.pem + + openssl x509 -in clicert.pem -out client.crt + openssl rsa -in clikey.pem -out client.key + popd + cp ./Server_Cert/cert.pem ./Server_Cert/key.pem ${ROOT_DIR}/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config + mv ./Server_Cert ${ROOT_DIR}/tools/docker/tcs-huawei-testnet/agent/example +} + +start_ledgers + +echo "All Done." diff --git a/examples/cactus-example-tcs-huawei/tools/periodicExecuter/package.json b/examples/cactus-example-tcs-huawei/tools/periodicExecuter/package.json new file mode 100644 index 0000000000..12f02997ee --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tools/periodicExecuter/package.json @@ -0,0 +1,17 @@ +{ + "name": "periodicExecuter", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "npm run build-ts", + "build-ts": "tsc", + "tslint": "tslint -c tslint.json -p tsconfig.json" + }, + "dependencies": { + "@types/node": "14.18.12", + "ts-node": "9.1.1" + }, + "devDependencies": { + "typescript": "3.9.10" + } +} diff --git a/examples/cactus-example-tcs-huawei/tools/periodicExecuter/periodicExecuter.ts b/examples/cactus-example-tcs-huawei/tools/periodicExecuter/periodicExecuter.ts new file mode 100644 index 0000000000..36b33374af --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tools/periodicExecuter/periodicExecuter.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * periodicExecuter.ts + */ + +const { execSync } = require("child_process"); + +console.log("## start periodicExecuter"); + +if (process.argv.length !== 5 && process.argv.length !== 6) { + console.log("Number of parameters is abnormal."); + process.exit(-1); +} + +const interval = parseInt(process.argv[2]); +const keyString = process.argv[3]; +const valueAdd = process.argv[4]; +const dockerExecString = "docker exec sawtooth-shell-default "; +const submitCommand = + "sawtooth batch submit -f batches.intkey --url http://rest-api:8008"; +const incCommand = + "intkey create_batch --key-name " + + keyString + + " --value-inc-rand " + + valueAdd; + +if (process.argv.length === 6) { + const valueInitial = process.argv[5]; + + const setCommand = + "intkey create_batch --key-name " + + keyString + + " --value-set " + + valueInitial; + console.log(`setCommand : ${setCommand}`); + const stdoutSet = execSync(dockerExecString + setCommand); + console.log(`setCommand stdout: ${stdoutSet.toString()}`); + + console.log(`submitCommand(set) : ${submitCommand}`); + const stdoutSetSubmit = execSync(dockerExecString + submitCommand); + console.log(`submitCommand(set) stdout: ${stdoutSetSubmit.toString()}`); +} + +const timerIdArowDown = setInterval(function () { + console.log(`incCommand : ${incCommand}`); + const stdoutInc = execSync(dockerExecString + incCommand); + console.log(`incCommand stdout: ${stdoutInc.toString()}`); + + console.log(`submitCommand(inc) : ${submitCommand}`); + const stdoutIncSubmit = execSync(dockerExecString + submitCommand); + console.log(`submitCommand(inc) stdout: ${stdoutIncSubmit.toString()}`); + + console.log(`##${getCurrentTime()} execute intkey`); +}, interval * 1000); + +function getCurrentTime(): string { + const now = new Date(); + return ( + "[" + + now.getFullYear() + + "/" + + ("0" + (now.getMonth() + 1)).slice(-2) + + "/" + + ("0" + now.getDate()).slice(-2) + + " " + + ("0" + now.getHours()).slice(-2) + + ":" + + ("0" + now.getMinutes()).slice(-2) + + ":" + + ("0" + now.getSeconds()).slice(-2) + + "]" + ); +} diff --git a/examples/cactus-example-tcs-huawei/tools/periodicExecuter/tsconfig.json b/examples/cactus-example-tcs-huawei/tools/periodicExecuter/tsconfig.json new file mode 100644 index 0000000000..a19738369b --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tools/periodicExecuter/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist", + "sourceMap": false, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "ES2017", + "module": "CommonJS", + "typeRoots": [ + "node_modules/@types", + "./typings" + ], + "lib": [ + "es2017", + "dom" + ] + }, + "include": [ + "./*.ts" + ] +} diff --git a/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/config/default.js b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/config/default.js new file mode 100644 index 0000000000..2827d3391c --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/config/default.js @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * default.js + */ + +module.exports = { + // URL to validator + validatorUrl: "https://localhost:5050", + gethUrl: "http://localhost:8545", + + // forCustomChain + chainName: "geth1", + networkId: 10, + chainId: 10, +}; diff --git a/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/copyStaticAssets.ts b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/copyStaticAssets.ts new file mode 100644 index 0000000000..8aac76aa55 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/copyStaticAssets.ts @@ -0,0 +1,8 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * copyStaticAssets.ts + */ +import * as shell from "shelljs"; +shell.cp("config/default.js", "./dist/config"); diff --git a/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/package.json b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/package.json new file mode 100644 index 0000000000..e9b483161c --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/package.json @@ -0,0 +1,23 @@ +{ + "name": "transferNumericAsset", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "npm run build-ts && npm run copy-static-assets", + "build-ts": "tsc", + "tslint": "tslint -c tslint.json -p tsconfig.json", + "copy-static-assets": "ts-node copyStaticAssets.ts" + }, + "dependencies": { + "@types/node": "14.18.12", + "config": "1.31.0", + "ethereumjs-common": "1.5.2", + "ethereumjs-tx": "2.1.2", + "ts-node": "9.1.1", + "web3": "1.7.0", + "socket.io": "4.4.1" + }, + "devDependencies": { + "typescript": "3.9.10" + } +} \ No newline at end of file diff --git a/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/transferNumericAsset.ts b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/transferNumericAsset.ts new file mode 100644 index 0000000000..9af42138f2 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/transferNumericAsset.ts @@ -0,0 +1,121 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * transferNumericAsset.ts + */ + +import { io } from "socket.io-client"; + +{ + // Validator test program.(socket.io client) + const config = require("config"); + + // Specify the server (Validator) of the communication destination + const validatorUrl = config.validatorUrl; + console.log("validatorUrl: " + validatorUrl); + const options = { + rejectUnauthorized: false, // temporary avoidance since self-signed certificates are used + reconnection: false, + timeout: 20000, + }; + const socket = io(validatorUrl, options); + + // ## Request for "transferNumericAsset" + if (process.argv.length !== 5) { + console.log("Number of parameters is abnormal."); + process.exit(-1); + } + const paramAmount: number = parseInt(process.argv[4]); + if (Number.isNaN(paramAmount)) { + console.log(`Third parameter is not numeric. ${process.argv[4]}`); + process.exit(-1); + } + + console.log( + `execution parameter : fromAddress = ${process.argv[2]}, toAddress = ${process.argv[3]}, amount = ${process.argv[4]}`, + ); + + const fromAddress = process.argv[2]; + const toAddress = process.argv[3]; + const amount = paramAmount; + const reqID = "reqID_TNA_001"; + + // function param + const requestData = { + contract: {}, + method: { + type: "web3Eth", + command: "sendTransaction", + }, + args: { + args: [ + { + from: fromAddress, + to: toAddress, + value: amount, + }, + ], + }, + reqID: reqID, + }; + + const json2str = (jsonObj) => { + try { + return JSON.stringify(jsonObj); + } catch (error) { + return null; + } + }; + + socket.on("connect_error", (err) => { + console.log("####connect_error:", err); + // end communication + socket.disconnect(); + process.exit(0); + }); + + socket.on("connect_timeout", (err) => { + console.log("####Error:", err); + // end communication + socket.disconnect(); + process.exit(0); + }); + + socket.on("error", (err) => { + console.log("####Error:", err); + }); + + socket.on("eventReceived", function (res) { + // output the data received from the client + console.log("#[recv]eventReceived, res: " + json2str(res)); + }); + + const requestStopMonitor = () => { + console.log("##exec requestStopMonitor()"); + socket.emit("stopMonitor"); + + setTimeout(function () { + // end communication + socket.disconnect(); + process.exit(0); + }, 5000); + }; + + // request StartMonitor + const requestStartMonitor = () => { + console.log("##exec requestStartMonitor()"); + socket.emit("startMonitor"); + + setTimeout(requestStopMonitor, 15000); + }; + + const sendRequest = () => { + console.log("exec sendRequest()"); + console.log("#[send]requestData: " + json2str(requestData)); + socket.emit("request2", requestData); + }; + + setTimeout(requestStartMonitor, 2000); + setTimeout(sendRequest, 4000); +} diff --git a/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/tsconfig.json b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/tsconfig.json new file mode 100644 index 0000000000..b0f7b49357 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tools/transferNumericAsset/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist", + "sourceMap": false, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "ES2017", + "module": "CommonJS", + "typeRoots": [ + "node_modules/@types", + "./typings" + ], + "lib": [ + "es2017", + "dom" + ] + }, + "include": [ + "./*.ts" + ], + "exclude": [ + "copyStaticAssets.ts" + ] +} diff --git a/examples/cactus-example-tcs-huawei/tsconfig.json b/examples/cactus-example-tcs-huawei/tsconfig.json new file mode 100644 index 0000000000..bc78419774 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist/", + "tsBuildInfoFile": "../../.build-cache/cactus-example-electricity-trade.tsbuildinfo", + "strict": false // TODO - true, fix warnings + }, + "include": [ + "./*.ts", + ], + "references": [ + { + "path": "../../packages/cactus-cmd-socketio-server/tsconfig.json" + }, + { + "path": "../../packages/cactus-verifier-client/tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/examples/cactus-example-tcs-huawei/tslint.json b/examples/cactus-example-tcs-huawei/tslint.json new file mode 100644 index 0000000000..9a00b4aa19 --- /dev/null +++ b/examples/cactus-example-tcs-huawei/tslint.json @@ -0,0 +1,28 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "variable-name": { + "options": [ + "allow-leading-underscore", + "ban-keywords", + "check-format" + ] + }, + "no-string-literal": false, + "no-var-requires": false, + "object-literal-shorthand": false, + "max-classes-per-file": false, + "no-console": false + }, + "rulesDirectory": [], + "linterOptions": { + "exclude": [ + "packages/cactus-sdk/src/main/typescript/generated/**/*.ts", + "packages/cactus-sdk/src/main/typescript/public-api.ts" + ] + } +} diff --git a/examples/cactus-example-tcs-huawei/www.ts b/examples/cactus-example-tcs-huawei/www.ts new file mode 100644 index 0000000000..3c3295767b --- /dev/null +++ b/examples/cactus-example-tcs-huawei/www.ts @@ -0,0 +1,7 @@ +import { BusinessLogicElectricityTrade } from "./BusinessLogicTcsElectricityTrade"; +import { startCactusSocketIOServer } from "@hyperledger/cactus-cmd-socketio-server"; + +startCactusSocketIOServer({ + id: "h40Q9eMD", + plugin: new BusinessLogicElectricityTrade("h40Q9eMD"), +}); diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/.gitignore b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/.gitignore new file mode 100644 index 0000000000..849ddff3b7 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/Dockerfile b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/Dockerfile new file mode 100644 index 0000000000..97a0951d2e --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/Dockerfile @@ -0,0 +1,13 @@ +# TODO +# Install connector as yarn package like in @hyperledger/cactus-plugin-ledger-connector-besu + +FROM node:16 + +WORKDIR /root/cactus/ + +COPY ./dist ./dist/ +COPY ./dist/yarn.lock ./package.json ./ +RUN yarn install --production --frozen-lockfile --non-interactive --cache-folder ./.yarnCache; rm -rf ./.yarnCache + +EXPOSE 5140 +CMD [ "npm", "run", "start" ] diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/README.md b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/README.md new file mode 100644 index 0000000000..d9cb61167b --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/README.md @@ -0,0 +1,77 @@ + +# `@hyperledger/cactus-plugin-ledger-connector-tcs-huawei-socketio` + +This plugin provides `Cactus` a way to interact with tcs-huawei networks.tcs-huawei (Trusted Cross-chain Service-Huawei) is a blockchain integration service on Huawei Cloud. Using this we can perform: +- `startMonitor`: Start monitoring blocks on the ledgers +- `stopMonitor`: Stop the block monitoring + +## Summary +- [Getting started](#getting-started) +- [Usage samples](#usage-samples) +- [Contributing](#contributing) +- [License](#license) +- [Acknowledgments](#acknowledgments) + +## Getting started + +### Required software components +- OS: Linux (recommended Ubuntu20.04,18.04 or CentOS7) +- Docker (recommend: v17.06.2-ce or greater) +- node.js v16 (recommend: v16 or greater) + +### Prerequisites +- Please ensure that the destination ledger (default: [tcs-huawei](../../tools/docker/tcs-huawei-testnet)) is already launched. + +## Boot methods + +### Common setup +1. Always run configure command first, from the project root directory: + ``` + pushd ../.. + npm run configure + popd + ``` + +1. Copy default configuration +- **Remember to replace default CA and to adjust the `default.yaml` configuration on production deployments!** + ``` + mkdir -p /etc/cactus/connector-tcs-huawei-socketio/ + rm -r /etc/cactus/connector-tcs-huawei-socketio/* + cp -rf ./sample-config/* /etc/cactus/connector-tcs-huawei-socketio/ + ``` + +### Docker +``` +# Build +docker build . -t cactus-plugin-ledger-connector-tcs-huawei-socketio + + +``` + +### Manual +- Ensure ledger ports are exposed to the host first. + +``` +npm run start +``` + +## Usage samples +- To confirm the operation of this package, please refer to the following business-logic sample application: + - [cactus-example-electricity-trade](../../examples/cactus-example-electricity-trade) + +## Contributing + +We welcome contributions to Hyperledger Cactus in many forms, and there's always plenty to do! + +Please review [CONTIRBUTING.md](../../CONTRIBUTING.md) to get started. + +## License + +This distribution is published under the Apache License Version 2.0 found in the [LICENSE](../../LICENSE) file. + +## Acknowledgments diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/package.json b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/package.json new file mode 100644 index 0000000000..981c375208 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/package.json @@ -0,0 +1,33 @@ +{ + "name": "@hyperledger/cactus-plugin-ledger-connector-tcs-huawei-socketio", + "version": "2.0.0-alpha.1", + "license": "Apache-2.0", + "scripts": { + "start": "cd ./dist && node common/core/bin/www.js", + "debug": "nodemon --inspect ./dist/common/core/bin/www.js", + "build": "npm run build-ts && npm run build:dev:backend:postbuild", + "build-ts": "tsc", + "build:dev:backend:postbuild": "npm run init", + "init": "cp -af ../../yarn.lock ./dist/yarn.lock" + }, + "dependencies": { + "@types/node": "14.17.32", + "body-parser": "1.17.2", + "cbor": "6.0.1", + "config": "3.3.7", + "cookie-parser": "1.4.6", + "debug": "4.1.1", + "express": "4.17.3", + "js-yaml": "3.14.1", + "jsonwebtoken": "8.5.1", + "log4js": "6.4.1", + "morgan": "1.10.0", + "serve-favicon": "2.4.5", + "shelljs": "0.8.5", + "socket.io": "4.4.1", + "xmlhttprequest": "1.8.0" + }, + "devDependencies": { + "@types/config": "0.0.41" + } +} diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/default.yaml b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/default.yaml new file mode 100644 index 0000000000..6d63362cde --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/sample-config/default.yaml @@ -0,0 +1,21 @@ +sslParam: + port: 5140 + key: "/etc/cactus/connector-tcs-huawei/CA/connector.priv" + cert: "/etc/cactus/connector-tcs-huawei/CA/connector.crt" + clientCert: "/etc/cactus/connector-tcs-huawei/cert.pem" + clientKey: "/etc/cactus/connector-tcs-huawei/key.pem" +blockMonitor: + request: + method: "GET" + host: "https://127.0.0.1:30081/" + getChannelListCommand: "v1/blockcross/channel/list" + getLatestBlockNumberCommand: "blocks?limit=1" + periodicMonitoringCommand1: "blocks?start=" + periodicMonitoringCommand2: "&reverse" + pollingInterval: 5000 +testURL: + request: + method: "GET" + host: "https://relay1/v1/blockcross/channel/list" +logLevel: "debug" +CertCheck: diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/app.ts b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/app.ts new file mode 100644 index 0000000000..da68334782 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/app.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * app.js + */ + +/* Summary: + * + */ + +import { NextFunction, Request, Response } from "express"; +import createError = require("http-errors"); +import express = require("express"); +import cookieParser = require("cookie-parser"); +import bodyParser = require("body-parser"); + +const app: express.Express = express(); + +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); + +// catch 404 and forward to error handler +app.use((req: Request, res: Response, next: NextFunction) => { + next(createError(404)); +}); + +// error handler +app.use( + ( + err: { message: string; status?: number }, + req: Request, + res: Response, + next: NextFunction, + ) => { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get("env") === "development" ? err : {}; + + // set erreor response + const errorResponse: {} = { + statusCode: err.status || 500, + message: err.message, + }; + + // render the error page + res.status(err.status || 500); + res.send(errorResponse); + }, +); + +export default app; diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/bin/www.ts b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/bin/www.ts new file mode 100644 index 0000000000..c4ad1b794e --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/bin/www.ts @@ -0,0 +1,144 @@ +#!/usr/bin/env node +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * www.js + */ + +/* Summary: + * Connector: a part independent of end-chains + */ + +/** + * Module dependencies. + */ + + import app from "../app"; + const debug = require("debug")("connector:server"); + import https = require("https"); + import * as config from "../config"; + import fs = require("fs"); + import { Server } from "socket.io" + + // Log settings + import { getLogger } from "log4js"; + const logger = getLogger("connector_main[" + process.pid + "]"); + logger.level = config.read('logLevel', 'info'); + + // destination dependency (MONITOR) implementation class + import { ServerMonitorPlugin } from "../../../connector/ServerMonitorPlugin"; + const Smonitor = new ServerMonitorPlugin(); + + /** + * Get port from environment and store in Express. + */ + + const sslport = normalizePort(process.env.PORT || config.read('sslParam.port')); + app.set("port", sslport); + + // Specify private key and certificate + const sslParam = { + key: fs.readFileSync(config.read('sslParam.key')), + cert: fs.readFileSync(config.read('sslParam.cert')), + }; + + /** + * Create HTTPS server. + */ + + const server = https.createServer(sslParam, app); // Start as an https server. + const io = new Server(server); + + /** + * Listen on provided port, on all network interfaces. + */ + + server.listen(sslport, function () { + console.log("listening on *:" + sslport); + }); + server.on("error", onError); + server.on("listening", onListening); + + /** + * Normalize a port into a number, string, or false. + */ + + function normalizePort(val: string) { + const port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; + } + + /** + * Event listener for HTTPS server "error" event. + */ + + function onError(error: any) { + if (error.syscall !== "listen") { + throw error; + } + + const bind = + typeof sslport === "string" ? "Pipe " + sslport : "Port " + sslport; + + // handle specific listen errors with friendly messages + switch (error.code) { + case "EACCES": + console.error(bind + " requires elevated privileges"); + process.exit(1); + break; + case "EADDRINUSE": + console.error(bind + " is already in use"); + process.exit(1); + break; + default: + throw error; + } + } + + /** + * Event listener for HTTPS server "listening" event. + */ + + function onListening() { + const addr = server.address(); + + if (!addr) { + logger.error("Could not get running server address - exit."); + process.exit(1); + } + + const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; + debug("Listening on " + bind); + } + + io.on("connection", function (client) { + logger.info("Client " + client.id + " connected."); + + /** + * startMonitor: starting block generation event monitoring + **/ + client.on("startMonitor", function (data) { + Smonitor.startMonitor(client.id, data.filterKey, (callbackData) => { + let emitType = ""; + if (callbackData.status == 200) { + emitType = "eventReceived"; + logger.info("event data callbacked."); + } else { + emitType = "monitor_error"; + } + client.emit(emitType, callbackData); + }); + }); + }); \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/config.ts b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/config.ts new file mode 100644 index 0000000000..327e7efb4f --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/common/core/config.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * config.js + */ + +// TODO - Common + +export const DEFAULT_NODE_CONFIG_DIR = "/etc/cactus/connector-tcs-huawei/"; +if (!process.env["NODE_CONFIG_DIR"]) { + // Must be set before import config + process.env["NODE_CONFIG_DIR"] = DEFAULT_NODE_CONFIG_DIR; +} + +import config from "config"; + +/** + * Get configuration entry (uses node-config setup) + * + * @param key : Key to retrieve + * @param defaultValue : Value to return if key is not present in the config. + * @returns : Configuration value + */ +export function read(key: string, defaultValue?: T): T { + if (config.has(key)) { + return config.get(key); + } + + if (defaultValue) { + return defaultValue; + } + + throw Error( + `Missing configuration entry '${key}', config dir = ${process.env["NODE_CONFIG_DIR"]}`, + ); +} diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/PluginUtil.ts b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/PluginUtil.ts new file mode 100644 index 0000000000..d3aaddd292 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/PluginUtil.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * PluginUtil.js + */ + +const cbor = require("cbor"); + +/* + * Summary: + * Cooperation server: utility library dependent on endchains + * For example, implementing internal functions that should not be exposed as functions of server plugins. + */ + +/* + * convNum + * + * @param {String/Number} value: The scientific string or numeric value to be converted. For numbers, it is returned as is. + * @param {String/Number} defaultValue: The value to use if conversion was not possible. Text or number in scientific notation. Optional. + * + * @return {Number} The value converted to a number, or the defaultValue if the conversion failed. + * + * @desc exponentiation is also supported. The following formats are supported:. (value, defaultValue) + * 3.78*10^14 + * 3.78e14 + */ +exports.convNum = function convNum( + value: number | string, + defaultValue: number | string, +) { + let retValue = 0; + let defValue = 0; + + switch (typeof defaultValue) { + case "number": + defValue = defaultValue; + break; + case "string": + const defValueStr = defaultValue.replace(/\*10\^/g, "e"); + defValue = parseFloat(defValueStr); + break; + default: + // undefined should also be included here. + // Fixed value because of cumbersome handling. + defValue = 0; + break; + } // switch(typeof(defaultValue)) + + if (defValue == NaN) { + // number is guaranteed. + defValue = 0; + } + + switch (typeof value) { + case "number": + retValue = value; + break; + case "string": + const valueStr = value.replace(/\*10\^/g, "e"); + retValue = parseFloat(valueStr); + break; + default: + // Set default value. + retValue = defValue; + break; + } // switch(typeof(value)) + + if (retValue == NaN) { + // number is guaranteed. + retValue = defValue; + } + + return retValue; +}; + +exports.convertBlockNumber = function (value: number) { + return "0x" + ("0000000000000000" + value.toString(16)).substr(-16); +}; + +exports.decodeBase64 = function (encoded: string): Buffer { + return Buffer.from(encoded, "base64"); +}; + +exports.deccodeCbor = function (encoded: any): Array { + return cbor.decodeAllSync(encoded); +}; diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/ServerMonitorPlugin.ts b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/ServerMonitorPlugin.ts new file mode 100644 index 0000000000..5622b93bf8 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/ServerMonitorPlugin.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * ServerMonitorPlugin.js + */ + +/* + * Summary: + * Connector: monitoring class of a part dependent on end-chains + * Used in the case of monitoring continuously. + * Processing that ends with the acceptance of a single event is handled by a custom function implementation in server plugins. + * Unlike server plugins, it basically does not handle its own functions. + */ + +// configuration file +const SplugUtil = require("./PluginUtil"); +import * as config from "../common/core/config"; +// Log settings +import { getLogger } from "log4js"; +const logger = getLogger("ServerMonitorPlugin[" + process.pid + "]"); +logger.level = config.read("logLevel", "info"); +// utility +import { ValidatorAuthentication } from "./ValidatorAuthentication"; +const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; + +import https from "https"; +import fs = require("fs"); + +export type MonitorCallback = (callback: { + status: number; + blockData: string; +}) => void; + +/* + * ServerMonitorPlugin + * Class definitions of server monitoring + */ +export class ServerMonitorPlugin { + currentBlockNumber: number; + + /* + * constructors + */ + constructor() { + // Define dependent specific settings + this.currentBlockNumber = -1; + } + + /* + * startMonitor + * Start Monitoring + * @param {string} clientId: Client ID from which monitoring start request was made + * @param {string} filterKey: Key to filter blocks + * @param {function} cb: A callback function that receives monitoring results at any time. + */ + startMonitor(clientId: string, filterKey: string, cb: MonitorCallback) { + const request_blocknum_body = JSON.stringify({ + to_chain: "B0123456789012345678901234567890123456789", + from_chaincode_id: "QueryChainCode", + to_chaincode_id: "QueryChainCode", + to_query_func_name: "QueryBlockNum", + args: ["a"], + }); + //- (not recommended - only for development): Temporarily set CertCheck to false when using a self-signed certificate + var CertCheck = config.read("CertCheck") + if (CertCheck == undefined){ + CertCheck = true + } + const options: https.RequestOptions = { + hostname: "agent", + port: 8080, + path: "/v1/cross/transaction/query", + method: "POST", + headers: { + "Content-Type": "text/plain", + "Content-Length": Buffer.byteLength(request_blocknum_body), + }, + cert: fs.readFileSync(config.read("sslParam.clientCert")), + key: fs.readFileSync(config.read("sslParam.clientKey")), + rejectUnauthorized: CertCheck, + }; + + let resData; + let blockNum; + const that = this; + const req = https.request(options, (res) => { + res.on("data", (d) => { + resData = JSON.parse(d); + that.currentBlockNumber = resData.data.payload; + setInterval(function () { + that.periodicMonitoring(clientId, filterKey, cb); + }, 2000); + }); + }); + + req.write(request_blocknum_body); + + req.on("error", (error) => { + logger.error(error); + }); + req.end(); + + logger.info("*** START MONITOR ***"); + logger.info("Client ID :" + clientId); + logger.debug(`filterKey = ${filterKey}`); + } + + /* + * periodicMonitoring + * Periodic monitoring + * @param {string} clientId: Client ID from which monitoring start request was made + * @param {string} filterKey: Key to filter blocks + * @param {function} cb: A callback function that receives monitoring results at any time. + */ + periodicMonitoring(clientId: string, filterKey: string, cb: MonitorCallback) { + let targetBlockNum: number = this.currentBlockNumber; + targetBlockNum++; + const request_block_body = JSON.stringify({ + to_chain: "B0123456789012345678901234567890123456789", + from_chaincode_id: "QueryChainCode", + to_chaincode_id: "QueryChainCode", + to_query_func_name: "GetBlockByNumber", + args: [targetBlockNum.toString()], + }); + var CertCheck = config.read("CertCheck") + if (CertCheck == undefined){ + CertCheck = true + } + const options: https.RequestOptions = { + hostname: "agent", + port: 8080, + path: "/v1/cross/transaction/query", + method: "POST", + headers: { + "Content-Type": "text/plain", + "Content-Length": Buffer.byteLength(request_block_body), + }, + cert: fs.readFileSync(config.read("sslParam.clientCert")), + key: fs.readFileSync(config.read("sslParam.clientKey")), + rejectUnauthorized: CertCheck, + }; + + let resData; + const transactionDataArray: any[] = []; + const that = this; + const req = https.request(options, (res) => { + res.on("data", (d) => { + resData = JSON.parse(d); + if (resData.error_message == "block not found") { + logger.debug("block not found, continue to poll"); + return; + } + const payload = JSON.parse(resData.data.payload); + if (payload.Header.BlockNum != targetBlockNum) { + logger.error( + "expected ", + targetBlockNum, + " get ", + payload.Header.BlockNum, + ); + return; + } + for (const transaction of payload.Data) { + transactionDataArray.push(transaction); + } + if (this.currentBlockNumber < targetBlockNum) { + this.currentBlockNumber = targetBlockNum; + } + if (transactionDataArray.length > 0) { + logger.info("*** SEND TRANSACTION DATA ***"); + logger.debug( + `transactionDataArray = ${JSON.stringify(transactionDataArray)}`, + ); + const signedTransactionDataArray = ValidatorAuthentication.sign({ + blockData: transactionDataArray, + }); + logger.debug( + `signedTransactionDataArray = ${signedTransactionDataArray}`, + ); + const retObj = { + status: 200, + blockData: signedTransactionDataArray, + }; + cb(retObj); + } + this.currentBlockNumber + 1; + }); + }); + + req.write(request_block_body); + + req.on("error", (error) => { + logger.error(error); + }); + req.end(); + } +} diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/ValidatorAuthentication.ts b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/ValidatorAuthentication.ts new file mode 100644 index 0000000000..4ab02e4521 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/src/main/typescript/connector/ValidatorAuthentication.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * ValidatorAuthentication.ts + */ + +const fs = require("fs"); +const path = require("path"); +const jwt = require("jsonwebtoken"); +import * as config from "../common/core/config"; +import { getLogger } from "log4js"; +const logger = getLogger("ValidatorAuthentication[" + process.pid + "]"); +logger.level = config.read("logLevel", "info"); + +const privateKey = fs.readFileSync( + path.resolve(__dirname, config.read("sslParam.key")), +); + +export class ValidatorAuthentication { + static sign(payload: object): string { + const option = { + algorithm: "ES256", + expiresIn: "1000", + }; + + // logger.debug(`payload = ${JSON.stringify(payload)}`); + const signature: string = jwt.sign(payload, privateKey, option); + // logger.debug(`signature = ${signature}`); + logger.debug(`signature: OK`); + return signature; + } +} diff --git a/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/tsconfig.json b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/tsconfig.json new file mode 100644 index 0000000000..952920156b --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src/main/typescript", + "composite": true, + "outDir": "./dist", + "sourceMap": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "tsBuildInfoFile": "../../.build-cache/cactus-plugin-ledger-connector-tcs-huawei-socketio.tsbuildinfo", + }, + "include": [ + "./src/main/typescript/common/core/*.ts", + "./src/main/typescript/common/core/bin/*.ts", + "./src/main/typescript/common/core/config/*.ts", + "./src/main/typescript/connector/*.ts" + ] +} diff --git a/tools/docker/tcs-huawei-testnet/agent/example/cross_config/config.yaml b/tools/docker/tcs-huawei-testnet/agent/example/cross_config/config.yaml new file mode 100644 index 0000000000..9960c4ff77 --- /dev/null +++ b/tools/docker/tcs-huawei-testnet/agent/example/cross_config/config.yaml @@ -0,0 +1,35 @@ +General: + AgentServer: + Address: 0.0.0.0 + GRPCPort: 8098 + RelayInfo: + Address: relay1 + Port: 8082 +FabricConfiguration: + SupportContractEntry: + BlockCrossContract: blockcross + BlockCrossInvoke: invoke + FabricCfgFilePath: + CfgFile: /root/Certs/blockcrossA-NoDelete-channel-sdk-config.yaml + AppchainInfo: + UserName: Admin + ChaincodeID: blockcross + Name: A +CrossChannel: + CrossChannelID: channelidA + CrossChannelName: mychannel +CryptoPath: + SelfTLSPath: + PrivateKey: /root/Certs/A/channelidA/tls/A.key + Certificate: /root/Certs/A/channelidA/tls/A.crt + RootCA: /root/Certs/A/tlsca.crt + SelfMemPath: + PrivateKey: /root/Certs/A/membership/A.key + Certificate: /root/Certs/A/membership/A.crt + RootCA: /root/Certs/A/memca.crt + ServerTLSPath: + PrivateKey: /root/Certs/Server/key.pem + Certificate: /root/Certs/Server/cert.pem + RootCA: /root/Certs/Server/cacert.pem +PersistentData: + DBPath: /root/tcdas/production/agent/DB \ No newline at end of file diff --git a/tools/docker/tcs-huawei-testnet/agent/example/docker-compose.yml b/tools/docker/tcs-huawei-testnet/agent/example/docker-compose.yml new file mode 100644 index 0000000000..233d0f8fb0 --- /dev/null +++ b/tools/docker/tcs-huawei-testnet/agent/example/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3' +networks: + tcs_huawei_testnet_1x: +services: + agent1: + image: "registry-cbu.huawei.com/tcdas/tcdas-agent:0.0.1.test-x86_64" + container_name: agent + volumes: + # 以下为需要外部生成并导入容器的物料 + - $PWD/cross_config:/home/service/config + - $PWD/Server_Cert:/home/service/Certs/Server + # 以下为需要挂载到容器外部的文件夹 + - /var/paas/sys/log/agent1/tcdas:/var/log/tcdas + - /home/paas/tcdas/agent1:/home/service/tcdas/production/agent/DB + environment: + - AGENT_CONFIG_PATH=/home/service/config + - AGENT_GENERAL_RELAYINFO_ADDRESS=relay1 + - AGENT_FABRICCONFIGURATION_FABRICCFGFILEPATH_CFGFILE=/home/service/Certs/blockcrossA-NoDelete-channel-sdk-config.yaml + - AGENT_FABRICCONFIGURATION_APPCHAININFO_NAME=A0123456789012345678901234567890123456789 + - AGENT_GENERAL_AGENTID=channelidA + - AGENT_CROSSCHANNEL_CROSSCHANNELID=channelidA + - AGENT_CROSSCHANNEL_CROSSCHANNELNAME=mychannel + - AGENT_CRYPTOPATH_SERVERTLSPATH_PRIVATEKEY=/home/service/Certs/Server/key.pem + - AGENT_CRYPTOPATH_SERVERTLSPATH_CERTIFICATE=/home/service/Certs/Server/cert.pem + - AGENT_CRYPTOPATH_SERVERTLSPATH_ROOTCA=/home/service/Certs/Server/cacert.pem + - AGENT_PERSISTENTDATA_DBPATH=/home/service/tcdas/production/agent/DB + - AGENT_LOGGER_ROOT=/var/log/tcdas/agent + - AGENT_LOGGER_LEVEL=LevelInformational + ports: + - "8001:8080" + networks: + - tcs_huawei_testnet_1x + command: + - /bin/bash + - -c + - | + bash -x /usr/local/bin/start.sh \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index aebbe988f3..c329c56c0b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -67,6 +67,9 @@ { "path": "./packages/cactus-plugin-ledger-connector-iroha/tsconfig.json" }, + { + "path": "./packages/cactus-plugin-ledger-connector-tcs-huawei-socketio/tsconfig.json" + }, { "path": "./packages/cactus-plugin-ledger-connector-xdai/tsconfig.json" }, @@ -124,6 +127,9 @@ { "path": "./examples/cactus-example-supply-chain-frontend/tsconfig.json" }, + { + "path": "./examples/cactus-example-tcs-huawei/tsconfig.json" + }, { "path": "./packages/cactus-plugin-odap-hermes/tsconfig.json" },