DEV Community

Cover image for Хакатон Nervos - Broaden the Spectrum. Трансформация.
YD
YD

Posted on

1

Хакатон Nervos - Broaden the Spectrum. Трансформация.

В рамках Хакатона на площадке 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Чтобы скомпилировать этот файл, запускаем:

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
}
Enter fullscreen mode Exit fullscreen mode

Этот скрипт принимает в качестве параметра название файла смарт-контракта, в нашем случае это 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();
})();
Enter fullscreen mode Exit fullscreen mode

В этом скрипте вначале мы также указываем ключ нашего 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);
Enter fullscreen mode Exit fullscreen mode

Ну, а теперь давайте наконец-то поработаем с нашим смарт-контрактом. Для этого нам нужно вызвать те функции, которые в нем прописаны (если забыли, просьба вернуться к исходному коду нашего контракта):

Следующий блок кода вызывает функцию 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}`);
}
Enter fullscreen mode Exit fullscreen mode

А здесь мы вызываем функцию 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);
}
Enter fullscreen mode Exit fullscreen mode

Запускам node index.js и результат:

Call contract

Вот теперь, мы научились использовать наш 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. Выглядеть это должно вот так:

Godwoken Test Network

Развертывание смарт-контракта

Выбранное приложение имеет один смарт-контракт - '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'
Enter fullscreen mode Exit fullscreen mode

Замечение: этот способ требует знание 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>
Enter fullscreen mode Exit fullscreen mode

Не знаю почему, но мой 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}
Enter fullscreen mode Exit fullscreen mode

Почти все параметры уже были использованны нами в учебном репозитории, это предустановленные настройки которые нужны для 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");
        }

    }
Enter fullscreen mode Exit fullscreen mode

Загружаем смарт-контракты:

// 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);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Отображение баланса аккаунтов:

 // 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
        // }));
    },
Enter fullscreen mode Exit fullscreen mode

Вызываем функцию смарт-контракта, которая выдает список всех задач и проходимся по каждой из них:

// 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;
    },
Enter fullscreen mode Exit fullscreen mode

Вызываем функцию смарт-контракта, которая создает новую задачу:

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);
        }
    },
Enter fullscreen mode Exit fullscreen mode

Здесь важный момент использования 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();
    },
Enter fullscreen mode Exit fullscreen mode

Также, используется send().

На этом процесс портирование (перенос) Ethereum dapp приложения закончен. В моем случае осталось только подправить макет единственной страницы index.html и все, можно запускать npm run dev:

Видео работающего приложения на YouTube

Скриншоты:

Polyjuice App

Polyjuice App

Ссылки:

Github оригинального Ethereum приложения
Github трансформированного приложения на Polyjuice
Хакатон Nervos - Broaden The Spectrum

Знакомство с Nervos Network для новичков

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (2)

Collapse
 
michaelshapkin profile image
Michael Shapkin

Шикарно!

Collapse
 
x777 profile image
YD

Благодарю!

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay