Статьи

Эфириум DApps: создание веб-интерфейса для контракта DAO

В шестой части этой серии руководств по созданию DApp-приложений с помощью Ethereum мы продвинули DAO к завершению, добавив голосования, черные списки / черные списки, а также распределение и снятие дивидендов, добавив при этом некоторые дополнительные вспомогательные функции для хорошей меры. В этом руководстве мы создадим веб-интерфейс для взаимодействия с нашей историей, поскольку в противном случае мы не можем рассчитывать на участие пользователей. Так что это последняя часть нашей истории, прежде чем мы запустим ее в дикую природу.

Поскольку это не учебник по веб-приложениям, мы сделаем все очень просто. Приведенный ниже код не готов к работе и предназначен только для подтверждения концепции о том, как подключить JavaScript к блокчейну. Но сначала давайте добавим новую миграцию.

Автоматизация переводов

Прямо сейчас, когда мы развертываем наш токен и DAO, они сидят на блокчейне, но не взаимодействуют. Чтобы протестировать то, что мы создали, нам нужно вручную передать владение токеном и баланс в DAO, что может быть утомительно во время тестирования.

Давайте напишем новую миграцию, которая сделает это за нас. Создайте файл 4_configure_relationship.js и поместите туда следующее содержимое:

 var Migrations = artifacts.require("./Migrations.sol"); var StoryDao = artifacts.require("./StoryDao.sol"); var TNSToken = artifacts.require("./TNSToken.sol"); var storyInstance, tokenInstance; module.exports = function (deployer, network, accounts) { deployer.then(function () { return TNSToken.deployed(); }).then(function (tIns) { tokenInstance = tIns; return StoryDao.deployed(); }).then(function (sIns) { storyInstance = sIns; return balance = tokenInstance.totalSupply(); }).then(function (bal) { return tokenInstance.transfer(storyInstance.address, bal); }) .then(function (something) { return tokenInstance.transferOwnership(storyInstance.address); }); } 

Вот что делает этот код. Во-первых, вы заметите, что он основан на обещаниях. Там полно звонков. Это потому, что мы зависим от функции, возвращающей некоторые данные, прежде чем мы вызовем следующую. Все контрактные вызовы основаны на обещаниях, то есть они не возвращают данные немедленно, потому что Truffle должен запросить информацию у узла, поэтому обещание вернуть данные в будущем будет выполнено. Мы вынуждаем код ждать эти данные, используя then и предоставляя все вызовы then функциями обратного вызова, которые вызываются с этим результатом, когда он, наконец, передается.

Итак, по порядку:

  • сначала спросите у узла адрес развернутого токена и верните его
  • затем, приняв эти данные, сохраните их в глобальной переменной, запросите адрес развернутого DAO и верните его
  • затем, приняв эти данные, сохраните их в глобальной переменной и запросите баланс, который владелец токен-контракта будет иметь на своем счете, что технически является общей поставкой, и верните эти данные
  • затем, как только вы получите этот баланс, используйте его для вызова функции передачи этого токена и отправки токенов на адрес DAO и возврата результата.
  • затем проигнорируйте возвращенный результат — мы просто хотели знать, когда это будет сделано — и, наконец, перенесите владение токеном на адрес DAO, вернув данные, но не отбрасывая их.

Выполнение truffle migrate --reset теперь должно truffle migrate --reset результат, подобный этому:

Трюфель мигрирует

Передний конец

Внешний интерфейс — это обычная статическая HTML-страница с добавленным JavaScript для связи с блокчейном и CSS, чтобы сделать вещи менее уродливыми.

