Dapp. Vue.js + ethers.js

Kate

Administrator
Команда форума
Что уже нужно знать перед тем как начать:

  • Неплохие знания js, в частности Vue.js
  • Понимание принципов работы Blockchain (смарт-контрактов)
  • Базовые знания языка пр. Solidity

Установка кошелька​

Для подтверждения транзакций, общения с контрактом и проверки некоторых параметров этих транзакций мы будем использовать кошелек Metamask. Он довольно хорош и с его работой у меня почти никогда не возникало проблем. Процесс установки я показывать не буду, там все очевидно.

Когда расширение установится - вас попросят создать кошелек или импортировать если такой имеется. После создания аккаунта у вас сгенерируется мнемоническая фраза. НИКОГДА, НИКОМУ ее не разглашайте. И воаля - у нас есть готовый адрес кошелька)

Так как мы будет разрабатывать пример приложения - лучше использовать тестовую сеть, дабы не тратить реальные деньги на развертывание контракта и и выполнение транзакций. Для нашей задачи отлично подойдет Ropsten.

ff03b0a91c6a61a944225e86e94fb3a5.png

Здесь вы можете нафармить себе немного тестовых эфиров, чего нам хватит для работы.

Контракт​

Пока не на долго забудем о нашем кошельке и приступим к написанию смарт-контракта. Для примера я буду использовать уже готовый контракт. Более подробно о нем вы можете ознакомиться с ним в README (он довольно простой). Основная идея - это купля-продажа. Продавец выставляет свой товар - покупатель ставит свою цену и делает предложение, продавец в свою очередь принимает предложение. Много нюансов упущено, но для для примера сойдет. Попробовать попользоваться контрактом вы можете в бесплатной IDE Remix.

Развертывание контракта​

Для этой задачи мы будем использовать Truffle.js. Для его установки на вашей машине должен быть уже установлен node.js. Дальше открываем любой вам удобный редактор и в консоли через npm устанавливаем следующие пакеты (в моем случаи глобально): npm install -g truffleиnpm install -g @truffle/hdwallet-provider После чего создайте папку, где будет хранится ваш проект и инициализируйте Truffle проект в нем командой truffle init.

55ad8afb81171bc122315f45d482722e.png

В папке с проектом с начала обратим наше внимание на файл truffle-config.js. В нем хранятся конфигурации по развертыванию контрактов. Но прежде всего нам нужно будет зарегистрироваться в Infura и создать новый проект перейдя по вкладке Ethereum -> Create New Project соединяет интерфейс пользователя (UI) dApps с внешним смарт-контрактом на блокчейне Ethereum. Провайдер Infura может обрабатывать подписание транзакции, а также подключение к сети Ethereum без необходимости синхронизировать ноду и это именно то, что нам нужно. Дальше выбрав нужный созданный проект -> Settings -> Endpoints выбираем нашу сеть Ropsten и копируем указанный адрес.

74f445c9bd8e3d96d564b317b97a19a8.png

Теперь можно перейти, непосредственно, к настройке truffle-config.js. Во-первых, создайте в корневой папке проекта файл .secret в котором будет хранится наша мнемоническая фраза. Лучше всего использовать dotenv, но для ознакомления оставим так, как предлагает truffle. Во-вторых, раскомментируйте следующие строки:

const HDWalletProvider = require('@truffle/hdwallet-provider');

const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();
и также

ropsten: {
provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
network_id: 3, // Ropsten's id
gas: 5500000, // Ropsten has a lower block limit than mainnet
confirmations: 2, // # of confs to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
},
И вместо второго параметра в HDWalletProviderпоместите скопированный адрес.

В-третьих, создайте новый файл формата .sol в папке contracts, и поместите туда весь код контракта. В папке migrations создайте js файл и поместите туда следующий код:

const SimpleMarketplace = artifacts.require("SimpleMarketplace");

module.exports = function (deployer) {
deployer.deploy(SimpleMarketplace, 'description', 1000);
};
Обратите внимание, что при создании нового файла в migrations название файла должно начинаться с цифры следующей по порядку от файла 1_initial_migrations.js, то есть 2.

Так как у нас контракт содержит конструктор с параметрами - мы методе deploy вторым и третьим аргументами ставим именно их: description и price в нашем случае. Если у вас контракт без конструктора или с конструктором, но без параметром, то оставляем просто переменную SimpleMarketplace.

