Принимаем платежи в биткоинах или телеграмм-бот автопродаж

Введение

Данная статья написана мной(KONUNG), специально для конкурса на Exploit.in, здесь я рассмотрю вопрос создания Telegram-бота автопродаж с оплатой товаров биткоинами. Я буду использовать NodeJS, MySQL, Blockchain. Я решил взять NodeJS потому что, он преимущественно создан для серверов. Те кто знают другие ЯП, без проблем могут за пару часов освоить основы JS и свичнуться на него. До этого мне не приходилось писать телеграмм ботов, но посидев пару тройку деньков все стало очень просто. В интернете куча материалов как установить NodeJS и MySQL, поэтому я не буду тратить на это время, демонстрируя полную установку.

Концепция

Морда

Концепция бота следующая, клиент запускает бота, дальше у него есть две команды на выбор: /showproducts – показать продукты, /checkorder – проверить статус заказа. При просмотре всех продуктов клиент будет получать ID, имя, описание, цену(в долларах), количество товара в наличии и снизу под сообщением инлайновая кнопочка “Купить”. Все что ему нужно будет сделать это нажать на кнопку купить, получить ID заказа и реквизиты для пополнения, оплатить и проверить статус заказа по его ID.

Кишки

После того как клиент будет нажимать “Купить” нам будет прилетать ID товара и его цена в долларах, затем мы просто пересчитываем по текущему курсу, подбираем свободный для оплаты адресс, добавляем данные в бд и N-количество минут будем проверять прошла ли оплата, если оплата прошла будет добавлять в таблицу с ордерами продукт, по истечении определенного времени (90 минут в нашем случае) в случае не оплаты ордера он будет удаляться из бд, что бы не захламлять базу. Так же у нас будет админка со своими командами для добавления/удаления продуктов и т.д.

Создание и настройка бота

Первое что мы сделаем, это получим токен бота для дальнейшей работы. Находим в телеграмме botfather, запускаем его и пишем /newbot после чего он попросит дать название боту, а затем и юзернейм по которому его будут находить другие пользователи, юзернейм обязательно должен заканчиваться на bot, когда все будет сделано вы получите токен для доступа к боту, никому не пересылайте этот токен, это чревато компрометация вашего магазина. Небольшой список второстепенных настроек, которые вы можете сделать:

/setname – Изменяет имя бота
/setdescription – Устанавливает описание бота (В чате)
/setabouttext – Устанавливает описание бота (В профиле)
/setuserpic – Устанавливает аватарку бота
/setcommands – Устанавливает команды для бота с их описанием

Далее устанавливаем основные команды для бота которые будут видны пользователю, сначало отправляем бате ботов /setcommands, выбираем там нашего бота и отправляем ему следующие команды одним сообщением:

showproducts – Показать все продукты
checkorder – Проверить статус заказа

Ну вот впринципе и все настройки бота.

База данных

Теперь самое интересное, база данных, тут все проще чем может показаться на первый взгляд, я назвал свою бд my_store, у нас будет три таблицы: my_order, my_products, my_productsinfo. SQL не так хорошо знаю, по-этому возможно где-то можно было оптимизировать таблицы. Команда для создания таблиц:

CREATE TABLE my_products( — здесь будут сами товары
product_id INT NOT NULL,
product_data VARCHAR(255) NOT NULL
);

CREATE TABLE my_productsinfo( — здесь будет информация о самих товарах
product_id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description VARCHAR(255) NOT NULL,
price INT NOT NULL,
PRIMARY KEY(product_id)
);

CREATE TABLE my_orders( — здесь будут храниться непосредственно сами заказы
order_id CHAR(32) CHARACTER SET ‘latin1’ NOT NULL
address VARCHAR(255) NOT NULL,
status VARCHAR(255) NOT NULL,
price FLOAT(8,8) NOT NULL,
product_id INT NOT NULL,
product_data VARCHAR(255),
order_data TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
);

Бот

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

module.exports = {
authToken: “1701858052:AAGWYQ5Kp-4EiOi_9GJKBXqMYj3UrIzXHhk”,
MySQL: {
client: “mysql”,
connection: {
host: “127.0.0.1”,
user: “root”,
password: “”,
database: “my_store”
}
},
adminChatId: 437619229,
xPub: ‘xpub6FBEgyfiZ79TbeZdgo39Ahr4pRQaoqJMAs7mQNV8MLPaHB19PX7PMhPP12Hjp32jduEA2rQ93DNYgtzm92ZAUizKdUAGWnYdxWCmJwNCtpK’
}

