|
Полезные статьи. Разработка. Безопасность Полезные статьи о криптовалюте. Терминология блокчеин. |
Опции темы |
13.05.2021, 23:33 | #1 |
Регистрация: 01.01.2018
Сообщений: 168
|
Принимаем платежи в биткоинах или телеграмм-бот автопродаж
Принимаем платежи в биткоинах или телеграмм-бот автопродаж
Введение Данная статья написана мной(KONUNG), специально для конкурса на Exploit.in, здесь я рассмотрю вопрос создания Telegram-бота автопродаж с оплатой товаров биткоинами. Я буду использовать NodeJS, MySQL, Blockchain. Я решил взять NodeJS потому что, он преимущественно создан для серверов. Те кто знают другие ЯП, без проблем могут за пару часов освоить основы JS и свичнуться на него. До этого мне не приходилось писать телеграмм ботов, но посидев пару тройку деньков все стало очень просто. В интернете куча материалов как установить NodeJS и MySQL, поэтому я не буду тратить на это время, демонстрируя полную установку. Концепция
Создание и настройка бота Первое что мы сделаем, это получим токен бота для дальнейшей работы. Находим в телеграмме botfather, запускаем его и пишем /newbot после чего он попросит дать название боту, а затем и юзернейм по которому его будут находить другие пользователи, юзернейм обязательно должен заканчиваться на bot, когда все будет сделано вы получите токен для доступа к боту, никому не пересылайте этот токен, это чревато компрометация вашего магазина. Небольшой список второстепенных настроек, которые вы можете сделать:
База данных Теперь самое интересное, база данных, тут все проще чем может показаться на первый взгляд, я назвал свою бд 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) в котором будут храниться настройки нашего магазина. [CODE][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' }/CODE] Тут не так уж и много настроек, первое 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) }) Заключение Как вы можете видеть из статьи, гениальные вещи иногда очень просты. Мы с вами прошли по пути меньшего сопротивления и упростил все до невозможности, проще уже некуда. Можно прикрутить что бы бот там картинки выдавал или принимал оплату в другой крипте, но это уже на ваше усмотрение, я же не стал ничего этого делать, что бы оставить непринужденность и легкость, показал концепт, основные моменты и собственно что из этого всего получается. С радостью почитал бы что вы думаете об этом всем, возможно во время чтения у вас появились какие-либо вопросы, задавайте - отвечу, так же принял бы здравую критику. Перед запуском не забудьте инициализировать проект командой npm i в консоли, в случае если вы сами будете вручную переписывать, не забывайте устанавливать требующиеся фреймворки вручную. Ссылки и источники |