Давайте создадим файл index.html в подпапке public и дадим ему следующий контент:

 <!DOCTYPE HTML> <html lang="en"> <head> <title>The Neverending Story</title> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="description" content="The Neverending Story is an community curated and moderated Ethereum dapp-story"> <link rel="stylesheet" href="assets/css/main.css"/> </head> <body> <div class="grid-container"> <div class="header container"> <h1>The Neverending Story</h1> <p>A story on the Ethereum blockchain, community curated and moderated through a Decentralized Autonomous Organization (DAO)</p> </div> <div class="content container"> <div class="intro"> <h3>Chapter 0</h3> <p class="intro">It's a rainy night in central London.</p> </div> <hr> <div class="content-submissions"> <div class="submission"> <div class="submission-body">This is an example submission. A proposal for its deletion has been submitted.</div> <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div> <div class="submission-actions"> <div class="deletionproposed" data-votes="3024" data-deadline="1531607200"></div> </div> </div> <div class="submission"> <div class="submission-body">This is a long submission. It has over 244 characters, just we can see what it looks like when rendered in the UI. We need to make sure it doesn't break anything and the layout also needs to be maintained, not clashing with actions/buttons etc.</div> <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div> <div class="submission-actions"> <div class="delete"></div> </div> </div> <div class="submission"> <div class="submission-body">This is an example submission. A proposal for its deletion has been submitted but is looking like it'll be rejected.</div> <div class="submission-submitter">0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9</div> <div class="submission-actions"> <div class="deletionproposed" data-votes="-790024" data-deadline="1531607200"></div> </div> </div> </div> </div> <div class="events container"> <h3>Latest Events</h3> <ul class="eventlist"> </ul> </div> <div class="information container"> <p>Logged in / out</p> <div class="avatar"> <img src="http://placeholder.pics/svg/200/DEDEDE/555555/avatar" alt="avatar"> </div> <dl> <dt>Contributions</dt> <dd>0</dd> <dt>Deletions</dt> <dd>0</dd> <dt>Tokens</dt> <dd>0</dd> <dt>Proposals submitted</dt> <dd>0</dd> <dt>Proposals voted on</dt> <dd>0</dd> </dl> </div> </div> <script src="assets/js/web3.min.js"></script> <script src="assets/js/app.js"></script> <script src="assets/js/main.js"></script> </body> </html> 

Примечание: это действительно очень простой скелет, просто для демонстрации интеграции. Пожалуйста, не надейтесь, что это конечный продукт!

Возможно, вам не хватает папки dist папке web3 . Программное обеспечение все еще находится на стадии бета-тестирования, поэтому здесь возможны небольшие ошибки. Чтобы обойти это и установить web3 с папкой dist , запустите npm install ethereum/web3.js --save .

Для CSS давайте поместим что-нибудь элементарное в public/assets/css/main.css :

 @supports (grid-area: auto) { .grid-container{ display: grid; grid-template-columns: 6fr 5fr 4fr; grid-template-rows: 10rem ; grid-column-gap: 0.5rem; grid-row-gap: 0.5rem; justify-items: stretch; align-items: stretch; grid-template-areas: "header header information" "content events information"; height: 100vh; } .events { grid-area: events; } .content { grid-area: content; } .information { grid-area: information; } .header { grid-area: header; text-align: center; } .container { border: 1px solid black; padding: 15px; overflow-y: scroll; } p { margin: 0; } } body { padding: 0; margin: 0; font-family: sans-serif; } 

Затем, как JS, мы начнем с этого в public/assets/js/app.js :

 var Web3 = require('web3'); var web3 = new Web3(web3.currentProvider); console.log(web3); 

Что тут происходит?

Поскольку мы согласились с тем, что будем считать, что у всех наших пользователей установлен MetaMask , а MetaMask внедряет свой собственный экземпляр Web3 в DOM любой посещенной веб-страницы, мы в основном имеем доступ к «провайдеру кошельков» из MetaMask прямо на нашем веб-сайте. Действительно, если мы войдем в MetaMask, пока страница открыта, мы увидим это в консоли:

MetaMask провайдер активен

Обратите внимание, как активен MetamaskInpageProvider. Фактически, если мы web3.eth.accounts в консоль web3.eth.accounts , все учетные записи, к которым у нас есть доступ через MetaMask, будут распечатаны:

Аккаунт распечатывается в консоли

Этот конкретный аккаунт, однако, по умолчанию добавлен в мою личную Метамаску и будет иметь баланс 0 eth. Это не является частью нашей сети Ganache или PoA:

Пустой аккаунт

Обратите внимание, что запрос баланса нашего активного (MetaMasked) дает 0, в то время как запрос баланса одного из наших частных блоковых блоков приводит к 100 эфирам (в моем случае это Ganache, поэтому все учетные записи инициализируются 100 эфирами).

О синтаксисе

Вы заметите, что синтаксис этих вызовов выглядит немного странно:

 web3.eth.getBalance("0x35d4dCDdB728CeBF80F748be65bf84C776B0Fbaf", function(err, res){console.log(JSON.stringify(res));}); 

Чтобы читать данные блокчейна, большинство пользователей MetaMask не будут иметь локально работающий узел, а вместо этого будут запрашивать его у Infura или другого удаленного узла. Из-за этого мы можем практически рассчитывать на отставание. По этой причине синхронные методы обычно не поддерживаются . Вместо этого все делается с помощью обещаний или обратных вызовов — как в шаге развертывания в начале этого поста. Означает ли это, что вам нужно быть близко знакомым с обещаниями разработать JS для Ethereum? Нет. Это означает следующее. При выполнении вызовов JS в DOM …

  • всегда предоставлять функцию обратного вызова в качестве последнего аргумента вызываемой функции
  • предположим, что его возвращаемые значения будут двойными: сначала error , затем result .

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

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

