Клиент к Waves: приложение поверх сети
Зачем это нужно
dApp написан, установлен и работает. Но пользователь не будет вызывать его командой в терминале — ему нужен интерфейс, а интерфейсу нужен клиент.
Хорошая новость: клиент к Waves — это четыре функции. Никакого SDK со своим жизненным циклом, никакой синхронизации: узел говорит по обычному HTTP, транзакции подписывает библиотека, а всё остальное вы уже умеете по веб-блоку.
Эта глава — про эти четыре функции, про то, где может лежать сид, а где нет, и про подведение итогов: одна и та же зачётка теперь написана на трёх платформах. Весь код ниже запущен на локальном узле Waves 1.6.4.
Четыре функции
Читать — бесплатно, мгновенно, без подписи:
async function api(path) {
const r = await fetch(NODE + path);
return { status: r.status, body: await r.json().catch(() => null) };
}
async function pokazat(dapp, id) {
const r = await api(`/addresses/data/${dapp}/z_${id}`);
if (r.status === 404) return null;
return r.body;
}
z-1: {"key":"z_z-1","type":"string","value":"Kovalik Oleg"}
z-99: null
404 здесь — не ошибка, а «такого ключа нет». Поэтому клиент превращает его в null, а не бросает исключение. Смешивать «нет данных» и «сломалось» — верный способ получить приложение, которое падает на пустом списке.
Отправить — с честной обработкой отказа:
async function otpravit(tx) {
const r = await fetch(NODE + '/transactions/broadcast', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(tx),
});
const body = await r.json().catch(() => null);
if (r.status !== 200) {
throw new Error(`узел отказал (${body.error}): ${body.message}`);
}
return body;
}
поймали: узел отказал (306): Error while executing dApp: Zachetka z-7 uzhe zavedena
Дождаться — цикл с паузой:
async function dozhdatsya(id, popytok = 40) {
for (let i = 0; i < popytok; i++) {
const r = await api('/transactions/info/' + id);
if (r.status === 200) return r.body;
await new Promise((res) => setTimeout(res, 700));
}
throw new Error('не дождались ' + id);
}
в блоке: 413 | статус: succeeded
изменено: [{"key":"z_z-7","type":"string","value":"Petrov Ivan"},
{"key":"z_z-7_ball","type":"integer","value":0}]
Ограничение числа попыток обязательно. Без него цикл будет крутиться вечно, если транзакция не пройдёт, — и пользователь увидит вечный индикатор загрузки вместо сообщения об ошибке.
Список вместо перебора
Достать все зачётки одним запросом:
const q = encodeURIComponent('z_[^_]+$');
const vse = await api(`/addresses/data/${DAPP}?matches=${q}`);
найдено: 2 → [{"key":"z_z-1","type":"string","value":"Kovalik Oleg"},
{"key":"z_z-7","type":"string","value":"Petrov Ivan"}]
Образец z_[^_]+$ берёт ключи вида z_<что-то без подчёркиваний> и отсекает z_z-1_ball. Так составные ключи из главы про состояние и начинают работать как коллекция.
Где может лежать сид
Это самое важное место главы, и ошибаются здесь чаще всего.
На сервере сид приложения может лежать в переменной окружения или в хранилище секретов. Он нужен, чтобы подписывать транзакции своего аккаунта — например, начислять баллы.
Во фронтенде сида нет вообще. Транзакцию пользователя подписывает его кошелёк — расширение браузера или мобильное приложение. Страница собирает объект транзакции и отдаёт его на подпись; закрытый ключ её не покидает.
// на странице: объект собран, подпись — у кошелька
const tx = { type: 16, dApp, call: { function: 'zavesti', args: [...] } };
const podpisannaya = await signer.invoke(tx); // подписывает кошелёк
Сид в коде страницы виден всем: исходники фронтенда открыты по определению. Сид в репозитории остаётся в истории навсегда, даже после удаления файла. Сид в localStorage забирают одним посторонним скриптом. Это не перестраховка — это три способа, которыми деньги теряют чаще всего.
Что ещё пригодится клиенту
| Запрос | Зачем |
|---|---|
GET /addresses/balance/<адрес> | остаток в WAVES |
GET /assets/balance/<адрес> | все токены адреса разом |
GET /transactions/address/<адрес>/limit/10 | последние транзакции — история для интерфейса |
GET /blocks/height | жив ли узел вообще |
POST /transactions/calculateFee | узнать комиссию заранее, не угадывая |
GET /addresses/scriptInfo/<адрес> | есть ли скрипт и нужна ли доплата |
calculateFee стоит вызывать перед каждой транзакцией в настоящем приложении. Комиссия зависит и от типа, и от размера, и от скрипта на счёте — угадывать её в коде значит однажды получить отказ у пользователя.
Итог: одна задача, три платформы
Зачётка теперь написана трижды, и сравнение получается не на словах.
| Solidity | Чейнкод Fabric | dApp на RIDE | |
|---|---|---|---|
| Язык | Solidity | TypeScript | RIDE |
| Где данные | поля контракта | общее хранилище | состояние аккаунта |
| Проверка | require | throw | throw |
| Кто вызвал | msg.sender | ctx.clientIdentity | i.caller |
| Цена вызова | ~96 000 газа | нет платы | 0,005 WAVES |
| Вход в сеть | свой ключ | выданный сертификат | сид-фраза |
| Обновление кода | новый адрес или прокси | номер правки +1 | SetScript заново |
| «Всё или ничего» | revert | набор изменений | список не вернулся |
Три разных ответа на один вопрос — «как несколько сторон ведут общую запись, не доверяя друг другу». Выбор между ними — это выбор из главы про Fabric и Ethereum: можно ли назвать участников заранее, нужна ли плата, кто и как получает доступ.
Типичные ошибки
Ошибка 1. Сид во фронтенде.
Разобрана выше. Самая дорогая ошибка всего трека.
Ошибка 2. Считать 200 подтверждением.
Ответ broadcast говорит «принято в очередь». Без ожидания интерфейс покажет успех раньше, чем запись состоится.
Ошибка 3. Цикл ожидания без потолка.
Вечный индикатор загрузки вместо честной ошибки.
Ошибка 4. Терять message при отказе.
if (r.status !== 200) throw new Error('ошибка сети'); // потеряли причину
В body.message лежит текст из вашего же throw в RIDE. Выбросить его — значит отлаживать вслепую.
Ошибка 5. Читать состояние платным вызовом.
Вызов dApp ради чтения стоит 0,005 WAVES и попадает в блок. То же самое читается бесплатно запросом /addresses/data/.
Шпаргалка по главе
| Что нужно | Как |
|---|---|
| Прочитать ключ | GET /addresses/data/<dapp>/<ключ> — 404 значит «нет» |
| Прочитать по образцу | GET /addresses/data/<dapp>?matches=<регулярка в URL> |
| Собрать вызов | invokeScript({ dApp, call, payment, chainId }, seed) |
| Отправить | POST /transactions/broadcast |
| Дождаться | цикл по GET /transactions/info/<id> с потолком попыток |
| Узнать комиссию | POST /transactions/calculateFee |
| История адреса | GET /transactions/address/<адрес>/limit/10 |
| Подпись во фронтенде | кошелёк пользователя, сида в коде нет |
Словарь главы
| Термин | Что это |
|---|---|
| Клиент | Обёртка над REST узла: читать, собрать, отправить, дождаться |
broadcast | Отправка транзакции; 200 означает «принято в очередь» |
| Ожидание | Цикл по /transactions/info/<id> с ограничением попыток |
calculateFee | Запрос комиссии до отправки |
| Кошелёк | Расширение или приложение, подписывающее транзакции пользователя |
matches | Выборка ключей состояния по образцу |
error: 306 | Упал скрипт dApp; message — ваш текст |
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
const r = await fetch(NODE + path);
Что должен уметь после главы
- написать клиент из четырёх функций: читать, собрать, отправить, дождаться;
- отличить «нет данных» от «сломалось» и не смешивать их в коде;
- превратить отказ узла в исключение, не потеряв причину;
- достать коллекцию записей выборкой по образцу;
- сказать, где сид лежать может, а где — никогда;
- сравнить три платформы трека и обосновать выбор под задачу.
Проверка по главе
Сводная проверка по всей главе: 10 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Сводная проверка по 10 главам блока: 30 вопросов, 10:00 на всё, по одному вопросу, без возврата. Пересдавать можно сколько угодно раз.
Готовность: экзамены глав сданы 0 из 10. Блок можно сдавать и так, но шанс выше после всех глав.
Куда дальше: проверенные ресурсы
- Waves node REST API — все запросы с примерами.
- @waves/waves-transactions — сборка и подпись транзакций.
- Waves Signer — подпись во фронтенде без сида на странице.
- Waves Keeper — кошелёк-расширение, которым подписывают пользователи.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.