Кто про что, а я про телеграм бота…

Сейчас я работаю в компании Каруна на позиции старшего Go-разработчика. В свободное от работы время стараюсь смотреть по сторонам (нет — не в поиске работы, и да — это корпоративный блог, но пишу про пет-проект ????) и интересоваться разными областями IT, абсолютно отличными от того, чем ежедневно занимаюсь на работе.

Примерно полтора года назад я в качестве хобби занимался разработкой универсального телеграм бота для MQTT устройств, о чем уже рассказывал вот тут: (Не)очередной MQTT-телеграм-бот для IoT, а позже мой фокус внимания отошёл от темы IoT и сместился в сторону криптовалют, очень уж эта тема не давала мне покоя. На фоне прошлогоднего шума вокруг Chia захотелось вложить немного свободных средств в другой заинтересовавший меня альткоин и сделать что-нибудь полезное для комьюнити. В этой статье я делюсь исключительно техническими деталями реализации бота и намеренно опускаю любую маркетинговую информацию о блокчейне, дабы не разводить холивар про альткоины. И вас очень попрошу воздержаться!

Итак, задача:
  1. Иметь минимальный функционал эксплорера блокчейна прямо в телеграме: просматривать транзакции и статистику сети.
  2. Удобно отслеживать баланс нескольких кошельков и получать уведомления о поступлениях/списаниях с кошелька.
  3. Получать актуальную цену + график.
  4. Иметь калькулятор доходности майнинга.
  5. Иметь кран для активации новых кошельков.

На этом вроде бы и всё, поехали…

Исходный код того, что получилось лежит здесь: github.com/xDWart/signum-explorer-bot
Сам бот работает по адресу @signum_explorer_bot

Используемый стек технологий:


Язык: Go
База данных: Postgres
Котировки: CoinMarketCap API
Хостинг: был Heroku, но в связи с недавними событиями они ушли из России. Теперь, здравствуй, RUVDS (не реклама).

Теперь немного расскажу детали по фичам.

Отслеживание баланса + оповещение о транзакциях


Блокчейн Signum имеет распределённую сеть нод с публично доступным API (пример: europe.signum.network/api-doc). Все ноды синхронизируются между собой в пределах одного блока (4 минуты). Так что изначально, когда я ещё запускал бота на платформе Heroku, в основу клиента API я заложил рандомный опрос 8 официальных нод блокчейна и их искусственную балансировку: бывает такое, что нода по какой-либо причине может не отвечать или запаздывать с синхронизацией. Я реализовал периодическое ранжирование адресов по времени ответа и текущему блоку ноды, таким образом, в опросе участвовала только лучшая половина:

// отсортируем API клиентов нод по пингу и текущему блоку
func (c *SignumApiClient) upbuildApiClients() {
  clients := make([]*apiClient, 0, len(apiHosts))
  for _, host := range apiHosts {
    client, err := doRequestForHost(logger, host)
    if err != nil {
       continue
    }
    clients = append(clients, client)
  }
  sort.Slice(clients, func(i, j int) bool {
    // allow out of sync in 1 block
    if clients[i].blockchainStatus.NumberOfBlocks-1 > clients[j].blockchainStatus.NumberOfBlocks {
       return true
    }
    if clients[i].blockchainStatus.NumberOfBlocks < clients[j].blockchainStatus.NumberOfBlocks-1 {
       return false
    }
    return clients[i].latency < clients[j].latency
  })
}

// на каждый запрос перемешиваем первую половину, чтобы сначала запрос шел одной из лучших нод, а уже потом, в случае неудачи, всем остальным
func (c *SignumApiClient) doJsonReq() {
  rand.Shuffle(len(apiClients)/2, func(i, j int) {
    apiClients[i+offset], apiClients[j+offset] = apiClients[j+offset], apiClients[i+offset]
  })

  for _, apiClient := range apiClients {
    body, err = apiClient.DoJsonReq()
    if err != nil {
      continue
    }
    // success, process body
  }
}

С переходом на свой VPS я просто поднял локально свою ноду, дал ей приоритет среди других и сократил интервал опроса, чтобы оповещения о транзакциях срабатывали быстрее. Для отслеживания баланса аккаунта достаточно раз в 4 минуты (интервал между блоками) запрашивать информацию об этом аккаунте у любой из нод. Если бот видит новую транзакцию, то пользователю будет отправлена нотификация. Такие нотификации позволяют удобно отслеживать изменение баланса при поступлении выплаты с пула при майнинге или любом другом входящем платеже. Например, в комьюнити есть лотерея, несколько криптоигр и прочих несерьёзных развлекух на смарт контрактах.

Inline клавиатура


Тут я использовал такой же подход, как и в предыдущем своем боте, а именно base64 строка с закодированной протобаф структурой:

type QueryDataType struct {
  MessageId            int64        // id телеграм сообщения
  Account              string       // Signum аккаунт, с которым производятся действия
  Keyboard             KeyboardType // тип клавиатуры (аккаунт, график цены, калькулятор и т.п)
  Action               ActionType   // действие
}

func (m QueryDataType) GetBase64ProtoString() string {
  bytes, _ := proto.Marshal(&m)
  base64str := base64.StdEncoding.EncodeToString(bytes)
  return base64str
}

Это позволило удобно реализовать обратную связь от пользователя при его навигации по инлайн-меню аккаунта и взаимодействии с переключателями.



Актуальная цена + график


График рисуется средствами библиотеки github.com/wcharczuk/go-chart прямо на лету и отправляется пользователю в виде обычного изображения. Сами сэмплы котировок я беру с сайта coinmarketcap.com, у них есть бесплатное API с ограничением на количество запросов в сутки.



