diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/cacti-block-formatters.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/cacti-block-formatters.ts index 92c00aba10..4b03fb6d2a 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/cacti-block-formatters.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-block/cacti-block-formatters.ts @@ -79,7 +79,7 @@ export function formatCactiFullBlockResponse( const transaction = payload.data; const transactionActions: FullBlockTransactionActionV1[] = []; - for (const action of transaction.actions) { + for (const action of transaction.actions ?? []) { const actionPayload = action.payload; const proposalPayload = actionPayload.chaincode_proposal_payload; const invocationSpec = proposalPayload.input; diff --git a/packages/cactus-plugin-persistence-fabric/README.md b/packages/cactus-plugin-persistence-fabric/README.md index 041ee927b2..98d65bfce9 100644 --- a/packages/cactus-plugin-persistence-fabric/README.md +++ b/packages/cactus-plugin-persistence-fabric/README.md @@ -25,53 +25,94 @@ Clone the git repository on your local machine. Follow these instructions that w ### Prerequisites +#### Build + In the root of the project, execute the command to install and build the dependencies. It will also build this persistence plugin: ```sh yarn run configure ``` -### Usage +#### Hyperledger Fabric Ledger and Connector -Instantiate a new `PluginPersistenceFabric` instance: +This plugin requires a running Hyperledger Fabric ledger that you want to persist to a database. For testing purposes, you can use our [test fabric-all-in-one Docker image](../../tools/docker/fabric-all-in-one/README.md). To access the ledger you'll need your organization connection profile JSON and a wallet containing registered identity. If you are using our `fabric-all-in-one` image, you can run our [asset-transfer-basic-utils scripts](../../tools/docker/fabric-all-in-one/asset-transfer-basic-utils/README.md) to fetch Org1 connection profile from a docker and register new user to a localhost wallet. -```typescript -import { PluginPersistenceFabric } from "@hyperledger/cactus-plugin-persistence-fabric"; -import { v4 as uuidv4 } from "uuid"; +```shell +# Start the test ledger +docker compose -f tools/docker/fabric-all-in-one/docker-compose-v2.x.yml up +# Wait for it to start (status should become `healthy`) -const persistencePlugin = new PluginPersistenceFabric({ - apiClient, - logLevel: "info", - instanceId, - connectionString: "postgresql://postgres:your-super-secret-and-long-postgres-password@localhost:5432/postgres",, - channelName: "mychannel", - gatewayOptions: { - identity: signingCredential.keychainRef, - wallet: { - keychain: signingCredential, - }, - }, -}); +# Run asset-transfer-basic-utils scripts +cd tools/docker/fabric-all-in-one/asset-transfer-basic-utils +# Cleanup artifacts from previous runs +rm -fr wallet/ connection.json +# Fetch connection profile to `tools/docker/fabric-all-in-one/asset-transfer-basic-utils/connection.json` +# Enroll user using wallet under `tools/docker/fabric-all-in-one/asset-transfer-basic-utils/wallet` +npm install +CACTUS_FABRIC_ALL_IN_ONE_CONTAINER_NAME=fabric_all_in_one_testnet_2x ./setup.sh +``` -// Initialize the connection to the DB -await persistencePlugin.onPluginInit(); +Once you have an Fabric ledger ready, you need to start the [Ethereum Cacti Connector](../cactus-plugin-ledger-connector-fabric/README.md). We recommend running the connector on the same ApiServer instance as the persistence plugin for better performance and reduced network overhead. See the connector package README for more instructions, or check out the [setup sample scripts](./src/test/typescript/manual). + +#### Supabase Instance + +You need a running Supabase instance to serve as a database backend for this plugin. + +### Setup Tutorials + +We've created some sample scripts to help you get started quickly. All the steps have detailed comments on it so you can quickly understand the code. + +#### Sample Setup + +Location: [./src/test/typescript/manual/sample-setup.ts](./src/test/typescript/manual/sample-setup.ts) + +This sample script can be used to set up `ApiServer` with the Fabric connector and persistence plugins to monitor and store ledger data in a database. You need to have a ledger running before executing this script. + +To run the script you need to set the following environment variables: + +- `FABRIC_CONNECTION_PROFILE_PATH`: Full path to fabric ledger connection profile JSON file. +- `FABRIC_CHANNEL_NAME`: Name of the channel we want to connect to (to store it's data). +- `FABRIC_WALLET_PATH` : Full path to wallet containing our identity (that can connect and observe specified channel). +- `FABRIC_WALLET_LABEL`: Name (label) of our identity in a wallet provided in FABRIC_WALLET_PATH + +By default, the script will try to use our `supabase-all-in-one` instance running on localhost. This can be adjusted by setting PostgreSQL connection string in `SUPABASE_CONNECTION_STRING` environment variable (optional). + +```shell +# Example assumes fabric-all-in-one was used. Adjust the variables accordingly. +FABRIC_CONNECTION_PROFILE_PATH=/home/cactus/tools/docker/fabric-all-in-one/asset-transfer-basic-utils/connection.json FABRIC_CHANNEL_NAME=mychannel FABRIC_WALLET_PATH=/home/cactus/tools/docker/fabric-all-in-one/asset-transfer-basic-utils/wallet FABRIC_WALLET_LABEL=appUser +node ./dist/lib/test/typescript/manual/sample-setup.js ``` -Alternatively, import `PluginFactoryLedgerPersistence` from the plugin package and use it to create a plugin. +#### Complete Sample Scenario + +Location: [./src/test/typescript/manual/common-setup-methods](./src/test/typescript/manual/common-setup-methods) + +This script starts the test Hyperledger Fabric ledger for you and executes few transactions on a `basic` chaincode. Then, it synchronizes everything to a database and monitors for all new blocks. This script can also be used for manual, end-to-end tests of a plugin. + +By default, the script will try to use our `supabase-all-in-one` instance running on localhost. + +```shell +npm run complete-sample-scenario +``` + +Custom supabase can be set with environment variable `SUPABASE_CONNECTION_STRING`: + +```shell +SUPABASE_CONNECTION_STRING=postgresql://postgres:your-super-secret-and-long-postgres-password@127.0.0.1:5432/postgres npm run complete-sample-scenario +``` + +### Usage + +Instantiate a new `PluginPersistenceFabric` instance: ```typescript -import { PluginFactoryLedgerPersistence } from "@hyperledger/cactus-plugin-persistence-fabric"; -import { PluginImportType } from "@hyperledger/cactus-core-api"; +import { PluginPersistenceFabric } from "@hyperledger/cactus-plugin-persistence-fabric"; import { v4 as uuidv4 } from "uuid"; -const factory = new PluginFactoryLedgerPersistence({ - pluginImportType: PluginImportType.Local, -}); - -const persistencePlugin = await factory.create({ - apiClient, +const persistencePlugin = new PluginPersistenceFabric({ + apiClient: new FabricApiClient(apiConfigOptions), logLevel: "info", - instanceId, + instanceId: "my-instance", connectionString: "postgresql://postgres:your-super-secret-and-long-postgres-password@localhost:5432/postgres",, channelName: "mychannel", gatewayOptions: { diff --git a/packages/cactus-plugin-persistence-fabric/package.json b/packages/cactus-plugin-persistence-fabric/package.json index a223d115b8..0bf022be3e 100644 --- a/packages/cactus-plugin-persistence-fabric/package.json +++ b/packages/cactus-plugin-persistence-fabric/package.json @@ -47,14 +47,18 @@ "dist/*" ], "scripts": { - "build": "npm run build-ts", + "build": "npm run build-ts && npm run build:dev:backend:postbuild", "build-ts": "tsc", + "build:dev:backend:postbuild": "npm run copy-sql && npm run copy-yarn-lock", "codegen": "yarn run --top-level run-s 'codegen:*'", "codegen:openapi": "npm run generate-sdk", - "copy-yarn-lock": "cp -af ../../yarn.lock ./dist/yarn.lock", + "complete-sample-scenario": "npm run build && node ./dist/lib/test/typescript/manual/complete-sample-scenario.js", + "copy-sql": "mkdir -p ./dist/lib/main/ && cp -Rfp ./src/main/sql ./dist/lib/main/", + "copy-yarn-lock": "mkdir -p ./dist/lib/ && cp -rfp ../../yarn.lock ./dist/yarn.lock", "generate-sdk": "run-p 'generate-sdk:*'", "generate-sdk:go": "openapi-generator-cli generate -i ./src/main/json/openapi.json -g go -o ./src/main/go/generated/openapi/go-client/ --git-user-id hyperledger --git-repo-id $(echo $npm_package_name | replace @hyperledger/ \"\" -z)/src/main/go/generated/openapi/go-client --package-name $(echo $npm_package_name | replace @hyperledger/ \"\" -z) --reserved-words-mappings protected=protected --ignore-file-override ../../openapi-generator-ignore", "generate-sdk:typecript-axios": "openapi-generator-cli generate -i ./src/main/json/openapi.json -g typescript-axios -o ./src/main/typescript/generated/openapi/typescript-axios/ --reserved-words-mappings protected=protected --ignore-file-override ../../openapi-generator-ignore", + "sample-setup": "npm run build && node ./dist/lib/test/typescript/manual/sample-setup.js", "watch": "npm-watch" }, "dependencies": { @@ -69,6 +73,7 @@ "uuid": "10.0.0" }, "devDependencies": { + "@hyperledger/cactus-cmd-api-server": "2.0.0-rc.3", "@hyperledger/cactus-plugin-keychain-memory": "2.0.0-rc.3", "@hyperledger/cactus-test-tooling": "2.0.0-rc.3", "@openapitools/openapi-generator-cli": "2.7.0", @@ -76,6 +81,7 @@ "@types/pg": "8.6.5", "body-parser": "1.20.2", "express": "4.19.2", + "fabric-network": "2.5.0-snapshot.23", "jest-extended": "4.0.1", "rxjs": "7.8.1", "socket.io": "4.6.2" diff --git a/packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql b/packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql index 7e88965aa7..8b59c85e43 100644 --- a/packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql +++ b/packages/cactus-plugin-persistence-fabric/src/main/sql/schema.sql @@ -112,10 +112,8 @@ ALTER TABLE fabric.transaction OWNER TO postgres; ALTER TABLE ONLY fabric.transaction ADD CONSTRAINT transaction_pkey PRIMARY KEY (id); -ALTER TABLE ONLY fabric.transaction - ADD CONSTRAINT transaction_hash_key UNIQUE (hash); -CREATE UNIQUE INDEX transaction_hash_unique_idx ON fabric.transaction USING btree (hash); +CREATE INDEX transaction_hash_idx ON fabric.transaction (hash); -- -- Name: transaction_action; Type: TABLE; Schema: fabric; Owner: postgres -- diff --git a/packages/cactus-plugin-persistence-fabric/src/main/typescript/db-client/db-client.ts b/packages/cactus-plugin-persistence-fabric/src/main/typescript/db-client/db-client.ts index 7a330950e5..11c5ffb7c8 100644 --- a/packages/cactus-plugin-persistence-fabric/src/main/typescript/db-client/db-client.ts +++ b/packages/cactus-plugin-persistence-fabric/src/main/typescript/db-client/db-client.ts @@ -200,15 +200,18 @@ export default class PostgresDatabaseClient { * @returns Map of cert attributes */ private certificateAttrsStringToMap(attrString: string): Map { + const separatorSplitRegex = new RegExp(`[/,+;\n]`); + return new Map( - attrString.split("\n").map((a) => { + attrString.split(separatorSplitRegex).map((a) => { const splitAttrs = a.split("="); if (splitAttrs.length !== 2) { throw new Error( `Invalid certificate attribute string: ${attrString}`, ); } - return splitAttrs as [string, string]; + const [key, value] = splitAttrs; + return [key.trim(), value.trim()]; }), ); } diff --git a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/common-setup-methods.ts b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/common-setup-methods.ts new file mode 100644 index 0000000000..eabbfe999a --- /dev/null +++ b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/common-setup-methods.ts @@ -0,0 +1,171 @@ +/** + * Common setup code for the persistence plugin with detailed comments on each step. + * Requires environment variable `SUPABASE_CONNECTION_STRING` to be set before running the script that includes this! + * If not provided, a localhost instance of supabase will be assumed. + */ + +import process from "process"; +import { v4 as uuidV4 } from "uuid"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + LogLevelDesc, + LoggerProvider, + Logger, +} from "@hyperledger/cactus-common"; +import { Configuration } from "@hyperledger/cactus-core-api"; +import { + ApiServer, + AuthorizationProtocol, + ConfigService, +} from "@hyperledger/cactus-cmd-api-server"; +import { DiscoveryOptions, X509Identity } from "fabric-network"; +import { + DefaultEventHandlerStrategy, + FabricApiClient, + PluginLedgerConnectorFabric, +} from "@hyperledger/cactus-plugin-ledger-connector-fabric"; + +import { PluginPersistenceFabric } from "../../../main/typescript"; + +////////////////////////////////// +// Constants +////////////////////////////////// + +const SUPABASE_CONNECTION_STRING = + process.env.SUPABASE_CONNECTION_STRING ?? + "postgresql://postgres:your-super-secret-and-long-postgres-password@127.0.0.1:5432/postgres"; + +const testLogLevel: LogLevelDesc = "info"; +const sutLogLevel: LogLevelDesc = "info"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "common-setup-methods", + level: testLogLevel, +}); + +/** + * Common ApiServer instance, can be empty if setup was not called yet! + */ +let apiServer: ApiServer; + +////////////////////////////////// +// Methods +////////////////////////////////// + +/** + * Setup Cacti ApiServer instance containing Fabric Connector plugin (for accessing the fabric ledger) + * and Fabric Persistence plugin (for storing data read from ledger to the database). + * + * @param port Port under which an ApiServer will be started. Can't be 0. + * @param channelName Channel that we want to connect to. + * @param connectionProfile Fabric connection profile (JSON object, not a string!) + * @param userIdentity Signing identity to use to connect to the channel (object, not a string!) + * + * @returns `{ persistence, apiClient, signingCredential }` + */ +export async function setupApiServer( + port: number, + channelName: string, + connectionProfile: any, + userIdentity: X509Identity, +) { + // PluginLedgerConnectorFabric requires a keychain plugin to operate correctly, ensuring secure data storage. + // We will store our userIdentity in it. + // For testing and debugging purposes, we use PluginKeychainMemory, which stores all secrets in memory (remember: this is not secure!). + const keychainId = uuidV4(); + const keychainEntryKey = "monitorUser"; + const keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidV4(), + keychainId, + backend: new Map([[keychainEntryKey, JSON.stringify(userIdentity)]]), + logLevel: testLogLevel, + }); + const signingCredential = { + keychainId, + keychainRef: keychainEntryKey, + }; + + // We create fabric connector instance with some default settings assumed. + const discoveryOptions: DiscoveryOptions = { + enabled: true, + asLocalhost: true, + }; + const connector = new PluginLedgerConnectorFabric({ + instanceId: uuidV4(), + pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + sshConfig: {}, + cliContainerEnv: {}, + peerBinary: "/fabric-samples/bin/peer", + logLevel: sutLogLevel, + connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAnyfortx, + commitTimeout: 300, + }, + }); + + // Remember to initialize a plugin + await connector.onPluginInit(); + + // We need an `FabricApiClient` to access `PluginLedgerConnectorFabric` methods from our `PluginPersistenceFabric`. + const apiConfig = new Configuration({ basePath: `http://127.0.0.1:${port}` }); + const apiClient = new FabricApiClient(apiConfig); + + // We create persistence plugin, it will read data from fabric ledger through `apiClient` we've just created, + // and push it to PostgreSQL database accessed by it's SUPABASE_CONNECTION_STRING (read from the environment variable). + const persistence = new PluginPersistenceFabric({ + channelName, + gatewayOptions: { + identity: signingCredential.keychainRef, + wallet: { + keychain: signingCredential, + }, + }, + apiClient, + logLevel: sutLogLevel, + instanceId: uuidV4(), + connectionString: SUPABASE_CONNECTION_STRING, + }); + // Plugin initialization will check connection to the database and setup schema if needed. + await persistence.onPluginInit(); + + // The API Server is a common "container" service that manages our plugins (connector and persistence). + // We use a sample configuration with most security measures disabled for simplicity. + log.info("Create ApiServer..."); + const configService = new ConfigService(); + const cactusApiServerOptions = await configService.newExampleConfig(); + cactusApiServerOptions.authorizationProtocol = AuthorizationProtocol.NONE; + cactusApiServerOptions.configFile = ""; + cactusApiServerOptions.apiCorsDomainCsv = "*"; + cactusApiServerOptions.apiTlsEnabled = false; + cactusApiServerOptions.apiPort = port; + const config = await configService.newExampleConfigConvict( + cactusApiServerOptions, + ); + + apiServer = new ApiServer({ + config: config.getProperties(), + pluginRegistry: new PluginRegistry({ plugins: [connector, persistence] }), + }); + + const apiServerStartOut = await apiServer.start(); + log.debug(`apiServerStartOut:`, apiServerStartOut); + // Our setup is operational now! + + return { persistence, apiClient, signingCredential }; +} + +/** + * Cleanup all the resources allocated by our Api Server. + * Remember to call it before exiting! + */ +export async function cleanupApiServer() { + log.info("cleanupApiServer called."); + + if (apiServer) { + await apiServer.shutdown(); + } +} diff --git a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/complete-sample-scenario.ts b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/complete-sample-scenario.ts new file mode 100644 index 0000000000..78a582fdac --- /dev/null +++ b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/complete-sample-scenario.ts @@ -0,0 +1,184 @@ +/** + * Complete example of setting up and using the persistence plugin. This script will: + * - Start the test Fabric ledger with basic asset contract already deployed on it. + * - Begin monitoring ledger changes. + * - Run `CreateAsset` method few times to trigger more transactions. + * + * Each step is commented in detail to serve as a tutorial. + */ + +import { + DEFAULT_FABRIC_2_AIO_IMAGE_NAME, + FABRIC_25_LTS_AIO_FABRIC_VERSION, + FABRIC_25_LTS_AIO_IMAGE_VERSION, + FabricTestLedgerV1, +} from "@hyperledger/cactus-test-tooling"; +import { + LogLevelDesc, + LoggerProvider, + Logger, +} from "@hyperledger/cactus-common"; +import { cleanupApiServer, setupApiServer } from "./common-setup-methods"; +import { + FabricApiClient, + FabricContractInvocationType, + FabricSigningCredential, +} from "@hyperledger/cactus-plugin-ledger-connector-fabric"; + +////////////////////////////////// +// Constants +////////////////////////////////// + +const testLogLevel: LogLevelDesc = "debug"; + +// Ledger settings +const imageName = DEFAULT_FABRIC_2_AIO_IMAGE_NAME; +const imageVersion = FABRIC_25_LTS_AIO_IMAGE_VERSION; +const fabricEnvVersion = FABRIC_25_LTS_AIO_FABRIC_VERSION; +const ledgerChannelName = "mychannel"; +const ledgerContractName = "basic"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "complete-sample-scenario", + level: testLogLevel, +}); + +let ledger: FabricTestLedgerV1; + +////////////////////////////////// +// Environment Setup +////////////////////////////////// + +/** + * Create and start the test ledger to be used by sample scenario. + * + * @returns Fabric `connectionProfile` + */ +async function setupTestLedger(): Promise { + log.info("Start FabricTestLedgerV1..."); + log.debug("Version:", fabricEnvVersion); + ledger = new FabricTestLedgerV1({ + emitContainerLogs: false, + publishAllPorts: true, + logLevel: testLogLevel, + imageName, + imageVersion, + envVars: new Map([["FABRIC_VERSION", fabricEnvVersion]]), + }); + log.debug("Fabric image:", ledger.getContainerImageName()); + await ledger.start({ omitPull: false }); + + // Get connection profile + log.info("Get fabric connection profile for Org1..."); + const connectionProfile = await ledger.getConnectionProfileOrg1(); + log.debug("Fabric connection profile for Org1 OK: %o", connectionProfile); + + return connectionProfile; +} + +/** + * Stop the test ledger containers (if created). + * Remember to run it before exiting! + */ +export async function cleanupTestLedger() { + if (ledger) { + log.info("Stop the fabric ledger..."); + await ledger.stop(); + await ledger.destroy(); + } +} + +/** + * Called when exiting this script + */ +async function cleanupEnvironment() { + await cleanupApiServer(); + await cleanupTestLedger(); +} + +////////////////////////////////// +// Helper Methods +////////////////////////////////// + +/** + * Run `CreateAsset` method of a `basic` contract deployed on a test fabric ledger. + * + * @param newAssetId new asset ID (must be unique). + * @param apiClient Fabric API client to already running connector instance. + * @param signingCredential Identity to use when sending new transaction to a ledger. + */ +async function runCreateAssetTransaction( + newAssetId: string, + apiClient: FabricApiClient, + signingCredential: FabricSigningCredential, +) { + const createAssetResponse = await apiClient.runTransactionV1({ + signingCredential, + channelName: ledgerChannelName, + invocationType: FabricContractInvocationType.Send, + contractName: ledgerContractName, + methodName: "CreateAsset", + params: [newAssetId, "green", "111", "someOwner", "299"], + }); + log.info( + `Transaction with ID ${createAssetResponse.data.transactionId} sent.`, + ); + + return createAssetResponse.data; +} + +////////////////////////////////// +// Main Logic +////////////////////////////////// + +async function main() { + // Start the test ethereum ledger which we'll monitor and run some sample operations. + const connectionProfile = await setupTestLedger(); + + // Enroll admin and user + const enrollAdminOut = await ledger.enrollAdmin(); + log.debug("Enrolled admin OK."); + const adminWallet = enrollAdminOut[1]; + const [userIdentity] = await ledger.enrollUser(adminWallet); + log.debug("Enrolled user OK."); + + // Set up the ApiServer with Fabric Connector and Fabric Persistence plugins. + // It returns the persistence plugin, which we can use to run monitoring operations. + const { persistence, apiClient, signingCredential } = await setupApiServer( + 9781, // run at that port + ledgerChannelName, + connectionProfile, + userIdentity, + ); + console.log("Environment is running..."); + + // Start monitoring for ledger state changes. + // Any updates will be pushed to the database, and all errors will be printed to the console. + // Press Ctrl + C to stop. + persistence.startMonitor((err) => { + console.error("Persistence monitor error:", err); + }); + + // Run few `basic` contract methods to trigger more transactions. + await runCreateAssetTransaction("1234a", apiClient, signingCredential); + await runCreateAssetTransaction("1234b", apiClient, signingCredential); + await runCreateAssetTransaction("1234c", apiClient, signingCredential); + + // Feel free to include any custom code here for more complete test! + // Remember to press Ctrl + C to exit. +} + +// The following code is used to exit (and cleanup all the acquired resources) in case CTRL + C was pressed. +process.once("uncaughtException", async () => { + await cleanupEnvironment(); + process.exit(); +}); + +process.once("SIGINT", () => { + console.log("SIGINT received..."); + throw new Error(); +}); + +// Run the main application loop +main(); diff --git a/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/sample-setup.ts b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/sample-setup.ts new file mode 100644 index 0000000000..28aa4029e8 --- /dev/null +++ b/packages/cactus-plugin-persistence-fabric/src/test/typescript/manual/sample-setup.ts @@ -0,0 +1,99 @@ +/** + * This script demonstrates how to connect the Cacti fabric connector and persistence plugins + * to an existing Fabric ledger and Supabase database backend. + * Detailed steps are provided to ensure the reader can follow along and customize the code as needed. + * + * ## Usage + * + * Must be run with the following environment variables set: + * - `FABRIC_CONNECTION_PROFILE_PATH` => Full path to fabric ledger connection profile JSON file. + * - `FABRIC_CHANNEL_NAME` => Name of the channel we want to connect to (to store it's data). + * - `FABRIC_WALLET_PATH` => Full path to wallet containing our identity (that can connect and observe specified channel). + * - `FABRIC_WALLET_LABEL` => Name (label) of our identity in a wallet provided in FABRIC_WALLET_PATH + * - `SUPABASE_CONNECTION_STRING` (optional) => PostgreSQL connection string to supabase instance where we'll store the ledger data. If not provided, + * it will try to use a localhost instance (user should run `supabase-all-in-one` container before running a script). + */ + +import { exit } from "node:process"; +import { readFileSync } from "node:fs"; +import { Wallets } from "fabric-network"; +import { cleanupApiServer, setupApiServer } from "./common-setup-methods"; + +/** + * Simple helper method for reading environment variable and exiting if that variable was not found. + */ +function getRequiredEnvVariable(envVariableName: string) { + const value = process.env[envVariableName] ?? ""; + if (!value) { + console.error(`Missing environment variable ${envVariableName}!`); + exit(1); + } + return value; +} + +// Read the required environment variables +const FABRIC_CONNECTION_PROFILE_PATH = getRequiredEnvVariable( + "FABRIC_CONNECTION_PROFILE_PATH", +); +const FABRIC_CHANNEL_NAME = getRequiredEnvVariable("FABRIC_CHANNEL_NAME"); +const FABRIC_WALLET_PATH = getRequiredEnvVariable("FABRIC_WALLET_PATH"); +const FABRIC_WALLET_LABEL = getRequiredEnvVariable("FABRIC_WALLET_LABEL"); + +/** + * Main application function. + * Will read all the required configuration data, create an ApiServer and run the block monitoring. + */ +async function main() { + // First, we read and parse the connection profile JSON file. + const connectionProfileString = readFileSync( + FABRIC_CONNECTION_PROFILE_PATH, + "utf-8", + ); + console.log(`Connection profile path: ${FABRIC_CONNECTION_PROFILE_PATH}`); + if (!connectionProfileString) { + throw new Error("Could not read fabric connection profile (empty file)"); + } + const connectionProfile = JSON.parse(connectionProfileString); + + // Now we open the filesystem wallet and extract the signing identity (it will be needed to access the ledger + // in order to read it's data). + const wallet = await Wallets.newFileSystemWallet(FABRIC_WALLET_PATH); + console.log(`Wallet path: ${FABRIC_WALLET_PATH}`); + const userIdentity = await wallet.get(FABRIC_WALLET_LABEL); + if (!userIdentity) { + throw new Error( + `Missing identity of user ${FABRIC_WALLET_LABEL} in specified wallet!`, + ); + } + + // Set up the ApiServer with Fabric Connector and Fabric Persistence plugins. + // It returns the persistence plugin, which we can use to run monitoring operations. + const { persistence } = await setupApiServer( + 9781, // run at that port + FABRIC_CHANNEL_NAME, + connectionProfile, + userIdentity as any, + ); + console.log("Environment is running..."); + + // Start monitoring for ledger state changes. + // Any updates will be pushed to the database, and all errors will be printed to the console. + // Press Ctrl + C to stop. + persistence.startMonitor((err) => { + console.error("Persistence monitor error:", err); + }); +} + +// The following code is used to exit (and cleanup all the acquired resources) in case CTRL + C was pressed. +process.once("uncaughtException", async () => { + await cleanupApiServer(); + process.exit(); +}); + +process.once("SIGINT", () => { + console.log("SIGINT received..."); + throw new Error(); +}); + +// Run the main application loop +main(); diff --git a/packages/cactus-plugin-persistence-fabric/tsconfig.json b/packages/cactus-plugin-persistence-fabric/tsconfig.json index 9278a883a2..c9c56cc435 100644 --- a/packages/cactus-plugin-persistence-fabric/tsconfig.json +++ b/packages/cactus-plugin-persistence-fabric/tsconfig.json @@ -27,6 +27,9 @@ }, { "path": "../cactus-plugin-keychain-memory/tsconfig.json" + }, + { + "path": "../cactus-cmd-api-server/tsconfig.json" } ] } diff --git a/tools/docker/fabric-all-in-one/Dockerfile_v2.x b/tools/docker/fabric-all-in-one/Dockerfile_v2.x index a5fae68f43..ae40fd4a75 100644 --- a/tools/docker/fabric-all-in-one/Dockerfile_v2.x +++ b/tools/docker/fabric-all-in-one/Dockerfile_v2.x @@ -19,7 +19,7 @@ RUN apk add --no-cache git # Fabric Samples needs bash, sh is not good enough here RUN apk add --no-cache bash -# Need curl to download the Fabric bootstrap script +# Need curl to download the Fabric installation script RUN apk add --no-cache curl # The file binary is used to inspect exectubles when debugging container image issues @@ -158,14 +158,12 @@ RUN /download-frozen-image-v2.sh /etc/hyperledger/fabric/fabric-ca/ hyperledger/ RUN /download-frozen-image-v2.sh /etc/hyperledger/fabric/fabric-couchdb/ hyperledger/fabric-couchdb:${COUCH_VERSION_FABRIC} RUN /download-frozen-image-v2.sh /etc/couchdb/ couchdb:${COUCH_VERSION} -# Download and execute the Fabric bootstrap script, but instruct it with the -d +# Download and execute the Fabric installation script, but instruct it with the -d # flag to avoid pulling docker images because during the build phase of this image # there is no docker daemon running yet -RUN curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/54e27a66812845985c5c067d7f5244a05c6e719b/scripts/bootstrap.sh > /bootstrap.sh -RUN chmod +x bootstrap.sh -# Run the bootstrap here so that at least we can pre-fetch the git clone and the binary downloads resulting in -# faster container startup speed since these steps will not have to be done, only the docker image pulls. -RUN /bootstrap.sh ${FABRIC_VERSION} ${CA_VERSION} -d +RUN curl -sSLO https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh > /install-fabric.sh +RUN chmod +x install-fabric.sh +RUN /install-fabric.sh --fabric-version ${FABRIC_VERSION} --ca-version ${CA_VERSION} binary samples # Update the image version used by the Fabric peers when installing chaincodes. # This is necessary because the older (default) image uses NodeJS v12 and npm v6 @@ -182,7 +180,7 @@ RUN sed -i "s/FABRIC_LOGGING_SPEC=INFO/FABRIC_LOGGING_SPEC=DEBUG/g" /fabric-samp # We need to refactor those commands in the deployment endpoints so that they are immune to this logging setting. # RUN sed -i "s/FABRIC_LOGGING_SPEC=INFO/FABRIC_LOGGING_SPEC=DEBUG/g" /fabric-samples/test-network/compose/compose-test-net.yaml -# Update the docker-compose file of the fabric-samples repo so that the +# Update the docker-compose file of the fabric-samples repo so that the # core.yaml configuration file of the peer containers can be customized. # We need the above because we need to override the NodeJS version the peers are # using when building the chaincodes in the tests. This is necessary because the diff --git a/tools/docker/fabric-all-in-one/run-fabric-network.sh b/tools/docker/fabric-all-in-one/run-fabric-network.sh index 15bf786ccd..eaad1de0d4 100755 --- a/tools/docker/fabric-all-in-one/run-fabric-network.sh +++ b/tools/docker/fabric-all-in-one/run-fabric-network.sh @@ -33,7 +33,7 @@ function main() tar -cC '/etc/hyperledger/fabric/fabric-couchdb/' . | docker load tar -cC '/etc/couchdb/' . | docker load - /bootstrap.sh ${FABRIC_VERSION} ${CA_VERSION} -b -s + /install-fabric.sh --fabric-version ${FABRIC_VERSION} --ca-version ${CA_VERSION} binary samples cd /fabric-samples/test-network/ echo "[FabricAIO] >>> pulling up test network..." diff --git a/yarn.lock b/yarn.lock index 4ff092dd22..97e7930b68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10585,6 +10585,7 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperledger/cactus-plugin-persistence-fabric@workspace:packages/cactus-plugin-persistence-fabric" dependencies: + "@hyperledger/cactus-cmd-api-server": "npm:2.0.0-rc.3" "@hyperledger/cactus-common": "npm:2.0.0-rc.3" "@hyperledger/cactus-core": "npm:2.0.0-rc.3" "@hyperledger/cactus-core-api": "npm:2.0.0-rc.3" @@ -10598,6 +10599,7 @@ __metadata: axios: "npm:1.7.2" body-parser: "npm:1.20.2" express: "npm:4.19.2" + fabric-network: "npm:2.5.0-snapshot.23" jest-extended: "npm:4.0.1" pg: "npm:8.8.0" run-time-error-cjs: "npm:1.4.0"