В рамках Хакатона на площадке Gitcoin, нужно описать процедуру трансформации или переноса Ethereum приложения на Nervos Network Layer 2.
В одном посте вряд-ли получится описать процесс в деталях, особенно, если он похож на матрешку. Поэтому, настоятельно рекомендую ориентироваться по данному репозиторию:
https://github.com/Kuzirashi/gw-gitcoin-instruction
Я не буду сразу переходить к процессу переносу, так как считаю нужным хотя бы чуток объяснить как происходит работа со смарт-контрактами на базе Nervos Network Layer 2.
Первое, что нужно сделать это клонировать учебный репозиторий Хакатона, в котором лежат все нужные нам скрипты.
git clone https://github.com/kuzirashi/gw-gitcoin-instruction
И устанавливаем все нужные зависимости:
yarn install-all
Работаем со смарт-контрактами - компиляция и развертывание на блокчейне Nervos Layer 2.
Для начала, неможко гугл-транслейта документации:
Polyjuice - это среда исполнения, совместимая с Ethereum EVM, которая позволяет запускать смарт-контракты на основе Solidity на Nervos. Цель проекта - 100% совместимость, позволяющая всем контрактам Ethereum работать на Nervos без каких-либо изменений.
Polyjuice разработан для использования со сводной структурой Godwoken Layer 2. Это позволяет Polyjuice полностью перенести выполнение смарт-контрактов с уровня 1 на уровень 2, обеспечивая масштабируемость, выходящую далеко за рамки того, на что сегодня способна основная сеть Ethereum.
Другими словами, команда Nervos Network постаралась(и дальше старается), сделать так, чтобы смарт-контракты Ethereum (Solidity) были 100% совместимы с их блокчейном. Так что, если у нас есть готовый смарт-контракт в файле .sol, поидее, не должно возникнуть проблем чтобы развернуть его на блокчейне Nervos. Чем мы дальше и займемся.
Рабочая папка для нас сейчас это src/examples/2-deploy-contract/
клонированного выше репозитория. Там же, в папке contracts
и лежит файл с простейшим смарт-контрактом SimpleStorage.sol
:
pragma solidity >=0.8.0;
contract SimpleStorage {
uint storedData;
constructor() payable {
storedData = 123;
}
function set(uint x) public payable {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
Чтобы скомпилировать этот файл, запускаем:
yarn compile
В результате, у нас должен появиться в папке build/contracts/
новый файл SimpleStorage.json
скомпилированный truffle в EVM байткод (тут я не буду вдаваться в детали).
Для того, чтобы развернуть смарт-контракт на новом блокчейне находим в текущей рабочей папке файл index.js
:
const { existsSync } = require('fs');
const Web3 = require('web3');
const { PolyjuiceHttpProvider, PolyjuiceAccounts } = require("@polyjuice-provider/web3");
const contractName = process.argv.slice(2)[0];
if (!contractName) {
throw new Error(`No compiled contract specified to deploy. Please put it in "src/examples/2-deploy-contract/build/contracts" directory and provide its name as an argument to this program, eg.: "node index.js SimpleStorage.json"`);
}
let compiledContractArtifact = null;
const filenames = [`./build/contracts/${contractName}`, `./${contractName}`];
for(const filename of filenames)
{
if(existsSync(filename))
{
console.log(`Found file: ${filename}`);
compiledContractArtifact = require(filename);
break;
}
else
console.log(`Checking for file: ${filename}`);
}
if(compiledContractArtifact === null)
throw new Error(`Unable to find contract file: ${contractName}`);
const DEPLOYER_PRIVATE_KEY = 'ETHEREUM PRIVATE KEY'; // Replace this with your Ethereum private key with funds on Layer 2.
const GODWOKEN_RPC_URL = 'http://godwoken-testnet-web3-rpc.ckbapp.dev';
const polyjuiceConfig = {
rollupTypeHash: '0x4cc2e6526204ae6a2e8fcf12f7ad472f41a1606d5b9624beebd215d780809f6a',
ethAccountLockCodeHash: '0xdeec13a7b8e100579541384ccaf4b5223733e4a5483c3aec95ddc4c1d5ea5b22',
web3Url: GODWOKEN_RPC_URL
};
const provider = new PolyjuiceHttpProvider(
GODWOKEN_RPC_URL,
polyjuiceConfig,
);
const web3 = new Web3(provider);
web3.eth.accounts = new PolyjuiceAccounts(polyjuiceConfig);
const deployerAccount = web3.eth.accounts.wallet.add(DEPLOYER_PRIVATE_KEY);
web3.eth.Contract.setProvider(provider, web3.eth.accounts);
(async () => {
const balance = BigInt(await web3.eth.getBalance(deployerAccount.address));
if (balance === 0n) {
console.log(`Insufficient balance. Can't deploy contract. Please deposit funds to your Ethereum address: ${deployerAccount.address}`);
return;
}
console.log(`Deploying contract...`);
const deployTx = new web3.eth.Contract(compiledContractArtifact.abi).deploy({
data: getBytecodeFromArtifact(compiledContractArtifact),
arguments: []
}).send({
from: deployerAccount.address,
to: '0x' + new Array(40).fill(0).join(''),
gas: 6000000,
gasPrice: '0',
});
deployTx.on('transactionHash', hash => console.log(`Transaction hash: ${hash}`));
const receipt = await deployTx;
console.log(`Deployed contract address: ${receipt.contractAddress}`);
})();
function getBytecodeFromArtifact(contractArtifact) {
return contractArtifact.bytecode || contractArtifact.data?.bytecode?.object
}
Этот скрипт принимает в качестве параметра название файла смарт-контракта, в нашем случае это SimpleStorage.json
. Также, импортирует все необходимые библиотеки для работы с Web3 и Polyjuice.
Ну и самое основное, это нужно указать PRIVATE KEY вашего Ethereum аккаунта, от имени которого мы хотим развернуть наш смарт-контракт:
const DEPLOYER_PRIVATE_KEY = 'ETHEREUM PRIVATE KEY'; // Replace this with your Ethereum private key with funds on Layer 2.
Как экспортировать ключ от своего Ethereum аккаунта можно почитать здесь.
Теперь кульминация. Запускаем:
node index.js SimpleStorage.json
В результате мы получаем Deployed contract address
, который мы можем использовать вместе с файлом скомпилированного ранее контракта.
Важный момент - чтобы развернуть приложение на блокчейне нужен Godwoken Layer 2 аккаунт. Как его сделать и пополнить почитать можно здесь.
Я специально упускаю эти шаги, так как мне пришлось бы описывать весь Хакатон...
Итак, самый важный вывод - таким способом мы можем использовать любой Ethereum смарт-контракт. Но это еще не все - теперь нужно научиться использовать этот смарт-контракт в нашем приложении.
Использование смарт-контракта
Теперь переходим в нашу новую рабочую папку gw-gitcoin-instruction/src/examples/3-call-contract
, в которой лежит index.js
:
const Web3 = require('web3');
const { PolyjuiceHttpProvider, PolyjuiceAccounts } = require("@polyjuice-provider/web3");
const ACCOUNT_PRIVATE_KEY = ''; // Replace this with your Ethereum private key with funds on Layer 2.
const CONTRACT_ABI = [
{
"inputs": [],
"stateMutability": "payable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "x",
"type": "uint256"
}
],
"name": "set",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "get",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]; // this should be an Array []
const CONTRACT_ADDRESS = ''; // Deployed contract address
const GODWOKEN_RPC_URL = 'http://godwoken-testnet-web3-rpc.ckbapp.dev';
const polyjuiceConfig = {
rollupTypeHash: '0x4cc2e6526204ae6a2e8fcf12f7ad472f41a1606d5b9624beebd215d780809f6a',
ethAccountLockCodeHash: '0xdeec13a7b8e100579541384ccaf4b5223733e4a5483c3aec95ddc4c1d5ea5b22',
web3Url: GODWOKEN_RPC_URL
};
const provider = new PolyjuiceHttpProvider(
GODWOKEN_RPC_URL,
polyjuiceConfig,
);
const web3 = new Web3(provider);
web3.eth.accounts = new PolyjuiceAccounts(polyjuiceConfig);
const account = web3.eth.accounts.wallet.add(ACCOUNT_PRIVATE_KEY);
web3.eth.Contract.setProvider(provider, web3.eth.accounts);
async function readCall() {
const contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);
const callResult = await contract.methods.get().call({
from: account.address
});
console.log(`Read call result: ${callResult}`);
}
async function writeCall() {
const contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);
const tx = contract.methods.set(10).send(
{
from: account.address,
to: '0x' + new Array(40).fill(0).join(''),
gas: 6000000,
gasPrice: '100',
}
);
tx.on('transactionHash', hash => console.log(`Write call transaction hash: ${hash}`));
const receipt = await tx;
console.log('Write call transaction receipt: ', receipt);
}
(async () => {
const balance = BigInt(await web3.eth.getBalance(account.address));
if (balance === 0n) {
console.log(`Insufficient balance. Can't issue a smart contract call. Please deposit funds to your Ethereum address: ${account.address}`);
return;
}
console.log('Calling contract...');
// Check smart contract state before state change.
await readCall();
// Change smart contract state.
await writeCall();
// Check smart contract state after state change.
await readCall();
})();
В этом скрипте вначале мы также указываем ключ нашего Ethereum аккаунта.
const CONTRACT_ABI = []
- это объект ABI
нашего смарт-контракта. Мы его просто копируем с файла .json, и вставляем как именно массив.
const CONTRACT_ADDRESS = ''
- это адресс смарт-контракта развернутого в предыдущем шаге.
Следующий блок кода создает PolyjuiceHttpProvider с константными параметрами, который потом мы передаем Web3. Именно здесь начинается магия и приложение начинает работать с Nervos Layer 2:
const provider = new PolyjuiceHttpProvider(
GODWOKEN_RPC_URL,
polyjuiceConfig,
);
const web3 = new Web3(provider);
web3.eth.accounts = new PolyjuiceAccounts(polyjuiceConfig);
const account = web3.eth.accounts.wallet.add(ACCOUNT_PRIVATE_KEY);
web3.eth.Contract.setProvider(provider, web3.eth.accounts);
Ну, а теперь давайте наконец-то поработаем с нашим смарт-контрактом. Для этого нам нужно вызвать те функции, которые в нем прописаны (если забыли, просьба вернуться к исходному коду нашего контракта):
Следующий блок кода вызывает функцию get()
смарт-контракта, которая читает данные:
async function readCall() {
const contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);
const callResult = await contract.methods.get().call({
from: account.address
});
console.log(`Read call result: ${callResult}`);
}
А здесь мы вызываем функцию set(), которая принимает параметр (смотреть исходный код контракта), а значит мы записываем данные. Для этого в конце, вместо call()
уже используется send()
:
async function writeCall() {
const contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);
const tx = contract.methods.set(10).send(
{
from: account.address,
to: '0x' + new Array(40).fill(0).join(''),
gas: 6000000,
gasPrice: '100',
}
);
tx.on('transactionHash', hash => console.log(`Write call transaction hash: ${hash}`));
const receipt = await tx;
console.log('Write call transaction receipt: ', receipt);
}
Запускам node index.js
и результат:
Вот теперь, мы научились использовать наш Ethereum смарт-контракт, который был развернут на Layer 2 Nervos Network c помощью Polyjuice
Перенос Ethereum dapp приложения на Polyjuice
То, ради чего этот пост был написан.
И так, первое что нужно сделать это выбрать любое Ethereum приложение. Я выбрал "Список задач" с этого репозитория.
Клонируем:
git clone https://github.com/AndrewJBateman/blockchain-ethereum-contract
Добавление новой сети в MetaMask.
Для того, чтобы наше приложение могло общаться с новым блокчейном, нам необходима специальная сеть.
Чтобы добавить новую сеть, нажимаем на значок расширения, дальше во избежания исчезнования окошка, нажимите на вертикальное троиточие и выбирете Expand View, чтобы открыть его в отдельном окне. Дальше, нажимаем на список Networks, и выбираем Custom RPC.
Вводим необходимые параметры для нашей сети:
Network Name: Godwoken Test Network
New RPC URL: https://godwoken-testnet-web3-rpc.ckbapp.dev/
Chain ID: 71393
Currency Symbol (optional): N/A
Block Explorer URL (optional): N/A
Нажимаем Save. Выглядеть это должно вот так:
Развертывание смарт-контракта
Выбранное приложение имеет один смарт-контракт - 'TasksContract.sol', который необходимо скомпилировать и развернуть.
Здесь есть 3 варианта:
Первый. Использование нашего учебного репозитория. Просто копируем в папку
contracts
.json
файл контракта и запускаем те же команды описанные выше. На выходе нам нужно получить адрес по которому был развернут смарт-контракт, а также забрать.json
файл с папкиbuild/contracts/
Второй. Использование команды
truffle migrate --network godwoken
.
Файл настроек сети и Polyjuice провайдера лежит тут -truffle-config.js
.
Этот config файл использует файл.env
с такими параметрами:
PRIVATE_KEY='' - Ethereum ключ
WEB3_PROVIDER_URL='https://godwoken-testnet-web3-rpc.ckbapp.dev'
ROLLUP_TYPE_HASH='0x4cc2e6526204ae6a2e8fcf12f7ad472f41a1606d5b9624beebd215d780809f6a'
ETH_ACCOUNT_LOCK_CODE_HASH='0xdeec13a7b8e100579541384ccaf4b5223733e4a5483c3aec95ddc4c1d5ea5b22'
Замечение: этот способ требует знание truffle и неможко танцев с бубном...
- Третий. Это развертывание смарт-контракта внутри самого приложения - просто добавляется проверка адреса и если по этому адресу контракт не найден, тогда скрипт выполняет код, который выполняет этот процесс показанный в примере.
Добавляем Polyjuice провайдер
Так как выбранное мною приложение не использует никакой фронтенд фреймворк, подключаем в client/index.html
нужные с библиотеки помощью <script>
:
<script src="/libs/web3/dist/web3.min.js"></script>
<script src="/libs/@truffle/contract/dist/truffle-contract.min.js"></script>
<script src="./nervos-godwoken-integration.js"></script>
<script src="./polyjuice.js"></script>
Не знаю почему, но мой Nodejs отказался работать с require(), поэтому я использовал утилиту browserify
, чтобы "запаковать" главный файл client/app.js
Следующим шагом, для удобства, создаем файл client/config.js
:
const CONFIG = {
WEB3_PROVIDER_URL: 'https://godwoken-testnet-web3-rpc.ckbapp.dev',
ROLLUP_TYPE_HASH: '0x4cc2e6526204ae6a2e8fcf12f7ad472f41a1606d5b9624beebd215d780809f6a',
ETH_ACCOUNT_LOCK_CODE_HASH: '0xdeec13a7b8e100579541384ccaf4b5223733e4a5483c3aec95ddc4c1d5ea5b22',
DEFAULT_SEND_OPTIONS: {
gas: 6000000
},
CONTRACT_ADDRESS: '0x57a4d44de477A569766Ab934Fff38281d034f3EF',
SUDT_ERC20_PROXY_ADDRESS: '0xD5A6a78E967cd70C6791d5289B3E4b1D5D55eC27'
};
module.exports = {CONFIG}
Почти все параметры уже были использованны нами в учебном репозитории, это предустановленные настройки которые нужны для Polyjuice провайдера.
CONTRACT_ADDRESS
- это адрес нашего смарт-контракта, который был развернут в предыдущем шаге.
SUDT_ERC20_PROXY_ADDRESS
- это адрес отдельного смарт-контракта, который используется для отображения нужного SUDT (токена). Более подробно о SUDT можно почитать тут и тут
Далее, переходим в файл client/app.js
и подключаем наш config.js
и добавляем Polyjuice провайдер:
loadWeb3: async () => {
if (web3) {
// Polyjuice provider config
const providerConfig = {
rollupTypeHash: CONFIG.ROLLUP_TYPE_HASH,
ethAccountLockCodeHash: CONFIG.ETH_ACCOUNT_LOCK_CODE_HASH,
web3Url: CONFIG.WEB3_PROVIDER_URL
};
// Polyjuice provider
App.web3Provider = new PolyjuiceHttpProvider(CONFIG.WEB3_PROVIDER_URL, providerConfig);
web3 = new Web3(App.web3Provider);
} else {
alert("To use this DAPP you need setup godwoken network in your Metamask")
console.log("No ethereum browser is installed. Try installing MetaMask");
}
}
Загружаем смарт-контракты:
// load already deployed contract by address,
// to deploy use: truffle migrate --network godwoken
//
loadContract: async () => {
try {
const res = await fetch("TasksContract.json");
const tasksContractJSON = await res.json();
// Use CONFIG for contracts address
App.tasksContract = new web3.eth.Contract(tasksContractJSON.abi, CONFIG.CONTRACT_ADDRESS);
console.log('Task contract loaded:', App.tasksContract)
} catch (error) {
alert('Contract not found. Please deploy before continue')
console.error('Cannot find deployed contract:', error);
}
},
loadSudtContract: async () => {
try {
const res = await fetch("ERC20.json");
const sudtContract = await res.json();
// Use CONFIG for contracts address
App.sudtContract = new web3.eth.Contract(sudtContract.abi, CONFIG.SUDT_ERC20_PROXY_ADDRESS);
console.log('SUDT contract loaded:', App.sudtContract)
} catch (error) {
alert('Contract not found. Please deploy before continue')
console.error('Cannot find deployed contract:', error);
}
}
Отображение баланса аккаунтов:
// render account balance as inner text
renderBalance: async () => {
const addressTranslator = new AddressTranslator();
const polyjuiceAddress = addressTranslator.ethAddressToGodwokenShortAddress(App.account);
const ckETHAddress = await addressTranslator.getLayer2DepositAddress(web3, App.account);
const urlDeposit = "https://force-bridge-test.ckbapp.dev/bridge/Ethereum/Nervos?recipient=" + ckETHAddress.addressString
const ckbShn = BigInt(await web3.eth.getBalance(App.account));
const ckbBalance = parseInt(BigInt(ckbShn / 10n ** 8n));
const ckEth = BigInt(await App.sudtContract.methods.balanceOf(polyjuiceAddress).call({
from: App.account
}));
ckEthBalance = parseInt(BigInt(ckEth / 10n**8n));
document.getElementById("account").innerText = App.account;
document.getElementById("polyjuice-account").innerText = polyjuiceAddress;
document.getElementById("ck-eth").innerText = ckETHAddress.addressString;
// document.getElementById("ckb-balance").innerText = ckbBalance + " $CKB";
document.getElementById("do-deposit").href = urlDeposit
document.getElementById("ckb-balance").innerText = ckbBalance + " $CKB";
document.getElementById("cketh-balance").innerText = ckEthBalance + " $ckETH";
// Copy address
document.querySelector("#do-deposit").addEventListener("click", function copy() {
var copyText = document.querySelector("#ck-eth");
var elementText = copyText.textContent;
navigator.clipboard.writeText(elementText);
});
// console.log(await App.sudtContract.methods.balanceOf(polyjuiceAddress).call({
// from: App.account
// }));
},
Вызываем функцию смарт-контракта, которая выдает список всех задач и проходимся по каждой из них:
// render tasks
renderTasks: async () => {
const taskCounter = await App.tasksContract.methods.taskCounter().call(
{
from: App.account,
...CONFIG.DEFAULT_SEND_OPTIONS ,
}
);
const taskCounterNumber = parseInt(taskCounter);
let html = "";
for (let i = 1; i <= taskCounterNumber; i++) {
const task = await App.tasksContract.methods.tasks(i).call({
from: App.account,
...CONFIG.DEFAULT_SEND_OPTIONS ,
});
const taskId = parseInt(task[0]);
const taskTitle = task[1];
const taskDescription = task[2];
const taskDone = task[3];
const taskCreatedAt = task[4];
// Creating a task Card
let taskElement = `
<div class="card bg-light rounded-0 mb-2">
<div class="card-header d-flex justify-content-between align-items-center">
<span>${taskTitle}</span>
<div class="form-check form-switch">
<input class="form-check-input" data-id="${taskId}" type="checkbox" onchange="App.toggleDone(this)" ${
taskDone === true && "checked"
}>
</div>
</div>
<div class="card-body">
<span>${taskDescription}</span>
<p class="text-muted">Task was created ${new Date(
taskCreatedAt * 1000
).toLocaleString()}</p>
</label>
</div>
</div>
`;
html += taskElement;
}
document.querySelector("#tasksList").innerHTML = html;
},
Вызываем функцию смарт-контракта, которая создает новую задачу:
createTask: async (title, description) => {
try {
const result = await App.tasksContract.methods.createTask(title, description).send({
...CONFIG.DEFAULT_SEND_OPTIONS,
from: App.account,
});
alert("Transaction completed, page will be reload.")
window.location.reload();
} catch (error) {
console.error(error);
}
},
Здесь важный момент использования send()
вместо call()
, о котором я упоминал выше в учебном примере.
Ну, и последняя функция смарт-контракта, которая меняет статус задачи:
toggleDone: async (element) => {
const taskId = element.dataset.id;
console.log(taskId)
await App.tasksContract.methods.toggleDone(taskId).send({
...CONFIG.DEFAULT_SEND_OPTIONS,
from: App.account,
});
alert("Transaction completed, page will be reload.")
window.location.reload();
},
Также, используется send()
.
На этом процесс портирование (перенос) Ethereum dapp приложения закончен. В моем случае осталось только подправить макет единственной страницы index.html
и все, можно запускать npm run dev
:
Видео работающего приложения на YouTube
Скриншоты:
Ссылки:
Github оригинального Ethereum приложения
Github трансформированного приложения на Polyjuice
Хакатон Nervos - Broaden The Spectrum
Top comments (2)
Шикарно!
Благодарю!