Тут не так уж и много настроек, первое authToken это токен бота который мы получили на этапе создания бота в botfather просто вставляем свой токен и все. Далее настройка соединения с бд, тут мы указываем адресс хоста, имя пользователя, пароль и название базы данных если оно отличается. Что бы узнать свой ID чата, нужно отправить нашему боту /echo, дальше вы увидите это в коде.

xPub а вот это уже штучка поинтереснее, xPub – это расширенный открытый ключ. Он является частью стандарта биткоина BIP32. Если у вас есть xPub ключ, единственное что вы можете сделать это генерировать адреса, но уже без приватных ключей к ним, в нашем случае это очень удобно, так как если кто-то вдруг взломает ваш сервер и будет видеть код который выполняется, он не сможет спиздить ваши бетки, максимум что он сможет сделать это пососать яйца облизнуться при виде того сколько вы продали и на какую сумму, так уж устроен xPub. Где взять? В https://www.blockchain.com/, регистрируем там себе новый аккаунт, специально для нашего магазинчика и переходим в Настройки->Кошельки и адреса->Мой кошелек Bitcoin->Управлять->Дополнительные опции->Показать xPub, вуаля, вот и ваш xPub. Я выбрал Blockchain потому что это удобно и просто, да и вы 100% слышали об этом кошельке. Дальнейшие коментарии будут в коде.

const conf = require(‘./Config’) //
const MD5 = require(“md5″)
const {Telegraf, Markup} = require(‘telegraf’)
const bot = new Telegraf(conf.authToken)
const knex = require(‘knex’)(conf.MySQL)
const Axios = require(‘axios’)
const bjs = require(‘bitcoinjs-lib’);
const XPubGenerator = require(‘xpub-generator’).XPubGenerator;

const TenMinutes = 10 * 60 * 1000 //Интервал с которым мы будем проверять коши на оплату
var Status = ‘Sleep’ //Текущее действие в админке
var checkorder = [] //Массив в котором будут храниться id чатов для проверки ордеров, дальше поймете
var Product = {
Name: ”,
Description: ”,
Price: 0
}

bot.start(ctx => { //Собственно привественное сообщение, при старте бота
ctx.reply(`Добро пожаловать ${ctx.message.from.first_name}, рад приветствовать тебя в моем магазине\n/showproducts – Просмотр всех продуктов \n/checkorder – Проверить статус заказа`)
})

bot.help( ctx => ctx.reply(‘/showproducs – Просмотр всех продуктов \n/checkorder – Проверить статус заказа’))

/* Команды для покупателей*/

async function calcPrice(price){ //Эта функия будет пересчитывать $ в BTC по текущему курсу
try{
let response = await Axios.get(`https://web-api.coinmarketcap.com/v1/tools/price-conversion?amount=${price}&convert_id=1&id=2781`)
return Number(response.data.data.quote[‘1’].price.toFixed(8))
} catch(err){
return ‘Error’
}
}

async function getBalance(address){ //Функция проверки баланса
try{
let response = await Axios.get(`https://chain.api.btc.com/v3/address/${address}`)
return {received: Number((response.data.data.received * 0.00000001).toFixed(8)), unconfirmed: Number((response.data.data.unconfirmed_received * 0.00000001).toFixed(8))}
} catch(err){
return {received: ‘Error’, unconfirmed: ‘Error’}
}
}

bot.on(‘callback_query’, async ctx =>{//Событие которое срабатывает при нажатии на кнопку купить
try{
let t = ctx.update.callback_query.data.split(‘$’) //тут мы сплитаем дату которая вложена в кнопку которую нажали
let summa = await calcPrice(t[1]) //считаем цену
if (summa === ‘Error’) throw new Error(‘Во время просчета цены произошла ошибка’)
let didi = -1
let addresses = []
for (let addr of await knex(‘my_orders’).select(‘address’)) addresses.push(addr.address) //вытаскиваем из бд btc-адресса заказов

do {
didi++
t_address = new XPubGenerator(conf.xPub, bjs.networks.bitcoin).nthReceiving(didi)
} while (addresses.includes(t_address)) //По xPub генерируем адреса до тех пор пока не попадется тот которого нет в бд

let Arra = {
order_id: MD5(Date.now().toString+ctx.update.callback_query.id), //Уникальный ID заказа по которому в итоге клиент будет находить заказ
address: t_address,
status: ‘В ожидании оплаты’,
price: summa,
product_id: t[0],
product_data: ‘Будет доступно после оплаты’
}
await knex(‘my_orders’).insert(Arra) //Создаем ордер
ctx.reply(`Ваш заказ находится в обработке, в случае не оплаты в течении полутора часа, заказ будет ликвидирован. \nID заказа: ${Arra.order_id}\nРеквизиты для оплаты: ${Arra.address}\nСумма к оплате: ${Arra.price}\nВы можете проверить статус вашего заказа отправив отправив команду /checkorder`)
} catch(err){
ctx.reply(‘Произошла ошибка попробуйте позднее’)
}
})