Информация об учетной записи

Если мы откроем скелет сайта, как показано выше, мы получим что-то вроде этого:

Скелет сайта

Давайте заполним самый правый столбец с информацией об аккаунте реальными данными.

сессия

Когда пользователь не вошел в свое расширение MetaMask, список учетных записей будет пустым. Когда MetaMask даже не установлен, поставщик будет пуст (не определен). Когда они войдут в MetaMask, провайдер будет доступен и предложит информацию об учетной записи и взаимодействие с подключенным узлом Ethereum (прямой эфир, Ganache или что-то еще).

Совет: для тестирования вы можете выйти из MetaMask, щелкнув значок аватара в правом верхнем углу и выбрав «Выход». Если пользовательский интерфейс выглядит не так, как на скриншоте ниже, вам может потребоваться активировать бета-интерфейс, открыв меню и нажав «Попробуйте бета-версию».

MetaMask Выйти

Во-первых, давайте заменим все содержимое правого столбца состояния на сообщение для пользователя, если они вышли из системы:

 <div class="information container"> <div class="logged out"> <p>You seem to be logged out of MetaMask or MetaMask isn't installed. Please log into MetaMask - to learn more, see <a href="https://bitfalls.com/2018/02/16/metamask-send-receive-ether/">this tutorial</a>.</p> </div> <div class="logged in" style="display: none"> <p>You are logged in!</p> </div> </div> 

JS для обработки выглядит следующим образом (в public/assets/js/main.js ):

 var loggedIn; (function () { loggedIn = setLoggedIn(web3.currentProvider !== undefined && web3.eth.accounts.length > 0); })(); function setLoggedIn(isLoggedIn) { let loggedInEl = document.querySelector('div.logged.in'); let loggedOutEl = document.querySelector('div.logged.out'); if (isLoggedIn) { loggedInEl.style.display = "block"; loggedOutEl.style.display = "none"; } else { loggedInEl.style.display = "none"; loggedOutEl.style.display = "block"; } return isLoggedIn; } 

Первая часть — (function () { — оборачивает часть логики, которая будет выполнена после загрузки сайта. Поэтому все, что внутри, будет выполнено немедленно, когда страница будет готова. setLoggedIn одна функция setLoggedIn и ей передается условие Условие таково:

  1. web3 объекта web3 (т.е. на сайте присутствует клиент web3).
  2. Доступно ненулевое количество доступных учетных записей, т.е. учетная запись доступна для использования через этого провайдера web3. Другими словами, мы вошли как минимум в одну учетную запись.

Если эти условия вместе оцениваются как true , функция setLoggedIn делает сообщение «Выйти из системы» невидимым, а сообщение «Войдя в систему» ​​видимым.

Все это имеет дополнительное преимущество, заключающееся в возможности использовать любого другого веб-провайдера. Если в конечном итоге появится альтернатива MetaMask, она будет мгновенно совместима с этим кодом, потому что мы нигде не ожидаем MetaMask.

Аватар аккаунта

Поскольку каждый закрытый ключ к кошельку Ethereum уникален, его можно использовать для создания уникального изображения. Вот откуда появляются красочные аватары, подобные тем, которые вы видите в верхнем правом углу MetaMask или при использовании MyEtherWallet, хотя Mist, MyEtherWallet и MetaMask используют разные подходы. Давайте сгенерируем один для нашего вошедшего в систему пользователя и отобразим его.

Иконки в Mist генерируются с помощью библиотеки Blockies — но настраиваемой, поскольку оригинал имеет сломанный генератор случайных чисел и может создавать идентичные изображения для разных ключей. Таким образом, чтобы установить этот, загрузите этот файл в один в вашей папке assets/js . Затем в index.html мы включаем его перед main.js :

  <script src="assets/js/app.js"></script> <script src="assets/js/blockies.min.js"></script> <script src="assets/js/main.js"></script> </body> 

Мы также должны обновить контейнер logged.in :

 <div class="logged in" style="display: none"> <p>You are logged in!</p> <div class="avatar"> </div> </div> 

В main.js мы main.js функцию.

  if (isLoggedIn) { loggedInEl.style.display = "block"; loggedOutEl.style.display = "none"; var icon = blockies.create({ // All options are optional seed: web3.eth.accounts[0], // seed used to generate icon data, default: random size: 20, // width/height of the icon in blocks, default: 8 scale: 8, // width/height of each block in pixels, default: 4 }); document.querySelector("div.avatar").appendChild(icon); 

Таким образом, мы обновляем раздел авторизованного кода JS, чтобы сгенерировать значок и вставить его в раздел аватаров. Мы должны немного согласовать это с CSS перед рендерингом:

 div.avatar { width: 100%; text-align: center; margin: 10px 0; } 

Теперь, если мы обновим страницу при входе в MetaMask, мы должны увидеть созданный нами значок аватара.

Аватар Иконка

Остатки на счетах

Теперь давайте выведем некоторую информацию об остатке на счете.

В нашем распоряжении множество функций только для чтения, которые мы разработали исключительно для этой цели. Итак, давайте запросим блокчейн и попросим информацию. Для этого нам нужно вызвать функцию умного контракта, выполнив следующие шаги.

1. ABI

Получите ABI контрактов, чьи функции мы называем. ABI содержит сигнатуры функций, поэтому наш код JS знает, как их вызывать. Узнайте больше о ABI здесь .

Вы можете получить ABI токена TNS и StoryDAO, открыв файлы build/TNSToken.json и build/StoryDao.json в папке вашего проекта после компиляции и выбрав только часть abi — так что часть между квадратными скобками [ и ] :

Выбор ABI

Мы поместим этот ABI в начало нашего JavaScript-кода в main.js следующим образом:

Токен и ДАО АБИ загружены

Обратите внимание, что на приведенном выше снимке экрана показана сокращенная вставка, свернутая моим редактором кода (Microsoft Visual Code). Если вы посмотрите на номера строк, вы заметите, что ABI токена составляет 400 строк кода, а ABI DAO — еще 1000, поэтому вставлять это в эту статью не имеет смысла.

2. Создайте токен

 if (loggedIn) { var token = TNSToken.at('0x3134bcded93e810e1025ee814e87eff252cff422'); var story = StoryDao.at('0x729400828808bc907f68d9ffdeb317c23d2034d5'); token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))}); story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))}); //... 