Сохраняем и в терминале с проектом пробуем скомпилировать контракт введя команду truffle compile. Важно: Если у вас выкидывает ошибку, что не найден пакет @truffle/hdwallet-provider,(Error: Cannot find module '@truffle/hdwallet-provider'),то попробуйте установить этот пакет в текущую директорию командой npm install @truffle/hdwallet-provider. без опции-g и скомпилировать снова. После вывода в консоль Compiled successfully using... можно пробовать деплоить.

Для развертывания контракта в сети в консоль введите truffle migrate --network ropsten --reset. Где ropsten- это название сети, что мы указали в файле truffle-config.js, а --reset опция, которая просто передплоивает контракт наново если вы уже выполняли подобные действия (при первом разе можно и без нее).Важно: Если у вас выбрасывает подобную ошибку: var e = new Error('ETIMEDOUT'), то попробуйте в truffle-config.js в HDWalletProvider вставить другой адрес, что предлагает Infura - wss://ropsten.infura.io/ws/v3/YOUR-PROJECT-ID.

Если процесс прошел без ошибок и в Etherscan перейдя по своему адресу вы можете наблюдать транзакцию, о создании контракта - поздравляю, контракт находится в сети blockchain и можно идти дальше.

Создание Vue.js приложения​

И так, если все выше перечисленные шаги вам удалось произвести, то можно начинать писать веб-приложения для нашего контракта. Установку vue/cli и создание проекта я упущу, так как это детально описано в официальной документации Vue.js. Установка. Создание проекта.

После того как ваше приложение было создано можно начать писать саму логику. Для начала давайте удалим из все assets (в нем будет хранится логотип Vue.js, который нам не нужен), удалим компонент HelloWorld.vue из components и в файле App.vue уберем все лишнее (импорт компонента HelloWorld.vue, саму инициализацию компонента в объекте components и оставим чистый шаблон с <div id="app">внутри).

<template>
<div id="app">

</div>
</template>

<script>

export default {
name: 'App',
components: {
}
}
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Запустив проект командой npm run serve можете проверить, что все работает и нет никаких ошибок, как и в прочем, ничего вообще пока что.

Чтобы взаимодействовать с таким простым контрактом нам понадобится всего лишь один новый компонент - это главная страница на которой мы будем делать UI для нашего смарт-контракта. В папке components создайте новый компонент и назовите его, к примеру, Marketplace.vue. Так же создайте папку core и в ней файл core.js и в проекте, в котором мы развертывали контракт, перейдите в папку ./build/contracts и скопируйте от туда JSON - файл с названием вашего файла контракта (или же запомните путь к нему). Этот файл - это как-бы инструкция, которая говорит вам как можно обратится к контракту. Не обязательно создавать все эти папки и называть так файлы, но это просто считается хорошим тоном. В дальнейшем хорошо структурированный проект будет проще поддерживать. Перед тем как продолжить - установите в пакет npm install ethers.

В файле core.js проимпортируйте установленную библиотеку, укажите путь в JSON - файлу и возьмите оттуда адрес нашего контракта:

const { ethers } = require('ethers')
const ContractArtifact = require('./SimpleMarketplace.json')
const CONTRACT_ADDRESS = ContractArtifact.networks['3'].address
Дальше создадим переменную ABI, которая должна быть массивом строк и занесем туда все необходимые нам функции контракта, что мы будем использовать (Вы можете просто скопировать строку из контракта и занести сюда. Если в контракте просто public поле, для него можете тоже отдельно написать public view returns функцию и ethers не должен на это ругаться):

const ABI = [
'function InstanceOwner () public view returns(address)',
'function Description () public view returns(string)',
'function AskingPrice () public view returns(int)',

'function InstanceBuyer () public view returns(address)',
'function OfferPrice () public view returns(int)',

'function MakeOffer(int offerPrice) public',
'function Reject() public',
'function AcceptOffer() public'
]
После чего нам нужно определить ряд переменных:

let provider = new ethers.providers.Web3Provider(window.ethereum)
//С помощью провайдера мы подключаемся к сети Blockcain
let readOnlyContract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider)
//Дает нам возможность читать view методы контракта
let signer = provider.getSigner()
//Нужен для подтверждения транзакций
let contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer)
let contractSigner = contract.connect(signer)
//Дают возможность выполнять транзакции
Дальше определим методы которые будем вызывать во Vue компонентах:

