ontofractal: Разработка персональных ботов для Голоса. Урок 2. [2990613]



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

Предыдущие посты

Предисловие

  • Требуется базовый уровень понимания JavaScript, веб технологий и командной строки.
  • Задавайте вопросы в комментариях, если я что-то непонятно объясняю.
  • У меня минимальный опыт работы с русскоязычной терминологией в программировании, поэтому названия я буду оставлять на английском языке.
  • В этом уроке используется неоптимальный код, паттерны и структура, в приоритете находится простота и читаемость кода.

На чем мы остановились в прошлый раз?

У нас получилось построить поток новых блоков Голоса. В этом уроке у нас следующие задачи:

  • изучить структуру блоков и транзакций
  • отличать типы транзакций
  • обрабатывать данные о голосах пользователей

Для удобства я создал репозиторий на Github, в котором каждый коммит будет соответствовать уроку. Код предыдущего урока по ссылке.

Для предварительного ознакомления

Изучаем структуру блоков и транзакций

Для фана и профита посмотрим на блоки и транзакции в режиме реального времени на момент написания этого поста. Для этого нам нужно кое-что поменять. В прошлом скринкасте вместо объектов и списков содержащих данные на глубoких уровнях вложенности, мы видели только что-то вроде operations: [Object] вместо данных об операциях включенных в транзакцию. Это дефолтное поведение функции console.log. Воспользуемся включенным в nodejs модулем utils.

const util = require('util') // добавим в начало файла

const getBlockData = height => {
    golos.api.getBlock(height, (err, result) => {
        if (err) {
            console.log(err)
        }
        else {
            console.log('')
            console.log('============ НОВЫЙ БЛОК ============')
            // console.log(result) заменим на
            console.log(util.inspect(result, {showHidden: false, depth: null}))
        }
    })
}

asciicast

Структура блоков

Изучим структуру последнего блока в скинкасте. Транзакции в нем отсутствуют.

{ previous: '002d5b03fac21f67ea13fa5222f0e48a531397fe', // хеш поинтер на предыдущий блок
  timestamp: '2017-01-29T19:58:54', // время блока*
  witness: 'lehard', // делегат сгенерировавший блок
  transaction_merkle_root: '0000000000000000000000000000000000000000', // корень дерева Меркле
  extensions: [],
  // подпись делегата созданная с помощью приватного ключа @lehard, публичный ключ которого виден на блокчейне
  witness_signature: '1f0ec01cba24b62e6c2f7184b00bd7eab8b258b826d655db5fdc3764ba2006b2ed0286ef739a5d164bb4a873ef76ef9e11ddea76bc8c056a01dab89af6b0be1007',
  // пустой array транзакций
  transactions: [] }

\*о времени блока. Времени в блокчейне не существует, timestamp проверяется на соответствие правилам нодами.
Теперь изучим структуру предпоследнего блока в скринкасте, включающего в себя только 1 транзакцию.

{ previous: '002d5b02471c498fc3d199d414408845309bff1f',
  timestamp: '2017-01-29T19:58:51',
  witness: 'serejandmyself',
  transaction_merkle_root: 'bfad56084bf5792d94d938bc1215888b35c72b5c',
  extensions: [],
  // подпись делегата созданная с помощью приватного ключа @serejandmyself, публичный ключ которого виден на блокчейне
  witness_signature: '201831d219f8951b6e752b715c6856e528dab67ae50f21f49b7a5f6b483fade2497ee4ee91ede89f4e24582e4da74b1601b3105740ced935a22b25e604016bcf56',
  transactions:
   [ { ref_block_num: 22892,
       ref_block_prefix: 1807731190,
       expiration: '2017-01-29T19:59:00',
       operations:
        [ [ 'comment',
            { parent_author: 'makgorn',
              parent_permlink: 'sdelal-sam',
              author: 'strecoza',
              permlink: 're-makgorn-sdelal-sam-20170129t195847237z',
              title: '',
              body: 'классная такая корявая палка  сосны, я бы тоже такой стол хотела))',
              json_metadata: '{"tags":["handmade"]}' } ] ],
       extensions: [],
       // подпись транзакции аккаунтом @strecoza, создавшим транзакцию
       signatures: [ '1f670e47bcd6d92886ab7f3bde7f32f4e8401d55b91155dcbe8deaa313451640d400ec714f0a9662a273ce5f5e39643fa432ca76f7ee5bb0a767440f966ef813e4' ] } ] }