Мы призываем каждый контракт с адресом, данным нам Трюфелем, и создаем экземпляр для каждого token и story соответственно. Затем мы просто вызываем функции (асинхронно, как и раньше). Консоль дает нам два нуля, потому что учетная запись в MetaMask имеет 0 токенов, и потому что в истории на данный момент есть 0 представлений.

Консоль выдает два ноля

3. Чтение и вывод данных

Наконец, мы можем заполнить данные профиля пользователя имеющейся у нас информацией.

Давайте обновим наш JavaScript:

 var loggedIn; (function () { loggedIn = setLoggedIn(web3.currentProvider !== undefined && web3.eth.accounts.length > 0); if (loggedIn) { var token = TNSToken.at('0x3134bcded93e810e1025ee814e87eff252cff422'); var story = StoryDao.at('0x729400828808bc907f68d9ffdeb317c23d2034d5'); token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))}); story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))}); readUserStats().then(User => renderUserInfo(User)); } })(); async function readUserStats(address) { if (address === undefined) { address = web3.eth.accounts[0]; } var User = { numberOfSubmissions: await getSubmissionsCountForUser(address), numberOfDeletions: await getDeletionsCountForUser(address), isWhitelisted: await isWhitelisted(address), isBlacklisted: await isBlacklisted(address), numberOfProposals: await getProposalCountForUser(address), numberOfVotes: await getVotesCountForUser(address) } return User; } function renderUserInfo(User) { console.log(User); document.querySelector('#user_submissions').innerHTML = User.numberOfSubmissions; document.querySelector('#user_deletions').innerHTML = User.numberOfDeletions; document.querySelector('#user_proposals').innerHTML = User.numberOfProposals; document.querySelector('#user_votes').innerHTML = User.numberOfVotes; document.querySelector('dd.user_blacklisted').style.display = User.isBlacklisted ? 'inline-block' : 'none'; document.querySelector('dt.user_blacklisted').style.display = User.isBlacklisted ? 'inline-block' : 'none'; document.querySelector('dt.user_whitelisted').style.display = User.isWhitelisted ? 'inline-block' : 'none'; document.querySelector('dd.user_whitelisted').style.display = User.isWhitelisted ? 'inline-block' : 'none'; } async function getSubmissionsCountForUser(address) { if (address === undefined) { address = web3.eth.accounts[0]; } return new Promise(function (resolve, reject) { resolve(0); }); } async function getDeletionsCountForUser(address) { if (address === undefined) { address = web3.eth.accounts[0]; } return new Promise(function (resolve, reject) { resolve(0); }); } async function getProposalCountForUser(address) { if (address === undefined) { address = web3.eth.accounts[0]; } return new Promise(function (resolve, reject) { resolve(0); }); } async function getVotesCountForUser(address) { if (address === undefined) { address = web3.eth.accounts[0]; } return new Promise(function (resolve, reject) { resolve(0); }); } async function isWhitelisted(address) { if (address === undefined) { address = web3.eth.accounts[0]; } return new Promise(function (resolve, reject) { resolve(false); }); } async function isBlacklisted(address) { if (address === undefined) { address = web3.eth.accounts[0]; } return new Promise(function (resolve, reject) { resolve(false); }); } 

