From 54c9ff15780571ac8c55929e757bc063eb7c2205 Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Mon, 29 Jul 2024 14:49:31 +0200 Subject: [PATCH] feat(persistence-ethereum): add sample setup scripts, improve documentation - Fix ethereum connector openapi validation errors (mostly remove nullable from reference fields). - Add sample setup scripts. Simple can be used to run persistence againsy already running ethereum ledger, complete will setup entire environment and run some basic operations to generate sample data. - Improve documentation to include these new scripts and how to use them, fix smaller issues. Signed-off-by: Michal Bajer --- .../openapi/go-client/api/openapi.yaml | 1 - .../src/main/json/openapi.json | 47 ++-- .../src/main/json/openapi.tpl.json | 47 ++-- .../README.md | 75 ++++-- .../package.json | 5 +- .../typescript/manual/common-setup-methods.ts | 131 ++++++++++ .../manual/complete-sample-scenario.ts | 241 ++++++++++++++++++ .../test/typescript/manual/sample-setup.ts | 35 +++ .../tsconfig.json | 3 + tools/docker/geth-all-in-one/README.md | 2 +- yarn.lock | 1 + 11 files changed, 498 insertions(+), 90 deletions(-) create mode 100644 packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/common-setup-methods.ts create mode 100644 packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/complete-sample-scenario.ts create mode 100644 packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/sample-setup.ts diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/go/generated/openapi/go-client/api/openapi.yaml b/packages/cactus-plugin-ledger-connector-ethereum/src/main/go/generated/openapi/go-client/api/openapi.yaml index 41e7e6954ab..d7cfd74fd62 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/go/generated/openapi/go-client/api/openapi.yaml +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/go/generated/openapi/go-client/api/openapi.yaml @@ -1003,7 +1003,6 @@ components: - $ref: '#/components/schemas/ContractJsonDefinition' - $ref: '#/components/schemas/ContractKeychainDefinition' InvokeContractV1Request_contract: - nullable: false oneOf: - $ref: '#/components/schemas/DeployedContractJsonDefinition' - $ref: '#/components/schemas/ContractKeychainDefinition' diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.json index b14b1a536ba..68c973d2309 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.json @@ -210,8 +210,7 @@ "type": "string" }, "gasConfig": { - "$ref": "#/components/schemas/GasTransactionConfig", - "nullable": false + "$ref": "#/components/schemas/GasTransactionConfig" } } }, @@ -365,8 +364,7 @@ "additionalProperties": false, "properties": { "contractJSON": { - "$ref": "#/components/schemas/ContractJSON", - "nullable": false + "$ref": "#/components/schemas/ContractJSON" } } }, @@ -376,8 +374,7 @@ "additionalProperties": false, "properties": { "contractJSON": { - "$ref": "#/components/schemas/ContractJSON", - "nullable": false + "$ref": "#/components/schemas/ContractJSON" }, "contractAddress": { "type": "string", @@ -412,12 +409,10 @@ "additionalProperties": false, "properties": { "web3SigningCredential": { - "$ref": "#/components/schemas/Web3SigningCredential", - "nullable": false + "$ref": "#/components/schemas/Web3SigningCredential" }, "transactionConfig": { - "$ref": "#/components/schemas/EthereumTransactionConfig", - "nullable": false + "$ref": "#/components/schemas/EthereumTransactionConfig" }, "timeoutMs": { "type": "number", @@ -443,18 +438,15 @@ "additionalProperties": false, "properties": { "web3SigningCredential": { - "$ref": "#/components/schemas/Web3SigningCredential", - "nullable": false + "$ref": "#/components/schemas/Web3SigningCredential" }, "contract": { "oneOf": [ { - "$ref": "#/components/schemas/ContractJsonDefinition", - "description": "Send contract ABI directly in the request." + "$ref": "#/components/schemas/ContractJsonDefinition" }, { - "$ref": "#/components/schemas/ContractKeychainDefinition", - "description": "Read contract definition from the keychain plugin." + "$ref": "#/components/schemas/ContractKeychainDefinition" } ], "nullable": false @@ -466,8 +458,7 @@ "items": {} }, "gasConfig": { - "$ref": "#/components/schemas/GasTransactionConfig", - "nullable": false + "$ref": "#/components/schemas/GasTransactionConfig" }, "value": { "type": "string", @@ -484,15 +475,12 @@ "contract": { "oneOf": [ { - "$ref": "#/components/schemas/DeployedContractJsonDefinition", - "description": "Send contract ABI and address directly in the request." + "$ref": "#/components/schemas/DeployedContractJsonDefinition" }, { - "$ref": "#/components/schemas/ContractKeychainDefinition", - "description": "Read contract definition from the keychain plugin." + "$ref": "#/components/schemas/ContractKeychainDefinition" } - ], - "nullable": false + ] }, "methodName": { "description": "The name of the contract method to invoke.", @@ -508,17 +496,13 @@ "items": {} }, "invocationType": { - "$ref": "#/components/schemas/EthContractInvocationType", - "nullable": false, - "description": "Indicates wether it is a CALL or a SEND type of invocation where only SEND ends up creating an actual transaction on the ledger." + "$ref": "#/components/schemas/EthContractInvocationType" }, "web3SigningCredential": { - "$ref": "#/components/schemas/Web3SigningCredential", - "nullable": false + "$ref": "#/components/schemas/Web3SigningCredential" }, "gasConfig": { - "$ref": "#/components/schemas/GasTransactionConfig", - "nullable": false + "$ref": "#/components/schemas/GasTransactionConfig" }, "value": { "type": "string" @@ -599,7 +583,6 @@ "type": "string" }, "invocationType": { - "description": "Contract invocation method to be performed (send, call, etc...)", "$ref": "#/components/schemas/EthContractInvocationWeb3Method" }, "invocationParams": { diff --git a/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.tpl.json b/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.tpl.json index b14b1a536ba..68c973d2309 100644 --- a/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.tpl.json +++ b/packages/cactus-plugin-ledger-connector-ethereum/src/main/json/openapi.tpl.json @@ -210,8 +210,7 @@ "type": "string" }, "gasConfig": { - "$ref": "#/components/schemas/GasTransactionConfig", - "nullable": false + "$ref": "#/components/schemas/GasTransactionConfig" } } }, @@ -365,8 +364,7 @@ "additionalProperties": false, "properties": { "contractJSON": { - "$ref": "#/components/schemas/ContractJSON", - "nullable": false + "$ref": "#/components/schemas/ContractJSON" } } }, @@ -376,8 +374,7 @@ "additionalProperties": false, "properties": { "contractJSON": { - "$ref": "#/components/schemas/ContractJSON", - "nullable": false + "$ref": "#/components/schemas/ContractJSON" }, "contractAddress": { "type": "string", @@ -412,12 +409,10 @@ "additionalProperties": false, "properties": { "web3SigningCredential": { - "$ref": "#/components/schemas/Web3SigningCredential", - "nullable": false + "$ref": "#/components/schemas/Web3SigningCredential" }, "transactionConfig": { - "$ref": "#/components/schemas/EthereumTransactionConfig", - "nullable": false + "$ref": "#/components/schemas/EthereumTransactionConfig" }, "timeoutMs": { "type": "number", @@ -443,18 +438,15 @@ "additionalProperties": false, "properties": { "web3SigningCredential": { - "$ref": "#/components/schemas/Web3SigningCredential", - "nullable": false + "$ref": "#/components/schemas/Web3SigningCredential" }, "contract": { "oneOf": [ { - "$ref": "#/components/schemas/ContractJsonDefinition", - "description": "Send contract ABI directly in the request." + "$ref": "#/components/schemas/ContractJsonDefinition" }, { - "$ref": "#/components/schemas/ContractKeychainDefinition", - "description": "Read contract definition from the keychain plugin." + "$ref": "#/components/schemas/ContractKeychainDefinition" } ], "nullable": false @@ -466,8 +458,7 @@ "items": {} }, "gasConfig": { - "$ref": "#/components/schemas/GasTransactionConfig", - "nullable": false + "$ref": "#/components/schemas/GasTransactionConfig" }, "value": { "type": "string", @@ -484,15 +475,12 @@ "contract": { "oneOf": [ { - "$ref": "#/components/schemas/DeployedContractJsonDefinition", - "description": "Send contract ABI and address directly in the request." + "$ref": "#/components/schemas/DeployedContractJsonDefinition" }, { - "$ref": "#/components/schemas/ContractKeychainDefinition", - "description": "Read contract definition from the keychain plugin." + "$ref": "#/components/schemas/ContractKeychainDefinition" } - ], - "nullable": false + ] }, "methodName": { "description": "The name of the contract method to invoke.", @@ -508,17 +496,13 @@ "items": {} }, "invocationType": { - "$ref": "#/components/schemas/EthContractInvocationType", - "nullable": false, - "description": "Indicates wether it is a CALL or a SEND type of invocation where only SEND ends up creating an actual transaction on the ledger." + "$ref": "#/components/schemas/EthContractInvocationType" }, "web3SigningCredential": { - "$ref": "#/components/schemas/Web3SigningCredential", - "nullable": false + "$ref": "#/components/schemas/Web3SigningCredential" }, "gasConfig": { - "$ref": "#/components/schemas/GasTransactionConfig", - "nullable": false + "$ref": "#/components/schemas/GasTransactionConfig" }, "value": { "type": "string" @@ -599,7 +583,6 @@ "type": "string" }, "invocationType": { - "description": "Contract invocation method to be performed (send, call, etc...)", "$ref": "#/components/schemas/EthContractInvocationWeb3Method" }, "invocationParams": { diff --git a/packages/cactus-plugin-persistence-ethereum/README.md b/packages/cactus-plugin-persistence-ethereum/README.md index fd1b1e6af10..72b412c74b7 100644 --- a/packages/cactus-plugin-persistence-ethereum/README.md +++ b/packages/cactus-plugin-persistence-ethereum/README.md @@ -26,46 +26,75 @@ 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 +#### Ethereum Ledger and Connector -Instantiate a new `PluginPersistenceEthereum` instance: +This plugin requires a running Ethereum ledger that you want to persist to a database. For testing purposes, you can use our [test geth-all-in-one Docker image](../../tools/docker/geth-all-in-one/README.md). Make sure you have the JSON-RPC WS address ready. -```typescript -import { PluginPersistenceEthereum } from "@hyperledger/cactus-plugin-persistence-ethereum"; -import { v4 as uuidv4 } from "uuid"; +Once you have an Ethereum ledger ready, you need to start the [Ethereum Cacti Connector](../cactus-plugin-ledger-connector-ethereum/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). -const persistencePlugin = new PluginPersistenceEthereum({ - instanceId: uuidv4(), - apiClient: new SocketIOApiClient(apiConfigOptions), - logLevel: "info", - connectionString: - "postgresql://postgres:your-super-secret-and-long-postgres-password@localhost:5432/postgres", -}); +#### Supabase Instance -// Initialize the connection to the DB -await persistencePlugin.onPluginInit(); +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 Ethereum connector and persistence plugins to monitor and store ledger data in a database. You need to have a ledger running before executing this script. You can add custom code (e.g., to specify tokens to be monitored) after the comment `CUSTOM CODE GOES HERE !!!!` in the script file. + +By default, the script will try to use a localhost Ethereum ledger (`ws://127.0.0.1:8546`) and our `supabase-all-in-one` instance running on localhost. + +```shell +npm run sample-setup +``` + +Custom ledger and supabase can be set with environment variables `ETHEREUM_RPC_WS_HOST` and `SUPABASE_CONNECTION_STRING`: + +```shell +ETHEREUM_RPC_WS_HOST=ws://127.0.0.1:8546 SUPABASE_CONNECTION_STRING=postgresql://postgres:your-super-secret-and-long-postgres-password@127.0.0.1:5432/postgres npm run sample-setup +``` + +#### Complete Sample Scenario + +Location: [./src/test/typescript/manual/common-setup-methods](./src/test/typescript/manual/common-setup-methods) + +This script starts the test Ethereum ledger for you, deploys a sample ERC721 contract, and mints some tokens. 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 ``` -Alternatively, import `PluginFactoryLedgerPersistence` from the plugin package and use it to create a plugin. +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 `PluginPersistenceEthereum` instance: ```typescript -import { PluginFactoryLedgerPersistence } from "@hyperledger/cactus-plugin-persistence-ethereum"; -import { PluginImportType } from "@hyperledger/cactus-core-api"; +import { PluginPersistenceEthereum } from "@hyperledger/cactus-plugin-persistence-ethereum"; import { v4 as uuidv4 } from "uuid"; -const factory = new PluginFactoryLedgerPersistence({ - pluginImportType: PluginImportType.Local, -}); - -const persistencePlugin = await factory.create({ +const persistencePlugin = new PluginPersistenceEthereum({ instanceId: uuidv4(), - apiClient: new SocketIOApiClient(apiConfigOptions), + apiClient: new EthereumApiClient(apiConfigOptions), logLevel: "info", connectionString: "postgresql://postgres:your-super-secret-and-long-postgres-password@localhost:5432/postgres", diff --git a/packages/cactus-plugin-persistence-ethereum/package.json b/packages/cactus-plugin-persistence-ethereum/package.json index be47c38e046..98127d7e12e 100644 --- a/packages/cactus-plugin-persistence-ethereum/package.json +++ b/packages/cactus-plugin-persistence-ethereum/package.json @@ -56,7 +56,9 @@ "copy-yarn-lock": "mkdir -p ./dist/lib/ && cp -rfp ../../yarn.lock ./dist/yarn.lock", "generate-sdk": "run-p 'generate-sdk:*'", "generate-sdk:typescript-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", - "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: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", + "complete-sample-scenario": "npm run build && node ./dist/lib/test/typescript/manual/complete-sample-scenario.js", + "sample-setup": "npm run build && node ./dist/lib/test/typescript/manual/sample-setup.js" }, "dependencies": { "@ethersproject/abi": "5.7.0", @@ -73,6 +75,7 @@ "web3-validator": "2.0.2" }, "devDependencies": { + "@hyperledger/cactus-cmd-api-server": "2.0.0-rc.3", "@hyperledger/cactus-plugin-keychain-memory": "2.0.0-rc.3", "@hyperledger/cactus-test-geth-ledger": "2.0.0-rc.3", "@hyperledger/cactus-test-tooling": "2.0.0-rc.3", diff --git a/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/common-setup-methods.ts b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/common-setup-methods.ts new file mode 100644 index 00000000000..24d8790caab --- /dev/null +++ b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/common-setup-methods.ts @@ -0,0 +1,131 @@ +/** + * 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! + */ + +import process from "process"; +import { v4 as uuidV4 } from "uuid"; +import { + LoggerProvider, + Logger, + LogLevelDesc, +} from "@hyperledger/cactus-common"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Configuration } from "@hyperledger/cactus-core-api"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + EthereumApiClient, + PluginLedgerConnectorEthereum, +} from "@hyperledger/cactus-plugin-ledger-connector-ethereum"; +import { + ApiServer, + AuthorizationProtocol, + ConfigService, +} from "@hyperledger/cactus-cmd-api-server"; + +import { PluginPersistenceEthereum } from "../../../main/typescript/plugin-persistence-ethereum"; + +////////////////////////////////// +// 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 Ethereum Connector plugin (for accessing the ethereum ledger) + * and Ethereum 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 rpcApiWsHost Ledger RPC WS URL + */ +export async function setupApiServer(port: number, rpcApiWsHost: string) { + // PluginLedgerConnectorEthereum requires a keychain plugin to operate correctly, ensuring secure data storage. + // For testing and debugging purposes, we use PluginKeychainMemory, which stores all secrets in memory (remember: this is not secure!). + const keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidV4(), + keychainId: uuidV4(), + backend: new Map([]), + logLevel: testLogLevel, + }); + + // We create ethereum connector instance. It will connect to the ledger through RPC endpoints rpcApiHttpHost and rpcApiWsHost. + const connector = new PluginLedgerConnectorEthereum({ + instanceId: uuidV4(), + rpcApiWsHost, + logLevel: sutLogLevel, + pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + }); + await connector.onPluginInit(); + + // We need an `EthereumApiClient` to access `PluginLedgerConnectorEthereum` methods from our `PluginPersistenceEthereum`. + const apiConfig = new Configuration({ basePath: `http://127.0.0.1:${port}` }); + const apiClient = new EthereumApiClient(apiConfig); + + // We create persistence plugin, it will read data from ethereum 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 PluginPersistenceEthereum({ + 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; +} + +/** + * 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-ethereum/src/test/typescript/manual/complete-sample-scenario.ts b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/complete-sample-scenario.ts new file mode 100644 index 00000000000..6777699ba15 --- /dev/null +++ b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/complete-sample-scenario.ts @@ -0,0 +1,241 @@ +/** + * Complete example of setting up and using the persistence plugin. This script will: + * - Start the test Ethereum ledger. + * - Deploy an ERC721 contract and mint some tokens. + * - Begin monitoring ledger changes. The persistence plugin will detect the ERC721 tokens, + * as well as all blocks and transactions on the ledger. + * + * Each step is commented in detail to serve as a tutorial. + */ + +import { + LoggerProvider, + Logger, + LogLevelDesc, +} from "@hyperledger/cactus-common"; +import { + GethTestLedger, + WHALE_ACCOUNT_PRIVATE_KEY, +} from "@hyperledger/cactus-test-geth-ledger"; +import Web3, { ContractAbi, TransactionReceipt } from "web3"; +import { Web3Account } from "web3-eth-accounts"; +import TestERC721ContractJson from "../../solidity/TestERC721.json"; +import { cleanupApiServer, setupApiServer } from "./common-setup-methods"; + +////////////////////////////////// +// Constants +////////////////////////////////// + +const testLogLevel: LogLevelDesc = "info"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "complete-sample-scenario", + level: testLogLevel, +}); + +let ledger: GethTestLedger; +let web3: Web3; +let constTestAcc: Web3Account; +let defaultAccountAddress: string; +const constTestAccBalance = 2 * 10e18; + +// Geth environment +const containerImageName = "ghcr.io/hyperledger/cacti-geth-all-in-one"; +const containerImageVersion = "2023-07-27-2a8c48ed6"; + +////////////////////////////////// +// Environment Setup +////////////////////////////////// + +/** + * Create and start the test ledger to be used by sample scenario. + * + * @returns `[rpcApiHttpHost, rpcApiWsHost]` + */ +async function setupTestLedger(): Promise { + log.info(`Start Ledger ${containerImageName}:${containerImageVersion}...`); + ledger = new GethTestLedger({ + containerImageName, + containerImageVersion, + }); + await ledger.start(); + const rpcApiWsHost = await ledger.getRpcApiWebSocketHost(); + log.info(`Ledger started, WS RPC: ${rpcApiWsHost}`); + return rpcApiWsHost; +} + +/** + * Stop the test ledger containers (if created). + * Remember to run it before exiting! + */ +export async function cleanupTestLedger() { + if (ledger) { + log.info("Stop the ethereum ledger..."); + await ledger.stop(); + await ledger.destroy(); + } +} + +/** + * Called when exiting this script + */ +async function cleanupEnvironment() { + await cleanupApiServer(); + await cleanupTestLedger(); +} + +////////////////////////////////// +// Helper Methods +////////////////////////////////// + +/** + * Deploy ERC721 contract to the test leger. + */ +async function deploySmartContract( + abi: ContractAbi, + bytecode: string, + args?: unknown[], +): Promise> { + try { + const txReceipt = await ledger.deployContract(abi, "0x" + bytecode, args); + log.debug("deploySmartContract txReceipt:", txReceipt); + log.debug( + "Deployed test smart contract, TX on block number", + txReceipt.blockNumber, + ); + // Force response without optional fields + return txReceipt as Required; + } catch (error) { + log.error("deploySmartContract ERROR", error); + throw error; + } +} + +/** + * Mint ERC721 token given account. + * + * @param contractAddress ERC721 contract address + * @param targetAddress token recipient address + * @param tokenId token ID to mint + * + * @returns Response from mint operation. + */ +async function mintErc721Token( + contractAddress: string, + targetAddress: string, + tokenId: number, +): Promise { + try { + log.info( + `Mint ERC721 token ID ${tokenId} for address ${targetAddress} by ${defaultAccountAddress}`, + ); + + const tokenContract = new web3.eth.Contract( + TestERC721ContractJson.abi, + contractAddress, + ); + + const mintResponse = await (tokenContract.methods as any) + .safeMint(targetAddress, tokenId) + .send({ + from: defaultAccountAddress, + gas: 8000000, + }); + log.debug("mintErc721Token mintResponse:", mintResponse); + + return mintResponse; + } catch (error) { + log.error("mintErc721Token ERROR", error); + throw error; + } +} + +/** + * Deploy ERC721 contract and mint 3 tokens on it (all to constTestAcc) + * @returns Contract deployment transaction receipt. + */ +async function deployAndMintTokens() { + const erc721Bytecode = TestERC721ContractJson.data.bytecode.object; + const erc721ContractCreationReceipt = await deploySmartContract( + TestERC721ContractJson.abi, + erc721Bytecode, + ); + log.info( + "ERC721 deployed contract address:", + erc721ContractCreationReceipt.contractAddress, + ); + + await mintErc721Token( + erc721ContractCreationReceipt.contractAddress, + constTestAcc.address, + 1, + ); + await mintErc721Token( + erc721ContractCreationReceipt.contractAddress, + constTestAcc.address, + 2, + ); + await mintErc721Token( + erc721ContractCreationReceipt.contractAddress, + constTestAcc.address, + 3, + ); + + return erc721ContractCreationReceipt; +} + +////////////////////////////////// +// Main Logic +////////////////////////////////// + +async function main() { + // Start the test ethereum ledger which we'll monitor and run some sample operations. + const rpcApiWsHost = await setupTestLedger(); + + // Create test account + constTestAcc = await ledger.createEthTestAccount(constTestAccBalance); + + // Create Web3 provider that will be used by other methods. + web3 = new Web3(rpcApiWsHost); + const account = web3.eth.accounts.privateKeyToAccount( + "0x" + WHALE_ACCOUNT_PRIVATE_KEY, + ); + web3.eth.accounts.wallet.add(constTestAcc); + web3.eth.accounts.wallet.add(account); + defaultAccountAddress = account.address; + + // Set up the ApiServer with Ethereum Connector and Ethereum Persistence plugins. + // It returns the persistence plugin, which we can use to run monitoring operations. + const persistence = await setupApiServer(9782, rpcApiWsHost); + console.log("Environment is running..."); + + // Deploy an ERC721 contract to our test ledger and mint some tokens, + // so they can be later scraped by our persistence plugin. + const erc721ContractCreationReceipt = await deployAndMintTokens(); + + // Inform our persistence plugin about the deployed contract. + // From now on, the persistence plugin will monitor any token operations on this contract. + await persistence.addTokenERC721( + erc721ContractCreationReceipt.contractAddress, + ); + + // 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); + }); +} + +process.once("uncaughtException", async () => { + await cleanupEnvironment(); + process.exit(); +}); + +process.once("SIGINT", () => { + console.log("SIGINT received..."); + throw new Error(); +}); + +main(); diff --git a/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/sample-setup.ts b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/sample-setup.ts new file mode 100644 index 00000000000..8f3c2e588d8 --- /dev/null +++ b/packages/cactus-plugin-persistence-ethereum/src/test/typescript/manual/sample-setup.ts @@ -0,0 +1,35 @@ +import { cleanupApiServer, setupApiServer } from "./common-setup-methods"; + +const ETHEREUM_RPC_WS_HOST = + process.env.ETHEREUM_RPC_WS_HOST ?? "ws://127.0.0.1:8546"; + +async function main() { + // Set up the ApiServer with Ethereum Connector and Ethereum Persistence plugins. + // It returns the persistence plugin, which we can use to run monitoring operations. + const persistence = await setupApiServer(9781, ETHEREUM_RPC_WS_HOST); + console.log("Environment is running..."); + + // CUSTOM CODE GOES HERE !!!! + // Inform our persistence plugin about the deployed contract. + // From now on, the persistence plugin will monitor any token operations on this contract. + // await persistence.addTokenERC721("0x123"); + + // 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); + }); +} + +process.once("uncaughtException", async () => { + await cleanupApiServer(); + process.exit(); +}); + +process.once("SIGINT", () => { + console.log("SIGINT received..."); + throw new Error(); +}); + +main(); diff --git a/packages/cactus-plugin-persistence-ethereum/tsconfig.json b/packages/cactus-plugin-persistence-ethereum/tsconfig.json index be1914fcc1f..788c8d9e993 100644 --- a/packages/cactus-plugin-persistence-ethereum/tsconfig.json +++ b/packages/cactus-plugin-persistence-ethereum/tsconfig.json @@ -19,6 +19,9 @@ { "path": "../cactus-core-api/tsconfig.json" }, + { + "path": "../cactus-cmd-api-server/tsconfig.json" + }, { "path": "../cactus-test-tooling/tsconfig.json" }, diff --git a/tools/docker/geth-all-in-one/README.md b/tools/docker/geth-all-in-one/README.md index 529c0d209a7..3f3d5e4eb0f 100644 --- a/tools/docker/geth-all-in-one/README.md +++ b/tools/docker/geth-all-in-one/README.md @@ -25,7 +25,7 @@ docker-compose build && docker-compose up -d ```bash # Build -DOCKER_BUILDKIT=1 docker build ./tools/docker/geth-all-in-one/ -t cactus_geth_all_in_one +docker build ./tools/docker/geth-all-in-one/ -t cactus_geth_all_in_one # Run docker run --rm --name geth_aio_testnet --detach -p 8545:8545 -p 8546:8546 cactus_geth_all_in_one diff --git a/yarn.lock b/yarn.lock index caa2bee0f5f..21088b77ef6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10616,6 +10616,7 @@ __metadata: resolution: "@hyperledger/cactus-plugin-persistence-ethereum@workspace:packages/cactus-plugin-persistence-ethereum" dependencies: "@ethersproject/abi": "npm:5.7.0" + "@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"