В этом уроке нас особенно интересует структура данных на пути exampleBlock.transactions[0].operations, где exampleBlock -- блок над этой строчкой.

        [ [
        // тип операции
        'comment',
        // данные операции
        { parent_author: 'makgorn',
              parent_permlink: 'sdelal-sam',
              author: 'strecoza',
              permlink: 're-makgorn-sdelal-sam-20170129t195847237z',
              title: '',
              body: 'классная такая корявая палка  сосны, я бы тоже такой стол хотела))',
              json_metadata: '{"tags":["handmade"]}' } ] ]

Каждая операция имеет форму [opType, opData], где opType -- строка, а opData -- объект.

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

Теперь напишем код для обработки операций.

// создадим функцию, которая достанет все операции из всех транзакций блока и поместит их в array
const unnestOps = (blockData) => {
    // метод map создает новый array применяя функцию переданную в первый аргумент к каждому элементу
    // используем метод flatten модуля lodash для уплощения(?!) вложенных списков
    return _.flatten(blockData.transactions.map(tx => tx.operations))
}

// поменяем имя функции с getBlockData на processBlockData т.к. ее назначение изменилось
const processBlockData = height => {
    golos.api.getBlock(height, (err, result) => {
        if (err) {
            console.log(err)
        }
        else {
            console.log('')
            console.log('============ НОВЫЙ БЛОК ============')
            // console.log(result) заменим на
            unnestOps(result)
            // в отличие от map, метод forEach не возвращает список с результатом применения функции
            // также как и map, метод forEach применяет переданную в него функцию к каждому элементу array
            // forEach используется для указания того, что результат применения функции является side effect
                .forEach(op => console.log(util.inspect(op, {showHidden: false, depth: null})))
        }
    })
}

У нас получилось выводить на экран данные каждой операции в блоке. Теперт создадим функцию, которая будет определять тип транзакции и в соответствии с типом операции выберет подходящее действие. В нашем случае тип операции будет vote и пока мы продолжим выводить данные.
Форма данных операции vote выглядит так: {"voter": "exampleVoter", "author": "exampleAuthor", "permlink":"examplePermlink", "weight":"10000" }

const selectOpHandler = (op) => {
    // используем destructuring, очень удобную фичу EcmaScript2016
    // это, конечно, не паттерн метчинг Elixir или Elm, но все равно сильно помогает улучшить читаемость кода
    const [opType, opData] = op
    if (opType === 'vote') {
        // используем ES2016 template strings, которые позволяют форматировать строки интерполируя expressions с помощью ${}
        console.log(`@${opData.voter} проголосовал за пост ${opData.permlink} написанный @${opData.author} c весом ${opData.weight}`)
    }
}

