From 21f01821083502364fa4b85542f5301a14b795a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mona=20B=C3=A4renf=C3=A4nger?= Date: Fri, 20 Oct 2023 18:51:08 +0200 Subject: [PATCH] Enhance interop example scripts (#9109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌱 Add hello and react module * 🐛 Fix schema for react ccCommand * 🔥 Remove ccmID from CcmSendSuccessEvent topics * ♻️ Fix schema and ccCommand execute * Generalize sidechain_registration.ts * Generalize mainchain_registration.ts * Revert "🔥 Remove ccmID from CcmSendSuccessEvent topics" This reverts commit b4f0e3f8776da64239a62bfddbc075543bf61285. * transfer_lsk_sidechain_one.ts * Update mainchain_registration.ts for sc1&2 * Apply suggestions from code review Co-authored-by: !shan * Fix name bug * Fix error * Update sidechain_registration.ts --------- Co-authored-by: Ishan --- .../interop/common/mainchain_registration.ts | 67 ++++++--- .../config/scripts/sidechain_registration.ts | 48 +++--- .../scripts/transfer_lsk_sidechain_one.ts | 17 ++- .../config/default/config.json | 4 + .../config/scripts/mainchain_registration.ts | 7 +- .../pos-sidechain-example-one/src/app/app.ts | 6 + .../hello/cc_commands/react_command.ts | 82 ++++++++++ .../src/app/modules/hello/cc_method.ts | 3 + .../hello/commands/create_hello_command.ts | 89 +++++++++++ .../src/app/modules/hello/constants.ts | 4 + .../src/app/modules/hello/endpoint.ts | 47 ++++++ .../src/app/modules/hello/events/.gitkeep | 0 .../src/app/modules/hello/events/new_hello.ts | 39 +++++ .../src/app/modules/hello/method.ts | 14 ++ .../src/app/modules/hello/module.ts | 141 ++++++++++++++++++ .../src/app/modules/hello/schema.ts | 116 ++++++++++++++ .../src/app/modules/hello/stores/.gitkeep | 0 .../src/app/modules/hello/stores/counter.ts | 36 +++++ .../src/app/modules/hello/stores/message.ts | 34 +++++ .../src/app/modules/hello/stores/reaction.ts | 32 ++++ .../src/app/modules/hello/types.ts | 9 ++ .../config/default/config.json | 4 + .../config/scripts/mainchain_registration.ts | 7 +- .../config/scripts/transfer_lsk_mainchain.ts | 2 +- .../pos-sidechain-example-two/src/app/app.ts | 7 +- .../src/app/modules/react/cc_method.ts | 17 +++ .../modules/react/commands/react_command.ts | 108 ++++++++++++++ .../src/app/modules/react/constants.ts | 4 + .../src/app/modules/react/endpoint.ts | 3 + .../src/app/modules/react/events/.gitkeep | 0 .../src/app/modules/react/method.ts | 3 + .../src/app/modules/react/module.ts | 85 +++++++++++ .../src/app/modules/react/schemas.ts | 94 ++++++++++++ .../src/app/modules/react/stores/.gitkeep | 0 .../src/app/modules/react/types.ts | 28 ++++ examples/interop/run_sidechains.json | 18 ++- 36 files changed, 1130 insertions(+), 45 deletions(-) create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/cc_commands/react_command.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/cc_method.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/commands/create_hello_command.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/constants.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/endpoint.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/events/.gitkeep create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/events/new_hello.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/method.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/module.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/schema.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/.gitkeep create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/counter.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/message.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/reaction.ts create mode 100644 examples/interop/pos-sidechain-example-one/src/app/modules/hello/types.ts create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/cc_method.ts create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/commands/react_command.ts create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/constants.ts create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/endpoint.ts create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/events/.gitkeep create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/method.ts create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/module.ts create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/schemas.ts create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/stores/.gitkeep create mode 100644 examples/interop/pos-sidechain-example-two/src/app/modules/react/types.ts diff --git a/examples/interop/common/mainchain_registration.ts b/examples/interop/common/mainchain_registration.ts index cdd2bb1ea2a..d1f3216194f 100644 --- a/examples/interop/common/mainchain_registration.ts +++ b/examples/interop/common/mainchain_registration.ts @@ -1,22 +1,42 @@ -import { codec, cryptography, apiClient, Transaction } from 'lisk-sdk'; import { + codec, + cryptography, + apiClient, + Transaction, registrationSignatureMessageSchema, mainchainRegParams as mainchainRegParamsSchema, MESSAGE_TAG_CHAIN_REG, MODULE_NAME_INTEROPERABILITY, -} from 'lisk-framework'; -import { COMMAND_NAME_MAINCHAIN_REG } from 'lisk-framework/dist-node/modules/interoperability/constants'; - -export const registerMainchain = async ( - num: string, - sidechainDevValidators: any[], - sidechainValidatorsKeys: any[], -) => { +} from 'lisk-sdk'; + +/** + * Registers the mainchain to a specific sidechain node. + * + * @example + * ```js + * // Update path to point to the dev-validators.json file of the sidechain which shall be registered on the mainchain +import { keys as sidechainDevValidators } from '../default/dev-validators.json'; + + * (async () => { + * await registerMainchain("lisk-core","my-lisk-app",sidechainDevValidators); + *})(); + * ``` + * + * @param mc mainchain alias of the mainchain to be registered. + * @param sc sidechain alias of the sidechain, where the mainchain shall be registered. + * @param sidechainDevValidators the `key` property of the `dev-validators.json` file. + * Includes all keys of the sidechain validators to create the aggregated signature. + */ + +export const registerMainchain = async (mc: string, sc: string, sidechainDevValidators: any[]) => { const { bls, address } = cryptography; - const mainchainClient = await apiClient.createIPCClient(`~/.lisk/mainchain-node-${num}`); - const sidechainClient = await apiClient.createIPCClient(`~/.lisk/pos-sidechain-example-${num}`); + // Connect to the mainchain node + const mainchainClient = await apiClient.createIPCClient(`~/.lisk/${mc}`); + // Connect to the sidechain node + const sidechainClient = await apiClient.createIPCClient(`~/.lisk/${sc}`); + // Get node info data from sidechain and mainchain const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo'); const sidechainNodeInfo = await sidechainClient.invoke('system_getNodeInfo'); @@ -28,15 +48,17 @@ export const registerMainchain = async ( height: mainchainNodeInfo.height, }); + // Sort validator list lexicographically after their BLS key const paramsJSON = { ownChainID: sidechainNodeInfo.chainID, - ownName: `sidechain_example_${num}`, + ownName: sc.replace(/-/g, '_'), mainchainValidators: (mainchainActiveValidators as { blsKey: string; bftWeight: string }[]) .map(v => ({ blsKey: v.blsKey, bftWeight: v.bftWeight })) .sort((a, b) => Buffer.from(a.blsKey, 'hex').compare(Buffer.from(b.blsKey, 'hex'))), mainchainCertificateThreshold, }; + // Define parameters for the mainchain registration const params = { ownChainID: Buffer.from(paramsJSON.ownChainID as string, 'hex'), ownName: paramsJSON.ownName, @@ -47,17 +69,19 @@ export const registerMainchain = async ( mainchainCertificateThreshold: paramsJSON.mainchainCertificateThreshold, }; + // Encode parameters const message = codec.encode(registrationSignatureMessageSchema, params); - // Get active validators from sidechainChain + // Get active validators from sidechain const { validators: sidechainActiveValidators } = await sidechainClient.invoke( 'consensus_getBFTParameters', { height: sidechainNodeInfo.height }, ); + // Add validator private keys to the sidechain validator list const activeValidatorsWithPrivateKey: { blsPublicKey: Buffer; blsPrivateKey: Buffer }[] = []; for (const v of sidechainActiveValidators as { blsKey: string; bftWeight: string }[]) { - const validatorInfo = sidechainValidatorsKeys.find( + const validatorInfo = sidechainDevValidators.find( configValidator => configValidator.plain.blsKey === v.blsKey, ); if (validatorInfo) { @@ -68,11 +92,13 @@ export const registerMainchain = async ( } } console.log('Total activeValidatorsWithPrivateKey:', activeValidatorsWithPrivateKey.length); - // Sort active validators from sidechainChain + + // Sort active validators from sidechain lexicographically after their BLS public key activeValidatorsWithPrivateKey.sort((a, b) => a.blsPublicKey.compare(b.blsPublicKey)); const sidechainValidatorsSignatures: { publicKey: Buffer; signature: Buffer }[] = []; - // Sign with each active validator + + // Sign parameters with each active sidechain validator for (const validator of activeValidatorsWithPrivateKey) { const signature = bls.signData( MESSAGE_TAG_CHAIN_REG, @@ -92,18 +118,23 @@ export const registerMainchain = async ( sidechainValidatorsSignatures, ); + // Get public key and nonce of the sender account const relayerKeyInfo = sidechainDevValidators[0]; const { nonce } = await sidechainClient.invoke<{ nonce: string }>('auth_getAuthAccount', { address: address.getLisk32AddressFromPublicKey(Buffer.from(relayerKeyInfo.publicKey, 'hex')), }); + + // Add aggregated signature to the parameters of the mainchain registration const mainchainRegParams = { ...paramsJSON, signature: signature.toString('hex'), aggregationBits: aggregationBits.toString('hex'), }; + + // Create registerMainchain transaction const tx = new Transaction({ module: MODULE_NAME_INTEROPERABILITY, - command: COMMAND_NAME_MAINCHAIN_REG, + command: 'registerMainchain', fee: BigInt(2000000000), params: codec.encodeJSON(mainchainRegParamsSchema, mainchainRegParams), nonce: BigInt(nonce), @@ -111,11 +142,13 @@ export const registerMainchain = async ( signatures: [], }); + // Sign the transaction tx.sign( Buffer.from(sidechainNodeInfo.chainID as string, 'hex'), Buffer.from(relayerKeyInfo.privateKey, 'hex'), ); + // Post the transaction to a sidechain node const result = await sidechainClient.invoke<{ transactionId: string; }>('txpool_postTransaction', { diff --git a/examples/interop/pos-mainchain-fast/config/scripts/sidechain_registration.ts b/examples/interop/pos-mainchain-fast/config/scripts/sidechain_registration.ts index 082deab672f..050845b6336 100644 --- a/examples/interop/pos-mainchain-fast/config/scripts/sidechain_registration.ts +++ b/examples/interop/pos-mainchain-fast/config/scripts/sidechain_registration.ts @@ -1,57 +1,70 @@ -import { apiClient, codec, cryptography, Transaction } from 'lisk-sdk'; +import { apiClient, codec, sidechainRegParams, cryptography, Transaction } from 'lisk-sdk'; +// Replace this with the path to a file storing the public and private key of a mainchain account who will send the sidechain registration transaction. +// (Can be any account with enough tokens). import { keys } from '../default/dev-validators.json'; -import { sidechainRegParams } from 'lisk-framework'; (async () => { const { address } = cryptography; - const SIDECHAIN_ARRAY = ['one', 'two']; + // Replace this with alias of the sidechain node(s) + const SIDECHAIN_ARRAY = ['pos-sidechain-example-one', 'pos-sidechain-example-two']; + // Replace this with the alias of the mainchain node(s), e.g. lisk-core + // Note: Number of mainchain nodes should be equal to sidechain nodes, for this script to work properly. + const MAINCHAIN_ARRAY = ['mainchain-node-one', 'mainchain-node-two']; let i = 0; for (const nodeAlias of SIDECHAIN_ARRAY) { - const sidechainClient = await apiClient.createIPCClient( - `~/.lisk/pos-sidechain-example-${nodeAlias}`, - ); - const mainchainClient = await apiClient.createIPCClient(`~/.lisk/mainchain-node-${nodeAlias}`); + // Connect to the sidechain node + const sidechainClient = await apiClient.createIPCClient(`~/.lisk/${nodeAlias}`); + // Connect to the mainchain node + const mainchainClient = await apiClient.createIPCClient(`~/.lisk/${MAINCHAIN_ARRAY[i]}`); + // Get node info data from sidechain and mainchain const sidechainNodeInfo = await sidechainClient.invoke('system_getNodeInfo'); const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo'); - // Get active validators from sidechainchain - const { validators: sidehcainActiveValidators, certificateThreshold } = + + // Get info about the active sidechain validators and the certificate threshold + const { validators: sidechainActiveValidators, certificateThreshold } = await sidechainClient.invoke('consensus_getBFTParameters', { height: sidechainNodeInfo.height, }); - (sidehcainActiveValidators as { blsKey: string; bftWeight: string }[]).sort((a, b) => + // Sort validator list lexicographically after their BLS key + (sidechainActiveValidators as { blsKey: string; bftWeight: string }[]).sort((a, b) => Buffer.from(a.blsKey, 'hex').compare(Buffer.from(b.blsKey, 'hex')), ); + // Define parameters for the sidechain registration const params = { sidechainCertificateThreshold: certificateThreshold, - sidechainValidators: sidehcainActiveValidators, + sidechainValidators: sidechainActiveValidators, chainID: sidechainNodeInfo.chainID, - name: `sidechain_example_${nodeAlias}`, + name: nodeAlias.replace(/-/g, '_'), }; - const relayerkeyInfo = keys[2]; + // Get public key and nonce of the sender account + const relayerKeyInfo = keys[2]; const { nonce } = await mainchainClient.invoke<{ nonce: string }>('auth_getAuthAccount', { - address: address.getLisk32AddressFromPublicKey(Buffer.from(relayerkeyInfo.publicKey, 'hex')), + address: address.getLisk32AddressFromPublicKey(Buffer.from(relayerKeyInfo.publicKey, 'hex')), }); + // Create registerSidechain transaction const tx = new Transaction({ module: 'interoperability', command: 'registerSidechain', fee: BigInt(2000000000), params: codec.encodeJSON(sidechainRegParams, params), nonce: BigInt(nonce), - senderPublicKey: Buffer.from(relayerkeyInfo.publicKey, 'hex'), + senderPublicKey: Buffer.from(relayerKeyInfo.publicKey, 'hex'), signatures: [], }); + // Sign the transaction tx.sign( Buffer.from(mainchainNodeInfo.chainID as string, 'hex'), - Buffer.from(relayerkeyInfo.privateKey, 'hex'), + Buffer.from(relayerKeyInfo.privateKey, 'hex'), ); + // Post the transaction to a mainchain node const result = await mainchainClient.invoke<{ transactionId: string; }>('txpool_postTransaction', { @@ -59,11 +72,12 @@ import { sidechainRegParams } from 'lisk-framework'; }); console.log( - `Sent sidechain registration transaction on mainchain node ${nodeAlias}. Result from transaction pool is: `, + `Sent sidechain registration transaction on mainchain node ${MAINCHAIN_ARRAY[1]}. Result from transaction pool is: `, result, ); i += 1; + // Wait in case there are more elements in the SIDECHAIN_ARRAY, after performing another loop with the next element. const wait = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); if (i < SIDECHAIN_ARRAY.length) { const WAIT_PERIOD = 10000; diff --git a/examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts b/examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts index 8be39bb443c..65ba8d01be1 100644 --- a/examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts +++ b/examples/interop/pos-mainchain-fast/config/scripts/transfer_lsk_sidechain_one.ts @@ -1,4 +1,6 @@ import { apiClient, codec, cryptography, Schema, Transaction } from 'lisk-sdk'; +// Replace this with the path to a file storing the public and private key of a mainchain account who will send the sidechain registration transaction. +// (Can be any account with enough tokens). import { keys } from '../default/dev-validators.json'; type ModulesMetadata = [ { @@ -12,25 +14,30 @@ type ModulesMetadata = [ const { address } = cryptography; const nodeAlias = 'one'; + // Update this with the Token ID of the token you wish to transfer const tokenID = Buffer.from('0400000000000000', 'hex'); - const sidechainID = Buffer.from('04000001', 'hex'); // Update this to send to another sidechain + // Update this with the chain ID of the receiving chain + const sidechainID = Buffer.from('04000001', 'hex'); + // Update this with the recipient address const recipientLSKAddress = 'lskxz85sur2yo22dmcxybe39uvh2fg7s2ezxq4ny9'; const recipientAddress = address.getAddressFromLisk32Address(recipientLSKAddress); + // Connect to the mainchain node const mainchainClient = await apiClient.createIPCClient(`~/.lisk/mainchain-node-one`); + // Get node info data from mainchain const mainchainNodeInfo = await mainchainClient.invoke('system_getNodeInfo'); + // Get schema for the transferCrossChain command const { modules: modulesMetadata } = await mainchainClient.invoke<{ modules: ModulesMetadata; }>('system_getMetadata'); - const tokenMetadata = modulesMetadata.find(m => m.name === 'token'); - const ccTransferCMDSchema = tokenMetadata?.commands.filter( cmd => cmd.name == 'transferCrossChain', )[0].params as Schema; + // Define parameters for the cc transfer const params = { tokenID, amount: BigInt('10000000000'), @@ -41,11 +48,13 @@ type ModulesMetadata = [ messageFeeTokenID: tokenID, }; + // Get public key and nonce of the sender account const relayerkeyInfo = keys[2]; const { nonce } = await mainchainClient.invoke<{ nonce: string }>('auth_getAuthAccount', { address: address.getLisk32AddressFromPublicKey(Buffer.from(relayerkeyInfo.publicKey, 'hex')), }); + // Create transferCrossChain transaction const tx = new Transaction({ module: 'token', command: 'transferCrossChain', @@ -56,11 +65,13 @@ type ModulesMetadata = [ signatures: [], }); + // Sign the transaction tx.sign( Buffer.from(mainchainNodeInfo.chainID as string, 'hex'), Buffer.from(relayerkeyInfo.privateKey, 'hex'), ); + // Post the transaction to a mainchain node const result = await mainchainClient.invoke<{ transactionId: string; }>('txpool_postTransaction', { diff --git a/examples/interop/pos-sidechain-example-one/config/default/config.json b/examples/interop/pos-sidechain-example-one/config/default/config.json index b1cb2b0e91d..562a5b59e4b 100644 --- a/examples/interop/pos-sidechain-example-one/config/default/config.json +++ b/examples/interop/pos-sidechain-example-one/config/default/config.json @@ -48,6 +48,10 @@ "ccuFee": "500000000", "receivingChainIPCPath": "~/.lisk/mainchain-node-one", "registrationHeight": 10 + }, + "dashboard": { + "applicationUrl": "ws://127.0.0.1:7885/rpc-ws", + "port": 4006 } } } diff --git a/examples/interop/pos-sidechain-example-one/config/scripts/mainchain_registration.ts b/examples/interop/pos-sidechain-example-one/config/scripts/mainchain_registration.ts index f41598533a4..ba3439b6ed5 100644 --- a/examples/interop/pos-sidechain-example-one/config/scripts/mainchain_registration.ts +++ b/examples/interop/pos-sidechain-example-one/config/scripts/mainchain_registration.ts @@ -1,7 +1,10 @@ -import { keys as sidechainValidatorsKeys } from '../../config/default/dev-validators.json'; import { keys as sidechainDevValidators } from '../default/dev-validators.json'; import { registerMainchain } from '../../../common/mainchain_registration'; (async () => { - await registerMainchain('one', sidechainDevValidators, sidechainValidatorsKeys); + await registerMainchain( + 'mainchain-node-one', + 'pos-sidechain-example-one', + sidechainDevValidators, + ); })(); diff --git a/examples/interop/pos-sidechain-example-one/src/app/app.ts b/examples/interop/pos-sidechain-example-one/src/app/app.ts index d9dc8b2ad28..8b084b532fe 100644 --- a/examples/interop/pos-sidechain-example-one/src/app/app.ts +++ b/examples/interop/pos-sidechain-example-one/src/app/app.ts @@ -1,10 +1,16 @@ import { Application, PartialApplicationConfig } from 'lisk-sdk'; import { registerModules } from './modules'; import { registerPlugins } from './plugins'; +import { HelloModule } from './modules/hello/module'; export const getApplication = (config: PartialApplicationConfig): Application => { const { app } = Application.defaultApplication(config); + const helloModule = new HelloModule(); + app.registerModule(helloModule); + + app.registerInteroperableModule(helloModule); + registerModules(app); registerPlugins(app); diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/cc_commands/react_command.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/cc_commands/react_command.ts new file mode 100644 index 00000000000..de26cd3409c --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/cc_commands/react_command.ts @@ -0,0 +1,82 @@ +/* eslint-disable class-methods-use-this */ + +import { BaseCCCommand, CrossChainMessageContext, codec, cryptography, db } from 'lisk-sdk'; +import { crossChainReactParamsSchema, CCReactMessageParams } from '../schema'; +import { MAX_RESERVED_ERROR_STATUS, CROSS_CHAIN_COMMAND_NAME_REACT } from '../constants'; +import { ReactionStore, ReactionStoreData } from '../stores/reaction'; +import { MessageStore } from '../stores/message'; + +export class ReactCCCommand extends BaseCCCommand { + public schema = crossChainReactParamsSchema; + + public get name(): string { + return CROSS_CHAIN_COMMAND_NAME_REACT; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async verify(ctx: CrossChainMessageContext): Promise { + const { ccm } = ctx; + + if (ccm.status > MAX_RESERVED_ERROR_STATUS) { + throw new Error('Invalid CCM status code.'); + } + + const params = codec.decode(crossChainReactParamsSchema, ccm.params); + const messageCreatorAddress = cryptography.address.getAddressFromLisk32Address( + params.helloMessageID, + ); + if (!(await this.stores.get(MessageStore).has(ctx, messageCreatorAddress))) { + throw new Error('Message ID does not exists.'); + } + } + + public async execute(ctx: CrossChainMessageContext): Promise { + const { ccm, logger } = ctx; + logger.info('Executing React CCM'); + // const methodContext = ctx.getMethodContext(); + // const { sendingChainID, status, receivingChainID } = ccm; + const params = codec.decode(crossChainReactParamsSchema, ccm.params); + logger.info(params, 'parameters'); + const { helloMessageID, reactionType } = params; + const reactionSubstore = this.stores.get(ReactionStore); + + logger.info({ helloMessageID }, 'Contents of helloMessageID'); + const messageCreatorAddress = cryptography.address.getAddressFromLisk32Address(helloMessageID); + logger.info({ messageCreatorAddress }, 'Contents of messageCreatorAddress'); + + let msgReactions: ReactionStoreData; + + try { + msgReactions = await reactionSubstore.get(ctx, messageCreatorAddress); + } catch (error) { + if (!(error instanceof db.NotFoundError)) { + logger.info({ helloMessageID, crossChainCommand: this.name }, (error as Error).message); + logger.error({ error }, 'Error when getting the reaction substore'); + throw error; + } + + logger.info( + { helloMessageID, crossChainCommand: this.name }, + `No entry exists for given helloMessageID ${helloMessageID}. Creating a default entry.`, + ); + msgReactions = { reactions: { like: [] } }; + } + + logger.info( + { msgReactions }, + '+++++++++++++++++++++++++++++=============++++++++++++++++++++++++', + ); + logger.info({ msgReactions }, 'Contents of the reaction store PRE'); + logger.info(msgReactions, 'Contents of the reaction store PRE'); + if (reactionType === 0) { + // TODO: Check if the Likes array already contains the sender address. If yes, remove the address to unlike the post. + msgReactions.reactions.like.push(ctx.transaction.senderAddress); + } else { + logger.error({ reactionType }, 'invalid reaction type'); + } + + logger.info(msgReactions, 'Contents of the reaction store POST'); + logger.info({ msgReactions }, 'Contents of the reaction store POST'); + await reactionSubstore.set(ctx, messageCreatorAddress, msgReactions); + } +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/cc_method.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/cc_method.ts new file mode 100644 index 00000000000..f8535173f9a --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/cc_method.ts @@ -0,0 +1,3 @@ +import { BaseCCMethod } from 'lisk-sdk'; + +export class HelloInteroperableMethod extends BaseCCMethod {} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/commands/create_hello_command.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/commands/create_hello_command.ts new file mode 100644 index 00000000000..86cc22753bd --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/commands/create_hello_command.ts @@ -0,0 +1,89 @@ +/* eslint-disable class-methods-use-this */ + +import { + BaseCommand, + CommandVerifyContext, + CommandExecuteContext, + VerificationResult, + VerifyStatus, +} from 'lisk-sdk'; +import { createHelloSchema } from '../schema'; +import { MessageStore } from '../stores/message'; +import { counterKey, CounterStore, CounterStoreData } from '../stores/counter'; +import { ModuleConfig } from '../types'; +import { NewHelloEvent } from '../events/new_hello'; + +interface Params { + message: string; +} + +export class CreateHelloCommand extends BaseCommand { + public schema = createHelloSchema; + private _blacklist!: string[]; + + // eslint-disable-next-line @typescript-eslint/require-await + public async init(config: ModuleConfig): Promise { + // Set _blacklist to the value of the blacklist defined in the module config + this._blacklist = config.blacklist; + // Set the max message length to the value defined in the module config + this.schema.properties.message.maxLength = config.maxMessageLength; + // Set the min message length to the value defined in the module config + this.schema.properties.message.minLength = config.minMessageLength; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async verify(context: CommandVerifyContext): Promise { + let validation: VerificationResult; + const wordList = context.params.message.split(' '); + const found = this._blacklist.filter(value => wordList.includes(value)); + if (found.length > 0) { + context.logger.info('==== FOUND: Message contains a blacklisted word ===='); + throw new Error(`Illegal word in hello message: ${found.toString()}`); + } else { + context.logger.info('==== NOT FOUND: Message contains no blacklisted words ===='); + validation = { + status: VerifyStatus.OK, + }; + } + return validation; + } + + public async execute(context: CommandExecuteContext): Promise { + // 1. Get account data of the sender of the Hello transaction. + const { senderAddress } = context.transaction; + // 2. Get message and counter stores. + const messageSubstore = this.stores.get(MessageStore); + const counterSubstore = this.stores.get(CounterStore); + + // 3. Save the Hello message to the message store, using the senderAddress as key, and the message as value. + await messageSubstore.set(context, senderAddress, { + message: context.params.message, + }); + + // 3. Get the Hello counter from the counter store. + let helloCounter: CounterStoreData; + try { + helloCounter = await counterSubstore.get(context, counterKey); + } catch (error) { + helloCounter = { + counter: 0, + }; + } + // 5. Increment the Hello counter +1. + helloCounter.counter += 1; + + // 6. Save the Hello counter to the counter store. + await counterSubstore.set(context, counterKey, helloCounter); + + // 7. Emit a "New Hello" event + const newHelloEvent = this.events.get(NewHelloEvent); + newHelloEvent.add( + context, + { + senderAddress: context.transaction.senderAddress, + message: context.params.message, + }, + [context.transaction.senderAddress], + ); + } +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/constants.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/constants.ts new file mode 100644 index 00000000000..9b427343540 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/constants.ts @@ -0,0 +1,4 @@ +export const CROSS_CHAIN_COMMAND_NAME_REACT = 'reactCrossChain'; + +export const MAX_RESERVED_ERROR_STATUS = 63; +export const CCM_STATUS_OK = 0; diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/endpoint.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/endpoint.ts new file mode 100644 index 00000000000..4db39083be5 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/endpoint.ts @@ -0,0 +1,47 @@ +import { BaseEndpoint, ModuleEndpointContext, cryptography } from 'lisk-sdk'; +import { counterKey, CounterStore, CounterStoreData } from './stores/counter'; +import { MessageStore, MessageStoreData } from './stores/message'; +import { ReactionStore, ReactionStoreData } from './stores/reaction'; + +export class HelloEndpoint extends BaseEndpoint { + public async getHelloCounter(ctx: ModuleEndpointContext): Promise { + const counterSubStore = this.stores.get(CounterStore); + + const helloCounter = await counterSubStore.get(ctx, counterKey); + + return helloCounter; + } + + public async getReactions(ctx: ModuleEndpointContext): Promise { + const reactionSubStore = this.stores.get(ReactionStore); + + const { address } = ctx.params; + if (typeof address !== 'string') { + throw new Error('Parameter address must be a string.'); + } + cryptography.address.validateLisk32Address(address); + + const reactions = await reactionSubStore.get( + ctx, + cryptography.address.getAddressFromLisk32Address(address), + ); + + return reactions; + } + + public async getHello(ctx: ModuleEndpointContext): Promise { + const messageSubStore = this.stores.get(MessageStore); + + const { address } = ctx.params; + if (typeof address !== 'string') { + throw new Error('Parameter address must be a string.'); + } + cryptography.address.validateLisk32Address(address); + const helloMessage = await messageSubStore.get( + ctx, + cryptography.address.getAddressFromLisk32Address(address), + ); + + return helloMessage; + } +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/events/.gitkeep b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/events/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/events/new_hello.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/events/new_hello.ts new file mode 100644 index 00000000000..a613a9d18c0 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/events/new_hello.ts @@ -0,0 +1,39 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { BaseEvent } from 'lisk-sdk'; + +export const newHelloEventSchema = { + $id: '/hello/events/new_hello', + type: 'object', + required: ['senderAddress', 'message'], + properties: { + senderAddress: { + dataType: 'bytes', + fieldNumber: 1, + }, + message: { + dataType: 'string', + fieldNumber: 2, + }, + }, +}; + +export interface NewHelloEventData { + senderAddress: Buffer; + message: string; +} + +export class NewHelloEvent extends BaseEvent { + public schema = newHelloEventSchema; +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/method.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/method.ts new file mode 100644 index 00000000000..0ce458081f6 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/method.ts @@ -0,0 +1,14 @@ +import { BaseMethod, ImmutableMethodContext } from 'lisk-sdk'; +import { MessageStore, MessageStoreData } from './stores/message'; + +export class HelloMethod extends BaseMethod { + public async getHello( + methodContext: ImmutableMethodContext, + address: Buffer, + ): Promise { + const messageSubStore = this.stores.get(MessageStore); + const helloMessage = await messageSubStore.get(methodContext, address); + + return helloMessage; + } +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/module.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/module.ts new file mode 100644 index 00000000000..0711a02c6d0 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/module.ts @@ -0,0 +1,141 @@ +/* eslint-disable class-methods-use-this */ + +import { + validator, + BaseInteroperableModule, + BlockAfterExecuteContext, + BlockExecuteContext, + BlockVerifyContext, + GenesisBlockExecuteContext, + InsertAssetContext, + ModuleInitArgs, + ModuleMetadata, + TransactionExecuteContext, + TransactionVerifyContext, + utils, + VerificationResult, +} from 'lisk-sdk'; +import { CreateHelloCommand } from './commands/create_hello_command'; +import { ReactCCCommand } from './cc_commands/react_command'; +import { HelloEndpoint } from './endpoint'; +import { NewHelloEvent } from './events/new_hello'; +import { HelloMethod } from './method'; +import { + configSchema, + getHelloCounterResponseSchema, + getHelloRequestSchema, + getHelloResponseSchema, +} from './schema'; +import { CounterStore } from './stores/counter'; +import { MessageStore } from './stores/message'; +import { ReactionStore, reactionStoreSchema } from './stores/reaction'; +import { ModuleConfigJSON } from './types'; +import { HelloInteroperableMethod } from './cc_method'; + +export const defaultConfig = { + maxMessageLength: 256, + minMessageLength: 3, + blacklist: ['illegalWord1'], +}; + +export class HelloModule extends BaseInteroperableModule { + public constructor() { + super(); + // registration of stores and events + this.stores.register(CounterStore, new CounterStore(this.name, 0)); + this.stores.register(MessageStore, new MessageStore(this.name, 1)); + this.stores.register(ReactionStore, new ReactionStore(this.name, 2)); + this.events.register(NewHelloEvent, new NewHelloEvent(this.name)); + } + + public metadata(): ModuleMetadata { + return { + endpoints: [ + { + name: this.endpoint.getHello.name, + request: getHelloRequestSchema, + response: getHelloResponseSchema, + }, + { + name: this.endpoint.getReactions.name, + request: getHelloRequestSchema, + response: reactionStoreSchema, + }, + { + name: this.endpoint.getHelloCounter.name, + response: getHelloCounterResponseSchema, + }, + ], + commands: this.commands.map(command => ({ + name: command.name, + params: command.schema, + })), + events: this.events.values().map(v => ({ + name: v.name, + data: v.schema, + })), + assets: [], + stores: [], + }; + } + + // Lifecycle hooks + // eslint-disable-next-line @typescript-eslint/require-await + public async init(args: ModuleInitArgs): Promise { + // Get the module config defined in the config.json file + const { moduleConfig } = args; + // Overwrite the default module config with values from config.json, if set + const config = utils.objects.mergeDeep({}, defaultConfig, moduleConfig) as ModuleConfigJSON; + // Validate the provided config with the config schema + validator.validator.validate(configSchema, config); + // Call the command init() method with config values as parameters + this.commands[0].init(config).catch(err => { + // eslint-disable-next-line no-console + console.log('Error: ', err); + }); + } + + public async insertAssets(_context: InsertAssetContext) { + // initialize block generation, add asset + } + + public async verifyAssets(_context: BlockVerifyContext): Promise { + // verify block + } + + // Lifecycle hooks + // eslint-disable-next-line @typescript-eslint/require-await + public async verifyTransaction(context: TransactionVerifyContext): Promise { + // verify transaction will be called multiple times in the transaction pool + context.logger.info('TX VERIFICATION'); + const result = { + status: 1, + }; + return result; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async beforeCommandExecute(_context: TransactionExecuteContext): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async afterCommandExecute(_context: TransactionExecuteContext): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async initGenesisState(_context: GenesisBlockExecuteContext): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async finalizeGenesisState(_context: GenesisBlockExecuteContext): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async beforeTransactionsExecute(_context: BlockExecuteContext): Promise {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async afterTransactionsExecute(_context: BlockAfterExecuteContext): Promise {} + + public endpoint = new HelloEndpoint(this.stores, this.offchainStores); + public method = new HelloMethod(this.stores, this.events); + public commands = [new CreateHelloCommand(this.stores, this.events)]; + public reactCCCommand = new ReactCCCommand(this.stores, this.events); + public crossChainMethod = new HelloInteroperableMethod(this.stores, this.events); + public crossChainCommand = [this.reactCCCommand]; +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/schema.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/schema.ts new file mode 100644 index 00000000000..577f1f9deb4 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/schema.ts @@ -0,0 +1,116 @@ +export interface CreateHelloParams { + message: string; +} + +export const createHelloSchema = { + $id: 'hello/createHello-params', + title: 'CreateHelloCommand transaction parameter for the Hello module', + type: 'object', + required: ['message'], + properties: { + message: { + dataType: 'string', + fieldNumber: 1, + minLength: 3, + maxLength: 256, + }, + }, +}; + +export const configSchema = { + $id: '/hello/config', + type: 'object', + properties: { + maxMessageLength: { + type: 'integer', + format: 'uint32', + }, + minMessageLength: { + type: 'integer', + format: 'uint32', + }, + blacklist: { + type: 'array', + items: { + type: 'string', + minLength: 1, + maxLength: 40, + }, + }, + }, + required: ['maxMessageLength', 'minMessageLength', 'blacklist'], +}; + +export const getHelloCounterResponseSchema = { + $id: 'modules/hello/endpoint/getHelloCounter', + type: 'object', + required: ['counter'], + properties: { + counter: { + type: 'number', + format: 'uint32', + }, + }, +}; + +export const getHelloResponseSchema = { + $id: 'modules/hello/endpoint/getHello', + type: 'object', + required: ['message'], + properties: { + message: { + type: 'string', + format: 'utf8', + }, + }, +}; + +export const getHelloRequestSchema = { + $id: 'modules/hello/endpoint/getHelloRequest', + type: 'object', + required: ['address'], + properties: { + address: { + type: 'string', + format: 'lisk32', + }, + }, +}; + +/** + * Parameters of the cross-chain token transfer command + */ +export const crossChainReactParamsSchema = { + /** The unique identifier of the schema. */ + $id: '/lisk/ccReactParams', + type: 'object', + /** The required parameters for the command. */ + required: ['reactionType', 'helloMessageID', 'data'], + /** A list describing the available parameters for the command. */ + properties: { + reactionType: { + dataType: 'uint32', + fieldNumber: 1, + }, + /** + * ID of the message. + */ + helloMessageID: { + dataType: 'string', + fieldNumber: 2, + }, + /** Optional field for data / messages. */ + data: { + dataType: 'string', + fieldNumber: 3, + minLength: 0, + maxLength: 64, + }, + }, +}; + +export interface CCReactMessageParams { + reactionType: number; + helloMessageID: string; + data: string; +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/.gitkeep b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/counter.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/counter.ts new file mode 100644 index 00000000000..254e4976b68 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/counter.ts @@ -0,0 +1,36 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { BaseStore } from 'lisk-sdk'; + +export interface CounterStoreData { + counter: number; +} + +export const counterKey = Buffer.alloc(0); + +export const counterStoreSchema = { + $id: '/hello/counter', + type: 'object', + required: ['counter'], + properties: { + counter: { + dataType: 'uint32', + fieldNumber: 1, + }, + }, +}; + +export class CounterStore extends BaseStore { + public schema = counterStoreSchema; +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/message.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/message.ts new file mode 100644 index 00000000000..55e6af81051 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/message.ts @@ -0,0 +1,34 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import { BaseStore } from 'lisk-sdk'; + +export interface MessageStoreData { + message: string; +} + +export const messageStoreSchema = { + $id: '/hello/message', + type: 'object', + required: ['message'], + properties: { + message: { + dataType: 'string', + fieldNumber: 1, + }, + }, +}; + +export class MessageStore extends BaseStore { + public schema = messageStoreSchema; +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/reaction.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/reaction.ts new file mode 100644 index 00000000000..867c4a97e99 --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/stores/reaction.ts @@ -0,0 +1,32 @@ +import { BaseStore } from 'lisk-sdk'; + +export interface ReactionStoreData { + reactions: { + like: Buffer[]; + }; +} + +export const reactionStoreSchema = { + $id: '/hello/reaction', + type: 'object', + required: ['reactions'], + properties: { + reactions: { + type: 'object', + fieldNumber: 1, + properties: { + like: { + type: 'array', + fieldNumber: 1, + items: { + dataType: 'bytes', + }, + }, + }, + }, + }, +}; + +export class ReactionStore extends BaseStore { + public schema = reactionStoreSchema; +} diff --git a/examples/interop/pos-sidechain-example-one/src/app/modules/hello/types.ts b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/types.ts new file mode 100644 index 00000000000..d1c1ddc9f3f --- /dev/null +++ b/examples/interop/pos-sidechain-example-one/src/app/modules/hello/types.ts @@ -0,0 +1,9 @@ +import { JSONObject } from 'lisk-sdk'; + +export interface ModuleConfig { + maxMessageLength: number; + minMessageLength: number; + blacklist: string[]; +} + +export type ModuleConfigJSON = JSONObject; diff --git a/examples/interop/pos-sidechain-example-two/config/default/config.json b/examples/interop/pos-sidechain-example-two/config/default/config.json index fcf83e45fd0..b918eb9de54 100644 --- a/examples/interop/pos-sidechain-example-two/config/default/config.json +++ b/examples/interop/pos-sidechain-example-two/config/default/config.json @@ -48,6 +48,10 @@ "ccuFee": "500000000", "receivingChainIPCPath": "~/.lisk/mainchain-node-two", "registrationHeight": 10 + }, + "dashboard": { + "applicationUrl": "ws://127.0.0.1:7886/rpc-ws", + "port": 4007 } } } diff --git a/examples/interop/pos-sidechain-example-two/config/scripts/mainchain_registration.ts b/examples/interop/pos-sidechain-example-two/config/scripts/mainchain_registration.ts index 54a30dc39cc..569177d7285 100644 --- a/examples/interop/pos-sidechain-example-two/config/scripts/mainchain_registration.ts +++ b/examples/interop/pos-sidechain-example-two/config/scripts/mainchain_registration.ts @@ -1,7 +1,10 @@ -import { keys as sidechainValidatorsKeys } from '../default/dev-validators.json'; import { keys as sidechainDevValidators } from '../default/dev-validators.json'; import { registerMainchain } from '../../../common/mainchain_registration'; (async () => { - await registerMainchain('two', sidechainDevValidators, sidechainValidatorsKeys); + await registerMainchain( + 'mainchain-node-two', + 'pos-sidechain-example-two', + sidechainDevValidators, + ); })(); diff --git a/examples/interop/pos-sidechain-example-two/config/scripts/transfer_lsk_mainchain.ts b/examples/interop/pos-sidechain-example-two/config/scripts/transfer_lsk_mainchain.ts index fd7765b979e..596e8cfda0b 100644 --- a/examples/interop/pos-sidechain-example-two/config/scripts/transfer_lsk_mainchain.ts +++ b/examples/interop/pos-sidechain-example-two/config/scripts/transfer_lsk_mainchain.ts @@ -11,7 +11,7 @@ type ModulesMetadata = [ (async () => { const { address } = cryptography; - const nodeAlias = 'one'; + const nodeAlias = 'two'; const tokenID = Buffer.from('0400000000000000', 'hex'); const mainchainID = Buffer.from('04000000', 'hex'); const recipientLSKAddress = 'lskzjzeam6szx4a65sxgavr98m9h4kctcx85nvy7h'; diff --git a/examples/interop/pos-sidechain-example-two/src/app/app.ts b/examples/interop/pos-sidechain-example-two/src/app/app.ts index d9dc8b2ad28..62a607b9357 100644 --- a/examples/interop/pos-sidechain-example-two/src/app/app.ts +++ b/examples/interop/pos-sidechain-example-two/src/app/app.ts @@ -1,9 +1,14 @@ import { Application, PartialApplicationConfig } from 'lisk-sdk'; import { registerModules } from './modules'; import { registerPlugins } from './plugins'; +import { ReactModule } from './modules/react/module'; export const getApplication = (config: PartialApplicationConfig): Application => { - const { app } = Application.defaultApplication(config); + const { app, method } = Application.defaultApplication(config); + const reactModule = new ReactModule(); + app.registerModule(reactModule); + app.registerInteroperableModule(reactModule); + reactModule.addDependencies(method.interoperability); registerModules(app); registerPlugins(app); diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/cc_method.ts b/examples/interop/pos-sidechain-example-two/src/app/modules/react/cc_method.ts new file mode 100644 index 00000000000..b7a881b33db --- /dev/null +++ b/examples/interop/pos-sidechain-example-two/src/app/modules/react/cc_method.ts @@ -0,0 +1,17 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BaseCCMethod } from 'lisk-sdk'; + +export class ReactInteroperableMethod extends BaseCCMethod {} diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/commands/react_command.ts b/examples/interop/pos-sidechain-example-two/src/app/modules/react/commands/react_command.ts new file mode 100644 index 00000000000..fc737158a6c --- /dev/null +++ b/examples/interop/pos-sidechain-example-two/src/app/modules/react/commands/react_command.ts @@ -0,0 +1,108 @@ +/* eslint-disable class-methods-use-this */ + +import { + BaseCommand, + CommandVerifyContext, + CommandExecuteContext, + VerificationResult, + VerifyStatus, + codec, +} from 'lisk-sdk'; +import { ReactMethod } from '../method'; +import { CROSS_CHAIN_COMMAND_NAME_REACT } from '../constants'; +import { + crossChainReactParamsSchema, + CCReactMessageParams, + crossChainReactMessageSchema, +} from '../schemas'; +import { InteroperabilityMethod } from '../types'; + +interface Params { + reactionType: number; + helloMessageID: string; + amount: bigint; + receivingChainID: Buffer; + data: string; + messageFee: bigint; + messageFeeTokenID: Buffer; +} + +export class ReactCrossChainCommand extends BaseCommand { + private _interoperabilityMethod!: InteroperabilityMethod; + // private _moduleName!: string; + // private _method!: ReactMethod; + public schema = crossChainReactParamsSchema; + + public get name(): string { + return CROSS_CHAIN_COMMAND_NAME_REACT; + } + + public init(args: { + moduleName: string; + method: ReactMethod; + interoperabilityMethod: InteroperabilityMethod; + }) { + // this._moduleName = args.moduleName; + // this._method = args.method; + this._interoperabilityMethod = args.interoperabilityMethod; + } + + public addDependencies(interoperabilityMethod: InteroperabilityMethod) { + this._interoperabilityMethod = interoperabilityMethod; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async verify(context: CommandVerifyContext): Promise { + const { params, logger } = context; + + logger.info('+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'); + logger.info(params); + logger.info('+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'); + + try { + if (params.receivingChainID.equals(context.chainID)) { + throw new Error('Receiving chain cannot be the sending chain.'); + } + + const messageFeeTokenID = await this._interoperabilityMethod.getMessageFeeTokenID( + context.getMethodContext(), + params.receivingChainID, + ); + if (!messageFeeTokenID.equals(params.messageFeeTokenID)) { + throw new Error('Invalid message fee Token ID.'); + } + } catch (err) { + return { + status: VerifyStatus.FAIL, + error: err as Error, + }; + } + return { + status: VerifyStatus.OK, + }; + } + + public async execute(context: CommandExecuteContext): Promise { + const { + params, + transaction: { senderAddress }, + } = context; + + const reactCCM: CCReactMessageParams = { + reactionType: params.reactionType, + data: params.data, + helloMessageID: params.helloMessageID, + }; + + await this._interoperabilityMethod.send( + context.getMethodContext(), + senderAddress, + 'hello', + CROSS_CHAIN_COMMAND_NAME_REACT, + params.receivingChainID, + params.messageFee, + codec.encode(crossChainReactMessageSchema, reactCCM), + context.header.timestamp, + ); + } +} diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/constants.ts b/examples/interop/pos-sidechain-example-two/src/app/modules/react/constants.ts new file mode 100644 index 00000000000..9b427343540 --- /dev/null +++ b/examples/interop/pos-sidechain-example-two/src/app/modules/react/constants.ts @@ -0,0 +1,4 @@ +export const CROSS_CHAIN_COMMAND_NAME_REACT = 'reactCrossChain'; + +export const MAX_RESERVED_ERROR_STATUS = 63; +export const CCM_STATUS_OK = 0; diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/endpoint.ts b/examples/interop/pos-sidechain-example-two/src/app/modules/react/endpoint.ts new file mode 100644 index 00000000000..160b0b59d92 --- /dev/null +++ b/examples/interop/pos-sidechain-example-two/src/app/modules/react/endpoint.ts @@ -0,0 +1,3 @@ +import { BaseEndpoint } from 'lisk-sdk'; + +export class ReactEndpoint extends BaseEndpoint {} diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/events/.gitkeep b/examples/interop/pos-sidechain-example-two/src/app/modules/react/events/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/method.ts b/examples/interop/pos-sidechain-example-two/src/app/modules/react/method.ts new file mode 100644 index 00000000000..69f80f11e97 --- /dev/null +++ b/examples/interop/pos-sidechain-example-two/src/app/modules/react/method.ts @@ -0,0 +1,3 @@ +import { BaseMethod } from 'lisk-sdk'; + +export class ReactMethod extends BaseMethod {} diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/module.ts b/examples/interop/pos-sidechain-example-two/src/app/modules/react/module.ts new file mode 100644 index 00000000000..5d5aa03d6bc --- /dev/null +++ b/examples/interop/pos-sidechain-example-two/src/app/modules/react/module.ts @@ -0,0 +1,85 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable @typescript-eslint/member-ordering */ + +import { BaseInteroperableModule, ModuleMetadata, ModuleInitArgs } from 'lisk-sdk'; +import { ReactCrossChainCommand } from './commands/react_command'; +import { ReactEndpoint } from './endpoint'; +import { ReactMethod } from './method'; +import { ReactInteroperableMethod } from './cc_method'; +import { InteroperabilityMethod } from './types'; + +export class ReactModule extends BaseInteroperableModule { + public endpoint = new ReactEndpoint(this.stores, this.offchainStores); + public method = new ReactMethod(this.stores, this.events); + public commands = [new ReactCrossChainCommand(this.stores, this.events)]; + private _interoperabilityMethod!: InteroperabilityMethod; + + public crossChainMethod = new ReactInteroperableMethod(this.stores, this.events); + + /* public constructor() { + super(); + this.stores.register(ReactionStore, new ReactionStore(this.name, 0)); + } */ + + public metadata(): ModuleMetadata { + return { + ...this.baseMetadata(), + endpoints: [], + commands: this.commands.map(command => ({ + name: command.name, + params: command.schema, + })), + assets: [], + }; + } + + public addDependencies(interoperabilityMethod: InteroperabilityMethod) { + this._interoperabilityMethod = interoperabilityMethod; + } + + // Lifecycle hooks + // eslint-disable-next-line @typescript-eslint/require-await + public async init(_args: ModuleInitArgs) { + this.commands[0].init({ + interoperabilityMethod: this._interoperabilityMethod, + method: this.method, + moduleName: this.name, + }); + } + + // public async insertAssets(_context: InsertAssetContext) { + // // initialize block generation, add asset + // } + + // public async verifyAssets(_context: BlockVerifyContext): Promise { + // // verify block + // } + + // Lifecycle hooks + // public async verifyTransaction(_context: TransactionVerifyContext): Promise { + // verify transaction will be called multiple times in the transaction pool + // return { status: VerifyStatus.OK }; + // } + + // public async beforeCommandExecute(_context: TransactionExecuteContext): Promise { + // } + + // public async afterCommandExecute(_context: TransactionExecuteContext): Promise { + + // } + // public async initGenesisState(_context: GenesisBlockExecuteContext): Promise { + + // } + + // public async finalizeGenesisState(_context: GenesisBlockExecuteContext): Promise { + + // } + + // public async beforeTransactionsExecute(_context: BlockExecuteContext): Promise { + + // } + + // public async afterTransactionsExecute(_context: BlockAfterExecuteContext): Promise { + + // } +} diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/schemas.ts b/examples/interop/pos-sidechain-example-two/src/app/modules/react/schemas.ts new file mode 100644 index 00000000000..fc0033e9235 --- /dev/null +++ b/examples/interop/pos-sidechain-example-two/src/app/modules/react/schemas.ts @@ -0,0 +1,94 @@ +/** + * Parameters of the cross-chain token transfer command + */ +export const crossChainReactParamsSchema = { + /** The unique identifier of the schema. */ + $id: '/lisk/ccReactParams', + type: 'object', + /** The required parameters for the command. */ + required: [ + 'reactionType', + 'helloMessageID', + 'receivingChainID', + 'data', + 'messageFee', + 'messageFeeTokenID', + ], + /** A list describing the available parameters for the command. */ + properties: { + reactionType: { + dataType: 'uint32', + fieldNumber: 1, + }, + /** + * ID of the message. + */ + helloMessageID: { + dataType: 'string', + fieldNumber: 2, + }, + /** + * The chain ID of the receiving chain. + * + * `maxLength` and `minLength` are equal to 4. + */ + receivingChainID: { + dataType: 'bytes', + fieldNumber: 3, + minLength: 4, + maxLength: 4, + }, + /** Optional field for data / messages. */ + data: { + dataType: 'string', + fieldNumber: 4, + minLength: 0, + maxLength: 64, + }, + messageFee: { + dataType: 'uint64', + fieldNumber: 5, + }, + messageFeeTokenID: { + dataType: 'bytes', + fieldNumber: 6, + minLength: 8, + maxLength: 8, + }, + }, +}; + +export const crossChainReactMessageSchema = { + /** The unique identifier of the schema. */ + $id: '/lisk/ccReactMessage', + type: 'object', + /** The required parameters for the command. */ + required: ['reactionType', 'helloMessageID', 'data'], + /** A list describing the available parameters for the command. */ + properties: { + reactionType: { + dataType: 'uint32', + fieldNumber: 1, + }, + /** + * ID of the message. + */ + helloMessageID: { + dataType: 'string', + fieldNumber: 2, + }, + /** Optional field for data / messages. */ + data: { + dataType: 'string', + fieldNumber: 3, + minLength: 0, + maxLength: 64, + }, + }, +}; + +export interface CCReactMessageParams { + reactionType: number; + helloMessageID: string; + data: string; +} diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/stores/.gitkeep b/examples/interop/pos-sidechain-example-two/src/app/modules/react/stores/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/interop/pos-sidechain-example-two/src/app/modules/react/types.ts b/examples/interop/pos-sidechain-example-two/src/app/modules/react/types.ts new file mode 100644 index 00000000000..279823de2fe --- /dev/null +++ b/examples/interop/pos-sidechain-example-two/src/app/modules/react/types.ts @@ -0,0 +1,28 @@ +import { + MethodContext, + ImmutableMethodContext, + CCMsg, + ChannelData, + OwnChainAccount, +} from 'lisk-sdk'; + +export type TokenID = Buffer; + +export interface InteroperabilityMethod { + getOwnChainAccount(methodContext: ImmutableMethodContext): Promise; + send( + methodContext: MethodContext, + feeAddress: Buffer, + module: string, + crossChainCommand: string, + receivingChainID: Buffer, + fee: bigint, + parameters: Buffer, + timestamp?: number, + ): Promise; + error(methodContext: MethodContext, ccm: CCMsg, code: number): Promise; + terminateChain(methodContext: MethodContext, chainID: Buffer): Promise; + getChannel(methodContext: MethodContext, chainID: Buffer): Promise; + getMessageFeeTokenID(methodContext: ImmutableMethodContext, chainID: Buffer): Promise; + getMessageFeeTokenIDFromCCM(methodContext: ImmutableMethodContext, ccm: CCMsg): Promise; +} diff --git a/examples/interop/run_sidechains.json b/examples/interop/run_sidechains.json index b96ef8e9360..ea28f89c62e 100644 --- a/examples/interop/run_sidechains.json +++ b/examples/interop/run_sidechains.json @@ -3,12 +3,26 @@ { "name": "pos-sidechain-1", "script": "pos-sidechain-example-one/bin/run", - "args": ["start", "--api-ipc", "--api-http", "--enable-chain-connector-plugin"] + "args": [ + "start", + "--api-ipc", + "--api-http", + "--api-ws", + "--enable-dashboard-plugin", + "--enable-chain-connector-plugin" + ] }, { "name": "pos-sidechain-2", "script": "pos-sidechain-example-two/bin/run", - "args": ["start", "--api-ipc", "--api-http", "--enable-chain-connector-plugin"] + "args": [ + "start", + "--api-ipc", + "--api-http", + "--api-ws", + "--enable-dashboard-plugin", + "--enable-chain-connector-plugin" + ] } ] }