diff --git a/README.md b/README.md index f3b058ff..eed69ae9 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ Run the program to schedule automation and wait for cross-chain execution npm run rocstar ``` -## Moonbase Auto-restake Demo +## Moonbeam EVM smart contract automation ### Pre-requisites | Chain | Version | Commit hash | | :--- | :----: | ---: | @@ -288,57 +288,50 @@ npm run rocstar | Moonbeam | [runtime-2201](https://github.com/PureStake/moonbeam/releases/tag/runtime-2201) | [483f51e](https://github.com/PureStake/moonbeam/commit/483f51e8c2574732c97634c20345433a74c93fd5) | ### Steps #### Local dev environment -1. Launch OAK-blockchain, Rococo and Moonbase. +The local environment of Moonbeam is named Moonbase Local in its chain config. - Launch zombie in OAK-blockchain project root with Moonbase and Turing. +1. Launch Rococo Local, Turing Dev and Moonbase Local with zombienet. The zombienet config file is located at [OAK-blockchain repo](https://github.com/OAK-Foundation/OAK-blockchain/blob/master/zombienets/turing/moonbase.toml). Assuming you are at OAK-blockchain’s root folder, run the below command to spin up the networks. ``` - zombie spawn zombienets/turing/moonbase.toml + zombienet spawn zombienets/turing/moonbase.toml ``` -1. Deploy the smart contract to Moonbase. - - Please configure your wallet private key to the secrets.json file in the root directory. - +2. Run this program to schedule automation and wait for cross-chain execution ``` - { - "privateKey": "YOUR-WALLET-PRIVATE-KEY-HERE" - } + npm run moonbase ``` - Run the command below to deploy the contract. +3. The above step outlines the process of XCM automation with Moonbase Local. Upon completing the program, an 'ethereum.executed' event from Moonbase Local will be emitted. However, the event will exit with an EvmCoreErrorExitReason, which occurs because a smart contract has not been deployed yet. To successfully demonstrate smart contract automation, please follow the subsequent steps to set up a test smart contract. - ``` - cd src/moonbeam/contracts - # Install dependencies - npm install - # Compile smart contract - npx hardhat compile - # Deploy smart contract to Moonbase - npx hardhat run scripts/deploy.js - cd ../../.. - ``` + The default sudo wallet of Moonbase Local Alith is used to deploy a smart contract. Run the below commands to deploy a smart contract to Moonbase Local. -1. Run the program to schedule automation and wait for cross-chain execution - ``` - npm run moonbase - ``` + ``` + cd src/moonbeam/contracts + npm install + npx hardhat compile # Compile smart contract + npx hardhat run scripts/deploy.js # Deploy smart contract to Moonbase + ``` -#### Moonbase alpha environment -1. Place seed.json(for Turing) and seed-eth.json(for Moonbase) in 'private' folder. + The commands, if successful, will print out the newly deployed smart contract. Take the Incrementer contract’s Ethereum address, and set the value to CONTRACT_ADDRESS in the beginning of src/moonbeam/moonbase-local.js. You do not need to change the value CONTRACT_INPUT; + ``` + Incrementer deployed to: 0x711F8F079b0BB4D16bd8C5D049358d31a1694755 + ``` -The seed.json file is exported from the polkadot.js browser plugin. +#### Moonbase Alpha environment +The default staging environment of Moonbeam is Moonbase Alpha, since Moonbeam doesn’t have a parachain set up on Rococo. -The seed-eth.json file is downloaded after adding an account in the polkdot.js apps page of Moonbase. +1. First, we will need to set up wallets for the transactions in this demo. + 1. Assuming you have a wallet created and imported to both Turing Moonbase and Moonbase Alpha’s polkadot.js dashboard. + 2. Export a json file from Turing Moonbase‘s dashboard, name it `seed.json` and place it in the ./private folder. + 3. Export a json file from Moonbase Alpha’s dashboard, name it `seed-eth.json` and place it in the ./private folder. Please note that the account exported should be an Ethereum account. -How to add Moonbase alpha account: -https://docs.moonbeam.network/tokens/connect/polkadotjs/ + > How to add a polkadot account to Moonbase Alpha: https://docs.moonbeam.network/tokens/connect/polkadotjs/ -1. Make sure you have 25 TUR in Turing for the reserved fee required to add the proxy and the execution fee for automationTime. +1. Make sure your wallet is topped up with 25 TUR on Turing Moonbase for fees required to set up a proxy wallet and task execution. -1. Make sure you have 5 DEV in Moonbase, we will transfer some to Turing's proxy account and pay the execution fee. +2. Make sure your wallet is topped up with 5 DEV on Moonbase Alpha for fees required to set up a proxy wallet and execute task scheduling. -1. Run the program to schedule automation and wait for cross-chain execution +3. Run the below command to kick off the demo. The is your password to unlock the wallet on Turing Moonbase, and the is your password to unlock the ethereum wallet on Moonbase Alpha. ``` PASS_PHRASE= PASS_PHRASE_ETH= npm run moonbase-alpha ``` diff --git a/package.json b/package.json index 6c317d2c..fa22de4e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "rocstar": "dotenv -e .env babel-node src/astar/rocstar.js", "shiden": "dotenv -e .env babel-node src/astar/shiden.js", "moonbase-alpha": "dotenv -e .env babel-node src/moonbeam/moonbase-alpha.js", - "moonbase": "dotenv -e .env babel-node src/moonbeam/moonbase.js", + "moonbase-local": "dotenv -e .env babel-node src/moonbeam/moonbase-local.js", "test-moonbase-contract": "dotenv -e .env babel-node src/moonbeam/test-moonbase-contract/index.js", "utils": "dotenv -e .env babel-node src/utils.js", "lint": "eslint --fix src/*" diff --git a/src/config/index.js b/src/config/index.js index d4bcf8f8..ec70802f 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -9,7 +9,7 @@ import Shibuya from './shibuya'; import Rocstar from './rocstar'; import Shiden from './shiden'; -import MoonbaseDev from './moonbase-dev'; +import MoonbaseLocal from './moonbase-local'; import MoonbaseAlpha from './moonbase-alpha'; import TuringDev from './turing-dev'; @@ -22,5 +22,5 @@ export { MangataDev, MangataRococo, Mangata, TuringDev, Turing, TuringStaging, TuringMoonbase, Shibuya, Rocstar, Shiden, - MoonbaseDev, MoonbaseAlpha, + MoonbaseLocal, MoonbaseAlpha, }; diff --git a/src/config/moonbase-dev.js b/src/config/moonbase-local.js similarity index 67% rename from src/config/moonbase-dev.js rename to src/config/moonbase-local.js index 69a73f09..f771898e 100644 --- a/src/config/moonbase-dev.js +++ b/src/config/moonbase-local.js @@ -3,17 +3,17 @@ const assets = [ id: '0', chainId: 0, decimals: 18, - name: 'Moonbase Development Token', + name: 'Moonbase Local Token', symbol: 'UNIT', address: '', }, ]; const Config = { - name: 'Moonbase Development', - key: 'moonbase-dev', + name: 'Moonbase Local', + key: 'moonbase-local', endpoint: 'ws://127.0.0.1:9949', - relayChain: 'rococo', + relayChain: 'rococo-local', paraId: 1000, ss58: 1287, assets, diff --git a/src/config/turing-moonbase.js b/src/config/turing-moonbase.js index b0595551..1136e3f0 100644 --- a/src/config/turing-moonbase.js +++ b/src/config/turing-moonbase.js @@ -6,10 +6,10 @@ const assets = [ ]; const Config = { - name: 'Turing Moonbase Alpha', - key: 'turing-moonbase-alpha', + name: 'Turing Moonbase', + key: 'turing-moonbase', endpoint: 'ws://167.99.226.24:8846', - relayChain: 'rococo', + relayChain: 'moonbase-relay-testnet', paraId: 2114, ss58: 51, assets, diff --git a/src/moonbeam/contracts/.gitignore b/src/moonbeam/contracts/.gitignore index 81b6159c..c377ca80 100644 --- a/src/moonbeam/contracts/.gitignore +++ b/src/moonbeam/contracts/.gitignore @@ -1,5 +1,4 @@ node_modules -secrets.json coverage coverage.json typechain diff --git a/src/moonbeam/contracts/README.md b/src/moonbeam/contracts/README.md index 56723445..834de469 100644 --- a/src/moonbeam/contracts/README.md +++ b/src/moonbeam/contracts/README.md @@ -5,20 +5,14 @@ This is the Ethereum smart contracts for testing. Read about: https://docs.moonbeam.network/builders/build/eth-api/dev-env/hardhat/ -## Configuration - -Please configure your private key to the secrets.json file in the root directory. - -``` -{ - "privateKey": "YOUR-PRIVATE-KEY-HERE" -} -``` - ## Deploy +The default sudo wallet of Moonbase Local Alith is used to deploy a smart contract. Run the below commands to deploy a smart contract to Moonbase Local. + ``` npm install npx hardhat compile npx hardhat run scripts/deploy.js ``` + +The commands, if successful, will print out the newly deployed smart contract. Take the Incrementer contract’s Ethereum address, and set the value to CONTRACT_ADDRESS in the beginning of src/moonbeam/moonbase-local.js. You do not need to change the value CONTRACT_INPUT; \ No newline at end of file diff --git a/src/moonbeam/contracts/hardhat.config.js b/src/moonbeam/contracts/hardhat.config.js index 185398ea..c4b87317 100644 --- a/src/moonbeam/contracts/hardhat.config.js +++ b/src/moonbeam/contracts/hardhat.config.js @@ -1,8 +1,8 @@ -// 1. Import the Ethers plugin required to interact with the contract +// Import the Ethers plugin required to interact with the contract require('@nomiclabs/hardhat-ethers'); -// 2. Import your private key from your pre-funded Moonbase Alpha testing account -const { privateKey } = require('./secrets.json'); +// Alith’s private key. Alith is one of the default sudo wallet on Moonbase Local. +const PRIVATE_KEY = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133'; module.exports = { solidity: '0.8.18', @@ -10,7 +10,7 @@ module.exports = { 'moonbase-local': { url: 'http://127.0.0.1:9949', chainId: 1280, - accounts: [privateKey], + accounts: [PRIVATE_KEY], }, }, defaultNetwork: 'moonbase-local', diff --git a/src/moonbeam/moonbase-local.js b/src/moonbeam/moonbase-local.js new file mode 100644 index 00000000..8032baac --- /dev/null +++ b/src/moonbeam/moonbase-local.js @@ -0,0 +1,281 @@ +import _ from 'lodash'; +import Keyring from '@polkadot/keyring'; +import BN from 'bn.js'; +import moment from 'moment'; +import { hexToU8a, u8aToHex } from '@polkadot/util'; + +import Account from '../common/account'; +import { TuringDev, MoonbaseLocal } from '../config'; +import TuringHelper from '../common/turingHelper'; +import MoonbaseHelper from '../common/moonbaseHelper'; +import { + sendExtrinsic, getDecimalBN, bnToFloat, + // listenEvents, calculateTimeout, +} from '../common/utils'; + +// TODO: read this instruction value from Turing Staging +// One XCM operation is 1_000_000_000 weight - almost certainly a conservative estimate. +// It is defined as a UnitWeightCost variable in runtime. +const TURING_INSTRUCTION_WEIGHT = 1000000000; +const TASK_FREQUENCY = 3600; + +const CONTRACT_ADDRESS = '0x970951a12f975e6762482aca81e57d5a2a4e73f4'; +const CONTRACT_INPUT = '0xd09de08a'; + +const WEIGHT_PER_SECOND = 1000000000000; + +const keyring = new Keyring({ type: 'sr25519' }); + +// Alith is the default sodo wallet on Moonbase Local +const MoonbaseAlith = { + name: 'Alith', + privateKey: '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133', +}; + +// Alice is the default sodo wallet on Turing Dev +const TuringAlice = { + name: 'Alice', + privateKey: '0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a', +}; + +const sendXcmFromMoonbase = async ({ + turingHelper, parachainHelper, turingAddress, + paraTokenIdOnTuring, keyPair, +}) => { + console.log('\na). Create an ethereumXcm.transactThroughProxy extrinsic ...'); + const parachainProxyCall = parachainHelper.api.tx.ethereumXcm.transactThroughProxy( + keyPair.address, + { + V2: { + gasLimit: 71000, + action: { Call: CONTRACT_ADDRESS }, + value: 0, + input: CONTRACT_INPUT, + }, + }, + ); + // const parachainProxyCallFees = await parachainProxyCall.paymentInfo(keyPair.address); + + console.log('\nb). Create a payload to store in Turing’s task ...'); + const secondsInHour = 3600; + const millisecondsInHour = 3600 * 1000; + const currentTimestamp = moment().valueOf(); + const timestampNextHour = (currentTimestamp - (currentTimestamp % millisecondsInHour)) / 1000 + secondsInHour; + // const timestampTwoHoursLater = (currentTimestamp - (currentTimestamp % millisecondsInHour)) / 1000 + (secondsInHour * 2); + const providedId = `xcmp_automation_test_${(Math.random() + 1).toString(36).substring(7)}`; + const taskViaProxy = turingHelper.api.tx.automationTime.scheduleXcmpTaskThroughProxy( + providedId, + // { Fixed: { executionTimes: [timestampNextHour, timestampTwoHoursLater] } }, + { Fixed: { executionTimes: [0] } }, + // { Recurring: { frequency: TASK_FREQUENCY, nextExecutionTime: timestampNextHour } }, + parachainHelper.config.paraId, + 5, + { + V1: + { + parents: 1, + interior: + { X2: [{ Parachain: parachainHelper.config.paraId }, { PalletInstance: 3 }] }, + }, + }, + parachainProxyCall.method.toHex(), + '4000000000', + turingAddress, + ); + console.log(`Task extrinsic encoded call data: ${taskViaProxy.method.toHex()}`); + + // const taskExtrinsic = turingHelper.api.tx.system.remarkWithEvent('Hello!!!'); + const encodedTaskViaProxy = taskViaProxy.method.toHex(); + const taskViaProxyFees = await taskViaProxy.paymentInfo(turingAddress); + const requireWeightAtMost = parseInt(taskViaProxyFees.weight, 10); + + console.log(`Proxy task extrinsic encoded call data: ${encodedTaskViaProxy}`); + console.log(`requireWeightAtMost: ${requireWeightAtMost}`); + + console.log(`\nc) Execute the above an XCM from ${parachainHelper.config.name} to schedule a task on ${turingHelper.config.name} ...`); + const feePerSecond = await turingHelper.getFeePerSecond(paraTokenIdOnTuring); + + const instructionCount = 4; + const totalInstructionWeight = instructionCount * TURING_INSTRUCTION_WEIGHT; + const weightLimit = requireWeightAtMost + totalInstructionWeight; + const fungible = new BN(weightLimit).mul(feePerSecond).div(new BN(WEIGHT_PER_SECOND)).mul(new BN(10)); + const transactRequiredWeightAtMost = requireWeightAtMost + TURING_INSTRUCTION_WEIGHT; + // const overallWeight = (instructionCount - 1) * TURING_INSTRUCTION_WEIGHT; + const overallWeight = 8170208000; + console.log('transactRequiredWeightAtMost: ', transactRequiredWeightAtMost); + console.log('overallWeight: ', overallWeight); + console.log('fungible: ', fungible.toString()); + + const transactExtrinsic = parachainHelper.api.tx.xcmTransactor.transactThroughSigned( + { + V1: { + parents: 1, + interior: { X1: { Parachain: 2114 } }, + }, + }, + { + currency: { AsCurrencyId: 'SelfReserve' }, + feeAmount: fungible, + }, + encodedTaskViaProxy, + { transactRequiredWeightAtMost, overallWeight }, + ); + + console.log(`transactExtrinsic Encoded call data: ${transactExtrinsic.method.toHex()}`); + + await sendExtrinsic(parachainHelper.api, transactExtrinsic, keyPair); +}; + +const main = async () => { + const turingHelper = new TuringHelper(TuringDev); + await turingHelper.initialize(); + + const moonbaseHelper = new MoonbaseHelper(MoonbaseLocal); + await moonbaseHelper.initialize(); + + const turingChainName = turingHelper.config.name; + const parachainName = moonbaseHelper.config.name; + const parachainToken = _.first(moonbaseHelper.config.assets); + const decimalBN = getDecimalBN(parachainToken.decimals); + + console.log(`\n1. Setup accounts on ${parachainName} and ${turingChainName}`); + + const alithKeyPair = keyring.addFromSeed(hexToU8a(MoonbaseAlith.privateKey), undefined, 'ethereum'); + const aliceKeyPair = keyring.addFromSeed(hexToU8a(TuringAlice.privateKey), undefined, 'ethereum'); + + console.log(`Reading token and balance of Alice account on ${turingChainName} ...`); + const keyPair = keyring.addFromUri('//Alice', undefined, 'sr25519'); + keyPair.meta.name = 'Alice'; + + // TODO: Account should contain Address32, Address20 and its proxy address + const account = new Account(keyPair); + await account.init([turingHelper]); + account.print(); + + const turingAddress = account.getChainByName(turingHelper.config.key)?.address; + const turingAddressETH = aliceKeyPair.address; + console.log('Turing wallet’s Ethereum address:', turingAddressETH); + + const topUpAmount = (new BN(1000).mul(decimalBN)).toString(); + + // TODO: add balance check and skip this transfer if balance is not lower than topUpAmount + console.log(`\nTransfer ${parachainToken.symbol} from Alith to Alice on ${parachainName}, if Alice’s balance is low. `); + await sendExtrinsic( + moonbaseHelper.api, + moonbaseHelper.api.tx.balances.transfer(aliceKeyPair.address, topUpAmount), + alithKeyPair, + ); + + console.log('\n2. One-time proxy setup on Turing'); + console.log(`\na) Add a proxy for Alice If there is none setup on Turing (paraId:${moonbaseHelper.config.paraId})\n`); + const proxyOnTuring = turingHelper.getProxyAccount(aliceKeyPair.address, moonbaseHelper.config.paraId, { addressType: 'Ethereum' }); + const proxyAccountId = keyring.decodeAddress(proxyOnTuring); + + const proxyTypeTuring = 'Any'; + const proxiesOnTuring = await turingHelper.getProxies(turingAddress); + const proxyMatchTuring = _.find(proxiesOnTuring, { delegate: proxyOnTuring, proxyType: proxyTypeTuring }); + + if (proxyMatchTuring) { + console.log(`Proxy address ${proxyOnTuring} for paraId: ${moonbaseHelper.config.paraId} and proxyType: ${proxyTypeTuring} already exists; skipping creation ...`); + } else { + console.log(`Add a proxy for ${parachainName} (paraId:${moonbaseHelper.config.paraId}) and proxyType: ${proxyTypeTuring} on Turing ...\nProxy address: ${proxyOnTuring}\n`); + await sendExtrinsic(turingHelper.api, turingHelper.api.tx.proxy.addProxy(proxyOnTuring, proxyTypeTuring, 0), keyPair); + } + + // Reserve transfer DEV to the proxy account on Turing + console.log(`\nb) Reserve transfer ${parachainToken.symbol} to the proxy account on Turing ...`); + const minBalanceOnTuring = new BN(100).mul(decimalBN); + + // TODO: 1. the location of Moonbase token should be defined in src/config/moonbase-local + // 2. wrap this locationToAssetId call to turingHelper.locationToAssetId(location) + const paraTokenIdOnTuring = (await turingHelper.api.query.assetRegistry.locationToAssetId({ parents: 1, interior: { X2: [{ Parachain: moonbaseHelper.config.paraId }, { PalletInstance: 3 }] } })) + .unwrapOrDefault() + .toNumber(); + + const proxyOnTuringBalance = await turingHelper.getTokenBalance(proxyOnTuring, paraTokenIdOnTuring); + + // Transfer DEV from Moonbase to Turing + if (proxyOnTuringBalance.free.lt(minBalanceOnTuring)) { + console.log('\nTransfer DEV from Moonbase to Turing'); + const extrinsic = moonbaseHelper.api.tx.xTokens.transferMultiasset( + { + V1: { + id: { + Concrete: { + parents: 0, + interior: { + X1: { PalletInstance: 3 }, + }, + }, + }, + fun: { + Fungible: topUpAmount, + }, + }, + }, + { + V1: { + parents: 1, + interior: { + X2: [ + { + Parachain: turingHelper.config.paraId, + }, + { + AccountId32: { + network: 'Any', + id: u8aToHex(proxyAccountId), + }, + }, + ], + }, + }, + }, + 'Unlimited', + ); + await sendExtrinsic(moonbaseHelper.api, extrinsic, aliceKeyPair); + } else { + const freeBalanceOnTuring = (new BN(proxyOnTuringBalance.free)).div(decimalBN); + console.log(`Proxy’s balance is ${freeBalanceOnTuring.toString()}, no need to top it up with reserve transfer ...`); + } + + console.log('\n3. One-time proxy setup on Moonbase'); + console.log(`\na) Add a proxy for Alice If there is none setup on Moonbase (paraId:${moonbaseHelper.config.paraId})\n`); + const proxyOnMoonbase = moonbaseHelper.getProxyAccount(turingAddress, turingHelper.config.paraId); + console.log('proxyOnMoonbase: ', proxyOnMoonbase); + const proxyTypeMoonbase = 'Any'; + const proxiesOnMoonbase = await moonbaseHelper.getProxies(aliceKeyPair.address); + const proxyMatchMoonbase = _.find(proxiesOnMoonbase, { delegate: proxyOnMoonbase, proxyType: proxyTypeMoonbase }); + + if (proxyMatchMoonbase) { + console.log(`Proxy address ${proxyOnMoonbase} for paraId: ${moonbaseHelper.config.paraId} and proxyType: ${proxyTypeMoonbase} already exists; skipping creation ...`); + } else { + console.log(`Add a proxy of ${parachainName} (paraId:${moonbaseHelper.config.paraId}) and proxyType: ${proxyTypeMoonbase} on Turing ...\nProxy address: ${proxyOnMoonbase}\n`); + await sendExtrinsic(moonbaseHelper.api, moonbaseHelper.api.tx.proxy.addProxy(proxyOnMoonbase, proxyTypeMoonbase, 0), aliceKeyPair); + } + + console.log(`\nb) Topping up the proxy account on ${parachainName} with ${parachainToken.symbol} ...\n`); + const topUpExtrinsic = moonbaseHelper.api.tx.balances.transfer(proxyOnMoonbase, topUpAmount); + await sendExtrinsic(moonbaseHelper.api, topUpExtrinsic, aliceKeyPair); + + console.log(`\nUser ${account.name} ${turingChainName} address: ${turingAddress}, ${parachainName} address: ${turingAddressETH}`); + + console.log(`\n4. Execute an XCM from ${parachainName} to ${turingChainName} ...`); + + await sendXcmFromMoonbase({ + turingHelper, + parachainHelper: moonbaseHelper, + turingAddress, + turingAddressETH, + paraTokenIdOnTuring, + keyPair: aliceKeyPair, + proxyAccountId, + }); + + // TODO: need to add event verification of the ethereum execution +}; + +main().catch(console.error).finally(() => { + console.log('Reached the end of main() ...'); + process.exit(); +});