From 66538f48fb7ba0a7c6be0b9adbe156c878fea046 Mon Sep 17 00:00:00 2001 From: Bruno Mateus Date: Mon, 19 Aug 2024 12:43:30 +0100 Subject: [PATCH] feat(satp-hermes): docker and gateway runner for SATP Signed-off-by: Bruno Mateus refactor(satp-hermes): gateway container image definition with bundler Signed-off-by: Peter Somogyvari refactor(satp-hermes): merge policies type guard example Signed-off-by: Peter Somogyvari refactor(satp-hermes): type guards for SATP env variables Signed-off-by: Bruno Mateus squash! - peter's fixes for besu connectivity use LAN IP Instead of hardcoded localhost use the LAN IP of machine so that the gateway container can access it too. Why though? Because if you tell the gateway container that it should access the besu ledger via localhost then it will try to do that through the container's own localhost, which is different from the host machine's localhost (where the besu ledger is actually running). Using the actual IP address of the host machine's primary network interface resolves the ambiguity between the two differnet localhosts. Signed-off-by: Peter Somogyvari Co-authored-by: Peter Somogyvari --- .../cmd-api-server.Dockerfile.healthcheck.mjs | 5 +- .../src/main/typescript/public-api.ts | 3 + .../view-creation/privacy-policies.ts | 29 + .../typescript/view-merging/merge-policies.ts | 28 + packages/cactus-plugin-satp-hermes/.gitignore | 2 + packages/cactus-plugin-satp-hermes/README.md | 44 + .../config_example.json | 256 --- .../docker-compose.yml | 11 + .../gateway-config.json | 27 + packages/cactus-plugin-satp-hermes/makefile | 77 + .../cactus-plugin-satp-hermes/package.json | 20 +- .../satp-hermes-gateway.Dockerfile | 32 + ...atp-hermes-gateway.Dockerfile.dockerignore | 0 ...-hermes-gateway.Dockerfile.healthcheck.mjs | 53 + .../src/main/typescript/.env.example | 38 - .../src/main/typescript/blo/dispatcher.ts | 12 +- .../validateAsset.ts | 34 + .../validateBesuConfig.ts | 130 ++ .../validateBesuOptions.ts | 59 + .../validateBungeeOptions.ts | 72 + .../validateEthereumConfig.ts | 167 ++ .../validateEthereumOptions.ts | 59 + .../validateFabricConfig.ts | 128 ++ .../validateFabricOptions.ts | 272 ++++ .../validatePluginRegistryOptions.ts | 123 ++ .../validateKeyPairJSON.ts | 32 + .../validateSatpBridgesConfig.ts | 126 ++ .../validateSatpCounterPartyGateways.ts | 26 + .../validateSatpEnableOpenAPI.ts | 14 + .../validateSatpEnvironment.ts | 17 + .../validateSatpGatewayIdentity.ts | 103 ++ .../validateSatpLogLevel.ts | 30 + .../validateSatpMergePolicies.ts | 17 + .../validateSatpPrivacyPolicies.ts | 17 + .../validateSatpValidationOptions.ts | 74 + .../src/main/typescript/core/constants.ts | 3 + .../core/stage-handlers/stage0-handler.ts | 4 + .../core/stage-handlers/stage1-handler.ts | 4 + .../core/stage-handlers/stage2-handler.ts | 2 + .../core/stage-handlers/stage3-handler.ts | 6 + .../client/stage0-client-service.ts | 1 + .../client/stage2-client-service.ts | 1 + .../client/stage3-client-service.ts | 1 + .../server/stage0-server-service.ts | 1 + .../server/stage3-server-service.ts | 2 + .../typescript/gol/satp-bridges-manager.ts | 2 +- .../plugin-satp-hermes-gateway-cli.ts | 173 +- .../typescript/plugin-satp-hermes-gateway.ts | 81 +- .../web-services/health-check-endpoint.ts | 79 + .../integration/gateway-init-startup.test.ts | 32 +- ...d-transfer-1-gateway-dockerization.test.ts | 1305 +++++++++++++++ ...-transfer-2-gateways-dockerization.test.ts | 1439 +++++++++++++++++ ...c-transfer-1-gateway-dockerization.test.ts | 1263 +++++++++++++++ ...ric-transfer-1-gateway-with-bungee.test.ts | 2 +- .../SATPGatewayRunner-instantiation.test.ts | 63 + .../supervisord.conf | 16 + .../src/main/typescript/public-api.ts | 5 + .../satp-runner/satp-gateway-runner.ts | 320 ++++ yarn.lock | 22 +- 59 files changed, 6562 insertions(+), 402 deletions(-) delete mode 100644 packages/cactus-plugin-satp-hermes/config_example.json create mode 100644 packages/cactus-plugin-satp-hermes/docker-compose.yml create mode 100644 packages/cactus-plugin-satp-hermes/gateway-config.json create mode 100644 packages/cactus-plugin-satp-hermes/makefile create mode 100644 packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile create mode 100644 packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile.dockerignore create mode 100644 packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile.healthcheck.mjs delete mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/.env.example create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateAsset.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBesuConfig.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBesuOptions.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBungeeOptions.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateEthereumConfig.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateEthereumOptions.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateFabricConfig.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateFabricOptions.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validatePluginRegistryOptions.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateKeyPairJSON.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpBridgesConfig.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpCounterPartyGateways.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpEnableOpenAPI.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpEnvironment.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpGatewayIdentity.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpLogLevel.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpMergePolicies.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpPrivacyPolicies.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpValidationOptions.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/main/typescript/web-services/health-check-endpoint.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-end-to-end-transfer-1-gateway-dockerization.test.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-end-to-end-transfer-2-gateways-dockerization.test.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-ethereum-fabric-transfer-1-gateway-dockerization.test.ts create mode 100644 packages/cactus-plugin-satp-hermes/src/test/typescript/unit/SATPGatewayRunner-instantiation.test.ts create mode 100644 packages/cactus-plugin-satp-hermes/supervisord.conf create mode 100644 packages/cactus-test-tooling/src/main/typescript/satp-runner/satp-gateway-runner.ts diff --git a/packages/cactus-cmd-api-server/cmd-api-server.Dockerfile.healthcheck.mjs b/packages/cactus-cmd-api-server/cmd-api-server.Dockerfile.healthcheck.mjs index 3d2fc251c9..45d5223964 100644 --- a/packages/cactus-cmd-api-server/cmd-api-server.Dockerfile.healthcheck.mjs +++ b/packages/cactus-cmd-api-server/cmd-api-server.Dockerfile.healthcheck.mjs @@ -42,10 +42,11 @@ httpModule if (exitCode === 0) { console.log("%s Healthcheck OK: ", url, statusCode, statusMessage); } else { - console.error("%s Healthcheck FAIL: ", url, statusCode, statusMessage); + console.error("%s Healthcheck FAIL_1: ", url, statusCode, statusMessage); } process.exit(exitCode); }) .on("error", (ex) => { - console.error("%s Healthcheck FAIL: ", url, ex); + console.error("%s Healthcheck FAIL_2: ", url, ex); + process.exit(1); }); diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/public-api.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/public-api.ts index ff3dd0e66d..1c8403aa70 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/public-api.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/public-api.ts @@ -1,6 +1,9 @@ import { IPluginFactoryOptions } from "@hyperledger/cactus-core-api"; import { PluginFactoryBungeeHermes } from "./plugin-factory-bungee-hermes"; +export { isMergePolicyValueArray } from "./view-merging/merge-policies"; +export { isPrivacyPolicyValueArray } from "./view-creation/privacy-policies"; + export { PluginBungeeHermes, IPluginBungeeHermesOptions, diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/privacy-policies.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/privacy-policies.ts index 4e78f62009..7456816a3b 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/privacy-policies.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/privacy-policies.ts @@ -8,6 +8,35 @@ export interface IPrivacyPolicyValue { policy: PrivacyPolicyOpts; policyHash: string; } + +// Type guard for PrivacyPolicyOpts +export function isPrivacyPolicyOpts( + value: unknown, +): value is PrivacyPolicyOpts { + return ( + typeof value === "string" && + Object.values(PrivacyPolicyOpts).includes(value as PrivacyPolicyOpts) + ); +} + +// Type guard for IPrivacyPolicyValue +export function isPrivacyPolicyValue(obj: unknown): obj is IPrivacyPolicyValue { + return ( + typeof obj === "object" && + obj !== null && + "policy" in obj && // Ensure 'policy' key exists + isPrivacyPolicyOpts((obj as Record).policy) && // Check if policy is a valid PrivacyPolicyOpts value + typeof (obj as Record).policyHash === "string" // Ensure 'policyHash' is a string + ); +} + +// Type guard for an array of IPrivacyPolicyValue +export function isPrivacyPolicyValueArray( + input: unknown, +): input is Array { + return Array.isArray(input) && input.every(isPrivacyPolicyValue); +} + export class PrivacyPolicies { constructor() {} diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/merge-policies.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/merge-policies.ts index ec0efaa494..532cd30a95 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/merge-policies.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/merge-policies.ts @@ -8,6 +8,34 @@ export interface IMergePolicyValue { policy: MergePolicyOpts; policyHash?: string; //undefined if policy is NONE } + +// Type guard for MergePolicyOpts +export function isMergePolicyOpts(value: unknown): value is MergePolicyOpts { + return ( + typeof value === "string" && + Object.values(MergePolicyOpts).includes(value as MergePolicyOpts) + ); +} + +// Type guard for IMergePolicyValue +export function isMergePolicyValue(obj: unknown): obj is IMergePolicyValue { + return ( + typeof obj === "object" && + obj !== null && + "policy" in obj && // Ensure 'policy' key exists + isMergePolicyOpts((obj as Record).policy) && // Check if policy is a valid MergePolicyOpts value + (typeof (obj as Record).policyHash === "string" || + typeof (obj as Record).policyHash === "undefined") // Ensure 'policyHash' is either a string or undefined + ); +} + +// Type guard for an array of IMergePolicyValue +export function isMergePolicyValueArray( + input: unknown, +): input is IMergePolicyValue[] { + return Array.isArray(input) && input.every(isMergePolicyValue); +} + export class MergePolicies { constructor() {} diff --git a/packages/cactus-plugin-satp-hermes/.gitignore b/packages/cactus-plugin-satp-hermes/.gitignore index da6665bbc3..33645084b6 100644 --- a/packages/cactus-plugin-satp-hermes/.gitignore +++ b/packages/cactus-plugin-satp-hermes/.gitignore @@ -1,3 +1,5 @@ src/main/typescript/fabric-contracts/satp/chaincode-typescript/.yarn/ src/main/typescript/.env packages/cactus-plugin-satp-hermes/cache/solidity-files-cache.json +src/keys/ +src/test/typescript/integration/gateway-info \ No newline at end of file diff --git a/packages/cactus-plugin-satp-hermes/README.md b/packages/cactus-plugin-satp-hermes/README.md index f353eb76b1..5513b7ee0c 100644 --- a/packages/cactus-plugin-satp-hermes/README.md +++ b/packages/cactus-plugin-satp-hermes/README.md @@ -155,6 +155,50 @@ const serverGatewayOptions: IBesuSATPGatewayConstructorOptions = { Note that these gateways are extensions of the [default SATP Gateway class](https://github.com/hyperledger/cactus/blob/main/packages/cactus-plugin-satp-hermes/src/main/typescript/gateway/plugin-satp-gateway.ts), that implements the gateway functionality. Each of these extensions implements ledger-specific operations. +## Containerization + +### Building the container image locally + +In the project root directory run these commands on the terminal: + +```sh +yarn configure +yarn lerna run build:bundle --scope=@hyperledger/cactus-plugin-satp-hermes +``` + +Build the image: + +```sh +docker build \ + --file ./packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile \ + ./packages/cactus-plugin-satp-hermes/ \ + --tag shg \ + --tag satp-hermes-gateway \ + --tag ghcr.io/hyperledger/cacti-satp-hermes-gateway:$(date +"%Y-%m-%dT%H-%M-%S" --utc)-dev-$(git rev-parse --short HEAD) +``` + +Run the image: + +```sh +docker run \ + -it \ + satp-hermes-gateway +``` + +Alternatively you can use `docker compose up --build` from within the package directory or if you +prefer to run it from the project root directory then: + +```sh +docker compose \ + --project-directory ./packages/cactus-plugin-satp-hermes/ \ + -f ./packages/cactus-plugin-satp-hermes/docker-compose.yml \ + up \ + --build +``` + + +> The `--build` flag is going to save you 99% of the time from docker compose caching your image builds against your will or knowledge during development. + ## Contributing We welcome contributions to Hyperledger Cactus in many forms, and there’s always plenty to do! diff --git a/packages/cactus-plugin-satp-hermes/config_example.json b/packages/cactus-plugin-satp-hermes/config_example.json deleted file mode 100644 index a6764458a9..0000000000 --- a/packages/cactus-plugin-satp-hermes/config_example.json +++ /dev/null @@ -1,256 +0,0 @@ -{ - "gid": { - "id": "mockID", - "name": "mockName", - "version": [ - { - "Core": "v02", - "Architecture": "v02", - "Crash": "v02" - } - ], - "supportedDLTs": [ "FabricSATPGateway", "BesuSATPGateway", "mockDLT"], - "proofID": "mockProofID", - "address": "http://localhost", - "gatewayServerPort": 3110, - "gatewayClientPort": 3111, - "gatewayOpenAPIPort": 4110 - - }, - "logLevel": "INFO", - "counterPartyGateways": [ - { - "id": "mockCounterPartyID1", - "name": "mockName", - "address": "http://localhost", - "pubkey": "mockPubkey", - "version": [ - { - "Core": "v02", - "Architecture": "v02", - "Crash": "v02" - } - ], - "supportedDLTs": [ "BesuSATPGateway", "mockDLT"], - "proofID": "mockProofID", - "gatewayServerPort": 3000, - "gatewayClientPort": 3001, - "gatewayOpenAPIPort": 40001 - }, - { - "id": "mockCounterPartyID2", - "name": "mockName", - "address": "http://localhost", - "pubkey": "mockPubkey", - "version": [ - { - "Core": "v02", - "Architecture": "v02", - "Crash": "v02" - } - ], - "supportedDLTs": [ "FabricSATPGateway"], - "proofID": "mockProofID", - "gatewayServerPort": 5001, - "gatewayClientPort": 5002, - "gatewayOpenAPIPort": 6001 - } - ], - "keyPair": { - "privateKey": "mockPrivateKey", - "publicKey": "mockPublicKey" - }, - "bridgesConfig": [ - { - "network": "Fabric", - "signingCredential": { - "keychainId": "mockKeychainId", - "keychainRef": "string1", - "type": "X.509", - "vaultTransitKey": { - "keyName": "mockKeyName", - "token": "mockToken" - }, - "webSocketKey": { - "sessionId": "mockSessionId", - "signature": "mockSignature" - } - }, - "channelName": "mockChannelName", - "contractName": "mockContractName", - "options": { - "instanceId": "mockInstanceId", - "dockerBinary": "/usr/local/bin/docker", - "peerBinary": "/fabric-samples/bin/peer", - "goBinary": "/usr/local/go/bin/go", - "pluginRegistryOptions": { - "keyChainOptions": { - "instanceId": "mockInstanceId", - "keychainId": "mockKeychainId", - "logLevel": "INFO", - "backend": [ - { - "keychainEntry": "mockKeychainEntry", - "keychainEntryVale": "mockKeychainEntryVale" - }, - { - "keychainEntry": "mockKeychainEntry2", - "keychainEntryVale": "mockKeychainEntryVale2" - } - ] - } - }, - "cliContainerEnv": { - "CORE_PEER_ADDRESS": "mockCORE_PEER_ADDRESS", - "CORE_PEER_LOCALMSPID": "mockCORE_PEER_LOCALMSPID", - "CORE_PEER_TLS_ROOTCERT_FILE": "mockCORE_PEER_TLS_ROOTCERT_FILE", - "CORE_PEER_MSPCONFIGPATH": "mockCORE_PEER_MSPCONFIGPATH", - "CORE_PEER_TLS_ENABLED": "mockCORE_PEER_TLS_ENABLED", - "CORE_PEER_TLS_CLIENTAUTHREQUIRED": "mockCORE_PEER_TLS_CLIENTAUTHREQUIRED", - "CORE_PEER_TLS_CLIENTROOTCAS_FILES": "mockCORE_PEER_TLS_CLIENTROOTCAS_FILES", - "CORE_PEER_TLS_CLIENTCERT_FILE": "mockCORE_PEER_TLS_CLIENTCERT" - }, - "sshConfig": { - "host": "mockHost", - "port": 22, - "username": "mockUsername", - "privateKey": "mockPrivateKey" - }, - "logLevel": "INFO", - "connectionProfile": { - "mychannel": { - "orderers": [ - "orderer.example.com" - ], - "peers": { - "peer0.org1.example.com": { - "endorsingPeer": true, - "chaincodeQuery": true, - "ledgerQuery": true, - "eventSource": true - } - } - } - }, - "discoveryOptions": { - "enabled": true, - "asLocalhost": true - }, - "eventHandlerOptions": { - "strategy": "NETWORK_SCOPE_ALLFORTX", - "commitTimeout": 300 - } - }, - "bungeeOptions": { - "keyPair": { - "privateKey": "mockPrivateKey", - "publicKey": "mockPublicKey" - }, - "instanceId": "mockInstanceId", - "pluginRegistryOptions": [ - { - "keyChainOptions": { - "instanceId": "mockInstanceId", - "keychainId": "mockKeychainId", - "logLevel": "INFO", - "backend": [ - { - "keychainEntry": "mockKeychainEntry", - "keychainEntryVale": "mockKeychainEntryVale" - }, - { - "keychainEntry": "mockKeychainEntry2", - "keychainEntryVale": "mockKeychainEntryVale2" - } - ] - } - } - - ], - "logLevel": "INFO" - } - }, - { - "network": "Besu", - "keychainId": "mockKeychainId", - "signingCredential": { - "ethAccount": "mockEthAccount", - "secret": "privateKeyMock", - "type": "PRIVATE_HEX_KEY" - }, - "contractName": "mockContractName", - "contractAddress": "mockContractAddress", - "options": { - "instanceId": "mockInstanceId", - "rpcApiHttpHost": "http://localhost", - "rpcApiWsHost": "ws://localhost", - "pluginRegistryOptions": [ - { - "keyChainOptions": { - "instanceId": "mockInstanceId", - "keychainId": "mockKeychainId", - "logLevel": "INFO", - "backend": [ - { - "keychainEntry": "mockKeychainEntry", - "keychainEntryVale": "mockKeychainEntryVale" - }, - { - "keychainEntry": "mockKeychainEntry2", - "keychainEntryVale": "mockKeychainEntryVale2" - } - ] - } - }, - { - "keyChainOptions": { - "instanceId": "mockInstanceId", - "keychainId": "mockKeychainId", - "logLevel": "INFO", - "backend": [ - { - "keychainEntry": "mockKeychainEntry", - "keychainEntryVale": "mockKeychainEntryVale" - }, - { - "keychainEntry": "mockKeychainEntry2", - "keychainEntryVale": "mockKeychainEntryVale2" - } - ] - } - } - ], - "logLevel": "INFO" - }, - "bungeeOptions": { - "keyPair": { - "privateKey": "mockPrivateKey", - "publicKey": "mockPublicKey" - }, - "instanceId": "mockInstanceId", - "pluginRegistryOptions": [ - { - "keyChainOptions": { - "instanceId": "mockInstanceId", - "keychainId": "mockKeychainId", - "logLevel": "INFO", - "backend": [ - { - "keychainEntry": "mockKeychainEntry", - "keychainEntryVale": "mockKeychainEntryVale" - }, - { - "keychainEntry": "mockKeychainEntry2", - "keychainEntryVale": "mockKeychainEntryVale2" - } - ] - } - } - ], - "logLevel": "INFO" - }, - "gas": 99999999999999 - } - ] - -} \ No newline at end of file diff --git a/packages/cactus-plugin-satp-hermes/docker-compose.yml b/packages/cactus-plugin-satp-hermes/docker-compose.yml new file mode 100644 index 0000000000..75cc6307e4 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.8" + +services: + satp-hermes-gateway: + build: + context: ./ + dockerfile: satp-hermes-gateway.Dockerfile + ports: + - 3010:3010/tcp # SERVER_PORT + - 3011:3011/tcp # CLIENT_PORT + - 4010:4010/tcp # API_PORT diff --git a/packages/cactus-plugin-satp-hermes/gateway-config.json b/packages/cactus-plugin-satp-hermes/gateway-config.json new file mode 100644 index 0000000000..4406dc5289 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/gateway-config.json @@ -0,0 +1,27 @@ +{ + "gid": { + "id": "gatewayID", + "name": "gatewayName", + "version": [ + { + "Core": "v02", + "Architecture": "v02", + "Crash": "v02" + } + ], + "supportedDLTs": [ "FabricSATPGateway", "BesuSATPGateway"], + "proofID": "mockProofID", + "address": "http://localhost", + "gatewayServerPort": 3010, + "gatewayClientPort": 3011, + "gatewayOpenAPIPort": 4010 + }, + "gatewayKeyPair": { + "publicKey": "0c7556eda362eb8a9fee4505f7bee6375e2aeaa03a3c32c7309c13ab47f6640a6530af4db06b8acc5d31a26b8a7e930cf5df697b21ca1115771e9ea2e6fb1730", + "privateKey": "6039ac2c12e867a3b7d7a6becc829afbbd951270a62a1285aec003957d11efb9" + }, + "logLevel": "DEBUG", + "counterPartyGateways": [], + "environment": "development", + "enableOpenAPI": true +} diff --git a/packages/cactus-plugin-satp-hermes/makefile b/packages/cactus-plugin-satp-hermes/makefile new file mode 100644 index 0000000000..2dbc455239 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/makefile @@ -0,0 +1,77 @@ +.PHONY: setup-env generate-keys run-gateway clean + +# Define common variables +OPENSSL = openssl +KEYS_DIR = src/keys +CONFIG_FILE = gateway-config.json + +setup-env: + @echo "Setting up environment for SATP-Hermes..." + + @# Check if Node.js is installed, if not, install it + @if ! command -v node >/dev/null 2>&1; then \ + echo "Installing Node.js 18.18.2..."; \ + sudo curl -fsSL https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz | sudo tar -xJ -C /usr/local --strip-components=1; \ + fi + + @if ! command -v yarn >/dev/null 2>&1; then \ + echo "Installing Yarn..."; \ + sudo npm install -g yarn; \ + fi + + @echo "Installing dependencies and compiling the plugin..." + yarn install + yarn tsc + + @echo "Initializing databases..." + yarn db:init + + @echo "Environment setup complete." + +generate-keys: + @echo "Generating new keys..." + @mkdir -p $(KEYS_DIR) + + @echo "Generating private key..." + @$(OPENSSL) ecparam -name secp256k1 -genkey -noout -out $(KEYS_DIR)/secp256k1-privkey.pem + @echo "Private key file generated at $(KEYS_DIR)/secp256k1-privkey.pem" + + @echo "Generating public key..." + @$(OPENSSL) ec -in $(KEYS_DIR)/secp256k1-privkey.pem -pubout -out $(KEYS_DIR)/secp256k1-pubkey.pem + @echo "Public key file generated at $(KEYS_DIR)/secp256k1-pubkey.pem" + + @# Extracting private and public key in hex... + @$(OPENSSL) ec -in $(KEYS_DIR)/secp256k1-privkey.pem -text -noout | grep priv -A 3 | tail -n +2 | tr -d '\n[:space:]:' | sed 's/^00//' > $(KEYS_DIR)/private.hex + @$(OPENSSL) ec -in $(KEYS_DIR)/secp256k1-pubkey.pem -pubin -text -noout | grep pub -A 5 | tail -n +2 | tr -d '\n[:space:]:' | sed 's/^04//' > $(KEYS_DIR)/public.hex + + @echo "Updating gatewayKeyPair in gateway-config.json file..." + @PRIVATE_KEY=`cat $(KEYS_DIR)/private.hex` && \ + PUBLIC_KEY=`cat $(KEYS_DIR)/public.hex` && \ + sed -i \ + -e '/"gatewayKeyPair": {/,/}/{s/"publicKey": "[^"]*"/"publicKey": "'$$PUBLIC_KEY'"/}' \ + -e '/"gatewayKeyPair": {/,/}/{s/"privateKey": "[^"]*"/"privateKey": "'$$PRIVATE_KEY'"/}' \ + $(CONFIG_FILE) + + @echo "New secp256k1 keys generated and updated in gateway-config.json gatewayKeyPair:" + @grep -A 2 '"gatewayKeyPair"' $(CONFIG_FILE) + + @# Clean up temporary files + @rm -f $(KEYS_DIR)/private.hex $(KEYS_DIR)/public.hex + +run-gateway: generate-keys setup-env + @echo "Building SATP Gateway image and running container..." + docker compose up --build + +clean: + @echo "Cleaning up SATP-Hermes environment..." + + @echo "Rolling back database migrations..." + yarn db:rollback + + @echo "Removing SQLite database files..." + find src/knex -name "*.sqlite3" -type f -delete + + @echo "Removing generated keys..." + rm -rf $(KEYS_DIR) + + @echo "Cleanup complete." \ No newline at end of file diff --git a/packages/cactus-plugin-satp-hermes/package.json b/packages/cactus-plugin-satp-hermes/package.json index 86854babb9..ac4c67511d 100644 --- a/packages/cactus-plugin-satp-hermes/package.json +++ b/packages/cactus-plugin-satp-hermes/package.json @@ -35,6 +35,10 @@ { "name": "Carlos Amaro", "url": "https://github.com/LordKubaya" + }, + { + "name": "Bruno Mateus", + "url": "https://github.com/brunoffmateus" } ], "main": "dist/lib/main/typescript/index.js", @@ -44,6 +48,7 @@ "dist/*" ], "scripts": { + "build:bundle": "ncc build ./dist/lib/main/typescript/plugin-satp-hermes-gateway-cli.js --minify --out=./dist/bundle/ncc/ --external=fabric-common", "build": "run-s codegen tsc", "build-proto": "buf build --path src/main/proto --verbose", "build:dev:backend:postbuild": "mkdir -p ./dist/lib/knex && cp -r ./src/knex/* ./dist/lib/knex", @@ -53,6 +58,12 @@ "codegen:abi": "yarn forge:all && abi-types-generator './src/solidity/generated/satp-wrapper.sol/SATPWrapperContract.json' --output='./src/main/typescript/generated'", "codegen:openapi": "npm run generate-sdk", "codegen:proto": "npm run generate-proto", + "db:init": "run-s db:init:local db:init:remote", + "db:init:local": "knex migrate:latest --knexfile src/knex/knexfile.ts", + "db:init:remote": "knex migrate:latest --knexfile src/knex/knexfile-remote.ts", + "db:rollback": "run-s db:rollback:local db:rollback:remote", + "db:rollback:local": "knex migrate:rollback --knexfile src/knex/knexfile.ts", + "db:rollback:remote": "knex migrate:rollback --knexfile src/knex/knexfile-remote.ts", "forge": "forge build ./src/solidity/*.sol --out ./src/solidity/generated", "forge:all": "run-s 'forge' 'forge:test'", "forge:test": "forge build ./src/test/solidity/contracts/*.sol --out ./src/test/solidity/generated", @@ -60,13 +71,13 @@ "generate-sdk": "run-p 'generate-sdk:*'", "generate-sdk:go": "openapi-generator-cli generate -i ./src/main/yml/bol/openapi-blo-bundled.yml -g go -o ./src/main/go/generated/gateway-client --additional-properties=packageName=generated,generateInterfaces=true,packageVersion=v0.0.1,moduleName=github.com/hyperledger/cacti/packages/cactus-plugin-satp-hermes/src/main/go/generated --git-user-id hyperledger --git-repo-id cacti/packages/cactus-plugin-satp-hermes/src/main/go/generated", "generate-sdk:typescript-axios-bol": "yarn bundle-openapi-yaml && yarn bundle-openapi-json && openapi-generator-cli generate -i ./src/main/yml/bol/openapi-blo-bundled.yml -g typescript-axios -o ./src/main/typescript/generated/gateway-client/typescript-axios/ --reserved-words-mappings protected=protected --enable-post-process-file", - "preinstall": "curl -L https://foundry.paradigm.xyz | bash && foundryup", "lint": "run-p 'lint:*'", "lint:eslint": "eslint './src/**/*.{js,ts}' --quiet --fix && cspell \"*/*/src/**/*.{js,ts}\"", "lint:oapi": "vacuum lint -d -e ./src/main/yml/bol/openapi-blo-bundled.yml", "lint:protobuf": "buf lint --path src/main/proto --verbose", - "start-gateway": "ts-node /src/main/typescript/plugin-satp-hermes-gateway-cli.ts", + "preinstall": "curl -L https://foundry.paradigm.xyz | bash && foundryup", "pretsc": "npm run generate-sdk", + "start-gateway": "node ./dist/lib/main/typescript/plugin-satp-hermes-gateway-cli.js", "tsc": "tsc --project ./tsconfig.json", "watch": "tsc --build --watch" }, @@ -107,6 +118,7 @@ "fs-extra": "11.2.0", "google-protobuf": "3.21.2", "hardhat": "2.22.5", + "jsonc": "2.0.0", "knex": "2.4.0", "kubo-rpc-client": "3.0.1", "npm-run-all": "4.1.5", @@ -137,15 +149,19 @@ "@types/swagger-ui-express": "4.1.6", "@types/tape": "4.13.4", "@types/uuid": "10.0.0", + "@vercel/ncc": "0.38.1", "body-parser": "1.20.3", "express": "4.21.0", "fabric-network": "2.2.20", "grpc-tools": "1.12.4", "grpc_tools_node_protoc_ts": "5.3.3", + "internal-ip": "6.2.0", + "jsonc": "2.0.0", "kubo-rpc-client": "3.0.1", "make-dir-cli": "3.1.0", "protobufjs": "7.2.5", "swagger-cli": "4.0.4", + "ts-node": "10.9.1", "typescript": "5.5.2" }, "engines": { diff --git a/packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile b/packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile new file mode 100644 index 0000000000..74597c61b6 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile @@ -0,0 +1,32 @@ +FROM node:22.4.0-bookworm-slim + +RUN apt-get update && apt-get install -y supervisor curl + +# CVE-2023-31484 - perl: CPAN.pm does not verify TLS certificates when downloading distributions over HTTPS... +RUN apt-get remove -y --allow-remove-essential perl perl-base && apt-get autoremove -y + +ARG APP_DIR=/opt/cacti/satp-hermes +WORKDIR ${APP_DIR} +RUN mkdir -p /opt/cacti/satp-hermes/log/ + +COPY ./dist/bundle/ncc/ ${APP_DIR} +COPY ./satp-hermes-gateway.Dockerfile.healthcheck.mjs ${APP_DIR} +COPY ./gateway-config.json /gateway-config.json +COPY ./src/knex/ ${APP_DIR}/src/knex/ +COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY ./supervisord.conf /etc/supervisord.conf + +# fabric-common cannot be bundled due to some exotic transitive depenedencies +# so we have to install it within the container manually. +RUN npm install fabric-common + +ENTRYPOINT ["/usr/bin/supervisord"] +CMD ["--configuration", "/etc/supervisord.conf", "--nodaemon"] + + +HEALTHCHECK --interval=5s --timeout=1s --start-period=1s --retries=5 CMD [ "node", "./satp-hermes-gateway.Dockerfile.healthcheck.mjs", "http", "localhost", "4010" ] + +ENV TZ=Etc/UTC +ENV NODE_ENV=production + +EXPOSE 3010 3011 4010 diff --git a/packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile.dockerignore b/packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile.dockerignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile.healthcheck.mjs b/packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile.healthcheck.mjs new file mode 100644 index 0000000000..f33bc143ac --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/satp-hermes-gateway.Dockerfile.healthcheck.mjs @@ -0,0 +1,53 @@ +/** + * The healthcheck script for the Cacti API Server written in pure NodeJS without + * any external dependencies. + * + * USAGE + * ----- + * + * ```sh + * $ node cmd-api-server.Dockerfile.healthcheck.mjs http localhost 4000 + * http://localhost:4000/api/v1/api-server/healthcheck Healthcheck OK: 200 OK + * ``` + * + * FAQ + * --- + * + * Q: Why though? Why not just use cURL or wget + * or any other command line utility to perform HTTP requests? + * A: This has zero OS level package dependencies and will work without the + * container image having to have new software installed. This reduces the footprint + * of the image and also the attack surfaces that we have. The slight increase in + * complexity is a trade-off we consider worth having because this script is not + * part of the API server's own codebase and therefore does not affect the complexity + * of that as such. + */ + +import http from "http"; +import https from "https"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const [nodeBinary, script, protocol, host, port, path] = process.argv; + +const thePath = + path ?? "/api/v1/@hyperledger/cactus-plugin-satp-hermes/healthcheck"; +const isSecureHttp = protocol === "https"; +const httpModule = isSecureHttp ? https : http; + +const url = `${protocol}://${host}:${port}${thePath}`; + +httpModule + .get(url, (res) => { + const { statusCode, statusMessage } = res; + const exitCode = statusCode >= 200 && statusCode <= 300 ? 0 : 1; + if (exitCode === 0) { + console.log("%s Healthcheck OK: ", url, statusCode, statusMessage); + } else { + console.error("%s Healthcheck FAIL_1: ", url, statusCode, statusMessage); + } + process.exit(exitCode); + }) + .on("error", (ex) => { + console.error("%s Healthcheck FAIL_2: ", url, ex); + process.exit(1); + }); diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/.env.example b/packages/cactus-plugin-satp-hermes/src/main/typescript/.env.example deleted file mode 100644 index 5f11c435e1..0000000000 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/.env.example +++ /dev/null @@ -1,38 +0,0 @@ -# CREDENTIALs -SATP_PRIVATE_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef -SATP_PUBLIC_KEY=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 - - -# SATP Gateway Configuration -SATP_LOG_LEVEL=INFO - -# Environment and API -SATP_NODE_ENV=development -SATP_ENABLE_OPEN_API=true - -# Gateway Identity -SATP_GATEWAY_ID=gateway1 -SATP_GATEWAY_NAME=ExampleGateway -SATP_GATEWAY_VERSION=v02,v02,v02 -SATP_SUPPORTED_DLTS=FabricSATPGateway,BesuSATPGateway - -# Proof and Ports -SATP_PROOF_ID=proof123 -SATP_GATEWAY_SERVER_PORT=3010 -SATP_GATEWAY_CLIENT_PORT=3011 -SATP_GATEWAY_GRPC_PORT=3012 - -# Gateway Address -SATP_GATEWAY_ADDRESS=http://localhost:3010 - -# Counter Party Gateways (JSON array) -SATP_COUNTER_PARTY_GATEWAYS=[{"id":"gateway2","name":"OtherGateway","address":"http://other-gateway:3010"}] - -# Validation Options (JSON object) -SATP_VALIDATION_OPTIONS={"skipMissingProperties":true} - -# Privacy Policies (JSON array) -SATP_PRIVACY_POLICIES=[{"policy":"GDPR","policyHash":"hash123"}] - -# Merge Policies (JSON array) -SATP_MERGE_POLICIES=[{"policy":"LatestWins","policyHash":"hash456"}] diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/blo/dispatcher.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/blo/dispatcher.ts index 53e0254370..20be0488cd 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/blo/dispatcher.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/blo/dispatcher.ts @@ -11,6 +11,7 @@ import { IWebServiceEndpoint } from "@hyperledger/cactus-core-api"; //import { GatewayIdentity, GatewayChannel } from "../core/types"; //import { GetStatusError, NonExistantGatewayIdentity } from "../core/errors"; import { GetStatusEndpointV1 } from "../web-services/status-endpoint"; +import { HealthCheckEndpointV1 } from "../web-services/health-check-endpoint"; //import { GetAuditRequest, GetAuditResponse } from "../generated/gateway-client/typescript-axios"; import { @@ -97,7 +98,16 @@ export class BLODispatcher { logLevel: this.options.logLevel, }); - const theEndpoints = [getStatusEndpointV1, getSessionIdsEndpointV1]; + const healthCheckEndpointV1 = new HealthCheckEndpointV1({ + dispatcher: this, + logLevel: this.options.logLevel, + }); + + const theEndpoints = [ + getStatusEndpointV1, + getSessionIdsEndpointV1, + healthCheckEndpointV1, + ]; this.endpoints = theEndpoints; return theEndpoints; } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateAsset.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateAsset.ts new file mode 100644 index 0000000000..db0495ea9a --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateAsset.ts @@ -0,0 +1,34 @@ +import { + Asset, + TokenType, +} from "../../core/stage-services/satp-bridge/types/asset"; + +// Type guard for TokenType +function isTokenType(obj: unknown): obj is TokenType { + return ( + typeof obj === "number" && + obj !== null && + Object.values(TokenType).includes(obj) + ); +} + +// Type guard for Asset +export function isAsset(obj: unknown): obj is Asset { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "tokenId" in obj && + typeof objRecord.tokenId === "string" && + "tokenType" in obj && + isTokenType(objRecord.tokenType) && + "owner" in obj && + typeof objRecord.owner === "string" && + "amount" in obj && + typeof objRecord.amount === "number" && + "ontology" in obj && + typeof objRecord.ontology === "string" && + "contractName" in obj && + typeof objRecord.contractName === "string" + ); +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBesuConfig.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBesuConfig.ts new file mode 100644 index 0000000000..e86d7cd9a1 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBesuConfig.ts @@ -0,0 +1,130 @@ +import { EvmAsset } from "../../core/stage-services/satp-bridge/types/evm-asset"; +import { + Web3SigningCredential, + Web3SigningCredentialCactusKeychainRef, + Web3SigningCredentialNone, + Web3SigningCredentialPrivateKeyHex, + Web3SigningCredentialType, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; +import { NetworkConfigJSON } from "../validateSatpBridgesConfig"; +import { BesuOptionsJSON, isBesuOptionsJSON } from "./validateBesuOptions"; +import { + BungeeOptionsJSON, + isBungeeOptionsJSON, + isClaimFormat, +} from "./validateBungeeOptions"; +import { ClaimFormat } from "../../generated/proto/cacti/satp/v02/common/message_pb"; +import { isEvmAssetArray } from "./validateEthereumConfig"; + +export interface BesuConfigJSON extends NetworkConfigJSON { + keychainId: string; + signingCredential: Web3SigningCredential; + contractName: string; + contractAddress: string; + gas: number; + options: BesuOptionsJSON; + bungeeOptions: BungeeOptionsJSON; + besuAssets?: EvmAsset[]; + claimFormat: ClaimFormat; +} + +// Type guard for Web3SigningCredentialType +function isWeb3SigningCredentialType( + value: unknown, +): value is Web3SigningCredentialType { + return ( + typeof value === "string" && + value !== null && + (value === Web3SigningCredentialType.CactusKeychainRef || + value === Web3SigningCredentialType.PrivateKeyHex || + value === Web3SigningCredentialType.None) + ); +} + +// Type guard for Web3SigningCredentialCactusKeychainRef +function isWeb3SigningCredentialCactusKeychainRef( + obj: unknown, +): obj is Web3SigningCredentialCactusKeychainRef { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "ethAccount" in obj && + typeof objRecord.ethAccount === "string" && + "keychainEntryKey" in obj && + typeof objRecord.keychainEntryKey === "string" && + "keychainId" in obj && + typeof objRecord.keychainId === "string" && + "type" in obj && + isWeb3SigningCredentialType(objRecord.type) + ); +} + +// Type guard for Web3SigningCredentialPrivateKeyHex +function isWeb3SigningCredentialPrivateKeyHex( + obj: unknown, +): obj is Web3SigningCredentialPrivateKeyHex { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "ethAccount" in obj && + typeof objRecord.ethAccount === "string" && + "secret" in obj && + typeof objRecord.secret === "string" && + "type" in obj && + isWeb3SigningCredentialType(objRecord.type) + ); +} + +// Type guard for Web3SigningCredentialNone +function isWeb3SigningCredentialNone( + obj: unknown, +): obj is Web3SigningCredentialNone { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "type" in obj && + isWeb3SigningCredentialType(objRecord.type) + ); +} + +// Type guard for Web3SigningCredential +function isWeb3SigningCredential(obj: unknown): obj is Web3SigningCredential { + if (!obj || typeof obj !== "object") { + return false; + } + return ( + isWeb3SigningCredentialCactusKeychainRef(obj) || + isWeb3SigningCredentialPrivateKeyHex(obj) || + isWeb3SigningCredentialNone(obj) + ); +} + +// Type guard for BesuConfigJSON +export function isBesuConfigJSON(obj: unknown): obj is BesuConfigJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "keychainId" in obj && + typeof objRecord.keychainId === "string" && + "contractName" in obj && + typeof objRecord.contractName === "string" && + "contractAddress" in obj && + typeof objRecord.contractAddress === "string" && + "gas" in obj && + typeof objRecord.gas === "number" && + "signingCredential" in obj && + isWeb3SigningCredential(objRecord.signingCredential) && + (!("besuAssets" in obj) || isEvmAssetArray(objRecord.besuAssets)) && + "bungeeOptions" in obj && + isBungeeOptionsJSON(objRecord.bungeeOptions) && + "options" in obj && + isBesuOptionsJSON(objRecord.options) && + "claimFormat" in obj && + isClaimFormat(objRecord.claimFormat) + ); +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBesuOptions.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBesuOptions.ts new file mode 100644 index 0000000000..7626ba9a78 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBesuOptions.ts @@ -0,0 +1,59 @@ +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { isLogLevelDesc } from "../validateSatpLogLevel"; +import { + createPluginRegistry, + isPluginRegistryOptionsJSON, + PluginRegistryOptionsJSON, +} from "./validatePluginRegistryOptions"; +import { IPluginLedgerConnectorBesuOptions } from "@hyperledger/cactus-plugin-ledger-connector-besu"; + +export interface BesuOptionsJSON { + instanceId: string; + rpcApiHttpHost: string; + rpcApiWsHost: string; + pluginRegistryOptions: PluginRegistryOptionsJSON; + logLevel?: LogLevelDesc; +} + +// Type guard for BesuOptionsJSON +export function isBesuOptionsJSON(obj: unknown): obj is BesuOptionsJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "instanceId" in obj && + typeof objRecord.instanceId === "string" && + "rpcApiHttpHost" in obj && + typeof objRecord.rpcApiHttpHost === "string" && + "rpcApiWsHost" in obj && + typeof objRecord.rpcApiWsHost === "string" && + "pluginRegistryOptions" in obj && + isPluginRegistryOptionsJSON(objRecord.pluginRegistryOptions) && + (!("logLevel" in obj) || isLogLevelDesc(objRecord.logLevel)) + ); +} + +// Function to create IPluginLedgerConnectorBesuOptions from BesuOptionsJSON +export function createBesuOptions( + options: BesuOptionsJSON, +): IPluginLedgerConnectorBesuOptions { + if (!options) { + throw new TypeError( + "Invalid options in BesuConfig: " + JSON.stringify(options), + ); + } + + const besuOptions: IPluginLedgerConnectorBesuOptions = { + instanceId: options.instanceId, + rpcApiHttpHost: options.rpcApiHttpHost, + rpcApiWsHost: options.rpcApiWsHost, + pluginRegistry: createPluginRegistry( + options.pluginRegistryOptions, + options.logLevel, + ), + logLevel: options.logLevel, + }; + + return besuOptions; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBungeeOptions.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBungeeOptions.ts new file mode 100644 index 0000000000..ba5d45ab14 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateBungeeOptions.ts @@ -0,0 +1,72 @@ +import { LogLevelDesc, Secp256k1Keys } from "@hyperledger/cactus-common"; +import { IPluginBungeeHermesOptions } from "@hyperledger/cactus-plugin-bungee-hermes/dist/lib/main/typescript/plugin-bungee-hermes"; +import { isLogLevelDesc } from "../validateSatpLogLevel"; +import { + createPluginRegistry, + isPluginRegistryOptionsJSON, + PluginRegistryOptionsJSON, +} from "./validatePluginRegistryOptions"; +import { iskeyPairJSON } from "../validateKeyPairJSON"; +import { ClaimFormat } from "../../generated/proto/cacti/satp/v02/common/message_pb"; + +export interface BungeeOptionsJSON { + instanceId: string; + pluginRegistryOptions?: PluginRegistryOptionsJSON; + keyPair?: { privateKey: string; publicKey: string }; + logLevel?: LogLevelDesc; + disableSignalHandlers?: true; +} + +// Type guard for ClaimFormat +export function isClaimFormat(obj: unknown): obj is ClaimFormat { + if (typeof obj !== "number") { + return false; + } + return Object.values(ClaimFormat).includes(obj); +} + +// Type guard for BungeeOptionsJSON +export function isBungeeOptionsJSON(obj: unknown): obj is BungeeOptionsJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "instanceId" in obj && + typeof objRecord.instanceId === "string" && + (!("pluginRegistryOptions" in obj) || + isPluginRegistryOptionsJSON(objRecord.pluginRegistryOptions)) && + (!("keyPair" in obj) || iskeyPairJSON(objRecord.keyPair)) && + (!("logLevel" in obj) || isLogLevelDesc(objRecord.logLevel)) && + (!("disableSignalHandlers" in obj) || + objRecord.disableSignalHandlers === true) + ); +} + +// Function to create IPluginBungeeHermesOptions from BungeeOptionsJSON +export function createBungeeOptions( + options: BungeeOptionsJSON, +): IPluginBungeeHermesOptions { + if (!options) { + throw new TypeError( + "Invalid bungeeOptions in NetworkConfig: " + JSON.stringify(options), + ); + } + + return { + instanceId: options.instanceId, + pluginRegistry: createPluginRegistry( + options.pluginRegistryOptions, + options.logLevel, + ), + keyPair: + options.keyPair?.privateKey && options.keyPair?.publicKey + ? { + privateKey: Buffer.from(options.keyPair.privateKey, "hex"), + publicKey: Buffer.from(options.keyPair.publicKey, "hex"), + } + : Secp256k1Keys.generateKeyPairsBuffer(), + logLevel: options.logLevel, + disableSignalHandlers: options.disableSignalHandlers, + }; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateEthereumConfig.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateEthereumConfig.ts new file mode 100644 index 0000000000..8f50aeda50 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateEthereumConfig.ts @@ -0,0 +1,167 @@ +import { EvmAsset } from "../../core/stage-services/satp-bridge/types/evm-asset"; +import { + Web3SigningCredential, + Web3SigningCredentialCactiKeychainRef, + Web3SigningCredentialGethKeychainPassword, + Web3SigningCredentialNone, + Web3SigningCredentialPrivateKeyHex, + Web3SigningCredentialType, +} from "@hyperledger/cactus-plugin-ledger-connector-ethereum"; +import { isAsset } from "./validateAsset"; +import { NetworkConfigJSON } from "../validateSatpBridgesConfig"; +import { + EthereumOptionsJSON, + isEthereumOptionsJSON, +} from "./validateEthereumOptions"; +import { + BungeeOptionsJSON, + isBungeeOptionsJSON, + isClaimFormat, +} from "./validateBungeeOptions"; +import { ClaimFormat } from "../../generated/proto/cacti/satp/v02/common/message_pb"; + +export interface EthereumConfigJSON extends NetworkConfigJSON { + keychainId: string; + signingCredential: Web3SigningCredential; + contractName: string; + contractAddress: string; + gas: number; + options: EthereumOptionsJSON; + bungeeOptions: BungeeOptionsJSON; + ethereumAssets?: EvmAsset[]; + claimFormat: ClaimFormat; +} + +// Type guard for EvmAsset +function isEvmAsset(obj: unknown): obj is EvmAsset { + const objRecord = obj as Record; + return ( + isAsset(obj) && + "contractAddress" in obj && + typeof objRecord.contractAddress === "string" + ); +} + +// Type guard for an array of EvmAsset +export function isEvmAssetArray(input: unknown): input is Array { + return Array.isArray(input) && input.every(isEvmAsset); +} + +// Type guard for Web3SigningCredentialType +function isWeb3SigningCredentialType( + value: unknown, +): value is Web3SigningCredentialType { + return ( + typeof value === "string" && + value !== null && + (value === Web3SigningCredentialType.CactiKeychainRef || + value === Web3SigningCredentialType.GethKeychainPassword || + value === Web3SigningCredentialType.PrivateKeyHex || + value === Web3SigningCredentialType.None) + ); +} + +// Type guard for Web3SigningCredentialCactiKeychainRef +function isWeb3SigningCredentialCactiKeychainRef( + obj: unknown, +): obj is Web3SigningCredentialCactiKeychainRef { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "ethAccount" in obj && + typeof objRecord.ethAccount === "string" && + "keychainEntryKey" in obj && + typeof objRecord.keychainEntryKey === "string" && + (!("keychainId" in obj) || typeof objRecord.keychainId === "string") && + "type" in obj && + isWeb3SigningCredentialType(objRecord.type) + ); +} + +// Type guard for Web3SigningCredentialGethKeychainPassword +function isWeb3SigningCredentialGethKeychainPassword( + obj: unknown, +): obj is Web3SigningCredentialGethKeychainPassword { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "ethAccount" in obj && + typeof objRecord.ethAccount === "string" && + "secret" in obj && + typeof objRecord.secret === "string" && + "type" in obj && + isWeb3SigningCredentialType(objRecord.type) + ); +} + +// Type guard for Web3SigningCredentialPrivateKeyHex +function isWeb3SigningCredentialPrivateKeyHex( + obj: unknown, +): obj is Web3SigningCredentialPrivateKeyHex { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "ethAccount" in obj && + typeof objRecord.ethAccount === "string" && + "secret" in obj && + typeof objRecord.secret === "string" && + "type" in obj && + isWeb3SigningCredentialType(objRecord.type) + ); +} + +// Type guard for Web3SigningCredentialNone +function isWeb3SigningCredentialNone( + obj: unknown, +): obj is Web3SigningCredentialNone { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "type" in obj && + isWeb3SigningCredentialType(objRecord.type) + ); +} + +// Type guard for Web3SigningCredential +function isWeb3SigningCredential(obj: unknown): obj is Web3SigningCredential { + if (!obj || typeof obj !== "object") { + return false; + } + return ( + isWeb3SigningCredentialCactiKeychainRef(obj) || + isWeb3SigningCredentialGethKeychainPassword(obj) || + isWeb3SigningCredentialPrivateKeyHex(obj) || + isWeb3SigningCredentialNone(obj) + ); +} + +// Type guard for EthereumConfigJSON +export function isEthereumConfigJSON(obj: unknown): obj is EthereumConfigJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "keychainId" in obj && + typeof objRecord.keychainId === "string" && + "contractName" in obj && + typeof objRecord.contractName === "string" && + "contractAddress" in obj && + typeof objRecord.contractAddress === "string" && + "gas" in obj && + typeof objRecord.gas === "number" && + "signingCredential" in obj && + isWeb3SigningCredential(objRecord.signingCredential) && + (!("ethereumAssets" in obj) || isEvmAssetArray(objRecord.ethereumAssets)) && + "bungeeOptions" in obj && + isBungeeOptionsJSON(objRecord.bungeeOptions) && + "options" in obj && + isEthereumOptionsJSON(objRecord.options) && + "claimFormat" in obj && + isClaimFormat(objRecord.claimFormat) + ); +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateEthereumOptions.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateEthereumOptions.ts new file mode 100644 index 0000000000..54fb3d7113 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateEthereumOptions.ts @@ -0,0 +1,59 @@ +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { isLogLevelDesc } from "../validateSatpLogLevel"; +import { + createPluginRegistry, + isPluginRegistryOptionsJSON, + PluginRegistryOptionsJSON, +} from "./validatePluginRegistryOptions"; +import { IPluginLedgerConnectorEthereumOptions } from "@hyperledger/cactus-plugin-ledger-connector-ethereum"; + +export interface EthereumOptionsJSON { + instanceId: string; + rpcApiHttpHost: string; + rpcApiWsHost: string; + pluginRegistryOptions: PluginRegistryOptionsJSON; + logLevel?: LogLevelDesc; +} + +export function isEthereumOptionsJSON( + obj: unknown, +): obj is EthereumOptionsJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "instanceId" in obj && + typeof objRecord.instanceId === "string" && + (!("rpcApiHttpHost" in obj) || + typeof objRecord.rpcApiHttpHost === "string") && + (!("rpcApiWsHost" in obj) || typeof objRecord.rpcApiWsHost === "string") && + "pluginRegistryOptions" in obj && + isPluginRegistryOptionsJSON(objRecord.pluginRegistryOptions) && + (!("logLevel" in obj) || isLogLevelDesc(objRecord.logLevel)) + ); +} + +// Function to create IPluginLedgerConnectorEthereumOptions from EthereumOptionsJSON +export function createEthereumOptions( + options: EthereumOptionsJSON, +): IPluginLedgerConnectorEthereumOptions { + if (!options) { + throw new TypeError( + "Invalid options in EthereumConfig: " + JSON.stringify(options), + ); + } + + const ethereumOptions: IPluginLedgerConnectorEthereumOptions = { + instanceId: options.instanceId, + rpcApiHttpHost: options.rpcApiHttpHost, + rpcApiWsHost: options.rpcApiWsHost, + pluginRegistry: createPluginRegistry( + options.pluginRegistryOptions, + options.logLevel, + ), + logLevel: options.logLevel, + }; + + return ethereumOptions; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateFabricConfig.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateFabricConfig.ts new file mode 100644 index 0000000000..9e5563ae27 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateFabricConfig.ts @@ -0,0 +1,128 @@ +import { + FabricSigningCredential, + FabricSigningCredentialType, + VaultTransitKey, + WebSocketKey, +} from "@hyperledger/cactus-plugin-ledger-connector-fabric"; + +import { FabricAsset } from "../../core/stage-services/satp-bridge/types/fabric-asset"; +import { isAsset } from "./validateAsset"; +import { NetworkConfigJSON } from "../validateSatpBridgesConfig"; +import { + FabricOptionsJSON, + isFabricOptionsJSON, +} from "./validateFabricOptions"; +import { + BungeeOptionsJSON, + isBungeeOptionsJSON, + isClaimFormat, +} from "./validateBungeeOptions"; +import { ClaimFormat } from "../../generated/proto/cacti/satp/v02/common/message_pb"; + +export interface FabricConfigJSON extends NetworkConfigJSON { + signingCredential: FabricSigningCredential; + channelName: string; + contractName: string; + options: FabricOptionsJSON; + bungeeOptions: BungeeOptionsJSON; + fabricAssets?: FabricAsset[]; + claimFormat: ClaimFormat; +} + +// Type guard for FabricAsset +function isFabricAsset(obj: unknown): obj is FabricAsset { + const objRecord = obj as Record; + return ( + isAsset(obj) && + "mspId" in obj && + typeof objRecord.mspId === "string" && + "channelName" in obj && + typeof objRecord.channelName === "string" + ); +} + +// Type guard for an array of FabricAsset +function isFabricAssetArray(input: unknown): input is Array { + return Array.isArray(input) && input.every(isFabricAsset); +} + +// Type guard for FabricSigningCredentialType +export function isFabricSigningCredentialType( + value: unknown, +): value is FabricSigningCredentialType { + return ( + typeof value === "string" && + value !== null && + (value === FabricSigningCredentialType.X509 || + value === FabricSigningCredentialType.VaultX509 || + value === FabricSigningCredentialType.WsX509) + ); +} + +// Type guard for VaultTransitKey +function isVaultTransitKey(obj: unknown): obj is VaultTransitKey { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "keyName" in obj && + typeof objRecord.keyName === "string" && + "token" in obj && + typeof objRecord.token === "string" + ); +} + +// Type guard for WebSocketKey +function isWebSocketKey(obj: unknown): obj is WebSocketKey { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "sessionId" in obj && + typeof objRecord.sessionId === "string" && + "signature" in obj && + typeof objRecord.signature === "string" + ); +} + +// Type guard for FabricSigningCredential +function isFabricSigningCredential( + obj: unknown, +): obj is FabricSigningCredential { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "keychainId" in obj && + typeof objRecord.keychainId === "string" && + "keychainRef" in obj && + typeof objRecord.keychainRef === "string" && + (!("type" in obj) || isFabricSigningCredentialType(objRecord.type)) && + (!("vaultTransitKey" in obj) || + isVaultTransitKey(objRecord.vaultTransitKey)) && + (!("webSocketKey" in obj) || isWebSocketKey(objRecord.webSocketKey)) + ); +} + +// Type guard for FabricConfigJSON +export function isFabricConfigJSON(obj: unknown): obj is FabricConfigJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "channelName" in obj && + typeof objRecord.channelName === "string" && + "contractName" in obj && + typeof objRecord.contractName === "string" && + "signingCredential" in obj && + isFabricSigningCredential(objRecord.signingCredential) && + (!("fabricAssets" in obj) || isFabricAssetArray(objRecord.fabricAssets)) && + "bungeeOptions" in obj && + isBungeeOptionsJSON(objRecord.bungeeOptions) && + "options" in obj && + isFabricOptionsJSON(objRecord.options) && + "claimFormat" in obj && + isClaimFormat(objRecord.claimFormat) + ); +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateFabricOptions.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateFabricOptions.ts new file mode 100644 index 0000000000..ff22f97474 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validateFabricOptions.ts @@ -0,0 +1,272 @@ +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { isLogLevelDesc } from "../validateSatpLogLevel"; +import { + createPluginRegistry, + isPluginRegistryOptionsJSON, + PluginRegistryOptionsJSON, +} from "./validatePluginRegistryOptions"; +import { + ConnectionProfile, + ConnectionProfileClient, + DefaultEventHandlerStrategy, + FabricSigningCredentialType, + GatewayDiscoveryOptions, + GatewayEventHandlerOptions, + IPluginLedgerConnectorFabricOptions, + IVaultConfig, + IWebSocketConfig, + SignPayloadCallback, +} from "@hyperledger/cactus-plugin-ledger-connector-fabric"; +import { isFabricSigningCredentialType } from "./validateFabricConfig"; + +interface ConfigJSON { + host: string; + port: number; + username: string; + privateKey: string; +} + +export interface FabricOptionsJSON { + instanceId: string; + peerBinary: string; + dockerBinary?: string; + goBinary?: string; + cliContainerGoPath?: string; + cliContainerEnv: NodeJS.ProcessEnv; + sshConfig: ConfigJSON; + readonly sshDebugOn?: boolean; + connectionProfile: ConnectionProfile; + discoveryOptions?: GatewayDiscoveryOptions; + eventHandlerOptions?: GatewayEventHandlerOptions; + supportedIdentity?: FabricSigningCredentialType[]; + vaultConfig?: IVaultConfig; + webSocketConfig?: IWebSocketConfig; + signCallback?: SignPayloadCallback; + pluginRegistryOptions: PluginRegistryOptionsJSON; + logLevel?: LogLevelDesc; +} + +// Type guard for NodeJS.ProcessEnv +function isProcessEnv(obj: unknown): obj is NodeJS.ProcessEnv { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + Object.keys(obj).every( + (key) => + typeof objRecord[key] === "string" || + typeof objRecord[key] === "undefined", + ) && + (!("TZ" in obj) || typeof objRecord.TZ === "string") + ); +} + +// Type guard for ConfigJSON +function isConfigJSON(obj: unknown): obj is ConfigJSON { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "host" in obj && + typeof objRecord.host === "string" && + "port" in obj && + typeof objRecord.port === "number" && + "username" in obj && + typeof objRecord.username === "string" && + "privateKey" in obj && + typeof objRecord.privateKey === "string" + ); +} + +// Type guard for ConnectionProfileClient +function isConnectionProfileClient( + obj: unknown, +): obj is ConnectionProfileClient { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + (!("organization" in obj) || typeof objRecord.organization === "object") + ); +} + +// Type guard for ConnectionProfile +export function isConnectionProfile(obj: unknown): obj is ConnectionProfile { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "name" in obj && + typeof objRecord.name === "string" && + "version" in obj && + typeof objRecord.version === "string" && + "organizations" in obj && + typeof objRecord.organizations === "object" && + "peers" in obj && + typeof objRecord.peers === "object" && + (!("x-type" in obj) || typeof objRecord["x-type"] === "string") && + (!("description" in obj) || typeof objRecord.description === "string") && + (!("client" in obj) || isConnectionProfileClient(objRecord.client)) && + (!("channels" in obj) || typeof objRecord.channels === "object") && + (!("orderers" in obj) || typeof objRecord.orderers === "object") && + (!("certificateAuthorities" in obj) || + typeof objRecord.certificateAuthorities === "object") + ); +} + +// Type guard for GatewayDiscoveryOptions +function isGatewayDiscoveryOptions( + obj: unknown, +): obj is GatewayDiscoveryOptions { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + (!("asLocalhost" in obj) || typeof objRecord.asLocalhost === "boolean") && + (!("enabled" in obj) || typeof objRecord.enabled === "boolean") + ); +} + +// Type guard for DefaultEventHandlerStrategy +function isDefaultEventHandlerStrategy( + obj: unknown, +): obj is DefaultEventHandlerStrategy { + return ( + obj !== null && + typeof obj === "string" && + (obj === DefaultEventHandlerStrategy.MspidScopeAllfortx || + obj === DefaultEventHandlerStrategy.MspidScopeAnyfortx || + obj === DefaultEventHandlerStrategy.NetworkScopeAllfortx || + obj === DefaultEventHandlerStrategy.NetworkScopeAnyfortx) + ); +} + +// Type guard for GatewayEventHandlerOptions +function isGatewayEventHandlerOptions( + obj: unknown, +): obj is GatewayEventHandlerOptions { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + "strategy" in obj && + isDefaultEventHandlerStrategy(objRecord.strategy) && + (!("commitTimeout" in obj) || + typeof objRecord.commitTimeout === "number") && + (!("endorseTimeout" in obj) || typeof objRecord.endorseTimeout === "number") + ); +} + +// Type guard for IVaultConfig +function isIVaultConfig(obj: unknown): obj is IVaultConfig { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + (!("endpoint" in obj) || typeof objRecord.endpoint === "string") && + (!("transitEngineMountPath" in obj) || + typeof objRecord.transitEngineMountPath === "string") + ); +} + +// Type guard for IWebSocketConfig +function isIWebSocketConfig(obj: unknown): obj is IWebSocketConfig { + const objRecord = obj as Record; + return ( + typeof obj === "object" && + obj !== null && + (!("endpoint" in obj) || typeof objRecord.endpoint === "string") && + (!("pathPrefix" in obj) || typeof objRecord.pathPrefix === "string") && + (!("strictSSL" in obj) || typeof objRecord.strictSSL === "boolean") + ); +} + +// Type guard for SignPayloadCallback +function isSignPayloadCallback(obj: unknown): obj is SignPayloadCallback { + return ( + typeof obj === "function" && + // Check if the function accepts exactly two parameters: + obj.length === 2 && + // Check if the function returns a promise + (obj as (...args: unknown[]) => unknown)(Buffer.alloc(0), {}) instanceof + Promise + ); +} + +// Type guard for FabricOptionsJSON +export function isFabricOptionsJSON(obj: unknown): obj is FabricOptionsJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "instanceId" in obj && + typeof objRecord.instanceId === "string" && + "peerBinary" in obj && + typeof objRecord.peerBinary === "string" && + (!("dockerBinary" in obj) || typeof objRecord.dockerBinary === "string") && + (!("goBinary" in obj) || typeof objRecord.goBinary === "string") && + (!("cliContainerGoPath" in obj) || + typeof objRecord.cliContainerGoPath === "string") && + "cliContainerEnv" in obj && + isProcessEnv(objRecord.cliContainerEnv) && + "sshConfig" in obj && + isConfigJSON(obj.sshConfig) && + (!("sshDebugOn" in obj) || typeof objRecord.sshDebugOn === "boolean") && + (!("discoveryOptions" in obj) || + isGatewayDiscoveryOptions(objRecord.discoveryOptions)) && + (!("eventHandlerOptions" in obj) || + isGatewayEventHandlerOptions(objRecord.eventHandlerOptions)) && + (!("supportedIdentity" in obj) || + isFabricSigningCredentialType(objRecord.supportedIdentity)) && + (!("vaultConfig" in obj) || isIVaultConfig(objRecord.vaultConfig)) && + (!("webSocketConfig" in obj) || + isIWebSocketConfig(objRecord.webSocketConfig)) && + (!("signCallback" in obj) || + isSignPayloadCallback(objRecord.signCallback)) && + "pluginRegistryOptions" in obj && + isPluginRegistryOptionsJSON(objRecord.pluginRegistryOptions) && + (!("logLevel" in obj) || isLogLevelDesc(objRecord.logLevel)) + ); +} + +// Function to create IPluginLedgerConnectorFabricOptions from FabricOptionsJSON +export function createFabricOptions( + options: FabricOptionsJSON, +): IPluginLedgerConnectorFabricOptions { + if (!options) { + throw new TypeError( + "Invalid options in FabricConfig: " + JSON.stringify(options), + ); + } + + const fabricOptions: IPluginLedgerConnectorFabricOptions = { + instanceId: options.instanceId, + peerBinary: options.peerBinary, + dockerBinary: options.dockerBinary, + goBinary: options.goBinary, + cliContainerGoPath: options.cliContainerGoPath, + cliContainerEnv: options.cliContainerEnv, + sshConfig: { + host: options.sshConfig.host, + port: options.sshConfig.port, + privateKey: options.sshConfig.privateKey, + username: options.sshConfig.username, + }, + sshDebugOn: options.sshDebugOn, + connectionProfile: options.connectionProfile, + discoveryOptions: options.discoveryOptions, + eventHandlerOptions: options.eventHandlerOptions, + supportedIdentity: options.supportedIdentity, + vaultConfig: options.vaultConfig, + webSocketConfig: options.webSocketConfig, + signCallback: options.signCallback, + pluginRegistry: createPluginRegistry( + options.pluginRegistryOptions, + options.logLevel, + ), + logLevel: options.logLevel, + }; + + return fabricOptions; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validatePluginRegistryOptions.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validatePluginRegistryOptions.ts new file mode 100644 index 0000000000..67afa3f1e1 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/bridges-config-validating-functions/validatePluginRegistryOptions.ts @@ -0,0 +1,123 @@ +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { isLogLevelDesc } from "../validateSatpLogLevel"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; + +export interface KeychainBackendEntry { + keychainEntry: string; + keychainEntryValue: string; +} + +export interface KeychainOptionsJSON { + instanceId: string; + keychainId: string; + logLevel?: LogLevelDesc; + backend?: KeychainBackendEntry[]; + contractName?: string; + contractString?: string; +} + +export interface PluginRegistryOptionsJSON { + logLevel?: LogLevelDesc; + plugins?: KeychainOptionsJSON[]; +} + +// Type guard for the KeychainBackendEntry +function isKeychainBackendEntry(obj: unknown): obj is KeychainBackendEntry { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "keychainEntry" in obj && + typeof objRecord.keychainEntry === "string" && + "keychainEntryValue" in obj && + typeof objRecord.keychainEntryValue === "string" + ); +} + +// Type guard for an array of KeychainBackendEntry +function isKeychainBackendEntryArray( + input: unknown, +): input is Array { + return Array.isArray(input) && input.every(isKeychainBackendEntry); +} + +// Type guard for KeychainOptionsJSON +function isKeychainOptionsJSON(obj: unknown): obj is KeychainOptionsJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + "instanceId" in obj && + typeof objRecord.instanceId === "string" && + "keychainId" in obj && + typeof objRecord.keychainId === "string" && + (!("logLevel" in obj) || isLogLevelDesc(objRecord.logLevel)) && + (!("backend" in obj) || isKeychainBackendEntryArray(objRecord.backend)) && + (!("contractName" in obj) || typeof objRecord.contractName === "string") && + (!("contractString" in obj) || typeof objRecord.contractString === "string") + ); +} + +// Type guard for an array of KeychainOptionsJSON +function isKeychainOptionsJSONArray( + input: unknown, +): input is Array { + return Array.isArray(input) && input.every(isKeychainOptionsJSON); +} + +// Type guard for PluginRegistryOptionsJSON +export function isPluginRegistryOptionsJSON( + obj: unknown, +): obj is PluginRegistryOptionsJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + (!("logLevel" in obj) || isLogLevelDesc(objRecord.logLevel)) && + (!("plugins" in obj) || isKeychainOptionsJSONArray(objRecord.plugins)) + ); +} + +type KeyValuePair = [string, string]; + +// Function to create PluginRegistry from PluginRegistryOptionsJSON +export function createPluginRegistry( + pluginRegistryOptions?: PluginRegistryOptionsJSON, + logLevel?: LogLevelDesc, +): PluginRegistry { + if ( + pluginRegistryOptions === undefined || + pluginRegistryOptions.plugins === undefined + ) { + return new PluginRegistry(); + } + + const plugins: PluginKeychainMemory[] = []; + pluginRegistryOptions.plugins.forEach(async (pluginJSON) => { + const entryValuesArray: KeyValuePair[] = []; + pluginJSON.backend?.forEach((entry) => { + entryValuesArray.push([entry.keychainEntry, entry.keychainEntryValue]); + }); + const backend: Map = new Map(entryValuesArray); + const newPluginKeychainMemory = new PluginKeychainMemory({ + instanceId: pluginJSON.instanceId, + keychainId: pluginJSON.keychainId, + logLevel, + backend, + }); + if (pluginJSON.contractName && pluginJSON.contractString) { + await newPluginKeychainMemory.set( + pluginJSON.contractName, + pluginJSON.contractString, + ); + } + plugins.push(newPluginKeychainMemory); + }); + return new PluginRegistry({ + plugins: plugins, + }); +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateKeyPairJSON.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateKeyPairJSON.ts new file mode 100644 index 0000000000..e05d7db305 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateKeyPairJSON.ts @@ -0,0 +1,32 @@ +interface keyPairJSON { + privateKey: string; + publicKey: string; +} + +// Type guard for the keyPairJSON +export function iskeyPairJSON(obj: unknown): obj is keyPairJSON { + return ( + typeof obj === "object" && + obj !== null && + "privateKey" in obj && + typeof (obj as Record).privateKey === "string" && + "publicKey" in obj && + typeof (obj as Record).publicKey === "string" + ); +} + +export function validateSatpKeyPairJSON(opts: { + readonly configValue: unknown; +}): keyPairJSON | undefined { + if (!opts || !opts.configValue) { + return; + } + + if (!iskeyPairJSON(opts.configValue)) { + throw new TypeError( + `Invalid config.gatewayKeyPair: ${JSON.stringify(opts.configValue)}.` + + ` Expected a keyPair object with 'publicKey' and 'privateKey' string fields.`, + ); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpBridgesConfig.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpBridgesConfig.ts new file mode 100644 index 0000000000..a1291f0482 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpBridgesConfig.ts @@ -0,0 +1,126 @@ +import { SupportedChain } from "../core/types"; +import { + BesuConfig, + EthereumConfig, + FabricConfig, + NetworkConfig, +} from "../types/blockchain-interaction"; +import { isSupportedChain } from "./validateSatpGatewayIdentity"; +import { isFabricConfigJSON } from "./bridges-config-validating-functions/validateFabricConfig"; +import { createFabricOptions } from "./bridges-config-validating-functions/validateFabricOptions"; +import { isBesuConfigJSON } from "./bridges-config-validating-functions/validateBesuConfig"; +import { createBesuOptions } from "./bridges-config-validating-functions/validateBesuOptions"; +import { isEthereumConfigJSON } from "./bridges-config-validating-functions/validateEthereumConfig"; +import { createEthereumOptions } from "./bridges-config-validating-functions/validateEthereumOptions"; +import { createBungeeOptions } from "./bridges-config-validating-functions/validateBungeeOptions"; + +export interface NetworkConfigJSON { + network: SupportedChain; +} + +// Type guard for NetworkConfigJSON +function isNetworkConfigJSON(obj: unknown): obj is NetworkConfigJSON { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + if (!("network" in obj) || !isSupportedChain(objRecord.network)) { + return false; + } + return ( + isFabricConfigJSON(objRecord) || + isBesuConfigJSON(objRecord) || + isEthereumConfigJSON(objRecord) + ); +} + +// Type guard for an array of NetworkConfigJSON +function isNetworkConfigJSONArray( + input: unknown, +): input is Array { + return Array.isArray(input) && input.every(isNetworkConfigJSON); +} + +export function validateSatpBridgesConfig(opts: { + readonly configValue: unknown; +}): Array { + if (!opts || !opts.configValue) { + return []; + } + + if (!isNetworkConfigJSONArray(opts.configValue)) { + throw new TypeError( + "Invalid config.bridgesConfig: " + JSON.stringify(opts.configValue), + ); + } + + const bridgesConfigParsed: NetworkConfig[] = []; + opts.configValue.forEach((config) => { + if (isFabricConfigJSON(config)) { + console.log("Validating FabricConfig BungeeOptions..."); + const bungeeOptions = createBungeeOptions(config.bungeeOptions); + console.log("FabricConfig BungeeOptions is valid."); + console.log("Validating FabricConfig Options..."); + const fabricOptions = createFabricOptions(config.options); + console.log("FabricConfig Options is valid."); + + const fabricConfig: FabricConfig = { + network: config.network, + signingCredential: config.signingCredential, + channelName: config.channelName, + contractName: config.contractName, + options: fabricOptions, + bungeeOptions: bungeeOptions, + fabricAssets: config.fabricAssets, + claimFormat: config.claimFormat, + }; + + bridgesConfigParsed.push(fabricConfig); + } else if (isBesuConfigJSON(config)) { + console.log("Validating BesuConfig BungeeOptions..."); + const bungeeOptions = createBungeeOptions(config.bungeeOptions); + console.log("BesuConfig BungeeOptions is valid."); + console.log("Validating BesuConfig Options..."); + const besuOptions = createBesuOptions(config.options); + console.log("BesuConfig Options is valid."); + + const besuConfig: BesuConfig = { + network: config.network, + keychainId: config.keychainId, + signingCredential: config.signingCredential, + contractName: config.contractName, + contractAddress: config.contractAddress, + gas: config.gas, + options: besuOptions, + bungeeOptions: bungeeOptions, + besuAssets: config.besuAssets, + claimFormat: config.claimFormat, + }; + + bridgesConfigParsed.push(besuConfig); + } else if (isEthereumConfigJSON(config)) { + console.log("Validating EthereumConfig BungeeOptions..."); + const bungeeOptions = createBungeeOptions(config.bungeeOptions); + console.log("EthereumConfig BungeeOptions is valid."); + console.log("Validating EthereumConfig Options..."); + const besuOptions = createEthereumOptions(config.options); + console.log("EthereumConfig Options is valid."); + + const ethereumConfig: EthereumConfig = { + network: config.network, + keychainId: config.keychainId, + signingCredential: config.signingCredential, + contractName: config.contractName, + contractAddress: config.contractAddress, + gas: config.gas, + options: besuOptions, + bungeeOptions: bungeeOptions, + ethereumAssets: config.ethereumAssets, + claimFormat: config.claimFormat, + }; + + bridgesConfigParsed.push(ethereumConfig); + } + }); + return bridgesConfigParsed; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpCounterPartyGateways.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpCounterPartyGateways.ts new file mode 100644 index 0000000000..a2e3a05cc7 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpCounterPartyGateways.ts @@ -0,0 +1,26 @@ +import { GatewayIdentity } from "../core/types"; +import { isGatewayIdentity } from "./validateSatpGatewayIdentity"; + +// Type guard for an array of GatewayIdentity +function isGatewayIdentityArray( + input: unknown, +): input is Array { + return Array.isArray(input) && input.every(isGatewayIdentity); +} + +export function validateSatpCounterPartyGateways(opts: { + readonly configValue: unknown; +}): GatewayIdentity[] { + if ( + !opts || + !opts.configValue || + typeof opts.configValue !== "object" || + !isGatewayIdentityArray(opts.configValue) + ) { + throw new TypeError( + "Invalid config.counterPartyGateways: " + + JSON.stringify(opts.configValue), + ); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpEnableOpenAPI.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpEnableOpenAPI.ts new file mode 100644 index 0000000000..6a1fd68f26 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpEnableOpenAPI.ts @@ -0,0 +1,14 @@ +export function validateSatpEnableOpenAPI(opts: { + readonly configValue: unknown; +}): boolean { + if (!opts || !opts.configValue) { + return true; + } + + if (typeof opts.configValue !== "boolean") { + throw new TypeError( + `Invalid config.enableOpenAPI: ${opts.configValue}. Expected a boolean`, + ); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpEnvironment.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpEnvironment.ts new file mode 100644 index 0000000000..cc81ee7736 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpEnvironment.ts @@ -0,0 +1,17 @@ +export function validateSatpEnvironment(opts: { + readonly configValue: unknown; +}): "development" | "production" { + if (!opts || !opts.configValue) { + return "development"; + } + + if ( + typeof opts.configValue !== "string" || + (opts.configValue !== "development" && opts.configValue !== "production") + ) { + throw new TypeError( + `Invalid config.environment: ${opts.configValue}. Expected "development" or "production"`, + ); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpGatewayIdentity.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpGatewayIdentity.ts new file mode 100644 index 0000000000..7db5bb3a8f --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpGatewayIdentity.ts @@ -0,0 +1,103 @@ +import { + Address, + CurrentDrafts, + DraftVersions, + GatewayIdentity, + SupportedChain, +} from "../core/types"; + +// Type guard for Address +function isAddress(input: unknown): input is Address { + if (typeof input !== "string") { + return false; + } + + if (input.startsWith("http://") || input.startsWith("https://")) { + return true; + } + + const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; + if (ipv4Pattern.test(input)) { + const octets = input.split(".").map(Number); + return octets.every((octet) => octet >= 0 && octet <= 255); + } + return false; +} + +// Type guard for DraftVersions +function isDraftVersions(obj: unknown): obj is DraftVersions { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return Object.values(CurrentDrafts).every( + (draft) => typeof objRecord[draft] === "string", + ); +} + +// Type guard for an array of DraftVersions +function isPrivacyDraftVersionsArray( + input: unknown, +): input is Array { + return Array.isArray(input) && input.every(isDraftVersions); +} + +// Type guard for SupportedChain +export function isSupportedChain(obj: unknown): obj is SupportedChain { + return ( + typeof obj === "string" && + obj !== null && + Object.values(SupportedChain).includes(obj as SupportedChain) + ); +} + +// Type guard for an array of SupportedChain +function isSupportedChainArray(input: unknown): input is Array { + return Array.isArray(input) && input.every(isSupportedChain); +} + +// Type guard for GatewayIdentity +export function isGatewayIdentity(obj: unknown): obj is GatewayIdentity { + return ( + typeof obj === "object" && + obj !== null && + "id" in obj && + typeof (obj as Record).id === "string" && + "version" in obj && + isPrivacyDraftVersionsArray((obj as Record).version) && + "supportedDLTs" in obj && + isSupportedChainArray((obj as Record).supportedDLTs) && + (!("pubKey" in obj) || + typeof (obj as Record).pubKey === "string") && + (!("name" in obj) || + typeof (obj as Record).name === "string") && + (!("proofID" in obj) || + typeof (obj as Record).proofID === "string") && + (!("gatewayServerPort" in obj) || + typeof (obj as Record).gatewayServerPort === "number") && + (!("gatewayClientPort" in obj) || + typeof (obj as Record).gatewayClientPort === "number") && + (!("gatewayOpenAPIPort" in obj) || + typeof (obj as Record).gatewayOpenAPIPort === + "number") && + (!("gatewayUIPort" in obj) || + typeof (obj as Record).gatewayUIPort === "number") && + (!("address" in obj) || isAddress((obj as Record).address)) + ); +} + +export function validateSatpGatewayIdentity(opts: { + readonly configValue: unknown; +}): GatewayIdentity { + if ( + !opts || + !opts.configValue || + typeof opts.configValue !== "object" || + !isGatewayIdentity(opts.configValue) + ) { + throw new TypeError( + "Invalid config.gid: " + JSON.stringify(opts.configValue), + ); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpLogLevel.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpLogLevel.ts new file mode 100644 index 0000000000..855969b342 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpLogLevel.ts @@ -0,0 +1,30 @@ +import { LogLevelDesc } from "@hyperledger/cactus-common"; + +// Type guard for LogLevelDesc +export function isLogLevelDesc(input: unknown): input is LogLevelDesc { + if (typeof input === "number") { + return Number.isInteger(input) && input >= 0 && input <= 5; + } + if (typeof input === "string") { + const normalizedInput = input.toUpperCase(); + return ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "SILENT"].includes( + normalizedInput, + ); + } + return false; +} + +export function validateSatpLogLevel(opts: { + readonly configValue: unknown; +}): LogLevelDesc { + if (!opts || !opts.configValue) { + return "INFO"; + } + + if (!isLogLevelDesc(opts.configValue)) { + throw new TypeError( + `Invalid config.logLevel: ${JSON.stringify(opts.configValue)}. Valid levels are: TRACE, DEBUG, INFO, WARN, ERROR, SILENT`, + ); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpMergePolicies.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpMergePolicies.ts new file mode 100644 index 0000000000..122132e5f5 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpMergePolicies.ts @@ -0,0 +1,17 @@ +import { + IMergePolicyValue, + isMergePolicyValueArray, +} from "@hyperledger/cactus-plugin-bungee-hermes/dist/lib/main/typescript/view-merging/merge-policies"; + +export function validateSatpMergePolicies(opts: { + readonly configValue: unknown; +}): Array { + if (!opts || !opts.configValue) { + return []; + } + + if (!isMergePolicyValueArray(opts.configValue)) { + throw new TypeError(`Invalid config.mergePolicies: ${opts.configValue}.`); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpPrivacyPolicies.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpPrivacyPolicies.ts new file mode 100644 index 0000000000..fd14657464 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpPrivacyPolicies.ts @@ -0,0 +1,17 @@ +import { + IPrivacyPolicyValue, + isPrivacyPolicyValueArray, +} from "@hyperledger/cactus-plugin-bungee-hermes/dist/lib/main/typescript/view-creation/privacy-policies"; + +export function validateSatpPrivacyPolicies(opts: { + readonly configValue: unknown; +}): Array { + if (!opts || !opts.configValue) { + return []; + } + + if (!isPrivacyPolicyValueArray(opts.configValue)) { + throw new TypeError(`Invalid config.privacyPolicies: ${opts.configValue}.`); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpValidationOptions.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpValidationOptions.ts new file mode 100644 index 0000000000..c4f78373a6 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/config-validating-functions/validateSatpValidationOptions.ts @@ -0,0 +1,74 @@ +import { ValidatorOptions } from "class-validator"; + +interface ValidationError { + target?: boolean; + value?: boolean; +} + +// Type guard for strings[] +function isStringArray(input: unknown): input is string[] { + return ( + Array.isArray(input) && input.every((item) => typeof item === "string") + ); +} + +// Type guard for ValidationError +function isValidationError(obj: unknown): obj is ValidationError { + if (typeof obj !== "object" || obj === null) { + return false; + } + const objRecord = obj as Record; + return ( + (!("target" in obj) || typeof objRecord.target === "boolean") && + (!("value" in obj) || typeof objRecord.value === "boolean") + ); +} + +// Type guard for ValidatorOptions +function isValidatorOptions(obj: unknown): obj is ValidatorOptions { + if (typeof obj !== "object" || obj === null) { + return false; + } + + const objRecord = obj as Record; + + return ( + (!("enableDebugMessages" in obj) || + typeof objRecord.enableDebugMessages === "boolean") && + (!("skipUndefinedProperties" in obj) || + typeof objRecord.skipUndefinedProperties === "boolean") && + (!("skipNullProperties" in obj) || + typeof objRecord.skipNullProperties === "boolean") && + (!("skipMissingProperties" in obj) || + typeof objRecord.skipMissingProperties === "boolean") && + (!("whitelist" in obj) || typeof objRecord.whitelist === "boolean") && + (!("forbidNonWhitelisted" in obj) || + typeof objRecord.forbidNonWhitelisted === "boolean") && + (!("groups" in obj) || isStringArray(objRecord.groups)) && + (!("always" in obj) || typeof objRecord.always === "boolean") && + (!("strictGroups" in obj) || typeof objRecord.strictGroups === "boolean") && + (!("dismissDefaultMessages" in obj) || + typeof objRecord.dismissDefaultMessages === "boolean") && + (!("validationError" in obj) || + isValidationError(objRecord.validationError)) && + (!("forbidUnknownValues" in obj) || + typeof objRecord.forbidUnknownValues === "boolean") && + (!("stopAtFirstError" in obj) || + typeof objRecord.stopAtFirstError === "boolean") + ); +} + +export function validateSatpValidationOptions(opts: { + readonly configValue: unknown; +}): ValidatorOptions { + if (!opts || !opts.configValue) { + return {}; + } + + if (!isValidatorOptions(opts.configValue)) { + throw new TypeError( + `Invalid config.validationOptions: ${opts.configValue}.`, + ); + } + return opts.configValue; +} diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/constants.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/constants.ts index 176b8a960d..805f2c6760 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/constants.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/constants.ts @@ -3,3 +3,6 @@ export const DEFAULT_PORT_GATEWAY_CLIENT = DEFAULT_PORT_GATEWAY_SERVER + 1; export const DEFAULT_PORT_GATEWAY_UI = DEFAULT_PORT_GATEWAY_SERVER + 2; export const DEFAULT_PORT_GATEWAY_API = 4010; export const SATP_VERSION = "v02"; +export const SATP_CORE_VERSION = "v02"; +export const SATP_ARCHITETURE_VERSION = "v02"; +export const SATP_CRASH_VERSION = "v02"; diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage0-handler.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage0-handler.ts index fd1538c74a..79dba6cf99 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage0-handler.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage0-handler.ts @@ -100,6 +100,7 @@ export class Stage0SATPHandler implements SATPHandler { return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToCreateMessageError(fnTag, "NewSessionResponse", error); } } @@ -139,6 +140,7 @@ export class Stage0SATPHandler implements SATPHandler { return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToCreateMessageError(fnTag, "NewSessionResponse", error); } } @@ -188,6 +190,7 @@ export class Stage0SATPHandler implements SATPHandler { return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "NewSessionRequest", error); } } @@ -228,6 +231,7 @@ export class Stage0SATPHandler implements SATPHandler { return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "PreSATPTransferRequest", error); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage1-handler.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage1-handler.ts index 4f059edc34..7e0490d722 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage1-handler.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage1-handler.ts @@ -95,6 +95,7 @@ export class Stage1SATPHandler implements SATPHandler { } return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "TransferProposalRequest", error); } } @@ -134,6 +135,7 @@ export class Stage1SATPHandler implements SATPHandler { } return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "TransferCommenceResponse", error); } } @@ -181,6 +183,7 @@ export class Stage1SATPHandler implements SATPHandler { } return requestTransferProposal; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "TransferProposalRequest", error); } } @@ -218,6 +221,7 @@ export class Stage1SATPHandler implements SATPHandler { return requestTransferCommence; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "TransferCommenceRequest", error); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage2-handler.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage2-handler.ts index cdff4b874f..e5f89b03e6 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage2-handler.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage2-handler.ts @@ -87,6 +87,7 @@ export class Stage2SATPHandler implements SATPHandler { } return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError( fnTag, "LockAssertionImplementation", @@ -138,6 +139,7 @@ export class Stage2SATPHandler implements SATPHandler { } return request; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "LockAssertionRequest", error); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage3-handler.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage3-handler.ts index 571f3a403c..bf3a484ed6 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage3-handler.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-handlers/stage3-handler.ts @@ -92,6 +92,7 @@ export class Stage3SATPHandler implements SATPHandler { return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "CommitPreparationRequest", error); } } @@ -135,6 +136,7 @@ export class Stage3SATPHandler implements SATPHandler { return message; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError( fnTag, "CommitFinalAssertionRequest", @@ -165,6 +167,7 @@ export class Stage3SATPHandler implements SATPHandler { return new Empty({}); } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "TransferCompleteRequest", error); } } @@ -219,6 +222,7 @@ export class Stage3SATPHandler implements SATPHandler { } return request; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "CommitPreparationRequest", error); } } @@ -257,6 +261,7 @@ export class Stage3SATPHandler implements SATPHandler { } return request; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError( fnTag, "CommitFinalAssertionRequest", @@ -294,6 +299,7 @@ export class Stage3SATPHandler implements SATPHandler { } return request; } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "TransferCompleteRequest", error); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage0-client-service.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage0-client-service.ts index f8a3d485b0..f84684b64d 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage0-client-service.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage0-client-service.ts @@ -324,6 +324,7 @@ export class Stage0ClientService extends SATPService { sign(this.Signer, sessionData.senderWrapAssertionClaim.receipt), ); } catch (error) { + this.logger.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "WrapToken"); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage2-client-service.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage2-client-service.ts index f15d675bd4..a72238b5e7 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage2-client-service.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage2-client-service.ts @@ -241,6 +241,7 @@ export class Stage2ClientService extends SATPService { sign(this.Signer, sessionData.lockAssertionClaim.receipt), ); } catch (error) { + this.logger.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "LockAsset"); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage3-client-service.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage3-client-service.ts index 99169f647a..1d6db94301 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage3-client-service.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/client/stage3-client-service.ts @@ -504,6 +504,7 @@ export class Stage3ClientService extends SATPService { sign(this.Signer, sessionData.burnAssertionClaim.receipt), ); } catch (error) { + this.logger.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "BurnAsset"); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/server/stage0-server-service.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/server/stage0-server-service.ts index 8c0f103db5..f2bcf7b36e 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/server/stage0-server-service.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/server/stage0-server-service.ts @@ -405,6 +405,7 @@ export class Stage0ServerService extends SATPService { sign(this.Signer, sessionData.receiverWrapAssertionClaim.receipt), ); } catch (error) { + this.Log.debug(`Crash in ${fnTag}`, error); throw new Error(`${fnTag}, Failed to process Wrap Asset ${error}`); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/server/stage3-server-service.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/server/stage3-server-service.ts index b898d5eba5..1b4681f5dd 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/server/stage3-server-service.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/core/stage-services/server/stage3-server-service.ts @@ -435,6 +435,7 @@ export class Stage3ServerService extends SATPService { sign(this.Signer, sessionData.mintAssertionClaim.receipt), ); } catch (error) { + this.logger.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "MintAsset"); } } @@ -491,6 +492,7 @@ export class Stage3ServerService extends SATPService { sign(this.Signer, sessionData.assignmentAssertionClaim.receipt), ); } catch (error) { + this.logger.debug(`Crash in ${fnTag}`, error); throw new FailedToProcessError(fnTag, "AssignAsset"); } } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/gol/satp-bridges-manager.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/gol/satp-bridges-manager.ts index 536c407b44..c505dec979 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/gol/satp-bridges-manager.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/gol/satp-bridges-manager.ts @@ -53,7 +53,7 @@ export class SATPBridgesManager { bridge = new FabricBridge(bridgeConfig as FabricConfig, this.level); break; case SupportedChain.BESU: - bridge = new BesuBridge(bridgeConfig as BesuConfig); + bridge = new BesuBridge(bridgeConfig as BesuConfig, this.level); break; case SupportedChain.EVM: bridge = new EthereumBridge(bridgeConfig as EthereumConfig); diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts index 1b830bb5ed..5f02d4d2ae 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts @@ -1,63 +1,140 @@ #!/usr/bin/env node -import { LoggerProvider, LogLevelDesc } from "@hyperledger/cactus-common"; +import { LoggerProvider } from "@hyperledger/cactus-common"; import { SATPGateway, SATPGatewayConfig } from "./plugin-satp-hermes-gateway"; -import { SupportedChain, DraftVersions, CurrentDrafts } from "./core/types"; -import dotenv from "dotenv"; +import fs from "fs-extra"; -export async function launchGateway(env?: NodeJS.ProcessEnv): Promise { - dotenv.config(); +import { validateSatpGatewayIdentity } from "./config-validating-functions/validateSatpGatewayIdentity"; +import { validateSatpCounterPartyGateways } from "./config-validating-functions/validateSatpCounterPartyGateways"; +import { validateSatpLogLevel } from "./config-validating-functions/validateSatpLogLevel"; +import { validateSatpEnvironment } from "./config-validating-functions/validateSatpEnvironment"; +import { validateSatpEnableOpenAPI } from "./config-validating-functions/validateSatpEnableOpenAPI"; +import { validateSatpValidationOptions } from "./config-validating-functions/validateSatpValidationOptions"; +import { validateSatpPrivacyPolicies } from "./config-validating-functions/validateSatpPrivacyPolicies"; +import { validateSatpMergePolicies } from "./config-validating-functions/validateSatpMergePolicies"; +import { validateSatpKeyPairJSON } from "./config-validating-functions/validateKeyPairJSON"; +import { validateSatpBridgesConfig } from "./config-validating-functions/validateSatpBridgesConfig"; +import path from "path"; - const logLevel: LogLevelDesc = - (env?.SATP_LOG_LEVEL as LogLevelDesc) || "INFO"; +export async function launchGateway(): Promise { const logger = LoggerProvider.getOrCreate({ - level: logLevel, + level: "DEBUG", label: "SATP-Gateway", }); - // Parse the version string into DraftVersions object - const parseVersion = (versionString: string): DraftVersions => { - const [Core, Architecture, Crash] = versionString.split(","); - return { - [CurrentDrafts.Core]: Core, - [CurrentDrafts.Architecture]: Architecture, - [CurrentDrafts.Crash]: Crash, - }; - }; + logger.debug("Checking for configuration file..."); + let configFilePath: string | undefined; + + const possiblePaths = [ + "/opt/cacti/satp-hermes/gateway-config.json", + "/gateway-config.json", + path.join(process.cwd(), "gateway-config.json"), + ]; + + for (const path of possiblePaths) { + if (fs.existsSync(path)) { + configFilePath = path; + logger.debug(`Found gateway-config.json at: ${path}`); + break; + } + } + + if (!configFilePath) { + throw new Error( + `Could not find gateway-config.json in any of the expected locations: ${possiblePaths.join(", ")}`, + ); + } + + logger.debug(`Reading configuration from: ${configFilePath}`); + const config = await fs.readJson(configFilePath); + logger.debug(`Configuration read OK`); + + // validating gateway-config.json + + logger.debug("Validating SATP Gateway Identity..."); + const gid = validateSatpGatewayIdentity({ + configValue: config.gid, + }); + logger.debug("Valid SATP Gateway Identity"); + + logger.debug("Validating SATP Counter Party Gateways..."); + const counterPartyGateways = validateSatpCounterPartyGateways({ + configValue: config.counterPartyGateways, + }); + logger.debug("Valid SATP Counter Party Gateways"); + + logger.debug("Validating SATP Log Level..."); + const logLevel = validateSatpLogLevel({ + configValue: config.logLevel, + }); + logger.debug("SATP Log Level is valid."); + + logger.debug("Validating SATP Environment..."); + const environment = validateSatpEnvironment({ + configValue: config.environment, + }); + logger.debug("SATP Environment is valid."); + + logger.debug("Validating SATP Enable OpenAPI..."); + const enableOpenAPI = validateSatpEnableOpenAPI({ + configValue: config.enableOpenAPI, + }); + logger.debug("SATP Enable OpenAPI is valid."); + + logger.debug("Validating SATP Validation Options..."); + const validationOptions = validateSatpValidationOptions({ + configValue: config.validationOptions, + }); + logger.debug("SATP Validation Options is valid."); + + logger.debug("Validating SATP Privacy Policies..."); + const privacyPolicies = validateSatpPrivacyPolicies({ + configValue: config.validationOptions, + }); + logger.debug("SATP Privacy Policies is valid."); + privacyPolicies.forEach((p, i) => + logger.debug("Privacy Policy #%d => %o", i, p), + ); + + logger.debug("Validating SATP Merge Policies..."); + const mergePolicies = validateSatpMergePolicies({ + configValue: config.mergePolicies, + }); + logger.debug("SATP Merge Policies is valid."); + mergePolicies.forEach((p, i) => logger.debug("Merge Policy #%d => %o", i, p)); + + logger.debug("Validating SATP KeyPair..."); + const keyPair = validateSatpKeyPairJSON({ + configValue: config.gatewayKeyPair, + }); + logger.debug("SATP KeyPair is valid."); + + logger.debug("Validating SATP Bridges Config..."); + const bridgesConfig = validateSatpBridgesConfig({ + configValue: config.bridgesConfig, + }); + logger.debug("SATP Bridges Config is valid."); + logger.debug("Creating SATPGatewayConfig..."); const gatewayConfig: SATPGatewayConfig = { - gid: { - id: env?.SATP_GATEWAY_ID || "", - name: env?.SATP_GATEWAY_NAME, - version: env?.SATP_GATEWAY_VERSION - ? [parseVersion(env.SATP_GATEWAY_VERSION)] - : [], - supportedDLTs: - env?.SATP_SUPPORTED_DLTS?.split(",").map( - (dlt) => dlt as SupportedChain, - ) || [], - proofID: env?.SATP_PROOF_ID, - gatewayServerPort: parseInt(env?.SATP_GATEWAY_SERVER_PORT || "0", 10), - gatewayClientPort: parseInt(env?.SATP_GATEWAY_CLIENT_PORT || "0", 10), - gatewayOpenAPIPort: parseInt(env?.DEFAULT_PORT_GATEWAY_API || "0", 10), - address: env?.SATP_GATEWAY_ADDRESS as - | `http://${string}` - | `https://${string}` - | undefined, - }, - counterPartyGateways: JSON.parse(env?.SATP_COUNTER_PARTY_GATEWAYS || "[]"), + gid, + counterPartyGateways, logLevel, - keyPair: { - privateKey: Buffer.from(env?.SATP_PRIVATE_KEY || "", "hex"), - publicKey: Buffer.from(env?.SATP_PUBLIC_KEY || "", "hex"), - }, - environment: - (env?.SATP_NODE_ENV as "development" | "production") || "development", - enableOpenAPI: env?.SATP_ENABLE_OPEN_API === "true", - validationOptions: JSON.parse(env?.SATP_VALIDATION_OPTIONS || "{}"), - privacyPolicies: JSON.parse(env?.SATP_PRIVACY_POLICIES || "[]"), - mergePolicies: JSON.parse(env?.SATP_MERGE_POLICIES || "[]"), + keyPair: + keyPair === undefined + ? undefined + : { + publicKey: Buffer.from(keyPair.publicKey, "hex"), + privateKey: Buffer.from(keyPair.privateKey, "hex"), + }, + environment, + enableOpenAPI, + validationOptions, + privacyPolicies, + mergePolicies, + bridgesConfig, }; + logger.debug("SATPGatewayConfig created successfully"); const gateway = new SATPGateway(gatewayConfig); try { @@ -72,5 +149,5 @@ export async function launchGateway(env?: NodeJS.ProcessEnv): Promise { } if (require.main === module) { - launchGateway(process.env); + launchGateway(); } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway.ts index 38ed98a4b7..48e54902e7 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway.ts @@ -17,8 +17,6 @@ import { Contains, } from "class-validator"; -import path from "path"; - import { SATPGatewayConfig, GatewayIdentity, @@ -36,6 +34,9 @@ import { DEFAULT_PORT_GATEWAY_API, DEFAULT_PORT_GATEWAY_CLIENT, DEFAULT_PORT_GATEWAY_SERVER, + SATP_ARCHITETURE_VERSION, + SATP_CORE_VERSION, + SATP_CRASH_VERSION, } from "./core/constants"; import { bufArray2HexStr } from "./gateway-utils"; import { @@ -43,7 +44,6 @@ import { IRemoteLogRepository, } from "./repository/interfaces/repository"; import { BLODispatcher, BLODispatcherOptions } from "./blo/dispatcher"; -import fs from "fs"; import swaggerUi, { JsonObject } from "swagger-ui-express"; import { IPluginWebService, @@ -56,6 +56,9 @@ import { } from "./gol/satp-bridges-manager"; import bodyParser from "body-parser"; import cors from "cors"; + +import * as OAS from "../json/openapi-blo-bundled.json"; + export class SATPGateway implements IPluginWebService, ICactusPlugin { // todo more checks; example port from config is between 3000 and 9000 @IsDefined() @@ -170,11 +173,7 @@ export class SATPGateway implements IPluginWebService, ICactusPlugin { this.BLODispatcher = new BLODispatcher(dispatcherOps); this.OAPIServerEnabled = this.config.enableOpenAPI ?? true; - const specPath = path.join(__dirname, "../json/openapi-blo-bundled.json"); - this.OAS = JSON.parse(fs.readFileSync(specPath, "utf8")); - if (!this.OAS) { - this.logger.warn("Error loading OAS"); - } + this.OAS = OAS; } /* ICactus Plugin methods */ @@ -274,19 +273,16 @@ export class SATPGateway implements IPluginWebService, ICactusPlugin { name: id, version: [ { - Core: "v02", - Architecture: "v02", - Crash: "v02", + Core: SATP_CORE_VERSION, + Architecture: SATP_ARCHITETURE_VERSION, + Crash: SATP_CRASH_VERSION, }, ], - supportedDLTs: [ - SupportedChain.FABRIC, - SupportedChain.BESU, - SupportedChain.EVM, - ], + supportedDLTs: [], proofID: "mockProofID1", gatewayServerPort: DEFAULT_PORT_GATEWAY_SERVER, gatewayClientPort: DEFAULT_PORT_GATEWAY_CLIENT, + gatewayOpenAPIPort: DEFAULT_PORT_GATEWAY_API, address: "http://localhost", }; } else { @@ -307,19 +303,15 @@ export class SATPGateway implements IPluginWebService, ICactusPlugin { if (!pluginOptions.gid.version) { pluginOptions.gid.version = [ { - Core: "v02", - Architecture: "v02", - Crash: "v02", + Core: SATP_CORE_VERSION, + Architecture: SATP_ARCHITETURE_VERSION, + Crash: SATP_CRASH_VERSION, }, ]; } if (!pluginOptions.gid.supportedDLTs) { - pluginOptions.gid.supportedDLTs = [ - SupportedChain.FABRIC, - SupportedChain.BESU, - SupportedChain.EVM, - ]; + pluginOptions.gid.supportedDLTs = []; } if (!pluginOptions.gid.proofID) { @@ -337,23 +329,40 @@ export class SATPGateway implements IPluginWebService, ICactusPlugin { if (!pluginOptions.gid.gatewayOpenAPIPort) { pluginOptions.gid.gatewayOpenAPIPort = DEFAULT_PORT_GATEWAY_API; } + } - if (!pluginOptions.logLevel) { - pluginOptions.logLevel = "DEBUG"; - } + if (!pluginOptions.counterPartyGateways) { + pluginOptions.counterPartyGateways = []; + } - if (!pluginOptions.environment) { - pluginOptions.environment = "development"; - } + if (!pluginOptions.logLevel) { + pluginOptions.logLevel = "DEBUG"; + } - if (!pluginOptions.enableOpenAPI) { - pluginOptions.enableOpenAPI = true; - } + if (!pluginOptions.environment) { + pluginOptions.environment = "development"; + } - if (!pluginOptions.validationOptions) { - // do nothing - } + if (!pluginOptions.enableOpenAPI) { + pluginOptions.enableOpenAPI = true; + } + + if (!pluginOptions.validationOptions) { + pluginOptions.validationOptions = {}; + } + + if (!pluginOptions.privacyPolicies) { + pluginOptions.privacyPolicies = []; } + + if (!pluginOptions.mergePolicies) { + pluginOptions.mergePolicies = []; + } + + if (!pluginOptions.bridgesConfig) { + pluginOptions.bridgesConfig = []; + } + return pluginOptions; } diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/web-services/health-check-endpoint.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/web-services/health-check-endpoint.ts new file mode 100644 index 0000000000..63b416d61f --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/web-services/health-check-endpoint.ts @@ -0,0 +1,79 @@ +import type { Express, Request, Response } from "express"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { + Checks, + Logger, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; +import { IRequestOptions } from "../core/types"; + +export class HealthCheckEndpointV1 implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "HealthCheckEndpointV1"; + private readonly log: Logger; + + public get className(): string { + return HealthCheckEndpointV1.CLASS_NAME; + } + + constructor(public readonly options: IRequestOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.dispatcher, `${fnTag} arg options.connector`); + const level = "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getPath(): string { + return "/api/v1/@hyperledger/cactus-plugin-satp-hermes/healthcheck"; + } + + public getVerbLowerCase(): string { + return "get"; + } + + public getOperationId(): string { + return "HealthCheck"; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: false, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getPath()}`; + this.log.debug(reqTag); + try { + res.status(200).json({ status: "OK" }); + } catch (ex) { + this.log.error(`Crash while serving ${reqTag}`, ex); + res.status(500).json({ + message: "Internal Server Error", + error: ex?.stack || ex?.message, + }); + } + } +} diff --git a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/gateway-init-startup.test.ts b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/gateway-init-startup.test.ts index 54766843c0..dff6467ebc 100644 --- a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/gateway-init-startup.test.ts +++ b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/gateway-init-startup.test.ts @@ -18,6 +18,11 @@ import { ShutdownHook, SupportedChain, } from "./../../../main/typescript/core/types"; +import { + DEFAULT_PORT_GATEWAY_API, + DEFAULT_PORT_GATEWAY_CLIENT, + DEFAULT_PORT_GATEWAY_SERVER, +} from "../../../main/typescript/core/constants"; const logLevel: LogLevelDesc = "DEBUG"; const logger = LoggerProvider.getOrCreate({ @@ -58,12 +63,10 @@ describe("SATPGateway initialization", () => { Crash: "v02", }, ]); - expect(identity.supportedDLTs).toEqual([ - SupportedChain.FABRIC, - SupportedChain.BESU, - ]); - expect(identity.proofID).toBe("mockProofID1"); - expect(identity.gatewayServerPort).toBe(3010); + expect(identity.supportedDLTs).toEqual([]); + expect(identity.gatewayServerPort).toBe(DEFAULT_PORT_GATEWAY_SERVER); + expect(identity.gatewayClientPort).toBe(DEFAULT_PORT_GATEWAY_CLIENT); + expect(identity.gatewayOpenAPIPort).toBe(DEFAULT_PORT_GATEWAY_API); expect(identity.address).toBe("http://localhost"); }); @@ -214,12 +217,10 @@ describe("SATPGateway startup", () => { Crash: "v02", }, ]); - expect(identity.supportedDLTs).toEqual([ - SupportedChain.FABRIC, - SupportedChain.BESU, - ]); - expect(identity.proofID).toBe("mockProofID1"); - expect(identity.gatewayClientPort).toBe(3011); + expect(identity.supportedDLTs).toEqual([]); + expect(identity.gatewayServerPort).toBe(DEFAULT_PORT_GATEWAY_SERVER); + expect(identity.gatewayClientPort).toBe(DEFAULT_PORT_GATEWAY_CLIENT); + expect(identity.gatewayOpenAPIPort).toBe(DEFAULT_PORT_GATEWAY_API); expect(identity.address).toBe("http://localhost"); }); @@ -281,8 +282,8 @@ describe("SATPGateway startup", () => { ], supportedDLTs: [SupportedChain.FABRIC, SupportedChain.BESU], proofID: "mockProofID10", - gatewayClientPort: 3010, - gatewayServerPort: 3011, + gatewayServerPort: 13010, + gatewayClientPort: 13011, gatewayOpenAPIPort: 4010, address: "http://localhost", }, @@ -291,7 +292,8 @@ describe("SATPGateway startup", () => { expect(gateway).toBeInstanceOf(SATPGateway); const identity = gateway.Identity; - expect(identity.gatewayClientPort).toBe(3010); + expect(identity.gatewayServerPort).toBe(13010); + expect(identity.gatewayClientPort).toBe(13011); expect(identity.address).toBe("http://localhost"); await gateway.startup(); await gateway.shutdown(); diff --git a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-end-to-end-transfer-1-gateway-dockerization.test.ts b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-end-to-end-transfer-1-gateway-dockerization.test.ts new file mode 100644 index 0000000000..509a5e0ca8 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-end-to-end-transfer-1-gateway-dockerization.test.ts @@ -0,0 +1,1305 @@ +import { randomUUID as uuidv4 } from "node:crypto"; +import "jest-extended"; + +import { + IListenOptions, + LogLevelDesc, + LoggerProvider, + Secp256k1Keys, + Servers, +} from "@hyperledger/cactus-common"; +// import { v4 as internalIpV4 } from "internal-ip"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + ChainCodeProgrammingLanguage, + Configuration, + DefaultEventHandlerStrategy, + FabricSigningCredential, + FileBase64, + IPluginLedgerConnectorFabricOptions, + PluginLedgerConnectorFabric, + DefaultApi as FabricApi, + FabricContractInvocationType, + ConnectionProfile, +} from "@hyperledger/cactus-plugin-ledger-connector-fabric"; +import http, { Server } from "http"; +import fs from "fs-extra"; + +import { + pruneDockerAllIfGithubAction, + Containers, + FabricTestLedgerV1, + BesuTestLedger, + FABRIC_25_LTS_AIO_FABRIC_VERSION, + FABRIC_25_LTS_AIO_IMAGE_VERSION, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + SATPGatewayRunner, + ISATPGatewayRunnerConstructorOptions, +} from "@hyperledger/cactus-test-tooling"; +import bodyParser from "body-parser"; +import express from "express"; +import { DiscoveryOptions, X509Identity } from "fabric-network"; +import { AddressInfo } from "net"; +import path from "path"; +import { + BesuConfig, + FabricConfig, +} from "../../../main/typescript/types/blockchain-interaction"; +import { IPluginBungeeHermesOptions } from "@hyperledger/cactus-plugin-bungee-hermes"; +import { Account } from "web3-core"; +import { + EthContractInvocationType, + IPluginLedgerConnectorBesuOptions, + PluginLedgerConnectorBesu, + ReceiptType, + Web3SigningCredentialType, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; +import Web3 from "web3"; +import SATPContract from "../../solidity/generated/satp-erc20.sol/SATPContract.json"; +import SATPWrapperContract from "../../../solidity/generated/satp-wrapper.sol/SATPWrapperContract.json"; +import { TransactRequest, Asset } from "../../../main/typescript"; +import { + Address, + GatewayIdentity, + SupportedChain, +} from "../../../main/typescript/core/types"; +import FabricSATPInteraction from "../../../test/typescript/fabric/satp-erc20-interact.json"; +import BesuSATPInteraction from "../../solidity/satp-erc20-interact.json"; +import { createClient } from "../test-utils"; +import { + DEFAULT_PORT_GATEWAY_API, + DEFAULT_PORT_GATEWAY_CLIENT, + DEFAULT_PORT_GATEWAY_SERVER, + SATP_CORE_VERSION, + SATP_ARCHITETURE_VERSION, + SATP_CRASH_VERSION, +} from "../../../main/typescript/core/constants"; +import { ClaimFormat } from "../../../main/typescript/generated/proto/cacti/satp/v02/common/message_pb"; +const logLevel: LogLevelDesc = "DEBUG"; +const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: "BUNGEE - Hermes", +}); + +let fabricServer: Server; + +let besuLedger: BesuTestLedger; + +let fabricLedger: FabricTestLedgerV1; +let fabricSigningCredential: FabricSigningCredential; +let bridgeFabricSigningCredential: FabricSigningCredential; +let configFabric: Configuration; +let fabricChannelName: string; + +const FABRIC_ASSET_ID = uuidv4(); + +const BRIDGE_ID = + "x509::/OU=org2/OU=client/OU=department1/CN=bridge::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com"; + +let clientId: string; +let fabricConfig: FabricConfig; +let pluginOptionsFabricBridge: IPluginLedgerConnectorFabricOptions; +let pluginBungeeFabricOptions: IPluginBungeeHermesOptions; + +let erc20TokenContract: string; +let contractNameWrapper: string; + +let rpcApiHttpHost: string; +let rpcApiWsHost: string; +let web3: Web3; +let firstHighNetWorthAccount: string; +let testing_connector: PluginLedgerConnectorBesu; +let besuKeyPair: { privateKey: string }; +let bridgeEthAccount: Account; +let assigneeEthAccount: Account; +const BESU_ASSET_ID = uuidv4(); +let assetContractAddress: string; +let wrapperContractAddress: string; +let satpContractName: string; + +let pluginBungeeBesuOptions: IPluginBungeeHermesOptions; + +let besuConfig: BesuConfig; +let besuOptions: IPluginLedgerConnectorBesuOptions; + +let keychainPluginBridge: PluginKeychainMemory; +let keychainEntryKeyBridge: string; +let keychainEntryValueBridge: string; + +let keychainPlugin1: PluginKeychainMemory; +let keychainPlugin2: PluginKeychainMemory; + +let besuOptionsKeychainEntryValue: string; +let besuOptionsKeychainEntryKey: string; + +let discoveryOptions: DiscoveryOptions; + +let fabricUser: X509Identity; + +let apiClient: FabricApi; + +let gatewayRunner: SATPGatewayRunner; + +afterAll(async () => { + await gatewayRunner.stop(); + await gatewayRunner.destroy(); + await besuLedger.stop(); + await besuLedger.destroy(); + await fabricLedger.stop(); + await fabricLedger.destroy(); + await Servers.shutdown(fabricServer); + + await pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); +}); + +beforeAll(async () => { + pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); + + // currently not used due to GatewayRunner being in NetworkMode: "host" + // const lanIp = await internalIpV4(); + // if (!lanIp) { + // throw new Error(`LAN IP falsy. internal-ip package failed.`); + // } + + { + besuLedger = new BesuTestLedger({ + logLevel, + emitContainerLogs: true, + envVars: ["BESU_NETWORK=dev"], + }); + await besuLedger.start(); + + // Fabric ledger connection + const channelId = "mychannel"; + fabricChannelName = channelId; + + fabricLedger = new FabricTestLedgerV1({ + emitContainerLogs: true, + publishAllPorts: true, + imageName: "ghcr.io/hyperledger/cactus-fabric2-all-in-one", + imageVersion: FABRIC_25_LTS_AIO_IMAGE_VERSION, + envVars: new Map([["FABRIC_VERSION", FABRIC_25_LTS_AIO_FABRIC_VERSION]]), + logLevel: "INFO", + }); + + await fabricLedger.start(); + + log.info("Both Ledgers started successfully"); + } + + { + // setup fabric ledger + const connectionProfile: ConnectionProfile = + await fabricLedger.getConnectionProfileOrg1(); + expect(connectionProfile).not.toBeUndefined(); + + const bridgeProfile: ConnectionProfile = + await fabricLedger.getConnectionProfileOrgX("org2"); + expect(bridgeProfile).not.toBeUndefined(); + + const enrollAdminOut = await fabricLedger.enrollAdmin(); + const adminWallet = enrollAdminOut[1]; + + const enrollAdminBridgeOut = await fabricLedger.enrollAdminV2({ + organization: "org2", + }); + const bridgeWallet = enrollAdminBridgeOut[1]; + + const [userIdentity] = await fabricLedger.enrollUser(adminWallet); + fabricUser = userIdentity; + const opts = { + enrollmentID: "bridge", + organization: "org2", + wallet: bridgeWallet, + }; + + const [bridgeIdentity] = await fabricLedger.enrollUserV2(opts); + + const sshConfig = await fabricLedger.getSshConfig(); + + log.info("enrolled admin"); + + const keychainInstanceId = uuidv4(); + const keychainId = uuidv4(); + const keychainEntryKey = "user1"; + const keychainEntryValue = JSON.stringify(userIdentity); + + console.log("keychainEntryValue: ", keychainEntryValue); + + const keychainPlugin = new PluginKeychainMemory({ + instanceId: keychainInstanceId, + keychainId, + logLevel, + backend: new Map([ + [keychainEntryKey, keychainEntryValue], + ["some-other-entry-key", "some-other-entry-value"], + ]), + }); + + const pluginRegistry = new PluginRegistry({ plugins: [keychainPlugin] }); + + const keychainInstanceIdBridge = uuidv4(); + const keychainIdBridge = uuidv4(); + keychainEntryKeyBridge = "bridge1"; + keychainEntryValueBridge = JSON.stringify(bridgeIdentity); + + console.log("keychainEntryValueBridge: ", keychainEntryValueBridge); + + keychainPluginBridge = new PluginKeychainMemory({ + instanceId: keychainInstanceIdBridge, + keychainId: keychainIdBridge, + logLevel, + backend: new Map([ + [keychainEntryKeyBridge, keychainEntryValueBridge], + ["some-other-entry-key", "some-other-entry-value"], + ]), + }); + + const pluginRegistryBridge = new PluginRegistry({ + plugins: [keychainPluginBridge], + }); + + discoveryOptions = { + enabled: true, + asLocalhost: true, + }; + + const pluginOptions: IPluginLedgerConnectorFabricOptions = { + instanceId: uuidv4(), + dockerBinary: "/usr/local/bin/docker", + peerBinary: "/fabric-samples/bin/peer", + goBinary: "/usr/local/go/bin/go", + pluginRegistry, + cliContainerEnv: FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + sshConfig, + logLevel: "DEBUG", + connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + commitTimeout: 300, + }, + }; + + const fabricConnector = new PluginLedgerConnectorFabric(pluginOptions); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + fabricServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 3000, + server: fabricServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await fabricConnector.getOrCreateWebServices(); + await fabricConnector.registerWebServices(expressApp); + + log.info("Fabric Ledger connector check"); + + const apiUrl = `http://${address}:${port}`; + + configFabric = new Configuration({ basePath: apiUrl }); + + apiClient = new FabricApi(configFabric); + + // deploy contracts ... + satpContractName = "satp-contract"; + const satpWrapperContractName = "satp-wrapper-contract"; + const satpContractRelPath = + "../../../test/typescript/fabric/contracts/satp-contract/chaincode-typescript"; + const wrapperSatpContractRelPath = + "../../../main/typescript/fabric-contracts/satp-wrapper/chaincode-typescript"; + const satpContractDir = path.join(__dirname, satpContractRelPath); + + // ├── package.json + // ├── src + // │ ├── index.ts + // │ ├── ITraceableContract.ts + // │ ├── satp-contract-interface.ts + // │ ├── satp-contract.ts + // ├── tsconfig.json + // ├── lib + // │ └── tokenERC20.js + // -------- + const satpSourceFiles: FileBase64[] = []; + { + const filename = "./tsconfig.json"; + const relativePath = "./"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./package.json"; + const relativePath = "./"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./index.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./ITraceableContract.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-contract-interface.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-contract.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./tokenERC20.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + + const wrapperSatpContractDir = path.join( + __dirname, + wrapperSatpContractRelPath, + ); + + // ├── package.json + // ├── src + // │ ├── index.ts + // │ ├── interaction-signature.ts + // │ ├── ITraceableContract.ts + // │ ├── satp-wrapper.ts + // │ └── token.ts + // ├── tsconfig.json + // -------- + const wrapperSourceFiles: FileBase64[] = []; + { + const filename = "./tsconfig.json"; + const relativePath = "./"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./package.json"; + const relativePath = "./"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./index.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./interaction-signature.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./ITraceableContract.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-wrapper.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./token.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + + const res = await apiClient.deployContractV1({ + channelId: fabricChannelName, + ccVersion: "1.0.0", + sourceFiles: satpSourceFiles, + ccName: satpContractName, + targetOrganizations: [ + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + ], + caFile: + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1.ORDERER_TLS_ROOTCERT_FILE, + ccLabel: "satp-contract", + ccLang: ChainCodeProgrammingLanguage.Typescript, + ccSequence: 1, + orderer: "orderer.example.com:7050", + ordererTLSHostnameOverride: "orderer.example.com", + connTimeout: 60, + }); + + const { packageIds, lifecycle, success } = res.data; + expect(res.status).toBe(200); + expect(success).toBe(true); + expect(lifecycle).not.toBeUndefined(); + + const { + approveForMyOrgList, + installList, + queryInstalledList, + commit, + packaging, + queryCommitted, + } = lifecycle; + + expect(packageIds).toBeTruthy(); + expect(packageIds).toBeArray(); + + expect(approveForMyOrgList).toBeTruthy(); + expect(approveForMyOrgList).toBeArray(); + + expect(installList).toBeTruthy(); + expect(installList).toBeArray(); + expect(queryInstalledList).toBeTruthy(); + expect(queryInstalledList).toBeArray(); + + expect(commit).toBeTruthy(); + expect(packaging).toBeTruthy(); + expect(queryCommitted).toBeTruthy(); + log.info("SATP Contract deployed"); + + const res2 = await apiClient.deployContractV1({ + channelId: fabricChannelName, + ccVersion: "1.0.0", + sourceFiles: wrapperSourceFiles, + ccName: satpWrapperContractName, + targetOrganizations: [ + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + ], + caFile: + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2.ORDERER_TLS_ROOTCERT_FILE, + ccLabel: "satp-wrapper-contract", + ccLang: ChainCodeProgrammingLanguage.Typescript, + ccSequence: 1, + orderer: "orderer.example.com:7050", + ordererTLSHostnameOverride: "orderer.example.com", + connTimeout: 60, + }); + + const { + packageIds: packageIds2, + lifecycle: lifecycle2, + success: success2, + } = res2.data; + expect(res2.status).toBe(200); + expect(success2).toBe(true); + + const { + approveForMyOrgList: approveForMyOrgList2, + installList: installList2, + queryInstalledList: queryInstalledList2, + commit: commit2, + packaging: packaging2, + queryCommitted: queryCommitted2, + } = lifecycle2; + + expect(packageIds2).toBeTruthy(); + expect(packageIds2).toBeArray(); + + expect(approveForMyOrgList2).toBeTruthy(); + expect(approveForMyOrgList2).toBeArray(); + + expect(installList2).toBeTruthy(); + expect(installList2).toBeArray(); + expect(queryInstalledList2).toBeTruthy(); + expect(queryInstalledList2).toBeArray(); + + expect(commit2).toBeTruthy(); + expect(packaging2).toBeTruthy(); + expect(queryCommitted2).toBeTruthy(); + + log.info("SATP Wrapper Contract deployed"); + + fabricSigningCredential = { + keychainId, + keychainRef: keychainEntryKey, + }; + + bridgeFabricSigningCredential = { + keychainId: keychainIdBridge, + keychainRef: keychainEntryKeyBridge, + }; + + const mspId: string = userIdentity.mspId; + + const initializeResponse = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [mspId, FABRIC_ASSET_ID], + methodName: "InitToken", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(initializeResponse).not.toBeUndefined(); + expect(initializeResponse.status).toBeGreaterThan(199); + expect(initializeResponse.status).toBeLessThan(300); + + log.info( + `SATPContract.InitToken(): ${JSON.stringify(initializeResponse.data)}`, + ); + + const initializeResponse2 = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: [mspId], + methodName: "Initialize", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(initializeResponse2).not.toBeUndefined(); + expect(initializeResponse2.status).toBeGreaterThan(199); + expect(initializeResponse2.status).toBeLessThan(300); + + log.info( + `SATPWrapper.Initialize(): ${JSON.stringify(initializeResponse2.data)}`, + ); + + const setBridgeResponse = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: ["Org2MSP"], + methodName: "setBridge", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + const setBridgeResponse2 = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: ["Org2MSP", BRIDGE_ID], + methodName: "setBridge", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(setBridgeResponse2).not.toBeUndefined(); + expect(setBridgeResponse2.status).toBeGreaterThan(199); + expect(setBridgeResponse2.status).toBeLessThan(300); + + log.info( + `SATPWrapper.setBridge(): ${JSON.stringify(setBridgeResponse.data)}`, + ); + + const responseClientId = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: [], + methodName: "ClientAccountID", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + clientId = responseClientId.data.functionOutput.toString(); + + pluginBungeeFabricOptions = { + keyPair: Secp256k1Keys.generateKeyPairsBuffer(), + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry(), + logLevel, + }; + + pluginOptionsFabricBridge = { + instanceId: uuidv4(), + dockerBinary: "/usr/local/bin/docker", + peerBinary: "/fabric-samples/bin/peer", + goBinary: "/usr/local/go/bin/go", + pluginRegistry: pluginRegistryBridge, + cliContainerEnv: FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + sshConfig, + logLevel: "DEBUG", + connectionProfile: bridgeProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + commitTimeout: 300, + }, + }; + + fabricConfig = { + network: SupportedChain.FABRIC, + signingCredential: bridgeFabricSigningCredential, + channelName: fabricChannelName, + contractName: satpWrapperContractName, + options: pluginOptionsFabricBridge, + bungeeOptions: pluginBungeeFabricOptions, + claimFormat: ClaimFormat.DEFAULT, + } as FabricConfig; + + // networkDetails = { + // connectorApiPath: fabricPath, + // signingCredential: fabricSigningCredential, + // channelName: fabricChannelName, + // contractName: satpContractName, + // participant: "Org1MSP", + // }; + } + + { + //setup besu ledger + rpcApiHttpHost = await besuLedger.getRpcApiHttpHost(); + // rpcApiHttpHost = rpcApiHttpHost.replace("127.0.0.1", lanIp); + + rpcApiWsHost = await besuLedger.getRpcApiWsHost(); + // rpcApiWsHost = rpcApiWsHost.replace("127.0.0.1", lanIp); + + console.log("test - rpcApiHttpHost:"); + console.log(rpcApiHttpHost); + console.log("test - rpcApiWsHost:"); + console.log(rpcApiWsHost); + web3 = new Web3(rpcApiHttpHost); + firstHighNetWorthAccount = besuLedger.getGenesisAccountPubKey(); + + bridgeEthAccount = await besuLedger.createEthTestAccount(); + + assigneeEthAccount = await besuLedger.createEthTestAccount(); + + besuKeyPair = { + privateKey: besuLedger.getGenesisAccountPrivKey(), + }; + + besuOptionsKeychainEntryValue = besuKeyPair.privateKey; + besuOptionsKeychainEntryKey = uuidv4(); + + keychainPlugin1 = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + + backend: new Map([ + [besuOptionsKeychainEntryKey, besuOptionsKeychainEntryValue], + ]), + logLevel, + }); + + keychainPlugin2 = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + + backend: new Map([ + [besuOptionsKeychainEntryKey, besuOptionsKeychainEntryValue], + ]), + logLevel, + }); + + erc20TokenContract = "SATPContract"; + contractNameWrapper = "SATPWrapperContract"; + + keychainPlugin1.set(erc20TokenContract, JSON.stringify(SATPContract)); + keychainPlugin2.set( + contractNameWrapper, + JSON.stringify(SATPWrapperContract), + ); + + const pluginRegistry = new PluginRegistry({ + plugins: [keychainPlugin1, keychainPlugin2], + }); + + besuOptions = { + instanceId: uuidv4(), + rpcApiHttpHost, + rpcApiWsHost, + pluginRegistry, + logLevel, + }; + testing_connector = new PluginLedgerConnectorBesu(besuOptions); + + await testing_connector.transact({ + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NodeTxPoolAck, + }, + transactionConfig: { + from: firstHighNetWorthAccount, + to: bridgeEthAccount.address, + value: 10e9, + gas: 1000000, + }, + }); + + const balance = await web3.eth.getBalance(bridgeEthAccount.address); + expect(balance).toBeTruthy(); + expect(parseInt(balance, 10)).toBeGreaterThan(10e9); + log.info("Connector initialized"); + + const deployOutSATPContract = await testing_connector.deployContract({ + keychainId: keychainPlugin1.getKeychainId(), + contractName: erc20TokenContract, + contractAbi: SATPContract.abi, + constructorArgs: [firstHighNetWorthAccount, BESU_ASSET_ID], + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + bytecode: SATPContract.bytecode.object, + gas: 999999999999999, + }); + expect(deployOutSATPContract).toBeTruthy(); + expect(deployOutSATPContract.transactionReceipt).toBeTruthy(); + expect( + deployOutSATPContract.transactionReceipt.contractAddress, + ).toBeTruthy(); + + assetContractAddress = + deployOutSATPContract.transactionReceipt.contractAddress ?? ""; + + log.info("SATPContract Deployed successfully"); + + const deployOutWrapperContract = await testing_connector.deployContract({ + keychainId: keychainPlugin2.getKeychainId(), + contractName: contractNameWrapper, + contractAbi: SATPWrapperContract.abi, + constructorArgs: [bridgeEthAccount.address], + web3SigningCredential: { + ethAccount: bridgeEthAccount.address, + secret: bridgeEthAccount.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + bytecode: SATPWrapperContract.bytecode.object, + gas: 999999999999999, + }); + expect(deployOutWrapperContract).toBeTruthy(); + expect(deployOutWrapperContract.transactionReceipt).toBeTruthy(); + expect( + deployOutWrapperContract.transactionReceipt.contractAddress, + ).toBeTruthy(); + log.info("SATPWrapperContract Deployed successfully"); + + wrapperContractAddress = + deployOutWrapperContract.transactionReceipt.contractAddress ?? ""; + + pluginBungeeBesuOptions = { + keyPair: Secp256k1Keys.generateKeyPairsBuffer(), + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry(), + logLevel, + }; + + besuConfig = { + network: SupportedChain.BESU, + keychainId: keychainPlugin2.getKeychainId(), + signingCredential: { + ethAccount: bridgeEthAccount.address, + secret: bridgeEthAccount.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + contractName: contractNameWrapper, + contractAddress: wrapperContractAddress, + options: besuOptions, + bungeeOptions: pluginBungeeBesuOptions, + gas: 999999999999999, + claimFormat: ClaimFormat.DEFAULT, + }; + + const giveRoleRes = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "giveRole", + params: [wrapperContractAddress], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + + expect(giveRoleRes).toBeTruthy(); + expect(giveRoleRes.success).toBeTruthy(); + log.info("BRIDGE_ROLE given to SATPWrapperContract successfully"); + } + + const responseMint = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "mint", + params: [firstHighNetWorthAccount, "100"], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 999999999, + }); + expect(responseMint).toBeTruthy(); + expect(responseMint.success).toBeTruthy(); + log.info("Minted 100 tokens to firstHighNetWorthAccount"); + + const responseApprove = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "approve", + params: [wrapperContractAddress, "100"], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 999999999, + }); + expect(responseApprove).toBeTruthy(); + expect(responseApprove.success).toBeTruthy(); + log.info("Approved 100 tokens to SATPWrapperContract"); +}); +describe("SATPGateway sending a token from Besu to Fabric", () => { + it("should realize a transfer", async () => { + const address: Address = `http://localhost`; + + // gateway setup: + const gatewayIdentity = { + id: "mockID", + name: "CustomGateway", + version: [ + { + Core: SATP_CORE_VERSION, + Architecture: SATP_ARCHITETURE_VERSION, + Crash: SATP_CRASH_VERSION, + }, + ], + supportedDLTs: [SupportedChain.FABRIC, SupportedChain.BESU], + proofID: "mockProofID10", + address, + gatewayClientPort: DEFAULT_PORT_GATEWAY_CLIENT, + gatewayServerPort: DEFAULT_PORT_GATEWAY_SERVER, + gatewayOpenAPIPort: DEFAULT_PORT_GATEWAY_API, + } as GatewayIdentity; + + // besuConfig Json object setup: + const besuPluginRegistryOptionsJSON = { + plugins: [ + { + instanceId: keychainPlugin1.getInstanceId(), + keychainId: keychainPlugin1.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: besuOptionsKeychainEntryKey, + keychainEntryValue: besuOptionsKeychainEntryValue, + }, + ], + contractName: erc20TokenContract, + contractString: await keychainPlugin1.get(erc20TokenContract), + }, + { + instanceId: keychainPlugin2.getInstanceId(), + keychainId: keychainPlugin2.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: besuOptionsKeychainEntryKey, + keychainEntryValue: besuOptionsKeychainEntryValue, + }, + ], + contractName: contractNameWrapper, + contractString: await keychainPlugin2.get(contractNameWrapper), + }, + ], + }; + + const besuOptionsJSON = { + instanceId: besuOptions.instanceId, + rpcApiHttpHost: besuOptions.rpcApiHttpHost, + rpcApiWsHost: besuOptions.rpcApiWsHost, + pluginRegistryOptions: besuPluginRegistryOptionsJSON, + logLevel: besuOptions.logLevel, + }; + + const besuBungeeOptionsJSON = { + keyPair: { + privateKey: Buffer.from( + pluginBungeeBesuOptions.keyPair!.privateKey, + ).toString("hex"), + publicKey: Buffer.from( + pluginBungeeBesuOptions.keyPair!.publicKey, + ).toString("hex"), + }, + instanceId: pluginBungeeBesuOptions.instanceId, + logLevel: pluginBungeeBesuOptions.logLevel, + }; + + const besuConfigJSON = { + network: besuConfig.network, + keychainId: besuConfig.keychainId, + signingCredential: besuConfig.signingCredential, + contractName: besuConfig.contractName, + contractAddress: besuConfig.contractAddress, + gas: besuConfig.gas, + options: besuOptionsJSON, + bungeeOptions: besuBungeeOptionsJSON, + claimFormat: besuConfig.claimFormat, + }; + + // fabricConfig Json object setup: + const fabricPluginRegistryOptionsJSON = { + plugins: [ + { + instanceId: keychainPluginBridge.getInstanceId(), + keychainId: keychainPluginBridge.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: keychainEntryKeyBridge, + keychainEntryValue: keychainEntryValueBridge, + }, + { + keychainEntry: "some-other-entry-key", + keychainEntryValue: "some-other-entry-value", + }, + ], + }, + ], + }; + + const fabricOptionsJSON = { + instanceId: pluginOptionsFabricBridge.instanceId, + dockerBinary: pluginOptionsFabricBridge.dockerBinary, + peerBinary: pluginOptionsFabricBridge.peerBinary, + goBinary: pluginOptionsFabricBridge.goBinary, + pluginRegistryOptions: fabricPluginRegistryOptionsJSON, + cliContainerEnv: pluginOptionsFabricBridge.cliContainerEnv, + sshConfig: pluginOptionsFabricBridge.sshConfig, + logLevel: pluginOptionsFabricBridge.logLevel, + connectionProfile: pluginOptionsFabricBridge.connectionProfile, + discoveryOptions: pluginOptionsFabricBridge.discoveryOptions, + eventHandlerOptions: pluginOptionsFabricBridge.eventHandlerOptions, + }; + + const fabricBungeeOptionsJSON = { + keyPair: { + privateKey: Buffer.from( + pluginBungeeFabricOptions.keyPair!.privateKey, + ).toString("hex"), + publicKey: Buffer.from( + pluginBungeeFabricOptions.keyPair!.publicKey, + ).toString("hex"), + }, + instanceId: pluginBungeeFabricOptions.instanceId, + logLevel: pluginBungeeFabricOptions.logLevel, + }; + + const fabricConfigJSON = { + network: fabricConfig.network, + signingCredential: fabricConfig.signingCredential, + channelName: fabricConfig.channelName, + contractName: fabricConfig.contractName, + options: fabricOptionsJSON, + bungeeOptions: fabricBungeeOptionsJSON, + claimFormat: fabricConfig.claimFormat, + }; + + // gateway configuration setup: + const jsonObject = { + gid: gatewayIdentity, + logLevel: "DEBUG", + counterPartyGateways: [], //only knows itself + environment: "development", + enableOpenAPI: true, + bridgesConfig: [besuConfigJSON, fabricConfigJSON], + }; + + const configDir = path.join(__dirname, "gateway-info/config"); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + const configFile = path.join(configDir, "gateway-config.json"); + fs.writeFileSync(configFile, JSON.stringify(jsonObject, null, 2)); + + expect(fs.existsSync(configFile)).toBe(true); + + // gateway outputLogFile and errorLogFile setup: + const logDir = path.join(__dirname, "gateway-info/logs"); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + const outputLogFile = path.join(logDir, "gateway-logs-output.log"); + const errorLogFile = path.join(logDir, "gateway-logs-error.log"); + + // Clear any existing logs + fs.writeFileSync(outputLogFile, ""); + fs.writeFileSync(errorLogFile, ""); + + expect(fs.existsSync(outputLogFile)).toBe(true); + expect(fs.existsSync(errorLogFile)).toBe(true); + + // gatewayRunner setup: + const gatewayRunnerOptions: ISATPGatewayRunnerConstructorOptions = { + containerImageVersion: "2024-10-30T19-54-20-dev-5e06263e0", + containerImageName: "ghcr.io/hyperledger/cacti-satp-hermes-gateway", + logLevel, + emitContainerLogs: true, + configFile, + outputLogFile, + errorLogFile, + }; + gatewayRunner = new SATPGatewayRunner(gatewayRunnerOptions); + console.log("starting gatewayRunner..."); + await gatewayRunner.start(); + console.log("gatewayRunner started sucessfully"); + + const sourceAsset: Asset = { + owner: firstHighNetWorthAccount, + ontology: JSON.stringify(BesuSATPInteraction), + contractName: erc20TokenContract, + contractAddress: assetContractAddress, + }; + const receiverAsset: Asset = { + owner: clientId, + ontology: JSON.stringify(FabricSATPInteraction), + contractName: satpContractName, + mspId: fabricUser.mspId, + channelName: fabricChannelName, + }; + const req: TransactRequest = { + contextID: "mockContext", + fromDLTNetworkID: SupportedChain.BESU, + toDLTNetworkID: SupportedChain.FABRIC, + fromAmount: "100", + toAmount: "1", + originatorPubkey: assigneeEthAccount.address, + beneficiaryPubkey: fabricUser.credentials.certificate, + sourceAsset, + receiverAsset, + }; + + const port = await gatewayRunner.getHostPort(DEFAULT_PORT_GATEWAY_API); + + const transactionApiClient = createClient( + "TransactionApi", + address, + port, + log, + ); + const adminApi = createClient("AdminApi", address, port, log); + + const res = await transactionApiClient.transact(req); + log.info(res?.data.statusResponse); + + const sessions = await adminApi.getSessionIds({}); + expect(sessions.data).toBeTruthy(); + expect(sessions.data.length).toBe(1); + expect(sessions.data[0]).toBe(res.data.sessionID); + + const responseBalanceOwner = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + contractAbi: SATPContract.abi, + invocationType: EthContractInvocationType.Call, + contractAddress: assetContractAddress, + methodName: "checkBalance", + params: [firstHighNetWorthAccount], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 999999999, + }); + expect(responseBalanceOwner).toBeTruthy(); + expect(responseBalanceOwner.success).toBeTruthy(); + console.log( + `Balance Besu Owner Account: ${responseBalanceOwner.callOutput}`, + ); + expect(responseBalanceOwner.callOutput).toBe("0"); + log.info("Amount was transfer correctly from the Owner account"); + + const responseBalanceBridge = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + contractAbi: SATPContract.abi, + invocationType: EthContractInvocationType.Call, + contractAddress: assetContractAddress, + methodName: "checkBalance", + params: [wrapperContractAddress], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 999999999, + }); + expect(responseBalanceBridge).toBeTruthy(); + expect(responseBalanceBridge.success).toBeTruthy(); + console.log( + `Balance Besu Bridge Account: ${responseBalanceBridge.callOutput}`, + ); + expect(responseBalanceBridge.callOutput).toBe("0"); + log.info("Amount was transfer correctly to the Wrapper account"); + + const responseBalance1 = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [BRIDGE_ID], + methodName: "ClientIDAccountBalance", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(responseBalance1).not.toBeUndefined(); + expect(responseBalance1.status).toBeGreaterThan(199); + expect(responseBalance1.status).toBeLessThan(300); + expect(responseBalance1.data).not.toBeUndefined(); + expect(responseBalance1.data.functionOutput).toBe("0"); + console.log( + `Balance Fabric Bridge Account: ${responseBalance1.data.functionOutput}`, + ); + log.info("Amount was transfer correctly from the Bridge account"); + + const responseBalance2 = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [clientId], + methodName: "ClientIDAccountBalance", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + expect(responseBalance2).not.toBeUndefined(); + expect(responseBalance2.status).toBeGreaterThan(199); + expect(responseBalance2.status).toBeLessThan(300); + expect(responseBalance2.data).not.toBeUndefined(); + expect(responseBalance2.data.functionOutput).toBe("1"); + console.log( + `Balance Fabric Owner Account: ${responseBalance2.data.functionOutput}`, + ); + log.info("Amount was transfer correctly to the Owner account"); + }); +}); diff --git a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-end-to-end-transfer-2-gateways-dockerization.test.ts b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-end-to-end-transfer-2-gateways-dockerization.test.ts new file mode 100644 index 0000000000..1f6dc3d255 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-end-to-end-transfer-2-gateways-dockerization.test.ts @@ -0,0 +1,1439 @@ +import { randomUUID as uuidv4 } from "node:crypto"; +import "jest-extended"; + +import { + IListenOptions, + LogLevelDesc, + LoggerProvider, + Secp256k1Keys, + Servers, +} from "@hyperledger/cactus-common"; +// import { v4 as internalIpV4 } from "internal-ip"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + ChainCodeProgrammingLanguage, + Configuration, + DefaultEventHandlerStrategy, + FabricSigningCredential, + FileBase64, + IPluginLedgerConnectorFabricOptions, + PluginLedgerConnectorFabric, + DefaultApi as FabricApi, + FabricContractInvocationType, + ConnectionProfile, +} from "@hyperledger/cactus-plugin-ledger-connector-fabric"; +import http, { Server } from "http"; +import fs from "fs-extra"; + +import { + pruneDockerAllIfGithubAction, + Containers, + FabricTestLedgerV1, + BesuTestLedger, + FABRIC_25_LTS_AIO_FABRIC_VERSION, + FABRIC_25_LTS_AIO_IMAGE_VERSION, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + SATPGatewayRunner, + ISATPGatewayRunnerConstructorOptions, +} from "@hyperledger/cactus-test-tooling"; +import bodyParser from "body-parser"; +import express from "express"; +import { DiscoveryOptions, X509Identity } from "fabric-network"; +import { AddressInfo } from "net"; +import path from "path"; +import { + BesuConfig, + FabricConfig, +} from "../../../main/typescript/types/blockchain-interaction"; +import { IPluginBungeeHermesOptions } from "@hyperledger/cactus-plugin-bungee-hermes"; +import { Account } from "web3-core"; +import { + EthContractInvocationType, + IPluginLedgerConnectorBesuOptions, + PluginLedgerConnectorBesu, + ReceiptType, + Web3SigningCredentialType, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; +import Web3 from "web3"; +import SATPContract from "../../solidity/generated/satp-erc20.sol/SATPContract.json"; +import SATPWrapperContract from "../../../solidity/generated/satp-wrapper.sol/SATPWrapperContract.json"; +import { TransactRequest, Asset } from "../../../main/typescript"; +import { + Address, + GatewayIdentity, + SupportedChain, +} from "../../../main/typescript/core/types"; +import FabricSATPInteraction from "../../../test/typescript/fabric/satp-erc20-interact.json"; +import BesuSATPInteraction from "../../solidity/satp-erc20-interact.json"; +import { createClient } from "../test-utils"; +import { + DEFAULT_PORT_GATEWAY_API, + DEFAULT_PORT_GATEWAY_CLIENT, + DEFAULT_PORT_GATEWAY_SERVER, + SATP_CORE_VERSION, + SATP_ARCHITETURE_VERSION, + SATP_CRASH_VERSION, +} from "../../../main/typescript/core/constants"; +import { bufArray2HexStr } from "../../../main/typescript/gateway-utils"; +import { ClaimFormat } from "../../../main/typescript/generated/proto/cacti/satp/v02/common/message_pb"; +const logLevel: LogLevelDesc = "DEBUG"; +const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: "BUNGEE - Hermes", +}); + +let fabricServer: Server; + +let besuLedger: BesuTestLedger; + +let fabricLedger: FabricTestLedgerV1; +let fabricSigningCredential: FabricSigningCredential; +let bridgeFabricSigningCredential: FabricSigningCredential; +let configFabric: Configuration; +let fabricChannelName: string; + +const FABRIC_ASSET_ID = uuidv4(); + +const BRIDGE_ID = + "x509::/OU=org2/OU=client/OU=department1/CN=bridge::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com"; + +let clientId: string; +let fabricConfig: FabricConfig; +let pluginOptionsFabricBridge: IPluginLedgerConnectorFabricOptions; +let pluginBungeeFabricOptions: IPluginBungeeHermesOptions; + +let erc20TokenContract: string; +let contractNameWrapper: string; + +let rpcApiHttpHost: string; +let rpcApiWsHost: string; +let web3: Web3; +let firstHighNetWorthAccount: string; +let testing_connector: PluginLedgerConnectorBesu; +let besuKeyPair: { privateKey: string }; +let bridgeEthAccount: Account; +let assigneeEthAccount: Account; +const BESU_ASSET_ID = uuidv4(); +let assetContractAddress: string; +let wrapperContractAddress: string; +let satpContractName: string; + +let pluginBungeeBesuOptions: IPluginBungeeHermesOptions; + +let besuConfig: BesuConfig; +let besuOptions: IPluginLedgerConnectorBesuOptions; + +let keychainInstanceId: string; +let keychainId: string; +let keychainEntryKey: string; +let keychainEntryValue: string; + +let keychainPluginBridge: PluginKeychainMemory; +let keychainEntryKeyBridge: string; +let keychainEntryValueBridge: string; + +let keychainPlugin1: PluginKeychainMemory; +let keychainPlugin2: PluginKeychainMemory; + +let besuOptionsKeychainEntryValue: string; +let besuOptionsKeychainEntryKey: string; + +let discoveryOptions: DiscoveryOptions; + +let fabricUser: X509Identity; + +let apiClient: FabricApi; + +let gatewayRunner1: SATPGatewayRunner; +let gatewayRunner2: SATPGatewayRunner; + +afterAll(async () => { + await gatewayRunner1.stop(); + await gatewayRunner1.destroy(); + await gatewayRunner2.stop(); + await gatewayRunner2.destroy(); + await besuLedger.stop(); + await besuLedger.destroy(); + await fabricLedger.stop(); + await fabricLedger.destroy(); + await Servers.shutdown(fabricServer); + + await pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); +}); + +beforeAll(async () => { + pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); + + // currently not used due to GatewayRunner being in NetworkMode: "host" + // const lanIp = await internalIpV4(); + // if (!lanIp) { + // throw new Error(`LAN IP falsy. internal-ip package failed.`); + // } + + { + besuLedger = new BesuTestLedger({ + logLevel, + emitContainerLogs: true, + envVars: ["BESU_NETWORK=dev"], + }); + await besuLedger.start(); + + // Fabric ledger connection + const channelId = "mychannel"; + fabricChannelName = channelId; + + fabricLedger = new FabricTestLedgerV1({ + emitContainerLogs: true, + publishAllPorts: true, + imageName: "ghcr.io/hyperledger/cactus-fabric2-all-in-one", + imageVersion: FABRIC_25_LTS_AIO_IMAGE_VERSION, + envVars: new Map([["FABRIC_VERSION", FABRIC_25_LTS_AIO_FABRIC_VERSION]]), + logLevel: "INFO", + }); + + await fabricLedger.start(); + + log.info("Both Ledgers started successfully"); + } + + { + // setup fabric ledger + const connectionProfile: ConnectionProfile = + await fabricLedger.getConnectionProfileOrg1(); + expect(connectionProfile).not.toBeUndefined(); + + const bridgeProfile: ConnectionProfile = + await fabricLedger.getConnectionProfileOrgX("org2"); + expect(bridgeProfile).not.toBeUndefined(); + + const enrollAdminOut = await fabricLedger.enrollAdmin(); + const adminWallet = enrollAdminOut[1]; + + const enrollAdminBridgeOut = await fabricLedger.enrollAdminV2({ + organization: "org2", + }); + const bridgeWallet = enrollAdminBridgeOut[1]; + + const [userIdentity] = await fabricLedger.enrollUser(adminWallet); + fabricUser = userIdentity; + const opts = { + enrollmentID: "bridge", + organization: "org2", + wallet: bridgeWallet, + }; + + const [bridgeIdentity] = await fabricLedger.enrollUserV2(opts); + + const sshConfig = await fabricLedger.getSshConfig(); + + log.info("enrolled admin"); + + keychainInstanceId = uuidv4(); + keychainId = uuidv4(); + keychainEntryKey = "user1"; + keychainEntryValue = JSON.stringify(userIdentity); + + console.log("keychainEntryValue: ", keychainEntryValue); + + const keychainPlugin = new PluginKeychainMemory({ + instanceId: keychainInstanceId, + keychainId, + logLevel, + backend: new Map([ + [keychainEntryKey, keychainEntryValue], + ["some-other-entry-key", "some-other-entry-value"], + ]), + }); + + const pluginRegistry = new PluginRegistry({ plugins: [keychainPlugin] }); + + const keychainInstanceIdBridge = uuidv4(); + const keychainIdBridge = uuidv4(); + keychainEntryKeyBridge = "bridge1"; + keychainEntryValueBridge = JSON.stringify(bridgeIdentity); + + console.log("keychainEntryValueBridge: ", keychainEntryValueBridge); + + keychainPluginBridge = new PluginKeychainMemory({ + instanceId: keychainInstanceIdBridge, + keychainId: keychainIdBridge, + logLevel, + backend: new Map([ + [keychainEntryKeyBridge, keychainEntryValueBridge], + ["some-other-entry-key", "some-other-entry-value"], + ]), + }); + + const pluginRegistryBridge = new PluginRegistry({ + plugins: [keychainPluginBridge], + }); + + discoveryOptions = { + enabled: true, + asLocalhost: true, + }; + + const pluginOptions: IPluginLedgerConnectorFabricOptions = { + instanceId: uuidv4(), + dockerBinary: "/usr/local/bin/docker", + peerBinary: "/fabric-samples/bin/peer", + goBinary: "/usr/local/go/bin/go", + pluginRegistry, + cliContainerEnv: FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + sshConfig, + logLevel: "DEBUG", + connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + commitTimeout: 300, + }, + }; + + const fabricConnector = new PluginLedgerConnectorFabric(pluginOptions); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + fabricServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 3000, + server: fabricServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await fabricConnector.getOrCreateWebServices(); + await fabricConnector.registerWebServices(expressApp); + + log.info("Fabric Ledger connector check"); + + const apiUrl = `http://${address}:${port}`; + + configFabric = new Configuration({ basePath: apiUrl }); + + apiClient = new FabricApi(configFabric); + + // deploy contracts ... + satpContractName = "satp-contract"; + const satpWrapperContractName = "satp-wrapper-contract"; + const satpContractRelPath = + "../../../test/typescript/fabric/contracts/satp-contract/chaincode-typescript"; + const wrapperSatpContractRelPath = + "../../../main/typescript/fabric-contracts/satp-wrapper/chaincode-typescript"; + const satpContractDir = path.join(__dirname, satpContractRelPath); + + // ├── package.json + // ├── src + // │ ├── index.ts + // │ ├── ITraceableContract.ts + // │ ├── satp-contract-interface.ts + // │ ├── satp-contract.ts + // ├── tsconfig.json + // ├── lib + // │ └── tokenERC20.js + // -------- + const satpSourceFiles: FileBase64[] = []; + { + const filename = "./tsconfig.json"; + const relativePath = "./"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./package.json"; + const relativePath = "./"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./index.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./ITraceableContract.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-contract-interface.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-contract.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./tokenERC20.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + + const wrapperSatpContractDir = path.join( + __dirname, + wrapperSatpContractRelPath, + ); + + // ├── package.json + // ├── src + // │ ├── index.ts + // │ ├── interaction-signature.ts + // │ ├── ITraceableContract.ts + // │ ├── satp-wrapper.ts + // │ └── token.ts + // ├── tsconfig.json + // -------- + const wrapperSourceFiles: FileBase64[] = []; + { + const filename = "./tsconfig.json"; + const relativePath = "./"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./package.json"; + const relativePath = "./"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./index.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./interaction-signature.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./ITraceableContract.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-wrapper.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./token.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + + const res = await apiClient.deployContractV1({ + channelId: fabricChannelName, + ccVersion: "1.0.0", + sourceFiles: satpSourceFiles, + ccName: satpContractName, + targetOrganizations: [ + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + ], + caFile: + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1.ORDERER_TLS_ROOTCERT_FILE, + ccLabel: "satp-contract", + ccLang: ChainCodeProgrammingLanguage.Typescript, + ccSequence: 1, + orderer: "orderer.example.com:7050", + ordererTLSHostnameOverride: "orderer.example.com", + connTimeout: 60, + }); + + const { packageIds, lifecycle, success } = res.data; + expect(res.status).toBe(200); + expect(success).toBe(true); + expect(lifecycle).not.toBeUndefined(); + + const { + approveForMyOrgList, + installList, + queryInstalledList, + commit, + packaging, + queryCommitted, + } = lifecycle; + + expect(packageIds).toBeTruthy(); + expect(packageIds).toBeArray(); + + expect(approveForMyOrgList).toBeTruthy(); + expect(approveForMyOrgList).toBeArray(); + + expect(installList).toBeTruthy(); + expect(installList).toBeArray(); + expect(queryInstalledList).toBeTruthy(); + expect(queryInstalledList).toBeArray(); + + expect(commit).toBeTruthy(); + expect(packaging).toBeTruthy(); + expect(queryCommitted).toBeTruthy(); + log.info("SATP Contract deployed"); + + const res2 = await apiClient.deployContractV1({ + channelId: fabricChannelName, + ccVersion: "1.0.0", + sourceFiles: wrapperSourceFiles, + ccName: satpWrapperContractName, + targetOrganizations: [ + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + ], + caFile: + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2.ORDERER_TLS_ROOTCERT_FILE, + ccLabel: "satp-wrapper-contract", + ccLang: ChainCodeProgrammingLanguage.Typescript, + ccSequence: 1, + orderer: "orderer.example.com:7050", + ordererTLSHostnameOverride: "orderer.example.com", + connTimeout: 60, + }); + + const { + packageIds: packageIds2, + lifecycle: lifecycle2, + success: success2, + } = res2.data; + expect(res2.status).toBe(200); + expect(success2).toBe(true); + + const { + approveForMyOrgList: approveForMyOrgList2, + installList: installList2, + queryInstalledList: queryInstalledList2, + commit: commit2, + packaging: packaging2, + queryCommitted: queryCommitted2, + } = lifecycle2; + + expect(packageIds2).toBeTruthy(); + expect(packageIds2).toBeArray(); + + expect(approveForMyOrgList2).toBeTruthy(); + expect(approveForMyOrgList2).toBeArray(); + + expect(installList2).toBeTruthy(); + expect(installList2).toBeArray(); + expect(queryInstalledList2).toBeTruthy(); + expect(queryInstalledList2).toBeArray(); + + expect(commit2).toBeTruthy(); + expect(packaging2).toBeTruthy(); + expect(queryCommitted2).toBeTruthy(); + + log.info("SATP Wrapper Contract deployed"); + + fabricSigningCredential = { + keychainId, + keychainRef: keychainEntryKey, + }; + + bridgeFabricSigningCredential = { + keychainId: keychainIdBridge, + keychainRef: keychainEntryKeyBridge, + }; + + const mspId: string = userIdentity.mspId; + + const initializeResponse = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [mspId, FABRIC_ASSET_ID], + methodName: "InitToken", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(initializeResponse).not.toBeUndefined(); + expect(initializeResponse.status).toBeGreaterThan(199); + expect(initializeResponse.status).toBeLessThan(300); + + log.info( + `SATPContract.InitToken(): ${JSON.stringify(initializeResponse.data)}`, + ); + + const initializeResponse2 = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: [mspId], + methodName: "Initialize", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(initializeResponse2).not.toBeUndefined(); + expect(initializeResponse2.status).toBeGreaterThan(199); + expect(initializeResponse2.status).toBeLessThan(300); + + log.info( + `SATPWrapper.Initialize(): ${JSON.stringify(initializeResponse2.data)}`, + ); + + const setBridgeResponse = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: ["Org2MSP"], + methodName: "setBridge", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + const setBridgeResponse2 = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: ["Org2MSP", BRIDGE_ID], + methodName: "setBridge", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(setBridgeResponse2).not.toBeUndefined(); + expect(setBridgeResponse2.status).toBeGreaterThan(199); + expect(setBridgeResponse2.status).toBeLessThan(300); + + log.info( + `SATPWrapper.setBridge(): ${JSON.stringify(setBridgeResponse.data)}`, + ); + + const responseClientId = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: [], + methodName: "ClientAccountID", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + clientId = responseClientId.data.functionOutput.toString(); + + pluginBungeeFabricOptions = { + keyPair: Secp256k1Keys.generateKeyPairsBuffer(), + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry(), + logLevel, + }; + + pluginOptionsFabricBridge = { + instanceId: uuidv4(), + dockerBinary: "/usr/local/bin/docker", + peerBinary: "/fabric-samples/bin/peer", + goBinary: "/usr/local/go/bin/go", + pluginRegistry: pluginRegistryBridge, + cliContainerEnv: FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + sshConfig, + logLevel: "DEBUG", + connectionProfile: bridgeProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + commitTimeout: 300, + }, + }; + + fabricConfig = { + network: SupportedChain.FABRIC, + signingCredential: bridgeFabricSigningCredential, + channelName: fabricChannelName, + contractName: satpWrapperContractName, + options: pluginOptionsFabricBridge, + bungeeOptions: pluginBungeeFabricOptions, + claimFormat: ClaimFormat.DEFAULT, + } as FabricConfig; + + // networkDetails = { + // connectorApiPath: fabricPath, + // signingCredential: fabricSigningCredential, + // channelName: fabricChannelName, + // contractName: satpContractName, + // participant: "Org1MSP", + // }; + } + + { + //setup besu ledger + rpcApiHttpHost = await besuLedger.getRpcApiHttpHost(); + // rpcApiHttpHost = rpcApiHttpHost.replace("127.0.0.1", lanIp); + + rpcApiWsHost = await besuLedger.getRpcApiWsHost(); + // rpcApiWsHost = rpcApiWsHost.replace("127.0.0.1", lanIp); + + console.log("test - rpcApiHttpHost:"); + console.log(rpcApiHttpHost); + console.log("test - rpcApiWsHost:"); + console.log(rpcApiWsHost); + web3 = new Web3(rpcApiHttpHost); + firstHighNetWorthAccount = besuLedger.getGenesisAccountPubKey(); + + bridgeEthAccount = await besuLedger.createEthTestAccount(); + + assigneeEthAccount = await besuLedger.createEthTestAccount(); + + besuKeyPair = { + privateKey: besuLedger.getGenesisAccountPrivKey(), + }; + + besuOptionsKeychainEntryValue = besuKeyPair.privateKey; + besuOptionsKeychainEntryKey = uuidv4(); + + keychainPlugin1 = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + + backend: new Map([ + [besuOptionsKeychainEntryKey, besuOptionsKeychainEntryValue], + ]), + logLevel, + }); + + keychainPlugin2 = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + + backend: new Map([ + [besuOptionsKeychainEntryKey, besuOptionsKeychainEntryValue], + ]), + logLevel, + }); + + erc20TokenContract = "SATPContract"; + contractNameWrapper = "SATPWrapperContract"; + + keychainPlugin1.set(erc20TokenContract, JSON.stringify(SATPContract)); + keychainPlugin2.set( + contractNameWrapper, + JSON.stringify(SATPWrapperContract), + ); + + const pluginRegistry = new PluginRegistry({ + plugins: [keychainPlugin1, keychainPlugin2], + }); + + besuOptions = { + instanceId: uuidv4(), + rpcApiHttpHost, + rpcApiWsHost, + pluginRegistry, + logLevel, + }; + testing_connector = new PluginLedgerConnectorBesu(besuOptions); + + await testing_connector.transact({ + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NodeTxPoolAck, + }, + transactionConfig: { + from: firstHighNetWorthAccount, + to: bridgeEthAccount.address, + value: 10e9, + gas: 1000000, + }, + }); + + const balance = await web3.eth.getBalance(bridgeEthAccount.address); + expect(balance).toBeTruthy(); + expect(parseInt(balance, 10)).toBeGreaterThan(10e9); + log.info("Connector initialized"); + + const deployOutSATPContract = await testing_connector.deployContract({ + keychainId: keychainPlugin1.getKeychainId(), + contractName: erc20TokenContract, + contractAbi: SATPContract.abi, + constructorArgs: [firstHighNetWorthAccount, BESU_ASSET_ID], + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + bytecode: SATPContract.bytecode.object, + gas: 999999999999999, + }); + expect(deployOutSATPContract).toBeTruthy(); + expect(deployOutSATPContract.transactionReceipt).toBeTruthy(); + expect( + deployOutSATPContract.transactionReceipt.contractAddress, + ).toBeTruthy(); + + assetContractAddress = + deployOutSATPContract.transactionReceipt.contractAddress ?? ""; + + log.info("SATPContract Deployed successfully"); + + const deployOutWrapperContract = await testing_connector.deployContract({ + keychainId: keychainPlugin2.getKeychainId(), + contractName: contractNameWrapper, + contractAbi: SATPWrapperContract.abi, + constructorArgs: [bridgeEthAccount.address], + web3SigningCredential: { + ethAccount: bridgeEthAccount.address, + secret: bridgeEthAccount.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + bytecode: SATPWrapperContract.bytecode.object, + gas: 999999999999999, + }); + expect(deployOutWrapperContract).toBeTruthy(); + expect(deployOutWrapperContract.transactionReceipt).toBeTruthy(); + expect( + deployOutWrapperContract.transactionReceipt.contractAddress, + ).toBeTruthy(); + log.info("SATPWrapperContract Deployed successfully"); + + wrapperContractAddress = + deployOutWrapperContract.transactionReceipt.contractAddress ?? ""; + + pluginBungeeBesuOptions = { + keyPair: Secp256k1Keys.generateKeyPairsBuffer(), + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry(), + logLevel, + }; + + besuConfig = { + network: SupportedChain.BESU, + keychainId: keychainPlugin2.getKeychainId(), + signingCredential: { + ethAccount: bridgeEthAccount.address, + secret: bridgeEthAccount.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + contractName: contractNameWrapper, + contractAddress: wrapperContractAddress, + options: besuOptions, + bungeeOptions: pluginBungeeBesuOptions, + gas: 999999999999999, + claimFormat: ClaimFormat.DEFAULT, + }; + + const giveRoleRes = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "giveRole", + params: [wrapperContractAddress], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + + expect(giveRoleRes).toBeTruthy(); + expect(giveRoleRes.success).toBeTruthy(); + log.info("BRIDGE_ROLE given to SATPWrapperContract successfully"); + } + + const responseMint = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "mint", + params: [firstHighNetWorthAccount, "100"], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 999999999, + }); + expect(responseMint).toBeTruthy(); + expect(responseMint.success).toBeTruthy(); + log.info("Minted 100 tokens to firstHighNetWorthAccount"); + + const responseApprove = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "approve", + params: [wrapperContractAddress, "100"], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 999999999, + }); + expect(responseApprove).toBeTruthy(); + expect(responseApprove.success).toBeTruthy(); + log.info("Approved 100 tokens to SATPWrapperContract"); +}); +describe("SATPGateway sending a token from Besu to Fabric", () => { + it("should realize a transfer", async () => { + // besuConfig Json object setup: + const besuPluginRegistryOptionsJSON = { + plugins: [ + { + instanceId: keychainPlugin1.getInstanceId(), + keychainId: keychainPlugin1.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: besuOptionsKeychainEntryKey, + keychainEntryValue: besuOptionsKeychainEntryValue, + }, + ], + contractName: erc20TokenContract, + contractString: await keychainPlugin1.get(erc20TokenContract), + }, + { + instanceId: keychainPlugin2.getInstanceId(), + keychainId: keychainPlugin2.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: besuOptionsKeychainEntryKey, + keychainEntryValue: besuOptionsKeychainEntryValue, + }, + ], + contractName: contractNameWrapper, + contractString: await keychainPlugin2.get(contractNameWrapper), + }, + ], + }; + + const besuOptionsJSON = { + instanceId: besuOptions.instanceId, + rpcApiHttpHost: besuOptions.rpcApiHttpHost, + rpcApiWsHost: besuOptions.rpcApiWsHost, + pluginRegistryOptions: besuPluginRegistryOptionsJSON, + logLevel: besuOptions.logLevel, + }; + + const besuBungeeOptionsJSON = { + keyPair: { + privateKey: Buffer.from( + pluginBungeeBesuOptions.keyPair!.privateKey, + ).toString("hex"), + publicKey: Buffer.from( + pluginBungeeBesuOptions.keyPair!.publicKey, + ).toString("hex"), + }, + instanceId: pluginBungeeBesuOptions.instanceId, + logLevel: pluginBungeeBesuOptions.logLevel, + }; + + const besuConfigJSON = { + network: besuConfig.network, + keychainId: besuConfig.keychainId, + signingCredential: besuConfig.signingCredential, + contractName: besuConfig.contractName, + contractAddress: besuConfig.contractAddress, + gas: besuConfig.gas, + options: besuOptionsJSON, + bungeeOptions: besuBungeeOptionsJSON, + claimFormat: besuConfig.claimFormat, + }; + + // fabricConfig Json object setup: + const fabricPluginRegistryOptionsJSON = { + plugins: [ + { + instanceId: keychainPluginBridge.getInstanceId(), + keychainId: keychainPluginBridge.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: keychainEntryKeyBridge, + keychainEntryValue: keychainEntryValueBridge, + }, + { + keychainEntry: "some-other-entry-key", + keychainEntryValue: "some-other-entry-value", + }, + ], + }, + ], + }; + + const fabricOptionsJSON = { + instanceId: pluginOptionsFabricBridge.instanceId, + dockerBinary: pluginOptionsFabricBridge.dockerBinary, + peerBinary: pluginOptionsFabricBridge.peerBinary, + goBinary: pluginOptionsFabricBridge.goBinary, + pluginRegistryOptions: fabricPluginRegistryOptionsJSON, + cliContainerEnv: pluginOptionsFabricBridge.cliContainerEnv, + sshConfig: pluginOptionsFabricBridge.sshConfig, + logLevel: pluginOptionsFabricBridge.logLevel, + connectionProfile: pluginOptionsFabricBridge.connectionProfile, + discoveryOptions: pluginOptionsFabricBridge.discoveryOptions, + eventHandlerOptions: pluginOptionsFabricBridge.eventHandlerOptions, + }; + + const fabricBungeeOptionsJSON = { + keyPair: { + privateKey: Buffer.from( + pluginBungeeFabricOptions.keyPair!.privateKey, + ).toString("hex"), + publicKey: Buffer.from( + pluginBungeeFabricOptions.keyPair!.publicKey, + ).toString("hex"), + }, + instanceId: pluginBungeeFabricOptions.instanceId, + logLevel: pluginBungeeFabricOptions.logLevel, + }; + + const fabricConfigJSON = { + network: fabricConfig.network, + signingCredential: fabricConfig.signingCredential, + channelName: fabricConfig.channelName, + contractName: fabricConfig.contractName, + options: fabricOptionsJSON, + bungeeOptions: fabricBungeeOptionsJSON, + claimFormat: fabricConfig.claimFormat, + }; + + // gatewayIds setup: + const gateway1KeyPair = Secp256k1Keys.generateKeyPairsBuffer(); + const gateway2KeyPair = Secp256k1Keys.generateKeyPairsBuffer(); + const address: Address = `http://localhost`; + + const gatewayIdentity1 = { + id: "mockID-1", + name: "CustomGateway", + version: [ + { + Core: SATP_CORE_VERSION, + Architecture: SATP_ARCHITETURE_VERSION, + Crash: SATP_CRASH_VERSION, + }, + ], + supportedDLTs: [SupportedChain.BESU], + proofID: "mockProofID10", + address, + gatewayClientPort: DEFAULT_PORT_GATEWAY_CLIENT, + gatewayServerPort: DEFAULT_PORT_GATEWAY_SERVER, + gatewayOpenAPIPort: DEFAULT_PORT_GATEWAY_API, + } as GatewayIdentity; + + const gatewayIdentity2 = { + id: "mockID-2", + name: "CustomGateway", + version: [ + { + Core: SATP_CORE_VERSION, + Architecture: SATP_ARCHITETURE_VERSION, + Crash: SATP_CRASH_VERSION, + }, + ], + supportedDLTs: [SupportedChain.FABRIC], + proofID: "mockProofID11", + address, + gatewayServerPort: 3110, + gatewayClientPort: 3111, + gatewayOpenAPIPort: 4110, + } as GatewayIdentity; + + // configFile setup for gateway1: + console.log("Creating gatewayJSON1..."); + const gatewayJSON1 = { + gid: gatewayIdentity1, + logLevel: "DEBUG", + gatewayKeyPair: { + privateKey: Buffer.from(gateway1KeyPair.privateKey).toString("hex"), + publicKey: Buffer.from(gateway1KeyPair.publicKey).toString("hex"), + }, + counterPartyGateways: [ + { + id: "mockID-2", + name: "CustomGateway", + pubKey: bufArray2HexStr(gateway2KeyPair.publicKey), + version: [ + { + Core: SATP_CORE_VERSION, + Architecture: SATP_ARCHITETURE_VERSION, + Crash: SATP_CRASH_VERSION, + }, + ], + supportedDLTs: [SupportedChain.FABRIC], + proofID: "mockProofID11", + address, + gatewayServerPort: 3110, + gatewayClientPort: 3111, + gatewayOpenAPIPort: 4110, + }, + ], + environment: "development", + enableOpenAPI: true, + bridgesConfig: [besuConfigJSON], + }; + + // configFile setup for gateway2: + console.log("Creating gatewayJSON2..."); + const gatewayJSON2 = { + gid: gatewayIdentity2, + logLevel: "DEBUG", + gatewayKeyPair: { + privateKey: Buffer.from(gateway2KeyPair.privateKey).toString("hex"), + publicKey: Buffer.from(gateway2KeyPair.publicKey).toString("hex"), + }, + counterPartyGateways: [ + { + id: "mockID-1", + name: "CustomGateway", + pubKey: bufArray2HexStr(gateway1KeyPair.publicKey), + version: [ + { + Core: SATP_CORE_VERSION, + Architecture: SATP_ARCHITETURE_VERSION, + Crash: SATP_CRASH_VERSION, + }, + ], + supportedDLTs: [SupportedChain.BESU], + proofID: "mockProofID10", + address, + gatewayClientPort: DEFAULT_PORT_GATEWAY_CLIENT, + gatewayServerPort: DEFAULT_PORT_GATEWAY_SERVER, + gatewayOpenAPIPort: DEFAULT_PORT_GATEWAY_API, + }, + ], + environment: "development", + enableOpenAPI: true, + bridgesConfig: [fabricConfigJSON], + }; + + // gateway 1 configuration setup: + const configDir1 = path.join(__dirname, "gateway-info/config/gateway-1"); + if (!fs.existsSync(configDir1)) { + fs.mkdirSync(configDir1, { recursive: true }); + } + const configFile1 = path.join(configDir1, "gateway-1-config.json"); + console.log("Creating gateway-1-config.json..."); + fs.writeFileSync(configFile1, JSON.stringify(gatewayJSON1, null, 2)); + expect(fs.existsSync(configFile1)).toBe(true); + + // gateway 2 configuration setup: + const configDir2 = path.join(__dirname, "gateway-info/config/gateway-2"); + if (!fs.existsSync(configDir2)) { + fs.mkdirSync(configDir2, { recursive: true }); + } + const configFile2 = path.join(configDir2, "gateway-2-config.json"); + console.log("Creating gateway-2-config.json..."); + fs.writeFileSync(configFile2, JSON.stringify(gatewayJSON2, null, 2)); + expect(fs.existsSync(configFile2)).toBe(true); + + // gateway 1 outputLogFile and errorLogFile setup: + const logDir1 = path.join(__dirname, "gateway-info/logs/gateway-1"); + if (!fs.existsSync(logDir1)) { + fs.mkdirSync(logDir1, { recursive: true }); + } + const outputLogFile1 = path.join(logDir1, "gateway-logs-output.log"); + const errorLogFile1 = path.join(logDir1, "gateway-logs-error.log"); + // Clear any existing logs + fs.writeFileSync(outputLogFile1, ""); + fs.writeFileSync(errorLogFile1, ""); + // existance check + expect(fs.existsSync(outputLogFile1)).toBe(true); + expect(fs.existsSync(errorLogFile1)).toBe(true); + + // gateway 2 outputLogFile and errorLogFile setup: + const logDir2 = path.join(__dirname, "gateway-info/logs/gateway-2"); + if (!fs.existsSync(logDir2)) { + fs.mkdirSync(logDir2, { recursive: true }); + } + const outputLogFile2 = path.join(logDir2, "gateway-logs-output.log"); + const errorLogFile2 = path.join(logDir2, "gateway-logs-error.log"); + // Clear any existing logs + fs.writeFileSync(outputLogFile2, ""); + fs.writeFileSync(errorLogFile2, ""); + // existance check + expect(fs.existsSync(outputLogFile2)).toBe(true); + expect(fs.existsSync(errorLogFile2)).toBe(true); + + // gatewayRunner1 setup: + const gatewayRunnerOptions1: ISATPGatewayRunnerConstructorOptions = { + containerImageVersion: "2024-10-30T19-54-20-dev-5e06263e0", + containerImageName: "ghcr.io/hyperledger/cacti-satp-hermes-gateway", + logLevel, + emitContainerLogs: true, + configFile: configFile1, + outputLogFile: outputLogFile1, + errorLogFile: errorLogFile1, + serverPort: gatewayIdentity1.gatewayServerPort, + clientPort: gatewayIdentity1.gatewayClientPort, + apiPort: gatewayIdentity1.gatewayOpenAPIPort, + }; + + // gatewayRunner2 setup: + const gatewayRunnerOptions2: ISATPGatewayRunnerConstructorOptions = { + containerImageVersion: "2024-10-30T19-54-20-dev-5e06263e0", + containerImageName: "ghcr.io/hyperledger/cacti-satp-hermes-gateway", + logLevel, + emitContainerLogs: true, + configFile: configFile2, + outputLogFile: outputLogFile2, + errorLogFile: errorLogFile2, + serverPort: gatewayIdentity2.gatewayServerPort, + clientPort: gatewayIdentity2.gatewayClientPort, + apiPort: gatewayIdentity2.gatewayOpenAPIPort, + }; + + gatewayRunner1 = new SATPGatewayRunner(gatewayRunnerOptions1); + gatewayRunner2 = new SATPGatewayRunner(gatewayRunnerOptions2); + + console.log("starting gatewayRunner1..."); + await gatewayRunner1.start(); + console.log("gatewayRunner1 started sucessfully"); + console.log("starting gatewayRunner2..."); + await gatewayRunner2.start(); + console.log("gatewayRunner2 started sucessfully"); + + const sourceAsset: Asset = { + owner: firstHighNetWorthAccount, + ontology: JSON.stringify(BesuSATPInteraction), + contractName: erc20TokenContract, + contractAddress: assetContractAddress, + }; + const receiverAsset: Asset = { + owner: clientId, + ontology: JSON.stringify(FabricSATPInteraction), + contractName: satpContractName, + mspId: fabricUser.mspId, + channelName: fabricChannelName, + }; + const req: TransactRequest = { + contextID: "mockContext", + fromDLTNetworkID: SupportedChain.BESU, + toDLTNetworkID: SupportedChain.FABRIC, + fromAmount: "100", + toAmount: "1", + originatorPubkey: assigneeEthAccount.address, + beneficiaryPubkey: fabricUser.credentials.certificate, + sourceAsset, + receiverAsset, + }; + + const port = await gatewayRunner1.getHostPort(DEFAULT_PORT_GATEWAY_API); + + const transactionApiClient = createClient( + "TransactionApi", + address, + port, + log, + ); + const adminApi = createClient("AdminApi", address, port, log); + + const res = await transactionApiClient.transact(req); + log.info(res?.data.statusResponse); + + const sessions = await adminApi.getSessionIds({}); + expect(sessions.data).toBeTruthy(); + expect(sessions.data.length).toBe(1); + expect(sessions.data[0]).toBe(res.data.sessionID); + + const responseBalanceOwner = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + contractAbi: SATPContract.abi, + invocationType: EthContractInvocationType.Call, + contractAddress: assetContractAddress, + methodName: "checkBalance", + params: [firstHighNetWorthAccount], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 999999999, + }); + expect(responseBalanceOwner).toBeTruthy(); + expect(responseBalanceOwner.success).toBeTruthy(); + console.log( + `Balance Besu Owner Account: ${responseBalanceOwner.callOutput}`, + ); + expect(responseBalanceOwner.callOutput).toBe("0"); + log.info("Amount was transfer correctly from the Owner account"); + + const responseBalanceBridge = await testing_connector.invokeContract({ + contractName: erc20TokenContract, + contractAbi: SATPContract.abi, + invocationType: EthContractInvocationType.Call, + contractAddress: assetContractAddress, + methodName: "checkBalance", + params: [wrapperContractAddress], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 999999999, + }); + expect(responseBalanceBridge).toBeTruthy(); + expect(responseBalanceBridge.success).toBeTruthy(); + console.log( + `Balance Besu Bridge Account: ${responseBalanceBridge.callOutput}`, + ); + expect(responseBalanceBridge.callOutput).toBe("0"); + log.info("Amount was transfer correctly to the Wrapper account"); + + const responseBalance1 = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [BRIDGE_ID], + methodName: "ClientIDAccountBalance", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(responseBalance1).not.toBeUndefined(); + expect(responseBalance1.status).toBeGreaterThan(199); + expect(responseBalance1.status).toBeLessThan(300); + expect(responseBalance1.data).not.toBeUndefined(); + expect(responseBalance1.data.functionOutput).toBe("0"); + console.log( + `Balance Fabric Bridge Account: ${responseBalance1.data.functionOutput}`, + ); + log.info("Amount was transfer correctly from the Bridge account"); + + const responseBalance2 = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [clientId], + methodName: "ClientIDAccountBalance", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + expect(responseBalance2).not.toBeUndefined(); + expect(responseBalance2.status).toBeGreaterThan(199); + expect(responseBalance2.status).toBeLessThan(300); + expect(responseBalance2.data).not.toBeUndefined(); + expect(responseBalance2.data.functionOutput).toBe("1"); + console.log( + `Balance Fabric Owner Account: ${responseBalance2.data.functionOutput}`, + ); + log.info("Amount was transfer correctly to the Owner account"); + }); +}); diff --git a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-ethereum-fabric-transfer-1-gateway-dockerization.test.ts b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-ethereum-fabric-transfer-1-gateway-dockerization.test.ts new file mode 100644 index 0000000000..8ee38f4c63 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-ethereum-fabric-transfer-1-gateway-dockerization.test.ts @@ -0,0 +1,1263 @@ +import { randomUUID as uuidv4 } from "node:crypto"; +import "jest-extended"; + +import { + IListenOptions, + LogLevelDesc, + LoggerProvider, + Secp256k1Keys, + Servers, +} from "@hyperledger/cactus-common"; +// import { v4 as internalIpV4 } from "internal-ip"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + ChainCodeProgrammingLanguage, + Configuration, + DefaultEventHandlerStrategy, + FabricSigningCredential, + FileBase64, + IPluginLedgerConnectorFabricOptions, + PluginLedgerConnectorFabric, + DefaultApi as FabricApi, + FabricContractInvocationType, + ConnectionProfile, +} from "@hyperledger/cactus-plugin-ledger-connector-fabric"; +import http, { Server } from "http"; +import fs from "fs-extra"; +import { + GethTestLedger, + WHALE_ACCOUNT_ADDRESS, +} from "@hyperledger/cactus-test-geth-ledger"; +import { + pruneDockerAllIfGithubAction, + Containers, + FabricTestLedgerV1, + FABRIC_25_LTS_AIO_FABRIC_VERSION, + FABRIC_25_LTS_AIO_IMAGE_VERSION, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + SATPGatewayRunner, + ISATPGatewayRunnerConstructorOptions, +} from "@hyperledger/cactus-test-tooling"; +import bodyParser from "body-parser"; +import express from "express"; +import { DiscoveryOptions, X509Identity } from "fabric-network"; +import { AddressInfo } from "net"; +import path from "path"; +import { + EthereumConfig, + FabricConfig, +} from "../../../main/typescript/types/blockchain-interaction"; +import { IPluginBungeeHermesOptions } from "@hyperledger/cactus-plugin-bungee-hermes"; +import SATPContract from "../../solidity/generated/satp-erc20.sol/SATPContract.json"; +import SATPWrapperContract from "../../../solidity/generated/satp-wrapper.sol/SATPWrapperContract.json"; +import { TransactRequest, Asset } from "../../../main/typescript"; +import { + Address, + GatewayIdentity, + SupportedChain, +} from "../../../main/typescript/core/types"; +import FabricSATPInteraction from "../../../test/typescript/fabric/satp-erc20-interact.json"; +import BesuSATPInteraction from "../../solidity/satp-erc20-interact.json"; +import { createClient } from "../test-utils"; +import { + DEFAULT_PORT_GATEWAY_API, + DEFAULT_PORT_GATEWAY_CLIENT, + DEFAULT_PORT_GATEWAY_SERVER, + SATP_CORE_VERSION, + SATP_ARCHITETURE_VERSION, + SATP_CRASH_VERSION, +} from "../../../main/typescript/core/constants"; +import { ClaimFormat } from "../../../main/typescript/generated/proto/cacti/satp/v02/common/message_pb"; +import { + EthContractInvocationType, + IPluginLedgerConnectorEthereumOptions, + PluginLedgerConnectorEthereum, + Web3SigningCredentialType, +} from "@hyperledger/cactus-plugin-ledger-connector-ethereum"; + +const logLevel: LogLevelDesc = "DEBUG"; +const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: "BUNGEE - Hermes", +}); + +let fabricServer: Server; + +let ethereumLedger: GethTestLedger; + +let fabricLedger: FabricTestLedgerV1; +let fabricSigningCredential: FabricSigningCredential; +let bridgeFabricSigningCredential: FabricSigningCredential; +let configFabric: Configuration; +let fabricChannelName: string; + +const FABRIC_ASSET_ID = uuidv4(); + +const BRIDGE_ID = + "x509::/OU=org2/OU=client/OU=department1/CN=bridge::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com"; + +let clientId: string; +let fabricConfig: FabricConfig; +let pluginOptionsFabricBridge: IPluginLedgerConnectorFabricOptions; +let pluginBungeeFabricOptions: IPluginBungeeHermesOptions; + +let erc20TokenContract: string; +let contractNameWrapper: string; + +let rpcApiHttpHost: string; + +let testing_connector: PluginLedgerConnectorEthereum; +let bridgeEthAccount: string; +const ETH_ASSET_ID = uuidv4(); +let assetContractAddress: string; +let wrapperContractAddress: string; +let satpContractName: string; + +let pluginBungeeEthOptions: IPluginBungeeHermesOptions; + +let ethereumConfig: EthereumConfig; +let ethereumOptions: IPluginLedgerConnectorEthereumOptions; + +let keychainPluginBridge: PluginKeychainMemory; +let keychainEntryKeyBridge: string; +let keychainEntryValueBridge: string; + +let keychainPlugin1: PluginKeychainMemory; +let keychainPlugin2: PluginKeychainMemory; + +let ethOptionsKeychainEntryValue: string; +let ethOptionsKeychainEntryKey: string; + +let discoveryOptions: DiscoveryOptions; + +let fabricUser: X509Identity; + +let apiClient: FabricApi; + +const SATPContract1 = { + contractName: "SATPContract", + abi: SATPContract.abi, + bytecode: SATPContract.bytecode.object, +}; +const SATPWrapperContract1 = { + contractName: "SATPWrapperContract", + abi: SATPWrapperContract.abi, + bytecode: SATPWrapperContract.bytecode.object, +}; + +let gatewayRunner: SATPGatewayRunner; + +afterAll(async () => { + await gatewayRunner.stop(); + await gatewayRunner.destroy(); + await ethereumLedger.stop(); + await ethereumLedger.destroy(); + await fabricLedger.stop(); + await fabricLedger.destroy(); + await Servers.shutdown(fabricServer); + + await pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); +}); + +beforeAll(async () => { + pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); + + // currently not used due to GatewayRunner being in NetworkMode: "host" + // const lanIp = await internalIpV4(); + // if (!lanIp) { + // throw new Error(`LAN IP falsy. internal-ip package failed.`); + // } + + { + const containerImageName = "ghcr.io/hyperledger/cacti-geth-all-in-one"; + const containerImageVersion = "2023-07-27-2a8c48ed6"; + ethereumLedger = new GethTestLedger({ + containerImageName, + containerImageVersion, + }); + await ethereumLedger.start(); + + // Fabric ledger connection + const channelId = "mychannel"; + fabricChannelName = channelId; + + fabricLedger = new FabricTestLedgerV1({ + emitContainerLogs: true, + publishAllPorts: true, + imageName: "ghcr.io/hyperledger/cactus-fabric2-all-in-one", + imageVersion: FABRIC_25_LTS_AIO_IMAGE_VERSION, + envVars: new Map([["FABRIC_VERSION", FABRIC_25_LTS_AIO_FABRIC_VERSION]]), + logLevel: "INFO", + }); + + await fabricLedger.start(); + + log.info("Both Ledgers started successfully"); + } + + { + // setup fabric ledger + const connectionProfile: ConnectionProfile = + await fabricLedger.getConnectionProfileOrg1(); + expect(connectionProfile).not.toBeUndefined(); + + const bridgeProfile: ConnectionProfile = + await fabricLedger.getConnectionProfileOrgX("org2"); + expect(bridgeProfile).not.toBeUndefined(); + + const enrollAdminOut = await fabricLedger.enrollAdmin(); + const adminWallet = enrollAdminOut[1]; + + const enrollAdminBridgeOut = await fabricLedger.enrollAdminV2({ + organization: "org2", + }); + const bridgeWallet = enrollAdminBridgeOut[1]; + + const [userIdentity] = await fabricLedger.enrollUser(adminWallet); + fabricUser = userIdentity; + const opts = { + enrollmentID: "bridge", + organization: "org2", + wallet: bridgeWallet, + }; + + const [bridgeIdentity] = await fabricLedger.enrollUserV2(opts); + + const sshConfig = await fabricLedger.getSshConfig(); + + log.info("enrolled admin"); + + const keychainInstanceId = uuidv4(); + const keychainId = uuidv4(); + const keychainEntryKey = "user1"; + const keychainEntryValue = JSON.stringify(userIdentity); + + console.log("keychainEntryValue: ", keychainEntryValue); + + const keychainPlugin = new PluginKeychainMemory({ + instanceId: keychainInstanceId, + keychainId, + logLevel, + backend: new Map([ + [keychainEntryKey, keychainEntryValue], + ["some-other-entry-key", "some-other-entry-value"], + ]), + }); + + const pluginRegistry = new PluginRegistry({ plugins: [keychainPlugin] }); + + const keychainInstanceIdBridge = uuidv4(); + const keychainIdBridge = uuidv4(); + keychainEntryKeyBridge = "bridge1"; + keychainEntryValueBridge = JSON.stringify(bridgeIdentity); + + console.log("keychainEntryValueBridge: ", keychainEntryValueBridge); + + keychainPluginBridge = new PluginKeychainMemory({ + instanceId: keychainInstanceIdBridge, + keychainId: keychainIdBridge, + logLevel, + backend: new Map([ + [keychainEntryKeyBridge, keychainEntryValueBridge], + ["some-other-entry-key", "some-other-entry-value"], + ]), + }); + + const pluginRegistryBridge = new PluginRegistry({ + plugins: [keychainPluginBridge], + }); + + discoveryOptions = { + enabled: true, + asLocalhost: true, + }; + + const pluginOptions: IPluginLedgerConnectorFabricOptions = { + instanceId: uuidv4(), + dockerBinary: "/usr/local/bin/docker", + peerBinary: "/fabric-samples/bin/peer", + goBinary: "/usr/local/go/bin/go", + pluginRegistry, + cliContainerEnv: FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + sshConfig, + logLevel: "DEBUG", + connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + commitTimeout: 300, + }, + }; + + const fabricConnector = new PluginLedgerConnectorFabric(pluginOptions); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + fabricServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 3000, + server: fabricServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await fabricConnector.getOrCreateWebServices(); + await fabricConnector.registerWebServices(expressApp); + + log.info("Fabric Ledger connector check"); + + const apiUrl = `http://${address}:${port}`; + + configFabric = new Configuration({ basePath: apiUrl }); + + apiClient = new FabricApi(configFabric); + + // deploy contracts ... + satpContractName = "satp-contract"; + const satpWrapperContractName = "satp-wrapper-contract"; + const satpContractRelPath = + "../../../test/typescript/fabric/contracts/satp-contract/chaincode-typescript"; + const wrapperSatpContractRelPath = + "../../../main/typescript/fabric-contracts/satp-wrapper/chaincode-typescript"; + const satpContractDir = path.join(__dirname, satpContractRelPath); + + // ├── package.json + // ├── src + // │ ├── index.ts + // │ ├── ITraceableContract.ts + // │ ├── satp-contract-interface.ts + // │ ├── satp-contract.ts + // ├── tsconfig.json + // ├── lib + // │ └── tokenERC20.js + // -------- + const satpSourceFiles: FileBase64[] = []; + { + const filename = "./tsconfig.json"; + const relativePath = "./"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./package.json"; + const relativePath = "./"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./index.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./ITraceableContract.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-contract-interface.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-contract.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./tokenERC20.ts"; + const relativePath = "./src/"; + const filePath = path.join(satpContractDir, relativePath, filename); + const buffer = await fs.readFile(filePath); + satpSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + + const wrapperSatpContractDir = path.join( + __dirname, + wrapperSatpContractRelPath, + ); + + // ├── package.json + // ├── src + // │ ├── index.ts + // │ ├── interaction-signature.ts + // │ ├── ITraceableContract.ts + // │ ├── satp-wrapper.ts + // │ └── token.ts + // ├── tsconfig.json + // -------- + const wrapperSourceFiles: FileBase64[] = []; + { + const filename = "./tsconfig.json"; + const relativePath = "./"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./package.json"; + const relativePath = "./"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./index.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./interaction-signature.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./ITraceableContract.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./satp-wrapper.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + { + const filename = "./token.ts"; + const relativePath = "./src/"; + const filePath = path.join( + wrapperSatpContractDir, + relativePath, + filename, + ); + const buffer = await fs.readFile(filePath); + wrapperSourceFiles.push({ + body: buffer.toString("base64"), + filepath: relativePath, + filename, + }); + } + + const res = await apiClient.deployContractV1({ + channelId: fabricChannelName, + ccVersion: "1.0.0", + sourceFiles: satpSourceFiles, + ccName: satpContractName, + targetOrganizations: [ + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + ], + caFile: + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1.ORDERER_TLS_ROOTCERT_FILE, + ccLabel: "satp-contract", + ccLang: ChainCodeProgrammingLanguage.Typescript, + ccSequence: 1, + orderer: "orderer.example.com:7050", + ordererTLSHostnameOverride: "orderer.example.com", + connTimeout: 60, + }); + + const { packageIds, lifecycle, success } = res.data; + expect(res.status).toBe(200); + expect(success).toBe(true); + expect(lifecycle).not.toBeUndefined(); + + const { + approveForMyOrgList, + installList, + queryInstalledList, + commit, + packaging, + queryCommitted, + } = lifecycle; + + expect(packageIds).toBeTruthy(); + expect(packageIds).toBeArray(); + + expect(approveForMyOrgList).toBeTruthy(); + expect(approveForMyOrgList).toBeArray(); + + expect(installList).toBeTruthy(); + expect(installList).toBeArray(); + expect(queryInstalledList).toBeTruthy(); + expect(queryInstalledList).toBeArray(); + + expect(commit).toBeTruthy(); + expect(packaging).toBeTruthy(); + expect(queryCommitted).toBeTruthy(); + log.info("SATP Contract deployed"); + + const res2 = await apiClient.deployContractV1({ + channelId: fabricChannelName, + ccVersion: "1.0.0", + sourceFiles: wrapperSourceFiles, + ccName: satpWrapperContractName, + targetOrganizations: [ + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_1, + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + ], + caFile: + FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2.ORDERER_TLS_ROOTCERT_FILE, + ccLabel: "satp-wrapper-contract", + ccLang: ChainCodeProgrammingLanguage.Typescript, + ccSequence: 1, + orderer: "orderer.example.com:7050", + ordererTLSHostnameOverride: "orderer.example.com", + connTimeout: 60, + }); + + const { + packageIds: packageIds2, + lifecycle: lifecycle2, + success: success2, + } = res2.data; + expect(res2.status).toBe(200); + expect(success2).toBe(true); + + const { + approveForMyOrgList: approveForMyOrgList2, + installList: installList2, + queryInstalledList: queryInstalledList2, + commit: commit2, + packaging: packaging2, + queryCommitted: queryCommitted2, + } = lifecycle2; + + expect(packageIds2).toBeTruthy(); + expect(packageIds2).toBeArray(); + + expect(approveForMyOrgList2).toBeTruthy(); + expect(approveForMyOrgList2).toBeArray(); + + expect(installList2).toBeTruthy(); + expect(installList2).toBeArray(); + expect(queryInstalledList2).toBeTruthy(); + expect(queryInstalledList2).toBeArray(); + + expect(commit2).toBeTruthy(); + expect(packaging2).toBeTruthy(); + expect(queryCommitted2).toBeTruthy(); + + log.info("SATP Wrapper Contract deployed"); + + fabricSigningCredential = { + keychainId, + keychainRef: keychainEntryKey, + }; + + bridgeFabricSigningCredential = { + keychainId: keychainIdBridge, + keychainRef: keychainEntryKeyBridge, + }; + + const mspId: string = userIdentity.mspId; + + const initializeResponse = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [mspId, FABRIC_ASSET_ID], + methodName: "InitToken", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(initializeResponse).not.toBeUndefined(); + expect(initializeResponse.status).toBeGreaterThan(199); + expect(initializeResponse.status).toBeLessThan(300); + + log.info( + `SATPContract.InitToken(): ${JSON.stringify(initializeResponse.data)}`, + ); + + const initializeResponse2 = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: [mspId], + methodName: "Initialize", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(initializeResponse2).not.toBeUndefined(); + expect(initializeResponse2.status).toBeGreaterThan(199); + expect(initializeResponse2.status).toBeLessThan(300); + + log.info( + `SATPWrapper.Initialize(): ${JSON.stringify(initializeResponse2.data)}`, + ); + + const setBridgeResponse = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: ["Org2MSP"], + methodName: "setBridge", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + const setBridgeResponse2 = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: ["Org2MSP", BRIDGE_ID], + methodName: "setBridge", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(setBridgeResponse2).not.toBeUndefined(); + expect(setBridgeResponse2.status).toBeGreaterThan(199); + expect(setBridgeResponse2.status).toBeLessThan(300); + + log.info( + `SATPWrapper.setBridge(): ${JSON.stringify(setBridgeResponse.data)}`, + ); + + const responseClientId = await apiClient.runTransactionV1({ + contractName: satpWrapperContractName, + channelName: fabricChannelName, + params: [], + methodName: "ClientAccountID", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + clientId = responseClientId.data.functionOutput.toString(); + + pluginBungeeFabricOptions = { + keyPair: Secp256k1Keys.generateKeyPairsBuffer(), + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry(), + logLevel, + }; + + pluginOptionsFabricBridge = { + instanceId: uuidv4(), + dockerBinary: "/usr/local/bin/docker", + peerBinary: "/fabric-samples/bin/peer", + goBinary: "/usr/local/go/bin/go", + pluginRegistry: pluginRegistryBridge, + cliContainerEnv: FABRIC_25_LTS_FABRIC_SAMPLES_ENV_INFO_ORG_2, + sshConfig, + logLevel: "DEBUG", + connectionProfile: bridgeProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + commitTimeout: 300, + }, + }; + + fabricConfig = { + network: SupportedChain.FABRIC, + signingCredential: bridgeFabricSigningCredential, + channelName: fabricChannelName, + contractName: satpWrapperContractName, + options: pluginOptionsFabricBridge, + bungeeOptions: pluginBungeeFabricOptions, + claimFormat: ClaimFormat.DEFAULT, + } as FabricConfig; + + // networkDetails = { + // connectorApiPath: fabricPath, + // signingCredential: fabricSigningCredential, + // channelName: fabricChannelName, + // contractName: satpContractName, + // participant: "Org1MSP", + // }; + } + + { + //setup ethereum ledger + rpcApiHttpHost = await ethereumLedger.getRpcApiHttpHost(); + + bridgeEthAccount = await ethereumLedger.newEthPersonalAccount(); + + erc20TokenContract = "SATPContract"; + contractNameWrapper = "SATPWrapperContract"; + + ethOptionsKeychainEntryValue = "test"; + ethOptionsKeychainEntryKey = bridgeEthAccount; + keychainPlugin1 = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + + backend: new Map([ + [ethOptionsKeychainEntryKey, ethOptionsKeychainEntryValue], + ]), + logLevel, + }); + + keychainPlugin2 = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + + backend: new Map([ + [ethOptionsKeychainEntryKey, ethOptionsKeychainEntryValue], + ]), + logLevel, + }); + + keychainPlugin1.set(erc20TokenContract, JSON.stringify(SATPContract1)); + keychainPlugin2.set( + contractNameWrapper, + JSON.stringify(SATPWrapperContract1), + ); + + const pluginRegistry = new PluginRegistry({ + plugins: [keychainPlugin1, keychainPlugin2], + }); + + ethereumOptions = { + instanceId: uuidv4(), + rpcApiHttpHost, + pluginRegistry, + logLevel, + }; + testing_connector = new PluginLedgerConnectorEthereum(ethereumOptions); + pluginRegistry.add(testing_connector); + + const deployOutSATPContract = await testing_connector.deployContract({ + contract: { + keychainId: keychainPlugin1.getKeychainId(), + contractName: erc20TokenContract, + }, + constructorArgs: [WHALE_ACCOUNT_ADDRESS, ETH_ASSET_ID], + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + expect(deployOutSATPContract).toBeTruthy(); + expect(deployOutSATPContract.transactionReceipt).toBeTruthy(); + expect( + deployOutSATPContract.transactionReceipt.contractAddress, + ).toBeTruthy(); + + assetContractAddress = + deployOutSATPContract.transactionReceipt.contractAddress ?? ""; + + log.info("SATPContract Deployed successfully"); + const deployOutWrapperContract = await testing_connector.deployContract({ + contract: { + keychainId: keychainPlugin2.getKeychainId(), + contractName: contractNameWrapper, + }, + constructorArgs: [bridgeEthAccount], + web3SigningCredential: { + ethAccount: bridgeEthAccount, + secret: "test", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + expect(deployOutWrapperContract).toBeTruthy(); + expect(deployOutWrapperContract.transactionReceipt).toBeTruthy(); + expect( + deployOutWrapperContract.transactionReceipt.contractAddress, + ).toBeTruthy(); + log.info("SATPWrapperContract Deployed successfully"); + + wrapperContractAddress = + deployOutWrapperContract.transactionReceipt.contractAddress ?? ""; + + pluginBungeeEthOptions = { + keyPair: Secp256k1Keys.generateKeyPairsBuffer(), + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry(), + logLevel, + }; + + ethereumConfig = { + network: SupportedChain.EVM, + keychainId: keychainPlugin2.getKeychainId(), + signingCredential: { + ethAccount: bridgeEthAccount, + secret: "test", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + contractName: contractNameWrapper, + contractAddress: wrapperContractAddress, + options: ethereumOptions, + bungeeOptions: pluginBungeeEthOptions, + gas: 5000000, + claimFormat: ClaimFormat.DEFAULT, + }; + + const giveRoleRes = await testing_connector.invokeContract({ + contract: { + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + }, + invocationType: EthContractInvocationType.Send, + methodName: "giveRole", + params: [wrapperContractAddress], + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + + expect(giveRoleRes).toBeTruthy(); + expect(giveRoleRes.success).toBeTruthy(); + log.info("BRIDGE_ROLE given to SATPWrapperContract successfully"); + } + + const responseMint = await testing_connector.invokeContract({ + contract: { + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + }, + invocationType: EthContractInvocationType.Send, + methodName: "mint", + params: [WHALE_ACCOUNT_ADDRESS, "100"], + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + expect(responseMint).toBeTruthy(); + expect(responseMint.success).toBeTruthy(); + log.info("Minted 100 tokens to firstHighNetWorthAccount"); + + const responseApprove = await testing_connector.invokeContract({ + contract: { + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + }, + invocationType: EthContractInvocationType.Send, + methodName: "approve", + params: [wrapperContractAddress, "100"], + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + expect(responseApprove).toBeTruthy(); + expect(responseApprove.success).toBeTruthy(); + log.info("Approved 100 tokens to SATPWrapperContract"); +}); + +describe("SATPGateway sending a token from Ethereum to Fabric", () => { + it("should realize a transfer", async () => { + const address: Address = `http://localhost`; + + // gateway setup: + const gatewayIdentity = { + id: "mockID", + name: "CustomGateway", + version: [ + { + Core: SATP_CORE_VERSION, + Architecture: SATP_ARCHITETURE_VERSION, + Crash: SATP_CRASH_VERSION, + }, + ], + supportedDLTs: [SupportedChain.FABRIC, SupportedChain.EVM], + proofID: "mockProofID10", + address, + gatewayClientPort: DEFAULT_PORT_GATEWAY_CLIENT, + gatewayServerPort: DEFAULT_PORT_GATEWAY_SERVER, + gatewayOpenAPIPort: DEFAULT_PORT_GATEWAY_API, + } as GatewayIdentity; + + // ethereumConfig Json object setup: + const ethPluginRegistryOptionsJSON = { + plugins: [ + { + instanceId: keychainPlugin1.getInstanceId(), + keychainId: keychainPlugin1.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: ethOptionsKeychainEntryKey, + keychainEntryValue: ethOptionsKeychainEntryValue, + }, + ], + contractName: erc20TokenContract, + contractString: await keychainPlugin1.get(erc20TokenContract), + }, + { + instanceId: keychainPlugin2.getInstanceId(), + keychainId: keychainPlugin2.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: ethOptionsKeychainEntryKey, + keychainEntryValue: ethOptionsKeychainEntryValue, + }, + ], + contractName: contractNameWrapper, + contractString: await keychainPlugin2.get(contractNameWrapper), + }, + ], + }; + + const ethereumOptionsJSON = { + instanceId: ethereumOptions.instanceId, + rpcApiHttpHost: ethereumOptions.rpcApiHttpHost, + rpcApiWsHost: ethereumOptions.rpcApiWsHost, + pluginRegistryOptions: ethPluginRegistryOptionsJSON, + logLevel: ethereumOptions.logLevel, + }; + + const ethBungeeOptionsJSON = { + keyPair: { + privateKey: Buffer.from( + pluginBungeeEthOptions.keyPair!.privateKey, + ).toString("hex"), + publicKey: Buffer.from( + pluginBungeeEthOptions.keyPair!.publicKey, + ).toString("hex"), + }, + instanceId: pluginBungeeEthOptions.instanceId, + logLevel: pluginBungeeEthOptions.logLevel, + }; + + const ethereumConfigJSON = { + network: ethereumConfig.network, + keychainId: ethereumConfig.keychainId, + signingCredential: ethereumConfig.signingCredential, + contractName: ethereumConfig.contractName, + contractAddress: ethereumConfig.contractAddress, + gas: ethereumConfig.gas, + options: ethereumOptionsJSON, + bungeeOptions: ethBungeeOptionsJSON, + claimFormat: ethereumConfig.claimFormat, + }; + + // fabricConfig Json object setup: + const fabricPluginRegistryOptionsJSON = { + plugins: [ + { + instanceId: keychainPluginBridge.getInstanceId(), + keychainId: keychainPluginBridge.getKeychainId(), + logLevel, + backend: [ + { + keychainEntry: keychainEntryKeyBridge, + keychainEntryValue: keychainEntryValueBridge, + }, + { + keychainEntry: "some-other-entry-key", + keychainEntryValue: "some-other-entry-value", + }, + ], + }, + ], + }; + + const fabricOptionsJSON = { + instanceId: pluginOptionsFabricBridge.instanceId, + dockerBinary: pluginOptionsFabricBridge.dockerBinary, + peerBinary: pluginOptionsFabricBridge.peerBinary, + goBinary: pluginOptionsFabricBridge.goBinary, + pluginRegistryOptions: fabricPluginRegistryOptionsJSON, + cliContainerEnv: pluginOptionsFabricBridge.cliContainerEnv, + sshConfig: pluginOptionsFabricBridge.sshConfig, + logLevel: pluginOptionsFabricBridge.logLevel, + connectionProfile: pluginOptionsFabricBridge.connectionProfile, + discoveryOptions: pluginOptionsFabricBridge.discoveryOptions, + eventHandlerOptions: pluginOptionsFabricBridge.eventHandlerOptions, + }; + + const fabricBungeeOptionsJSON = { + keyPair: { + privateKey: Buffer.from( + pluginBungeeFabricOptions.keyPair!.privateKey, + ).toString("hex"), + publicKey: Buffer.from( + pluginBungeeFabricOptions.keyPair!.publicKey, + ).toString("hex"), + }, + instanceId: pluginBungeeFabricOptions.instanceId, + logLevel: pluginBungeeFabricOptions.logLevel, + }; + + const fabricConfigJSON = { + network: fabricConfig.network, + signingCredential: fabricConfig.signingCredential, + channelName: fabricConfig.channelName, + contractName: fabricConfig.contractName, + options: fabricOptionsJSON, + bungeeOptions: fabricBungeeOptionsJSON, + claimFormat: fabricConfig.claimFormat, + }; + + // gateway configuration setup: + const jsonObject = { + gid: gatewayIdentity, + logLevel: "DEBUG", + counterPartyGateways: [], //only knows itself + environment: "development", + enableOpenAPI: true, + bridgesConfig: [ethereumConfigJSON, fabricConfigJSON], + }; + + const configDir = path.join(__dirname, "gateway-info/config"); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + const configFile = path.join(configDir, "gateway-config.json"); + fs.writeFileSync(configFile, JSON.stringify(jsonObject, null, 2)); + + expect(fs.existsSync(configFile)).toBe(true); + + // gateway outputLogFile and errorLogFile setup: + const logDir = path.join(__dirname, "gateway-info/logs"); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + const outputLogFile = path.join(logDir, "gateway-logs-output.log"); + const errorLogFile = path.join(logDir, "gateway-logs-error.log"); + + // Clear any existing logs + fs.writeFileSync(outputLogFile, ""); + fs.writeFileSync(errorLogFile, ""); + + expect(fs.existsSync(outputLogFile)).toBe(true); + expect(fs.existsSync(errorLogFile)).toBe(true); + + //TODO: when ready, change to official hyperledger image + // -- for now use your local image (the name might be different) + // gatewayRunner setup: + const gatewayRunnerOptions: ISATPGatewayRunnerConstructorOptions = { + containerImageVersion: "latest", + containerImageName: "cactus-plugin-satp-hermes-satp-hermes-gateway", + logLevel, + emitContainerLogs: true, + configFile, + outputLogFile, + errorLogFile, + }; + gatewayRunner = new SATPGatewayRunner(gatewayRunnerOptions); + console.log("starting gatewayRunner..."); + await gatewayRunner.start(true); + console.log("gatewayRunner started sucessfully"); + + const sourceAsset: Asset = { + owner: WHALE_ACCOUNT_ADDRESS, + ontology: JSON.stringify(BesuSATPInteraction), + contractName: erc20TokenContract, + contractAddress: assetContractAddress, + }; + const receiverAsset: Asset = { + owner: clientId, + ontology: JSON.stringify(FabricSATPInteraction), + contractName: satpContractName, + mspId: fabricUser.mspId, + channelName: fabricChannelName, + }; + const req: TransactRequest = { + contextID: "mockContext", + fromDLTNetworkID: SupportedChain.EVM, + toDLTNetworkID: SupportedChain.FABRIC, + fromAmount: "100", + toAmount: "1", + originatorPubkey: WHALE_ACCOUNT_ADDRESS, + beneficiaryPubkey: fabricUser.credentials.certificate, + sourceAsset, + receiverAsset, + }; + + const port = await gatewayRunner.getHostPort(DEFAULT_PORT_GATEWAY_API); + + const transactionApiClient = createClient( + "TransactionApi", + address, + port, + log, + ); + const adminApi = createClient("AdminApi", address, port, log); + + const res = await transactionApiClient.transact(req); + log.info(res?.data.statusResponse); + + const sessions = await adminApi.getSessionIds({}); + expect(sessions.data).toBeTruthy(); + expect(sessions.data.length).toBe(1); + expect(sessions.data[0]).toBe(res.data.sessionID); + + const responseBalanceOwner = await testing_connector.invokeContract({ + contract: { + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + }, + invocationType: EthContractInvocationType.Call, + methodName: "checkBalance", + params: [WHALE_ACCOUNT_ADDRESS], + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + expect(responseBalanceOwner).toBeTruthy(); + expect(responseBalanceOwner.success).toBeTruthy(); + expect(responseBalanceOwner.callOutput).toBe(BigInt(0)); + log.info("Amount was transfer correctly from the Owner account"); + + const responseBalanceBridge = await testing_connector.invokeContract({ + contract: { + contractName: erc20TokenContract, + keychainId: keychainPlugin1.getKeychainId(), + }, + invocationType: EthContractInvocationType.Call, + methodName: "checkBalance", + params: [wrapperContractAddress], + web3SigningCredential: { + ethAccount: WHALE_ACCOUNT_ADDRESS, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + }); + expect(responseBalanceBridge).toBeTruthy(); + expect(responseBalanceBridge.success).toBeTruthy(); + expect(responseBalanceBridge.callOutput).toBe(BigInt(0)); + log.info("Amount was transfer correctly to the Wrapper account"); + + const responseBalance1 = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [BRIDGE_ID], + methodName: "ClientIDAccountBalance", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + + expect(responseBalance1).not.toBeUndefined(); + expect(responseBalance1.status).toBeGreaterThan(199); + expect(responseBalance1.status).toBeLessThan(300); + expect(responseBalance1.data).not.toBeUndefined(); + expect(responseBalance1.data.functionOutput).toBe("0"); + log.info("Amount was transfer correctly from the Bridge account"); + + const responseBalance2 = await apiClient.runTransactionV1({ + contractName: satpContractName, + channelName: fabricChannelName, + params: [clientId], + methodName: "ClientIDAccountBalance", + invocationType: FabricContractInvocationType.Send, + signingCredential: fabricSigningCredential, + }); + expect(responseBalance2).not.toBeUndefined(); + expect(responseBalance2.status).toBeGreaterThan(199); + expect(responseBalance2.status).toBeLessThan(300); + expect(responseBalance2.data).not.toBeUndefined(); + expect(responseBalance2.data.functionOutput).toBe("1"); + log.info("Amount was transfer correctly to the Owner account"); + }); +}); diff --git a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-ethereum-fabric-transfer-1-gateway-with-bungee.test.ts b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-ethereum-fabric-transfer-1-gateway-with-bungee.test.ts index de8975ab02..359be80132 100644 --- a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-ethereum-fabric-transfer-1-gateway-with-bungee.test.ts +++ b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/satp-ethereum-fabric-transfer-1-gateway-with-bungee.test.ts @@ -912,7 +912,7 @@ beforeAll(async () => { // }; } }); -describe("SATPGateway sending a token from Besu to Fabric", () => { +describe("SATPGateway sending a token from Ethereum to Fabric", () => { it("should realize a transfer", async () => { //setup satp gateway const factoryOptions: IPluginFactoryOptions = { diff --git a/packages/cactus-plugin-satp-hermes/src/test/typescript/unit/SATPGatewayRunner-instantiation.test.ts b/packages/cactus-plugin-satp-hermes/src/test/typescript/unit/SATPGatewayRunner-instantiation.test.ts new file mode 100644 index 0000000000..7d88a46375 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/test/typescript/unit/SATPGatewayRunner-instantiation.test.ts @@ -0,0 +1,63 @@ +import "jest-extended"; +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { + ISATPGatewayRunnerConstructorOptions, + pruneDockerAllIfGithubAction, + SATPGatewayRunner, +} from "@hyperledger/cactus-test-tooling"; +import { + DEFAULT_PORT_GATEWAY_API, + DEFAULT_PORT_GATEWAY_CLIENT, + DEFAULT_PORT_GATEWAY_SERVER, +} from "../../../main/typescript/core/constants"; + +const testCase = "Instantiate SATP Gateway Runner"; +const logLevel: LogLevelDesc = "TRACE"; + +describe(testCase, () => { + let gatewayRunner: SATPGatewayRunner; + + const gatewayRunnerOptions: ISATPGatewayRunnerConstructorOptions = { + containerImageVersion: "2024-10-30T19-54-20-dev-5e06263e0", + containerImageName: "ghcr.io/hyperledger/cacti-satp-hermes-gateway", + serverPort: DEFAULT_PORT_GATEWAY_SERVER, + clientPort: DEFAULT_PORT_GATEWAY_CLIENT, + apiPort: DEFAULT_PORT_GATEWAY_API, + logLevel, + emitContainerLogs: true, + }; + + beforeAll(async () => { + const pruning = pruneDockerAllIfGithubAction({ logLevel }); + await expect(pruning).toResolve(); + }); + + afterAll(async () => { + await gatewayRunner.stop(); + await gatewayRunner.destroy(); + await pruneDockerAllIfGithubAction({ logLevel }); + }); + + test(testCase, async () => { + gatewayRunner = new SATPGatewayRunner(gatewayRunnerOptions); + + await gatewayRunner.start(); + expect(gatewayRunner).toBeTruthy(); + expect(gatewayRunner.getContainer()).toBeTruthy(); + + const serverHost = await gatewayRunner.getServerHost(); + expect(serverHost).toBeTruthy(); + expect(serverHost).toMatch(/^http:\/\/localhost:\d+$/); + console.log(serverHost); + + const clientHost = await gatewayRunner.getClientHost(); + expect(clientHost).toBeTruthy(); + expect(clientHost).toMatch(/^http:\/\/localhost:\d+$/); + console.log(clientHost); + + const apiHost = await gatewayRunner.getApiHost(); + expect(apiHost).toBeTruthy(); + expect(apiHost).toMatch(/^http:\/\/localhost:\d+$/); + console.log(apiHost); + }); +}); diff --git a/packages/cactus-plugin-satp-hermes/supervisord.conf b/packages/cactus-plugin-satp-hermes/supervisord.conf new file mode 100644 index 0000000000..65e81504b9 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/supervisord.conf @@ -0,0 +1,16 @@ +[supervisord] +logfile=/opt/cacti/satp-hermes/log/supervisord.log +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=info +user=root + +[program:satp-gateway] +command=/usr/local/bin/node index.js +autostart=true +stderr_logfile=/opt/cacti/satp-hermes/log/satp-gateway-error.log +stderr_logfile_maxbytes=10MB +stderr_logfile_backups=5 +stdout_logfile=/opt/cacti/satp-hermes/log/satp-gateway-output.log +stdout_logfile_maxbytes=10MB +stdout_logfile_backups=5 diff --git a/packages/cactus-test-tooling/src/main/typescript/public-api.ts b/packages/cactus-test-tooling/src/main/typescript/public-api.ts index 6054d279b0..768b86294e 100755 --- a/packages/cactus-test-tooling/src/main/typescript/public-api.ts +++ b/packages/cactus-test-tooling/src/main/typescript/public-api.ts @@ -8,6 +8,11 @@ export { BESU_TEST_LEDGER_OPTIONS_JOI_SCHEMA, } from "./besu/besu-test-ledger"; +export { + SATPGatewayRunner, + ISATPGatewayRunnerConstructorOptions, +} from "./satp-runner/satp-gateway-runner"; + export { BesuMpTestLedger, IBesuMpTestLedgerOptions, diff --git a/packages/cactus-test-tooling/src/main/typescript/satp-runner/satp-gateway-runner.ts b/packages/cactus-test-tooling/src/main/typescript/satp-runner/satp-gateway-runner.ts new file mode 100644 index 0000000000..0598f850fc --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/satp-runner/satp-gateway-runner.ts @@ -0,0 +1,320 @@ +import Docker, { Container, ContainerInfo } from "dockerode"; +import Joi from "joi"; +import { EventEmitter } from "events"; +import { + LogLevelDesc, + Logger, + LoggerProvider, + Bools, +} from "@hyperledger/cactus-common"; +import { ITestLedger } from "../i-test-ledger"; +import { Containers } from "../common/containers"; + +export interface ISATPGatewayRunnerConstructorOptions { + containerImageVersion?: string; + containerImageName?: string; + serverPort?: number; + clientPort?: number; + apiPort?: number; + logLevel?: LogLevelDesc; + emitContainerLogs?: boolean; + configFile?: string; + outputLogFile?: string; + errorLogFile?: string; + knexDir?: string; +} + +export const SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS = Object.freeze({ + containerImageVersion: "2024-10-30T19-54-20-dev-5e06263e0", + containerImageName: "ghcr.io/hyperledger/cacti-satp-hermes-gateway", + serverPort: 3010, + clientPort: 3011, + apiPort: 4010, +}); + +export const SATP_GATEWAY_RUNNER_OPTIONS_JOI_SCHEMA: Joi.Schema = + Joi.object().keys({ + containerImageVersion: Joi.string().min(1).required(), + containerImageName: Joi.string().min(1).required(), + serverPort: Joi.number() + .integer() + .positive() + .min(1024) + .max(65535) + .required(), + clientPort: Joi.number() + .integer() + .positive() + .min(1024) + .max(65535) + .required(), + apiPort: Joi.number().integer().positive().min(1024).max(65535).required(), + }); + +export class SATPGatewayRunner implements ITestLedger { + public readonly containerImageVersion: string; + public readonly containerImageName: string; + public readonly serverPort: number; + public readonly clientPort: number; + public readonly apiPort: number; + public readonly emitContainerLogs: boolean; + public readonly configFile?: string; + public readonly outputLogFile?: string; + public readonly errorLogFile?: string; + public readonly knexDir?: string; + + private readonly log: Logger; + private container: Container | undefined; + private containerId: string | undefined; + + constructor( + public readonly options: ISATPGatewayRunnerConstructorOptions = {}, + ) { + if (!options) { + throw new TypeError(`SATPGatewayRunner#ctor options was falsy.`); + } + this.containerImageVersion = + options.containerImageVersion || + SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.containerImageVersion; + this.containerImageName = + options.containerImageName || + SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.containerImageName; + this.serverPort = + options.serverPort || SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.serverPort; + this.clientPort = + options.clientPort || SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.clientPort; + this.apiPort = + options.apiPort || SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.apiPort; + this.configFile = options.configFile; + this.outputLogFile = options.outputLogFile; + this.errorLogFile = options.errorLogFile; + this.knexDir = options.knexDir; + + this.emitContainerLogs = Bools.isBooleanStrict(options.emitContainerLogs) + ? (options.emitContainerLogs as boolean) + : true; + + this.validateConstructorOptions(); + const label = "satp-gateway-runner"; + const level = options.logLevel || "INFO"; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getContainer(): Container { + const fnTag = "SATPGatewayRunner#getContainer()"; + if (!this.container) { + throw new Error(`${fnTag} container not yet started by this instance.`); + } else { + return this.container; + } + } + + public getContainerImageName(): string { + return `${this.containerImageName}:${this.containerImageVersion}`; + } + + public async getServerHost(): Promise { + const hostPort = await this.getHostPort(this.serverPort); + this.log.debug(`getServerHost: ${hostPort}`); + return `http://localhost:${hostPort}`; + } + + public async getClientHost(): Promise { + const hostPort = await this.getHostPort(this.clientPort); + this.log.debug(`getClientHost: ${hostPort}`); + return `http://localhost:${hostPort}`; + } + + public async getApiHost(): Promise { + const hostPort = await this.getHostPort(this.apiPort); + this.log.debug(`getApiHost: ${hostPort}`); + return `http://localhost:${hostPort}`; + } + + public async getHostPort(configuredPort: number): Promise { + if (this.container) { + const containerInfo = await this.getContainerInfo(); + if (containerInfo.HostConfig.NetworkMode === "host") { + // When using host network mode, return the configured port + return configuredPort; + } else { + // For other network modes, use the existing logic + return await Containers.getPublicPort(configuredPort, containerInfo); + } + } else { + throw new Error("Container not started"); + } + } + + private createDockerHostConfig(): Docker.HostConfig { + const hostConfig: Docker.HostConfig = { + PublishAllPorts: true, + Binds: [], + NetworkMode: "host", + }; + + if (this.configFile) { + const containerPath = "/opt/cacti/satp-hermes/gateway-config.json"; + hostConfig.Binds!.push(`${this.configFile}:${containerPath}:ro`); + } + + if (this.outputLogFile) { + const containerPath = + "/opt/cacti/satp-hermes/log/satp-gateway-output.log"; + hostConfig.Binds!.push(`${this.outputLogFile}:${containerPath}:rw`); + } + + if (this.errorLogFile) { + const containerPath = "/opt/cacti/satp-hermes/log/satp-gateway-error.log"; + hostConfig.Binds!.push(`${this.errorLogFile}:${containerPath}:rw`); + } + + if (this.knexDir) { + const containerPath = "/opt/cacti/satp-hermes/src/knex/"; + hostConfig.Binds!.push(`${this.knexDir}:${containerPath}:rw`); + } + return hostConfig; + } + + public async start(omitPull = false): Promise { + const imageFqn = this.getContainerImageName(); + + if (this.container) { + await this.container.stop(); + await this.container.remove(); + } + const docker = new Docker(); + + if (!omitPull) { + this.log.debug(`Pulling container image ${imageFqn} ...`); + await Containers.pullImage(imageFqn, {}, "DEBUG"); + this.log.debug(`Pulled ${imageFqn} OK. Starting container...`); + } + + this.log.debug(`Starting container with image: ${imageFqn}...`); + return new Promise((resolve, reject) => { + const hostConfig: Docker.HostConfig = this.createDockerHostConfig(); + + const eventEmitter: EventEmitter = docker.run( + imageFqn, + [], + [], + { + ExposedPorts: { + [`${this.serverPort}/tcp`]: {}, // SERVER_PORT + [`${this.clientPort}/tcp`]: {}, // CLIENT_PORT + [`${this.apiPort}/tcp`]: {}, // API_PORT + }, + HostConfig: hostConfig, + }, + {}, + (err: unknown) => { + if (err) { + reject(err); + } + }, + ); + + eventEmitter.once("start", async (container: Container) => { + this.log.debug(`Started container OK. Waiting for healthcheck...`); + this.container = container; + this.containerId = container.id; + + if (this.emitContainerLogs) { + const fnTag = `[${this.getContainerImageName()}]`; + await Containers.streamLogs({ + container: this.getContainer(), + tag: fnTag, + log: this.log, + }); + } + try { + await this.waitForHealthCheck(); + this.log.debug(`Healthcheck passing OK.`); + resolve(container); + } catch (ex) { + reject(ex); + } + }); + }); + } + + public async waitForHealthCheck(timeoutMs = 60000): Promise { + const fnTag = "SATPGatewayRunner#waitForHealthCheck()"; + const startedAt = Date.now(); + let isHealthy = false; + do { + if (Date.now() >= startedAt + timeoutMs) { + throw new Error(`${fnTag} timed out (${timeoutMs}ms)`); + } + const { Status, State } = await this.getContainerInfo(); + this.log.debug(`ContainerInfo.Status=%o, State=O%`, Status, State); + isHealthy = Status.endsWith("(healthy)"); + if (!isHealthy) { + await new Promise((resolve2) => setTimeout(resolve2, 1000)); + } + } while (!isHealthy); + this.log.debug(`Left waitForHealthCheck`); + } + + public stop(): Promise { + const fnTag = "SATPGatewayRunner#stop()"; + return new Promise((resolve, reject) => { + if (this.container) { + this.container.stop({}, (err: unknown, result: unknown) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + } else { + return reject(new Error(`${fnTag} Container was not running.`)); + } + }); + } + + public destroy(): Promise { + const fnTag = "SATPGatewayRunner#destroy()"; + if (this.container) { + return this.container.remove(); + } else { + const ex = new Error(`${fnTag} Container not found, nothing to destroy.`); + return Promise.reject(ex); + } + } + + public async getContainerInfo(): Promise { + const docker = new Docker(); + const containerInfos = await docker.listContainers({}); + + let aContainerInfo; + if (this.containerId !== undefined) { + aContainerInfo = containerInfos.find((ci) => ci.Id === this.containerId); + } + + if (aContainerInfo) { + return aContainerInfo; + } else { + throw new Error( + `SATPGatewayRunner#getContainerInfo() no container with ID "${this.containerId}"`, + ); + } + } + + private validateConstructorOptions(): void { + const validationResult = SATP_GATEWAY_RUNNER_OPTIONS_JOI_SCHEMA.validate({ + containerImageVersion: this.containerImageVersion, + containerImageName: this.containerImageName, + serverPort: this.serverPort, + clientPort: this.clientPort, + apiPort: this.apiPort, + }); + + if (validationResult.error) { + throw new Error( + `SATPGatewayRunner#ctor ${validationResult.error.annotate()}`, + ); + } + } +} diff --git a/yarn.lock b/yarn.lock index d94fd29224..eb5ad4bedf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10917,6 +10917,7 @@ __metadata: "@types/swagger-ui-express": "npm:4.1.6" "@types/tape": "npm:4.13.4" "@types/uuid": "npm:10.0.0" + "@vercel/ncc": "npm:0.38.1" axios: "npm:1.7.7" bignumber.js: "npm:9.1.2" bn.js: "npm:5.2.1" @@ -10935,6 +10936,8 @@ __metadata: grpc-tools: "npm:1.12.4" grpc_tools_node_protoc_ts: "npm:5.3.3" hardhat: "npm:2.22.5" + internal-ip: "npm:6.2.0" + jsonc: "npm:2.0.0" knex: "npm:2.4.0" kubo-rpc-client: "npm:3.0.1" make-dir-cli: "npm:3.1.0" @@ -10947,6 +10950,7 @@ __metadata: sqlite3: "npm:5.1.5" swagger-cli: "npm:4.0.4" swagger-ui-express: "npm:5.0.0" + ts-node: "npm:10.9.1" typescript: "npm:5.5.2" typescript-optional: "npm:2.0.1" uuid: "npm:10.0.0" @@ -30710,7 +30714,7 @@ __metadata: languageName: node linkType: hard -"fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": +"fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.0.6, fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 @@ -37781,6 +37785,20 @@ __metadata: languageName: node linkType: hard +"jsonc@npm:2.0.0": + version: 2.0.0 + resolution: "jsonc@npm:2.0.0" + dependencies: + fast-safe-stringify: "npm:^2.0.6" + graceful-fs: "npm:^4.1.15" + mkdirp: "npm:^0.5.1" + parse-json: "npm:^4.0.0" + strip-bom: "npm:^4.0.0" + strip-json-comments: "npm:^3.0.1" + checksum: 10/a4d3052bea15baad31e95b8a8be5f0ddcd7f710cd0426e41efb1325a476605a27f7489706c52d96eeaa9295b543382afa5633e43cfef5e5756d70ae2a40091b4 + languageName: node + linkType: hard + "jsonfile@npm:^2.1.0": version: 2.4.0 resolution: "jsonfile@npm:2.4.0" @@ -51387,7 +51405,7 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.0.1, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 10/492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443