Весь код

 const golos = require('golos') // импортируем модуль голоса
 const util = require('util')
 const Promise = require("bluebird") // импортируем модуль Bluebird -- самую популярную имплементацию Promise
 const _ = require('lodash')
 const accountName = '' // аккаунт пользователя, который запускает бота
 const postingKey = '' // приватный постинг ключ пользователя, который запускает бота
 const accountNameToFollow = 'academy' // аккаунт пользователя за которым следим
 // создаем новый Promise обворачивая golos.api.getDynamicGlobalProperties
 const dynamicGlobalProperties = new Promise((resolve, reject) => {
     golos.api.getDynamicGlobalProperties((err, result) => {
         if (err) {
             reject(err)
         }
         else {
             resolve(result)
         }
     })
 })

 const pluckBlockHeight = x => x.head_block_number

 // создадим функцию, которая достанет все операции из всех транзакций блока и поместит их в array
 const unnestOps = (blockData) => {
     // метод map создает новый array применяя функцию переданную в первый аргумент к каждому элементу
     // используем метод flatten модуля lodash для уплощения(?!) вложенных списков
     return _.flatten(blockData.transactions.map(tx => tx.operations))
 }

 const selectOpHandler = (op) => {
     // используем destructuring, очень удобную фичу EcmaScript2016
     // это, конечно, не паттерн метчинг Elixir или Elm, но все равно сильно помогает улучшить читаемость кода
     const [opType, opData] = op
     if (opType === 'vote') {
         // используем ES2016 template strings, которые позволяют форматировать строки интерполируя expressions с помощью ${}
         console.log(`@${opData.voter} проголосовал за пост ${opData.permlink} написанный @${opData.author} c весом ${opData.weight}`)
     }
 }

 // поменяем имя функции с getBlockData на processBlockData т.к. ее назначение изменилось
 const processBlockData = height => {
     golos.api.getBlock(height, (err, result) => {
         if (err) {
             console.log(err)
         }
         else {
             console.log('')
             console.log('============ НОВЫЙ БЛОК ============')
             // console.log(result) заменим на
             unnestOps(result)
             // в отличие от map, метод forEach не возвращает список с результатом применения функции
             // также как и map, метод forEach применяет переданную в него функцию к каждому элементу array
             // forEach используется для указания того, что результат применения функции является side effect
                 .forEach(selectOpHandler) // передаем функцию, которая определит, что делать
         }
     })
 }

 const startFetchingBlocks = startingHeight => {
     let height = startingHeight
     setInterval(() => {
         processBlockData(height)
         height = height + 1 // брррр, мутация
         // у нас есть доступ к переменной height благодаря closure
     }, 3000)
     // Задаем интервал в 3000 мс т.к. блок Голоса генерируется каждые три секунды
 }

 // резолвим Promise
 dynamicGlobalProperties
     .then(pluckBlockHeight)
     .then(startFetchingBlocks)
     .catch(e => console.log(e))

Как выглядит наш работающий бейби бот

asciicast

Заключение

Мы научились "распаковывать" блоки, доставать операции, определять операцию "vote" и выводить отформатированные голоса аккаунтов.
Финальный код также опубликован на Гитхабе в коммите этого урока.

В следующем уроке мы научимся:

  • как передавать операции на ноды Голоса
  • как голосовать за посты
  • как повторять голоса @academy или другого аккаунта
(∩`-´)⊃━炎炎炎炎炎

Оригинал поста создан 30-01-2017 11:08:30 UTC


2990549 Привет всем! Я Infovore. И Вы, вероятно, тоже…
2990585 Как начать карьеру в digital маркетинге [Инструкция для чайников]
2990613 Разработка персональных ботов для Голоса. Урок 2.
2990640 Родны куточак - детский стих, первый в Голосе на белорусском языке!
2990726 Cон
2990910 Что такое "мувик"?
2990913

#Тай! Хочу обратно!!!

2990920 Чем вы готовы расплатиться за Нужную вещь?
2991049 Мемуарник №64: Runeuro,
2991169 Камень-загадка Жумбактас (Республика Казахстан курорт Боровое) (Рассказ и фотографии @lyubovbar)
2991295 Композиция в фотографии и почему это важно
2991380 Ⓢ Магазин обуви
2991456 Вечер с ГОЛОСОМ - краткий обзор недели 23/01 - 26/01 и планы на будущие выпуски
2991457 Неделя в Голосе. Интересно! Но очень сложно и пока не рентабельно.
2991464 Дежавю или Судьба ? Наука , мышление или бренный воздух.

Прежде чем писать комментарий прочитайте О ПРОЕКТЕ
Поддержите проект донатом!


Comments 0