Предыстория

Полгода назад взялся за один проект с возможностью оплаты биткойном. Так как проект делали на языке 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 проект. Я не профи в криптографии, так что скорее всего многие моменты упустил, но надеюсь кому-то эта статья будет полезна.