И давайте изменим раздел информации профиля:

 <div class="logged in" style="display: none"> <p>You are logged in!</p> <div class="avatar"> </div> <dl> <dt>Submissions</dt> <dd id="user_submissions"></dd> <dt>Proposals</dt> <dd id="user_proposals"></dd> <dt>Votes</dt> <dd id="user_votes"></dd> <dt>Deletions</dt> <dd id="user_deletions"></dd> <dt class="user_whitelisted">Whitelisted</dt> <dd class="user_whitelisted">Yes</dd> <dt class="user_blacklisted">Blacklisted</dt> <dd class="user_blacklisted">Yes</dd> </dl> </div> 

Вы заметите, что мы использовали обещания при извлечении данных, хотя наши функции в настоящее время являются просто фиктивными функциями: они немедленно возвращают плоские данные. Это связано с тем, что каждой из этих функций потребуется разное количество времени для извлечения данных, которые мы попросили их извлечь, поэтому мы дождемся их завершения, прежде чем заполнять объект User и затем передавать его в функцию рендеринга, которая обновляет информацию о экран.

Если вы не знакомы с обещаниями JS и хотели бы узнать больше, см. Этот пост .

На данный момент все наши функции являются ложными; нам нужно сделать несколько записей, прежде чем есть что почитать. Но сначала мы должны быть готовы заметить, что эти записи происходят!

Прослушивание событий

Чтобы иметь возможность следить за событиями, генерируемыми контрактом, нам нужно их прослушивать — иначе мы emit в код все эти операторы emit. Средняя часть макета пользовательского интерфейса, который мы создали, предназначена для хранения этих событий.

Вот как мы можем прослушивать события, генерируемые блокчейном:

 // Events var WhitelistedEvent = story.Whitelisted(function(error, result) { if (!error) { console.log(result); } }); 

Здесь мы вызываем функцию « Whitelisted список» в экземпляре истории нашего контракта StoryDao и передаем в него обратный вызов. Этот обратный вызов автоматически вызывается всякий раз, когда происходит данное событие. Поэтому, когда пользователь попадает в белый список, код автоматически регистрирует на консоли результаты этого события.