Изначально, с целью экономии места в базе данных (т.е на платформе Heroku бесплатно можно использовать только до 10 тысяч строк) я сделал механизм «прореживания» и усреднения старых данных по котировкам, из-за этого график на большом периоде выглядит грубым в начале и более детальным в конце:



Алгоритм по сути костыльно-простой: есть некая линейная функция k*x + b, определяющая минимальный интервал между значениями, и чем сэмпл дальше от текущего момента, тем больше значений он будет усреднять:

for i := 1; i < len(scannedPrices); i += 2 {
  price0 := scannedPrices[i-1]
  price1 := scannedPrices[i]
  X := time.Since(price0.CreatedAt) / time.Hour / 24
  delayM := pm.config.DelayFuncK*X + pm.config.DelayFuncB
  if price1.CreatedAt.Sub(price0.CreatedAt) < delayM {
     price0.SignaPrice = (price0.SignaPrice + price1.SignaPrice) / 2
     price0.BtcPrice = (price0.BtcPrice + price1.BtcPrice) / 2
     pm.db.Save(price0)
     pm.db.Unscoped().Delete(price1)
  }
}

Сейчас в этом уже нет необходимости, но я решил так и оставить.

Калькулятор доходности майнинга


Очень настоятельно прошу не поднимать тему заработка на майнинге — т.к на данном этапе майнинг конкретно Signum убыточен, и на фоне других блокчейнов здесь даже обсуждать нечего. Хотите зарабатывать — вам в PoW и иже с ними, не нужно тут холивара, все делается на чистом энтузиазме и получения опыта для!

В Signum используется алгоритм консенсуса Proof of Commitment (PoC+) — это смесь классических Proof of Capacity (PoC) и Proof of Stake (PoS), почитать про который можно в статье от одного из нынешних разработчиков: Proof of Commitment (PoC+): a Proof of Capacity Upgrade. Таким образом, заработок при майнинге зависит не только от предоставленного физического объёма жестких дисков, но и от “коммитмента” — объёма замороженных на счету монет.

func Calculate(miningInfo *signumapi.MiningInfo, tib float64, commit float64) *CalcResult {
  var calcResult = CalcResult{
     TiB:                tib,
     Commitment:         commit,
     MyCommitmentPerTiB: commit / tib,
  }

  e := calcResult.MyCommitmentPerTiB / miningInfo.AverageCommitment
  n := math.Pow(e, p)
  n = math.Min(8, n)
  n = math.Max(.125, n)
  calcResult.CapacityMultiplier = n

  calcResult.EffectiveCapacity = calcResult.CapacityMultiplier * calcResult.TiB
  calcResult.MyDaily = 360 / miningInfo.AverageNetworkDifficulty * float64(miningInfo.LastBlockReward) * calcResult.EffectiveCapacity
  calcResult.MyMonthly = calcResult.MyDaily * 30.4
  calcResult.MyYearly = calcResult.MyMonthly * 12

  return &calcResult
}

В калькуляторе для расчётов используются усреднённые значения сложности сети и среднего коммитмента, чтобы нивелировать пилу в течение дня (см. ниже график статистики сети).

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



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



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

Статистика сети + график


Со статистикой сети всё просто — данные берутся из API ноды раз в 4 минуты, складываются в базу, и строится такой же график, как у цены:



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

Кран для получения копеек для активации аккаунта


Сумма, которую можно получить с крана, не представляет какой-либо финансовой ценности (сейчас это 1 Signa за регистрацию в боте и 0.05 Signa еженедельно, что по текущему курсу около $0.004 и $0.0002 соответственно), но может быть полезна для активации аккаунта и подключении к пулу, т.к для подключения к пулу нужно заплатить комиссию за транзакцию в размере минимум 0.00735 Signa (после предстоящего хардфорка минимальная комиссия будет зафиксирована на уровне 0.01 Signa для кратности расчётов).

Но даже такой мизерный кран начали доить ????. Следовательно пришлось добавить различные ограничения на получение монет с одного телеграм аккаунта. Одно из них — дёргать кран не чаще одного раза в неделю:

var accountFaucet models.Faucet
err := user.db.
  Where("account = ? OR account_rs = ?", account, account).
  Where("amount = ?", amount).
  Last(&accountFaucet).Error
if err == nil && time.Since(accountFaucet.CreatedAt) < 24*time.Hour*time.Duration(config.FAUCET_DAYS_PERIOD) {
  return false, fmt.Sprintf("???? Sorry, you have used the faucet less than %v days ago!", config.FAUCET_DAYS_PERIOD)
}

Заключение


Бот делался в большей степени для себя, т.к я люблю телеграм и хочу иметь удобный инструмент под рукой. Также мой бот не единственный — параллельно другой человек разрабатывает ещё одного бота @Signa_Russia_bot, который имеет схожий функционал и местами даже больший (статистика пулов, оповещения о пропуске блоков при майнинге).
В следующей статье расскажу про попытку сделать проект для объединения майнеров с инвесторами, чтобы люди с жёсткими дисками могли объединяться с людьми с монетами и получать взаимную выгоду.

To be continued...

Комментарии (3)


  1. sv58
    12.05.2022 13:50

    Юзаю бота) рекомендую!


  1. aceofspades88
    12.05.2022 14:13

    у них на сайте приложения есть для мобилок, да и обычная веб-морда в наличии, они не решают каких-то вопросов которые умеет ваш бот?


    1. oWart Автор
      12.05.2022 14:19

      Основной посыл бота - отслеживание разных кошельков и получение уведомленией об изменении их баланса, такого приложение не умеет