DEV Community

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

Posted on

Хакатон 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 для новичков

Discussion (2)

Collapse
michaelshapkin profile image
Michael Shapkin

Шикарно!

Collapse
x777 profile image
YD Author

Благодарю!