Однако при этом будет получено только последнее событие последнего блока, добытого сетью. Таким образом, если есть несколько событий из белого списка, сгенерированных из блока 1–10, он покажет нам только те из блока 10, если таковые имеются. Лучше всего использовать этот подход:

 story.Whitelisted({}, { fromBlock: 0, toBlock: 'latest' }).get((error, eventResult) => { if (error) { console.log('Error in myEvent event handler: ' + error); } else { // eventResult contains list of events! console.log('Event: ' + JSON.stringify(eventResult[0].args)); } }); 

Примечание: поместите вышеперечисленное в отдельный раздел внизу вашего файла JS, посвященный событиям.

Здесь мы используем функцию get которая позволяет нам определить диапазон блоков, из которых нужно извлекать события. Мы используем 0 до последнего, что означает, что мы можем получать все события этого типа, когда-либо. Но у этого есть дополнительная проблема столкновения с методом наблюдения выше. Метод наблюдения выводит событие последнего блока, а метод get выводит все из них. Нам нужен способ заставить JS игнорировать двойные события. Не пишите те, которые вы уже взяли из истории. Мы сделаем это дальше, но сейчас давайте разберемся с белым списком.

Белый список аккаунтов

Наконец, давайте перейдем к некоторым операциям записи.

Первый и самый простой — попасть в белый список. Помните, что для того, чтобы попасть в белый список, учетной записи необходимо отправить не менее 0,01 эфира на адрес DAO. Вы получите этот адрес при развертывании. Если ваша цепочка Ganache / PoA перезапущена между частями этого курса, это нормально, просто перезапустите миграцию с помощью truffle migrate --reset и вы получите новые адреса для токена и DAO. В моем случае адрес DAO — 0x729400828808bc907f68d9ffdeb317c23d2034d5 а мой токен — 0x3134bcded93e810e1025ee814e87eff252cff422 .

Теперь, когда все настроено, давайте попробуем отправить количество эфира на адрес DAO. Давайте попробуем это с 0,05 эфира просто для удовольствия, чтобы мы могли увидеть, дает ли DAO дополнительные вычисленные токены для переплаты.

Примечание: не забудьте настроить количество газа — просто добавьте еще один ноль к пределу 21000 — используя значок, помеченный красным. Почему это необходимо? Потому что функция, которая запускается простой эфирной передачей (резервная функция), выполняет дополнительную логику, выходящую за пределы 21000, что достаточно для простых отправок. Таким образом, мы должны увеличить предел. Не волнуйтесь: все, что превышает этот лимит, немедленно возвращается. Чтобы узнать, как работает газ, смотрите здесь .

Отправка эфира в DAO

Настройка количества газа

После подтверждения транзакции (вы увидите это в MetaMask как «подтвержденный»), мы можем проверить сумму токена в нашей учетной записи MetaMask. Сначала нам нужно добавить наш собственный токен в MetaMask, чтобы он мог их отслеживать. Как показано на приведенной ниже анимации, процесс выглядит следующим образом: выберите меню «MetaMask», прокрутите вниз до « Добавить токены» , выберите « Пользовательский токен», вставьте адрес токена, предоставленного вам Truffle при миграции, нажмите « Далее» , посмотрите, есть ли баланс хорошо, а затем выберите Добавить токены .

Анимация добавления токена

Для 0,05 эт у нас должно быть 400 тыс. Токенов, и мы делаем.

Настройка количества газа

А как же событие? Были ли мы уведомлены об этом белом списке? Давайте посмотрим в консоли.

Событие в консоли

Действительно, имеется полный набор данных — адрес, который отправил событие, номер блока и хэш, в котором это было добыто, и так далее. Среди всего этого есть объект args , который сообщает нам данные события: адрес — это белый список адресов, а статус — добавлен ли он в белый список или удален из него. Успех!

Если мы обновим страницу сейчас, событие снова в консоли. Но как? Мы не добавили никого нового в белый список. Почему произошло событие? Особенность событий в EVM заключается в том, что они не являются одноразовыми, как в JavaScript. Конечно, они содержат произвольные данные и служат только для вывода, но их вывод навсегда зарегистрирован в блокчейне, потому что вызвавшая их транзакция также навсегда зарегистрирована в блокчейне. Таким образом, события останутся после их отправки, что избавит нас от необходимости хранить их где-то и вызывать их при обновлении страницы!

Теперь давайте добавим это на экран событий в пользовательском интерфейсе! Отредактируйте раздел «События» файла JavaScript следующим образом:

 // Events var highestBlock = 0; var WhitelistedEvent = story.Whitelisted({}, { fromBlock: 0, toBlock: "latest" }); WhitelistedEvent.get((error, eventResult) => { if (error) { console.log('Error in Whitelisted event handler: ' + error); } else { console.log(eventResult); let len = eventResult.length; for (let i = 0; i < len; i++) { console.log(eventResult[i]); highestBlock = highestBlock < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock; printEvent("Whitelisted", eventResult[i]); } } }); WhitelistedEvent.watch(function(error, result) { if (!error && result.blockNumber > highestBlock) { printEvent("Whitelisted", result); } }); function printEvent(type, object) { switch (type) { case "Whitelisted": let el; if (object.args.status === true) { el = "<li>Whitelisted address "+ object.args.addr +"</li>"; } else { el = "<li>Removed address "+ object.args.addr +" from whitelist!</li>"; } document.querySelector("ul.eventlist").innerHTML += el; break; default: break; } } 

Вау, события быстро усложнились, а? Не волнуйтесь, уточним.

Переменная highestBlock будет помнить последний блок, извлеченный из истории. Мы создаем экземпляр нашего события и подключаем к нему двух слушателей. Одним из них является get , который получает все события из истории и запоминает последний блок. Другой — watch , которые отслеживают события «вживую» и запускают, когда в самом последнем блоке появляется новый. Наблюдатель срабатывает только в том случае, если блок, который только что пришел, больше, чем блок, который мы запомнили как самый высокий, и гарантирует, что только новые события будут добавлены в список событий.

Мы также добавили функцию printEvent чтобы упростить задачу; мы можем использовать его и для других типов событий!

Если мы проверим это сейчас, мы действительно распечатаем это.

Событие на экране

Попробуйте сделать это сами сейчас для всех других событий, которые может выдать наша история! Посмотрите, сможете ли вы понять, как обрабатывать их все сразу, вместо того, чтобы писать эту логику для каждого из них. (Подсказка: определите их имена в массиве, затем прокрутите эти имена и динамически зарегистрируйте события!)

Ручная проверка

Вы также можете вручную проверить белый список и все другие открытые параметры StoryDAO, открыв его в MyEtherWallet и вызвав его функцию whitelist .

Проверка белого списка

Вы заметите, что если мы проверим аккаунт, с которого мы только что отправили сумму из белого списка, мы получим true подтверждение, указывающее, что этот аккаунт действительно существует в отображении whitelist .

Используйте это же меню функций, чтобы поэкспериментировать с другими функциями, прежде чем добавлять их в веб-интерфейс.

Отправка записи

Наконец, давайте сделаем правильный вызов функции записи из нашего пользовательского интерфейса. На этот раз мы отправим запись в нашу историю. Сначала нам нужно очистить образцы записей, которые мы поместили туда в начале. Измените HTML на это:

 <div class="content container"> <div class="intro"> <h3>Chapter 0</h3> <p class="intro">It's a rainy night in central London.</p> </div> <hr> <div class="submission_input"> <textarea name="submission-body" id="submission-body-input" rows="5"></textarea> <button id="submission-body-btn">Submit</button> </div> ... 

И некоторые основные CSS:

 .submission_input textarea { width: 100%; } 

Мы добавили очень простую текстовую область, с помощью которой пользователи могут отправлять новые записи.

Давайте сделаем часть JS сейчас.

Во-первых, давайте подготовимся принять это событие, добавив новое и изменив нашу функцию printEvent . Мы также можем немного реорганизовать весь раздел событий, чтобы сделать его более пригодным для повторного использования.

 // Events var highestBlock = 0; var WhitelistedEvent = story.Whitelisted({}, { fromBlock: 0, toBlock: "latest" }); var SubmissionCreatedEvent = story.SubmissionCreated({}, { fromBlock: 0, toBlock: "latest" }); var events = [WhitelistedEvent, SubmissionCreatedEvent]; for (let i = 0; i < events.length; i++) { events[i].get(historyCallback); events[i].watch(watchCallback); } function watchCallback(error, result) { if (!error && result.blockNumber > highestBlock) { printEvent(result.event, result); } } function historyCallback(error, eventResult) { if (error) { console.log('Error in event handler: ' + error); } else { console.log(eventResult); let len = eventResult.length; for (let i = 0; i < len; i++) { console.log(eventResult[i]); highestBlock = highestBlock < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock; printEvent(eventResult[i].event, eventResult[i]); } } } function printEvent(type, object) { let el; switch (type) { case "Whitelisted": if (object.args.status === true) { el = "<li>Whitelisted address "+ object.args.addr +"</li>"; } else { el = "<li>Removed address "+ object.args.addr +" from whitelist!</li>"; } document.querySelector("ul.eventlist").innerHTML += el; break; case "SubmissionCreated": el = "<li>User " + object.args.submitter + " created a"+ ((object.args.image) ? "n image" : " text") +" entry: #" + object.args.index + " of content " + object.args.content+"</li>"; document.querySelector("ul.eventlist").innerHTML += el; break; default: break; } } 

Теперь все, что нам нужно сделать, чтобы добавить новое событие, — это создать его экземпляр, а затем определить case для него.

Далее, давайте сделаем возможным сделать представление.

 document.getElementById("submission-body-btn").addEventListener("click", function(e) { if (!loggedIn) { return false; } var text = document.getElementById("submission-body-input").value; text = web3.toHex(text); story.createSubmission(text, false, {value: 0, gas: 400000}, function(error, result) { refreshSubmissions(); }); }); function refreshSubmissions() { story.getAllSubmissionHashes(function(error, result){ console.log(result); }); } 

Здесь мы добавляем прослушиватель событий в нашу форму отправки, которая после отправки сначала отклоняет все, если пользователь не вошел в систему, затем захватывает содержимое и преобразует его в шестнадцатеричный формат (что нам нужно для хранения значений в bytes ).

Наконец, он создает транзакцию, вызывая функцию createSubmission и предоставляя два параметра: текст записи и флаг false (то есть, не изображение). Третий аргумент — настройки транзакции: значение означает, сколько эфира отправлять, а газ — сколько газового лимита вы хотите установить по умолчанию. Это можно изменить в клиенте (MetaMask) вручную, но это хорошая отправная точка, чтобы убедиться, что мы не ограничиваемся. Последний аргумент — это функция обратного вызова, к которой мы уже привыкли, и этот обратный вызов вызовет функцию обновления, которая загружает все представления истории. В настоящее время эта функция обновления загружает только исторические хэши и помещает их в консоль, чтобы мы могли проверить, все ли работает.

Примечание: количество эфира равно 0, потому что первая запись бесплатна. Дальнейшие записи будут нуждаться в добавленном эфире к ним. Мы оставим вам этот динамический расчет для домашней работы. Подсказка: для этой цели в нашем DAO есть функция CalculateSee.

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

 if (loggedIn) { token.balanceOf(web3.eth.accounts[0], function(error, result) {console.log(JSON.stringify(result))}); story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))}); web3.eth.defaultAccount = web3.eth.accounts[0]; // CHANGE readUserStats().then(User => renderUserInfo(User)); refreshSubmissions(); // CHANGE } else { document.getElementById("submission-body-btn").disabled = "disabled"; } 

