Предыстория
Полгода назад взялся за один проект с возможностью оплаты биткойном. Так как проект делали на языке python, то и оплату хотелось реализовать на нем же. Сразу же взялся анализировать готовые решения, доступные библиотеки и Rest API Blockchain.com. С апи блокчейна я моментально обломался, так как их токен для использования апи довольно не просто получить.
Затем решил юзать различные библиотеки (block-io, bitcoinlib, blockchain и др.) После пару ночей попыток реализовать нормальную оплату, остановился на bitcoinlib, так как она более менее стабильно работала, и я спокойно переводил с одного кошелька на другой. Беда наступила когда появились первые 100 пользователей и вся оплата внезапно рухнула. Возможно я криво написал или что-то не так понял с работой библиотеки, но любые попытки восстановить работу оплаты были безуспешны, только если обнулять бдшку, но и так неизвестно сколько бы она продержалась.
В итоге решили оставить без BTC оплаты. Я опечалился и не связывался с оплатой биткойном полгода.
К чему я пришел
На днях я все-таки решил добить этот вопрос для себя, надеюсь кому-то еще пригодятся мои наработки.
Все начинается с seed фразы. Мнемоническая фраза (англ. Mnemonic phrase или Seed фраза) - это список слов, которые хранят всю информацию, необходимую для восстановления биткоин-кошелька. Существуют несколько стандартов генерации фраз BIP 32, BIP 39, BIP 44, и еще BIP 49. Самый распространенный - это BIP 44 (12 слов).
Пример seed фразы:
vivid area able second bicycle advance demand alpha flip stable drift route
Чтобы сгенерировать фразу будем использовать библиотеку bipwallet. Чтобы ее установить воспользуемся командой pip install bipwallet.
from bipwallet import wallet
# generate 12 word mnemonic seed
seed = wallet.generate_mnemonic()
print(seed)
Если мы хотим получить напрямую доступ к биткойн кошельку, то, зная фразу, можем сразу восставновить кошелек в blockhain.com:
https://login.blockchain.com/#/recover
Следующим шагом мы будем преобразовывать нашу seed фразу до получения нужного адреса кошелька биткойна.
Чтобы во всем не запутаться и знать какие данные мы должны получить, я использовал сайт https://iancoleman.io/bip39/
Генерация дочернего адреса кошелька для каждого пользователя:
Чтобы получить наш нулевой адрес Биткойн кошелька на основе seed фразы (12VeK1eRgPHRUikNLXq3Nuz99gS2S46QMD), нам нужно пройти всю цепочку преобразований. Методом проб и ошибок мне все-таки удалось получить адрес кошелька следующим кодом:
from bipwallet.utils import *
def gen_address(index):
# Наша seed фраза
seed = 'vivid area able second bicycle advance demand alpha flip stable drift route'
# Мастер ключ из seed фразы
master_key = HDPrivateKey.master_key_from_mnemonic(seed)
# Public key из мастер ключа по пути 'm/44/0/0/0'
root_keys = HDKey.from_path(master_key, "m/44'/0'/0'/0")[-1].public_key.to_b58check()
# Extended public key
xpublic_key = str(root_keys, encoding="utf-8")
# Адрес дочернего кошелька в зависимости от значения index
address = Wallet.deserialize(xpublic_key, network='BTC').get_child(index, is_prime=False).to_address()
rootkeys_wif = HDKey.from_path(master_key, f"m/44'/0'/0'/0/{index}")[-1]
# Extended private key
xprivatekey = str(rootkeys_wif.to_b58check(), encoding="utf-8")
# Wallet import format
wif = Wallet.deserialize(xprivatekey, network='BTC').export_to_wif()
return address, str(wif, 'utf-8')
print(gen_address(0))
Данная функция возвращает адрес кошелька и wif в зависимости номера. Максимальное число с которым удалось получить адрес это 999999999.
wif (Wallet import format) - это просто кодирование байтов ключа в кодировку Base58 + контрольная сумма. Он нам понадобится в дальнейшем при генерации транзакции.
Это все значит, что имея только одну seed фразу мы можем создать 1 млрд дочерних адресов. Каждому пользователю при регистрации мы будем выдавать новый адрес, через который он сможет оплачивать по BTC. Появляется ограничение на 1 млрд пользователей, но нам никто не запрещает использовать несколько seed фраз или генерировать каждому юзеру новую фразу, но тогда каждая оплата будет кидаться не в общий ваш кошелек, а по разным.
Проверка баланса и транзакции:
Теперь когда у каждого пользователя свой личный адрес биткойн кошелька, нужно проверить баланс этого адреса. Для этого мы будем обращаться к сайту Blockchain.com дабы получить нужную информацию.
import requests
# Адрес кошелька пользователя
wallet = '12VeK1eRgPHRUikNLXq3Nuz99gS2S46QMD'
# wallet = gen_address(0)
url = f'https://blockchain.info/rawaddr/{wallet}'
x = requests.get(url)
wallet = x.json()
print('Итоговый баланс:'+str(wallet['final_balance']))
print('Транзакции:'+str(wallet['txs']))
if wallet['total_received']==0:
print('баланс пустой')
Вот таким простым кодом мы можем получить всю информацию по балансу и транзакциях пользователя. Дальше все зависит от логики самого приложения.
Транзакции
На данном этапе мы дали каждому пользователю свой адрес кошелька и знаем все транзакции с данным адресом, но этого недостаточно. Нам нужно чтобы мы могли отправить его же деньги обратно. Для этого воспользуемся библотекой bit. Чтобы ее установить воспользуемся командой pip install bit.
from bit import PrivateKey
# Приватный ключ из wif
my_key = PrivateKey(wif='L46ixenNSu8Bqk899ZrH8Y96t8DHqJ1ZyxzQBGFTbh38rLHLaPoY')
# Количество долларов перевода, можно поменять на btc
money=0.1
# Кошелек куда будут переведены деньги
wallet='17ya3bCpPioyPH8kAyFkEDBUqdjF6wwPxo'
# Коммисия перевода, если поставить слишком маленькую, то транзакцию не примут
# И чем больше коммисия, тем быстрее пройдет перевод
fee=2000
# Генерация транзакции
tx_hash = my_key.create_transaction([(wallet, money, 'usd')],fee=fee,absolute_fee=True)
print(tx_hash)
В итоге мы получили вот такую транзакцию:
0100000001fe64490fce5e85d5eb00865663a3d44f4108549fdb2840b086cfc781390d4a2d010000006a47304402202dc1496d28bb10d50d94d70870e2a79ea472c5960de8f7418bb30f9b96643efc02204691547c98edad3181a056bf6404601efe289200ba8e3073a2f5b7c0c7f4fec10121026516c551584b484ce3ca7bb71bbf24cce133bf40bdf4e2ce5a3936bc7e66a2abffffffff02e3020000000000001976a9144c83a20250ccb62ce2b3b1ea80c6082b634fdf9f88ac08f40200000000001976a9144c83a20250ccb62ce2b3b1ea80c6082b634fdf9f88ac00000000
Выглядит красиво, но что с этим делать?
Можно зайти на сайт https://www.blockchain.com/btc/pushtx
и вручную отправить эту транзакцию.
Также можем декодировать эту транзакцию и проверить все ли верно мы указали https://www.blockchain.com/btc/decode-tx
Но нам нужно это автоматизировать, поэтому напишем несколько строк:
import requests
url = 'https://blockchain.info/pushtx'
tx='0100000001fe64490fce5e85d5eb00865663a3d44f4108549fdb2840b086cfc781390d4a2d010000006a47304402202dc1496d28bb10d50d94d70870e2a79ea472c5960de8f7418bb30f9b96643efc02204691547c98edad3181a056bf6404601efe289200ba8e3073a2f5b7c0c7f4fec10121026516c551584b484ce3ca7bb71bbf24cce133bf40bdf4e2ce5a3936bc7e66a2abffffffff02e3020000000000001976a9144c83a20250ccb62ce2b3b1ea80c6082b634fdf9f88ac08f40200000000001976a9144c83a20250ccb62ce2b3b1ea80c6082b634fdf9f88ac00000000'
x = requests.post(url, data = {'tx':tx})
result = x.text
print(result)
Выполним пост запрос, если получаем ответ: Transaction Submitted. Это значит, что через несколько секунд транзакция появится в сети и деньги спишутся с пользователя.
Применение
Ну чтож, написав всего несколько десятков строк, мы можем генерировать для каждого пользователя свой адрес кошелка, проверять его баланс, переводить биткойны с одного кошелька на любой другой.
Для демонстрации работы BTC оплаты, я напишу простенького телеграм бота, который будет выполнять роль клиента Blockchain.com, то есть вы сможете хранить в нем свои биткойны и от туда же переводить другим людям. Ссылка на исходники бота будут в конце.
Проверить работу бота можно тут: https://t.me/Blockchain_client_bot
Задеплоил на heroku, так что надеюсь не будет падать)
Функционал бота
Регистрация пользователя
В качестве БД я использовал sqlite3 и создал одну таблицу пользователей:
import sqlite3
conn = sqlite3.connect("my.db") # или :memory: чтобы сохранить в RAM
cursor = conn.cursor()
cursor.execute("CREATE TABLE users (chatid INTEGER , name TEXT, balance INTEGER, btc_wallet TEXT, wif TEXT, btc_sent TEXT, state INTEGER)")
conn.commit()
При нажатии start мы регистрируем пользователя, генерируем для него адрес биткойн кошелька, wif и добавляем данные в БД:
sql = "SELECT COUNT(*) FROM users "
cursor.execute(sql)
user = cursor.fetchone()
address, wif= gen_address(user[0]+1)
sql_insert = "INSERT INTO users VALUES ({}, '{}', 0,'{}','{}','no',0)".format(message.chat.id,
message.chat.first_name,address,wif)
cursor.execute(sql_insert)
conn.commit()
Проверка баланса
if message.text == '? Ваш баланс':
url = f'https://blockchain.info/rawaddr/{data[3]}'
x = requests.get(url)
wallet = x.json()
await bot.send_message(message.chat.id, f'''? *Итоговый баланс:* {format(wallet['final_balance'] / 100000000, '.9f')} BTC
*Всего получено:* {format(wallet['total_received'] / 100000000, '.9f')} BTC
*Всего отправлено:* {format(wallet['total_sent'] / 100000000, '.9f')} BTC
https://www.blockchain.com/ru/btc/address/{data[3]}''', parse_mode= "Markdown")
Получить BTC
Для создания qr-кода я использовал библиотеку qrcode и на вход передал ранее сгенерированный адрес биткойн кошелька из БД.
if message.text == '? Получить BTC':
img = qrcode.make(data[3])
img.save('qr.jpg')
await bot.send_message(message.chat.id, f'''? Ваш адрес биткойн кошелька:
*{data[3]}*''', parse_mode= "Markdown")
await bot.send_photo(message.chat.id,photo=open('qr.jpg', 'rb'))
Отправить BTC
try:
sum = float(message.text)
url = f'https://blockchain.info/rawaddr/{data[3]}'
x = requests.get(url)
wallet = x.json()
if sum + 10000 <= wallet['final_balance'] / 100000000:
try:
my_key = PrivateKey(wif=data[4])
# Коммисия перевода, если поставить слишком маленькую, то транзакцию не примут
# И чем больше коммисия, тем быстрее пройдет перевод
fee = 10000
# Генерация транзакции
tx_hash = my_key.create_transaction([(data[5], sum, 'btc')], fee=fee, absolute_fee=True)
print(tx_hash)
url = 'https://blockchain.info/pushtx'
x = requests.post(url, data={'tx': tx_hash})
result = x.text
sql = "UPDATE users SET state = {} WHERE chatid = {}".format(0, message.chat.id)
cursor.execute(sql)
conn.commit()
await bot.send_message(message.chat.id, result)
except Exception:
await bot.send_message(message.chat.id, "? Ошибка при выолнении транзакции")
else:
await bot.send_message(message.chat.id, '?? На вашем балансе недостаточно средств.')
except ValueError:
await bot.send_message(message.chat.id, '??Неправильно введена сумма отправления, попробуйте еще раз')
Проверим через сайт, что транзакция отправилась:
Исходники и как запустить
Скачать исходники бота можно тут github.com/Lil-hack/blockchain-client
Склонировав репозиторий, устанавливаем необходимые пакеты:
pip install -r requirements.txt
Некоторые библиотеки у меня не заработали на windows, так что лучше сразу запускать на linux.
В файле main.py заменяем ваш токен телеграм бота:
# Ваш токен от BotFather
TOKEN = 'YOUR TOKEN'
В файле btc_core.py заменяем на вашу seed фразу:
# Ваша seed фраза
seed = 'YOUR SEED'
И запускаем бота командой: python main.py
Работает на python 3.7.0 и выше. Бот написан за один вечер, так что просьба строго не судить ^^
Итого
Как оказалось, все довольно не сложно, и в несколько десятков строк можно добавить оплату BTC в любой python проект. Я не профи в криптографии, так что скорее всего многие моменты упустил, но надеюсь кому-то эта статья будет полезна.
ne_kotin
как по мне — существенный минус данного решения — необходимость доверять сторонним ресурсам.
альтернатива:
1. ставим локальную ноду.
2. работаем с ней по json-rpc и вовремя бэкапим. можно обойтись просто requests в случае питона, или разнообразными curl.
минус: большой объем требуемого дискового пространства, 200 Гб в случае bitcoin.
ligor
Если полный блокчейн — то уже за 300 с лишним, но можно и урезать начальные блоки.
nikizan
"Bytes: 385 611 696 957" если точнее.
un1t
Не нужно хранить 300 ГБ, есть же легкие клиенты. Тот же electrum можно использовать
electrum.readthedocs.io/en/latest/merchant.html