export default {
async getDescription() {
const description = await readOnlyContract.Description()
return {description: description}
}
}
И таким образом для каждой функции, с которой хотите взаимодействовать определяете свой метод. В итоге должно получится что-то вроде:

const { ethers } = require('ethers')
const ContractArtifact = require('./SimpleMarketplace.json')
const CONTRACT_ADDRESS = ContractArtifact.networks['3'].address

const ABI = [
'function InstanceOwner () public view returns(address)',
'function Description () public view returns(string)',
'function AskingPrice () public view returns(int)',

'function InstanceBuyer () public view returns(address)',
'function OfferPrice () public view returns(int)',

'function MakeOffer(int offerPrice) public',
'function Reject() public',
'function AcceptOffer() public'
]

let provider = new ethers.providers.Web3Provider(window.ethereum)
let readOnlyContract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider)
let signer = provider.getSigner()
let contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer)
let contractSigner = contract.connect(signer)

export default {
async getInstanceOwner() {
const instanceOwner = await readOnlyContract.InstanceOwner()
return {instanceOwner: instanceOwner}
},
async getDescription() {
const description = await readOnlyContract.Description()
return {description: description}
},
async getAskingPrice() {
const askingPrice = await readOnlyContract.AskingPrice()
return {askingPrice: askingPrice}
},
async getInstanceBuyer() {
const instanceBuyer = await readOnlyContract.InstanceBuyer()
return {instanceBuyer: instanceBuyer}
},
async getOfferPrice() {
const offerPrice = await readOnlyContract.OfferPrice()
return {offerPrice: offerPrice}
},

async makeOffer(offerPrice) {
const txResponse = await contractSigner.MakeOffer(offerPrice, {gasLimit: 300000})
const txReceipt = await txResponse.wait()
return {transaction: txReceipt.transactionHash}
},
async reject() {
const txResponse = await contractSigner.Reject({gasLimit: 300000})
const txReceipt = await txResponse.wait()
return {transaction: txReceipt.transactionHash}
},
async acceptOffer() {
const txResponse = await contractSigner.AcceptOffer({gasLimit: 300000})
const txReceipt = await txResponse.wait()
return {transaction: txReceipt.transactionHash}
}
}
Дальше я в файле App.vue в mounted методе вызываю подключение кошелька и в свойство $root заношу проимпортированный заранее наш файл core.js:

const core = require('./core/core')
/*
Какой-то другой код
*/
mounted() {
window.ethereum.request({ method: 'eth_requestAccounts' })
this.$root.core = core.default
}
Дальше под каждое поле, что вы хотите читать с контракта создаете поле в в data и метод details в объекте methods :

data() {
return {
instanceOwner: '',
description: '',
askingPrice: '',

instanceBuyer: '',
offerPrice: ''
}
},
methods: {
async details() {
this.instanceOwner = (await this.$root.core.getInstanceOwner()).instanceOwner
this.description = (await this.$root.core.getDescription()).description
this.askingPrice = (await this.$root.core.getAskingPrice()).askingPrice
this.instanceBuyer = (await this.$root.core.getInstanceBuyer()).instanceBuyer
this.offerPrice = (await this.$root.core.getOfferPrice()).offerPrice
}
},
Тем временем в разметке выводим необходимые нам поля:

<button v-on:click="details">Get details</button>
<h3>Instance owner: {{ instanceOwner }}</h3>
<h3>Description: {{ description }}</h3>
<h3>Asking price: {{ askingPrice }}</h3>
<h3>Instance buyer: {{ instanceBuyer }}</h3>
<h3>Offer price: {{ offerPrice }}</h3>
Код компонента Marketplace.vue можете проверить в моем репозитории, дабы не захламлять статью лишним кодом.

После запуска проекта у вас должно появится окно подключения кошелька подобно этому:

ea7650ee587f1e7ed9f2b967d175389c.png

И после этого при нажатии на кнопку Get details мы получим данные, которые вводили при развертывании контракта.

12a72baff3ac64d1f8cf37074ffa5c8f.png

А так выглядит вывод если произвести транзакцию:

4219d38f428c23900f8272c1182c42c0.png



Источник статьи: https://habr.com/ru/post/551710/
 
Сверху