bot.command(‘/showproducts’, ctx =>{ //ответ на команду показать продукты
knex.select().from(‘my_productsinfo’)
.then( resp =>{
for (let product of resp){
knex(‘my_products’).where({product_id: product.product_id}).count({count: ‘*’})
.then( resp => ctx.reply(`ID: ${product.product_id}\nName: ${product.name}\nDescription: ${product.description}\nPrice: ${product.price}$\nCount: ${resp[0].count}`,
Markup.inlineKeyboard([Markup.button.callback(‘Купить’, `${product.product_id}$${product.price}`)]) )) //Дата в кнопке это ID$Price продукта
.catch( err => ctx.reply(‘Произошла ошибка при получении товаров’))
}
})
.catch(err => ctx.reply(‘Ошибка при получении списка продуктов’))
})

bot.command(‘/checkorder’, ctx =>{ //Переводим пользователя в режим проверки ордера
checkorder.push(ctx.message.chat.id)
ctx.reply(‘Введите ID заказа’)
})

bot.on(‘text’, async (ctx, next) =>{ //Это событие срабатывает на все текстовые сообщения
if (checkorder.includes(ctx.message.chat.id)){ //Собственно если чат в режиме проверки заказа выполняется следующий код
const STF = await knex(‘my_orders’).where({order_id: ctx.message.text})
if (STF[0] == undefined){
ctx.reply(‘Ордер не найден’)
} else {
ctx.reply(`ID заказа: ${STF[0].order_id}\nID продукта: ${STF[0].product_id}\nРеквизиты: ${STF[0].address}\nСумма к оплате: ${STF[0].price}\nСтатус: ${STF[0].status}\nТовар: ${STF[0].product_data}`)
}
checkorder.splice(checkorder.indexOf(ctx.message.chat.id), 1) //Удаляем из массива, соответственно статус проверки заказа убирается
}
next()
})

bot.command(‘/echo’, ctx =>{ //Эта команда нужна что бы узнать id чата с нами, после того как укажите нужный id в конфиге можете удалять эту команду
ctx.reply(ctx.message.chat.id)
})

/* Команды для администратора*/

bot.use((ctx, next) =>{ //Интересная вещь, middleware, те кто юзал фреймворк Express, точно знают что это за штучка
if (ctx.message.chat.id === conf.adminChatId) next() // Если мы из чата администратора то едем дальше и выполнятся следующие функции
})

bot.command(‘/cancel’, ctx =>{
Status = ‘Sleep’ //Отменяем текущие операции
ctx.reply(‘Все текущие операции были отменены’)
})

bot.command(‘/addproduct’, ctx =>{
Status = ‘AddProduct_N’ //перехоим в режим добавления продукта
ctx.reply(‘Укажите название товара’)
})

bot.command(‘/addproductdata’, ctx =>{
Status = ‘AddProductData’ //добавляем сами продукты
ctx.reply(‘Отправьте Данные для добавления в формате ID$ProductData\nНапример 3$email:password’)
})

bot.command(‘/showproductdata’, ctx =>{
knex(‘my_products’).select() //Показывает все товары которые есть на продажу
.then( resp => ctx.reply(resp))
.catch( err => ctx.reply(‘Произошла ошибка’))
})

bot.command(‘/delproductdata’, ctx =>{
Status = ‘DelProductData’ //Переходим в режим удаления какой-то определенного продукта из таблицы my_products
ctx.reply(‘Отправьте данные о продукте который хотите удалить в следующем формате ID$ProductData’)
})

bot.command(‘/delproduct’, ctx =>{
Status = ‘DelProduct’ //Удаляем продукты которые видит клиент
ctx.reply(‘Отправьте ID продукта который хотите удалить’)
})