Изменения помечены // CHANGE : первое позволяет нам установить учетную запись по умолчанию для выполнения транзакций. Вероятно, это будет сделано по умолчанию в будущей версии Web3. Второй обновляет материалы при загрузке страницы, поэтому мы получаем полностью загруженную историю при открытии сайта.

Если вы попытаетесь отправить запись сейчас, MetaMask должен открыться, как только вы нажмете « Отправить» и попросите подтвердить отправку.

Отправка записи

Подтверждение записи

Вы также должны увидеть распечатанное событие в разделе событий.

Событие подтверждено

Консоль должна повторить хэш этой новой записи.

Хэш напечатан

Примечание: MetaMask в настоящее время имеет проблемы с частной сетью и одноразовыми номерами. Он описан здесь и будет исправлен в ближайшее время, но в случае, если при отправке записей в консоли JavaScript возникнет nonce ошибка, временным решением будет переустановить MetaMask (отключение и включение не будут работать). ПОМНИТЕ, ЧТОБЫ ЗАПИСАТЬ ВАШУ ФРАЗУ СЕМЕНЫ: она понадобится для повторного импорта учетных записей MetaMask!

Наконец, давайте выберем эти записи и отобразим их. Давайте начнем с немного CSS:

 .content-submissions .submission-submitter { font-size: small; } 

