👨🏽🎓 Проблема бедной документации Golos API на примере set_block_applied_callback - правильная WebSocket подписка без интервалов-костылей
Пост оформлен в виде урока для начинающих, но будет так же полезен опытным пользователям голоса, поскольку даже в wiki set_block_applied_callback обозначен как таинственный и неиследованный метод :)
Некоторые разработчики писали в чатах - "что это за вебсокеты такие у голоса, которые не могут сами отправлять данные о блоке?", но как выяснилось опытным путем, вебсокеты голоса работают как положено и проблема только в отсутствии документации к главному API методу:
set_block_applied_callback
Я долгое время использовал в корне неправильный подход к способу "слушать блоки" применяя в скриптах интервалы. Как известно, блоки генерируются каждые 3 секунды и было традицией ставить такой же интервал в скриптах для получения актуального блока. Что бы нивелировать разные проблемы транспорта запросов я написал нагромождение таймеров в прошлой версии BlockSnobbery, и хоть это работает отлично, исключены пропуски блоков вашим приложением, такой мой механизм контроля актуального блока использовал слишком много лишних запросов к ноде. Да и сама концепция интервалов походит на какое-то гадание с постоянной проверкой на опоздание или опережение. После очередного недоумения в коментариях "неужели нет способа получать блоки без интервала?" я решил покопаться в исходниках и найти то, что мне нужно.
Как использовать set_block_applied_callback
Этот метод якобы присутствует в golos-js
, но там он не работает( issue), возможно просто по причине отсутствия документации к синтаксису. Но поскольку это запрос на чтение из блокчейна- golos-js можно не использовать вообще, будет достаточно пары строк на JS.
Никаких библиотек не нужно и вы можете проексперементировать в консоли своего браузера.
Откройте пустую вкладку и нажмите клавишу F12
, затем перейдите на вкладку console
Теперь мы можем вставлять сюда JS, который я предварительно опишу ниже
Так будут выглядеть скрипт на получение операций из блоков:
Он состоит из нескольких функций, а начинается все с подключения к ноде. Подержка websockets встроена в любой современный браузер и нам достаточно лишь указать переменную с паблик нодой
И добавить конструкцию внутри которой происходят события при открытии ноды:
socket.onopen
Внутри конструкции мы добавим функцию подписания на определененное событие из блокчейна,
а именно set_block_applied_callback
- уведомление о появлении каждого нового блока на голосе
Делаем это мы функцией socket.send
c id:1
После такой "подписки" на события, необходимо добавить функцию, которая и будет принимать сообщения о новых блоках
socket.onmessage
Теперь внутри socket.onmessage
мы получаем все сообщения от ноды голоса.
Что бы вывести их добавим внутри функцию
console.log(raw)
После запуска этого скрипта просто в консоли браузера мы станем получать строки, где будут заголовки блоков с голоса, начиная со второго сообщения. Первой строкой был ответ о том, что мы подписались на обновления. У него id 1. У заголовков блоков id нет, но есть method:notice - это понадобится позднее для фильтрации.
Давайте разберемся, какие данные нам отдает блокчейн при использовании set_block_applied_callback
. На скрине выше видно, что это сущий мизер, а именно
{"method":"notice","params":[0,[{"previous":"00a20…9de2c5bbe485fc0051df26bbf0cfa6669c6f3d01f5e2"}]]}
Мы получаем только закодированную строку previous.
Что бы получить из этого пользу, нам нужно получить номер блока. Именно он закодирован в этой строке. А если вернее - в первых 8 символах строки в формате heх.
Раскодировать это можно простыми функциями и разбором строк
Распарсим сырые данные в переменную data
var data = JSON.parse(raw.data)
Создадим условие if (если) которое поможет офильтровать другие сообщения и работать толко с теми, метод которых "notice" и в которых есть необходимые данные "data.params"
if (data.method === "notice" && data.params) {
Сохраним первые 8 символов previous в переменню hex
var hex = data.params[1][0].previous.slice(0, 8)
Наконец переведем hex в обычные числа и сохраним в переменную height
Теперь в height номер прошлого блока из блокчейн.
var height = parseInt(hex, 16)
Теперь, зная номер блока, мы можем получить из него данные. Для этого отправим сообщение ноде с запросом get_ops_in_block
(получить операции из блока) и присвоим этому сообщению id 2. Ответ на него будет тоже с id 2.
Там же, внутри функции socket.onmessage
мы добавим еще одно условие фильтр
"если id сообщения = 2" и выведем на экран функцией console.log(data.result)
} else if (data.id === 2) {
console.log(data.result)
}
Вставим весь код в консоль браузера
Нажмем ввод и начнем получать стрим операций из блоков
Именно это и есть основа многих ботов, которые работает с данными блокчейна в реальном времени!
Вы можете отфильтровать операции по типу и работать с ними как вам захочется
Например вместо функции console.log(data.result)
мы вызовем функцию opfilter(data.result)
Ниже мы создадим функцию opfilter и в ней будем фильровать операции из блоков по типу
Операции группированы по типу, например голоса и флаги - это операция vote c положительным числом для голос, отрицательным для флага и нулевым для отмены голоса.
На скрине выше 1 - это тип операции, 2 - это ее содержание.
Нужно создать условие, где мы будем определять, что за действие происходит и с кем.
На скрине ниже 3 условия для одной операции VOTE , в первом для апвота мы используем
type === "vote" && o.weight > 0
Для флага weight будет меньше 0
Ну и для снятия голоса или флага weight равен 0
Схожим образом обрабатывается и операция COMMENT которая общая для постов и комментов.
Отличить коммент от поста можно по автору-родителю. У комментария он есть. У постов - пуст.
Всего типов операций достаточно много и метод их фильтра тема отдельного поста.
Список операций:
fill_convert_request author_reward curation_reward comment_reward liquidity_reward interest fill_vesting_withdraw fill_order shutdown_witness fill_transfer_from_savings hardfork comment_payout_update vote comment transfer transfer_to_vesting withdraw_vesting limit_order_create limit_order_cancel feed_publish convert account_create account_update witness_update account_witness_vote account_witness_proxy pow custom report_over_production delete_comment custom_json comment_options set_withdraw_vesting_route limit_order_create2 challenge_authority prove_authority request_account_recovery recover_account change_recovery_account escrow_transfer escrow_dispute escrow_release pow2 escrow_approve transfer_to_savings transfer_from_savings cancel_transfer_from_savings custom_binary decline_voting_rights reset_account set_reset_account
На выше скрине есть функция ins (сокращение от insert) в которую мы передаем обработанные данные операции.
Функция выводит их в консоль
Теперь весь код выглядит так
var socket = new WebSocket('wss://api.golos.cf')
socket.onopen = function(event) {
socket.send(JSON.stringify({
id: 1,
method: 'call',
"params": ["database_api", "set_block_applied_callback", [0], ]
}));
socket.onmessage = function(raw) {
var data = JSON.parse(raw.data)
if (data.method === "notice" && data.params) {
var hex = data.params[1][0].previous.slice(0, 8)
var height = parseInt(hex, 16)
socket.send(JSON.stringify({
id: 2,
method: 'call',
params: ["database_api", "get_ops_in_block", [height, "false"]]
}));
} else if (data.id === 2) {
opfilter(data.result)
}
}
}
opfilter = function(d){
for (var i = 0; i < d.length; ++i) {
var b = d[i].block, tx = d[i].trx_id, t = d[i].timestamp, type = d[i].op[0],pre="", o = d[i].op[1];
if(type === "limit_order_create") ins(pre+" Сделка @"+o.owner+": обмен "+o.amount_to_sell+" на "+o.min_to_receive);
if(type === "comment"&&!o.parent_author) ins(pre+" Пост @"+o.author+": "+o.title);
if(type === "comment"&&o.parent_author) ins(pre+" Комментарий @"+o.author+" к посту @"+o.parent_author);
if(type === "vote" && o.weight > 0) ins(" Голос "+o.weight/100 +"% От @"+o.voter+" для @"+o.author);
if(type === "vote" && o.weight < 0) ins(pre+" Флаг -"+o.weight/100 +"% От @"+o.voter+" для @"+o.author);
if(type === "vote" && o.weight === 0) ins(pre+"⭕️ Отмена голоса от @"+o.voter+" за пост автора @"+o.author);
if(type === "pow") ins(pre+"⛏ Майнер доказал работу "+o.miner);
if(type === "transfer") ins(pre+" Трансфер от @"+o.from+" "+o.amount+" для "+o.to);
if(type === "transfer_to_vesting") ins(pre+" Повышение Силы Голоса от @"+o.from+" "+o.amount+" для "+o.to);
if(type === "account_create") ins(pre+" Новый аккаунт от "+o.creator+": @"+o.new_account_name);
if(type === "account_update") ins(pre+"♻️ Аккаунт отредактирован @"+o.account+" "+o.json_metadata);
if(type === "account_witness_vote") ins(pre+" Голос за делегата от "+o.account+". Делегат @"+o.witness);
if(type === "feed_publish") ins(pre+" Прайсфид от "+o.publisher+" "+o.exchange_rate.base+"/"+o.exchange_rate.quote);
if(type === "curation_reward") ins(pre+" Кураторские награды "+o.curator+" "+o.reward+" "+o.comment_author);
if(type === "author_reward") ins(pre+" Авторские награды "+o.author+" "+o.sbd_payout+" "+o.vesting_payout+" "+o.steem_payout+" " +o.permlink );
}
}
ins = function(x){
console.log(x)
}
Запустим его и увидим магию - события в блокчейне голоса в реальном времени
Это можно все офомить в виде вебстраницы, пример https://golos.cf/ops.html
Так же это может быть основой для различных ботов, голосования, поиска ключевых слов в контенте, или анализа торгов на внутренней бирже - чего угодно!
Эта и еще несколько нароботок будут внедрены в обновление ботов для голосования. Пока боты работают на старой версии.