bot.on(‘text’, ctx =>{ //Обрабатывает то что мы вводим, то что вводит админ
switch(Status){ //То что находится в этом свиче я описал выше
case ‘DelProduct’:
Status = ‘Sleep’
knex(‘my_productsinfo’).where({product_id: ctx.message.text}).del()
.then( resp => ctx.reply(‘Товар Успешно удален’))
.catch( err => ctx.reply(‘Во время удаления произошла ошибка’))
break
case ‘AddProduct_N’:
Status = ‘AddProduct_D’
Product.Name = ctx.message.text
ctx.reply(‘Укажите описание товара’)
break
case ‘AddProduct_D’:
Status = ‘AddProduct_P’
Product.Description = ctx.message.text
ctx.reply(‘Укажите цену товара’)
break
case ‘AddProduct_P’:
Status = ‘Sleep’
Product.Price = parseInt(ctx.message.text)
knex(‘my_productsinfo’).insert({name: Product.Name, description: Product.Description, price: Product.Price})
.then( resp =>ctx.reply(‘Товар успешно добавлен’))
.catch( err => ctx.reply(‘Произошла ошибка во время добавления товара’))
break
case ‘AddProductData’:
Status = ‘Sleep’
let t = ctx.message.text.split(‘$’)
knex(‘my_products’).insert({product_id: t[0], product_data: t[1]})
.then( resp => ctx.reply(‘Продукт успешно добавлен в БД’))
.catch( err => ctx.reply(‘Во время добавления в БД произошла ошибка’))
break
case ‘DelProductData’:
Status = ‘Sleep’
let t = ctx.message.text.split(‘$’)
knex(‘my_products’).where({product_id: t[0], product_data: t[1]}).del()
.then( resp => ctx.reply(‘Продукт успешно удален’))
.catch( err => ctx.reply(‘Во время удаления произошла ошибка’))
break
}
})

bot.launch().then( () =>{ //Собственно стартуем нашего бота
console.log(‘Bot Started!’)
let timerId = setInterval( async () => { //После старта запускаем таймер который будет срабатывать каждые 10 минут, для проверки ордеров и удаления лишнего
my_orders = await knex(‘my_orders’).whereNot({status: ‘Выполнен’}).select(‘address’, ‘status’, ‘price’, ‘product_id’, ‘order_data’)
for (let order of my_orders){ //Получаем заказы которые не выполнены и проходимся по каждому из заказов
let balance = await getBalance(order.address)
if (balance.received >= order.price){ //Если есть баланс то собственно изменяем статус, и закидываем продукт
let response = await knex(‘my_products’).where({product_id: order.product_id})
if (response != 0){
await knex(‘my_products’).where({product_id: response[0].product_id, product_data: response[0].product_data}).del()
await knex(‘my_orders’).where({address: order.address}).update({status: ‘Выполнен’, product_data: response[0].product_data})
}
} else if (balance.unconfirmed >= order.price){ //Смотрим есть ли не подтвержденные ордеры
await knex(‘my_orders’).where({address: order.address}).update({status: ‘В ожидании подтверждений’})
} else if (balance.received != ‘Error’){ //Удаляем лишние ордеры если прошло 90 и больше минут с момента его создания
if (order.order_data.setMinutes(order.order_data.getMinutes()+90) <= new Date ){ await knex('my_orders').where({address: order.address}).del() } } } }, TenMinutes) }) Запускаем бота, добавляем чат с самим собой в админку, добавляем продукты в лист, командой /addproduct и добавляем сами товары командой /addproductdata. Все, магазин запущен, можно смело продавать. Заключение Как вы можете видеть из статьи, гениальные вещи иногда очень просты. Мы с вами прошли по пути меньшего сопротивления и упростил все до невозможности, проще уже некуда. Можно прикрутить что бы бот там картинки выдавал или принимал оплату в другой крипте, но это уже на ваше усмотрение, я же не стал ничего этого делать, что бы оставить непринужденность и легкость, показал концепт, основные моменты и собственно что из этого всего получается. С радостью почитал бы что вы думаете об этом всем, возможно во время чтения у вас появились какие-либо вопросы, задавайте - отвечу, так же принял бы здравую критику. Ссылка на архив со всеми исходами проекта - КЛАЦ. Перед запуском не забудьте инициализировать проект командой npm i в консоли, в случае если вы сами будете вручную переписывать, не забывайте устанавливать требующиеся фреймворки вручную.

Оставьте комментарий