Теперь давайте обновим функцию refreshSubmissions :

 function refreshSubmissions() { story.getAllSubmissionHashes(function (error, result) { var entries = []; for (var i = 0; i < result.length; i++) { story.getSubmission(result[i], (err, res) => { if (res[2] === web3.eth.accounts[0]) { res[2] = 'you'; } let el = ""; el += '<div class="submission">'; el += '<div class="submission-body">' + web3.toAscii(res[0]) + '</div>'; el += '<div class="submission-submitter">by: ' + res[2] + '</div>'; el += '</div>'; el += '</div>'; document.querySelector('.content-submissions').innerHTML += el; }); } }); } 

Мы просматриваем все заявки, получаем их хэши, получаем каждую и выводим ее на экран. Если отправитель совпадает с вошедшим в систему пользователем, вместо адреса печатается «вы».

Предоставленная подача

Давайте добавим еще одну запись для тестирования.

Еще одна запись

Вывод

В этой части мы разработали начало базового интерфейса для нашего DApp.

Поскольку разработка полноценного фронт-энда также может быть самостоятельным ходом, мы оставим вам дальнейшую разработку в качестве домашней работы. Просто вызовите функции, как показано, свяжите их с обычным потоком JavaScript (либо с помощью фреймворка, такого как VueJS, либо старого jQuery, либо необработанного JS, как мы делали выше), и свяжите все это вместе. Это буквально похоже на общение со стандартным серверным API. Если вы застряли, проверьте репозиторий проекта на код!

Другие обновления вы можете сделать:

  • определить, когда меняется провайдер web3 или когда изменяется количество доступных учетных записей, указывая события входа в систему или выхода из системы, и автоматически перезагрузить страницу
  • запретить отображение формы отправки, если пользователь не вошел в систему
  • запретить рендеринг кнопок голосования и удаления, если у пользователя нет хотя бы одного токена и т. д.
  • пусть люди отправляют и отображают Markdown!
  • упорядочивать события по времени (номеру блока), а не по типу!
  • сделать события красивее и читабельнее: вместо показа шестнадцатеричного содержимого, переведите его в ASCII и обрежьте до 30 или около того символов
  • используйте подходящую среду JS, такую ​​как VueJS, чтобы получить возможность многократного использования вашего проекта и иметь более структурированный код.

В следующей и последней части мы сосредоточимся на развертывании нашего проекта в Интернете. Будьте на связи!