Как я сделал полноценный проект из ничего от одной Bitcoin платёжки на Python

Действия происходят в далёком — год назад

В этой статье хочу рассказать, как можно из обычных вещей сделать нечто большее и новое используя python, qt и bitcoin библиотеки.

С чего всё начиналось

В то время мне необходимо было сделать приём платежей в криптовалюте и в частности Bitcoin, для одного сервиса на заказ, дабы пользователи могли пополнять личный счёт, оплачивать товары и при необходимости выводить их.

Как работали такие платёжные системы я тогда не знал и понятия не имел как реализовать такую вещь, не говоря уже о знаниях о том из чего состоит блок, что такое цепочка иерархии, приватный ключ и так далее.

итоговая работаитоговая работа

Мне удалось найти пример того как реализована биткоин оплата в боте телеграм, так же на python и так же из одной статьи Habr.

Её я и взял за основу и потратил не мало времени что бы изучить базу всего строения.
Как работает блокчейн здесь описано не будет, таких статьей не мало. здесь будет то как я нашёл идею от простой задачи.

И так, вот как выглядит и работает платёжный приём на Bitcoin он же и получение средств в нашем клиенте на qt

для работы нам понадобиться библиотеки которые всё делают за нас ну или на половину…

в моём случае используется pywallet, но есть так же bitcoiblib, py-hd-wallet, hdwallet и другие неплохие либы, у каждого есть свои недостатки и плюсы, наиболее хорошо показали себя в работе hdwallet и pywallet, для создания иерархически детерминированного кошелька тобишь дочернего адреса для вашего кошелька.

# Индекс адреса, индекс определяет глубино адреса, так как на одной seed фразе может быть несколько адресов 
index = 0 # стартуем от нулевого индекса

# наша seed фраза или мнемоническая фраза, стандартная базовая абстракция при построении адреса, на ней всё осваивается
seed = 'one two one two one two ...'

# далее генерируем мастер ключ на основе мненоники 
master_key = wallet.HDPrivateKey.master_key_from_mnemonic(seed)

# и далее по путям генерируем адрес стандарта BIP 44 и проходимся по иирархии от ключа до паблик и приват ключа
root_keys = wallet.HDKey.from_path(master_key, "m/44'/0'/0'/0")[-1].public_key.to_b58check()
xpublic_key = (root_keys)

# Получаем наш дочерний адрес
address = Wallet.deserialize(xpublic_key, network='BTC').get_child(index,is_prime=False).to_address()

rootkeys_wif = wallet.HDKey.from_path(master_key, f"m/44'/0'/0'/0/{index}")[-1] # <- это для того что бы генерировать каждый раз новый адрес
xprivatekey = rootkeys_wif.to_b58check()

# Wallet Import Format он служит для осуществления транзакций 
wif = Wallet.deserialize(xprivatekey, network='BTC').export_to_wif()

вот так выглядт функция создания всех необходимых сущностей для дальнейшей работы

такая же по логике функция с библиотекой hdwallet.

index = [0, 1, 2]
bip44_btc = BIP44HDWallet(cryptocurrency=BitcoinMainnet)
bip44_btc.from_mnemonic(mnemonic=seedphrase, language="english")
bip44_btc.clean_derivation()

bip44_derivation: BIP44Derivation = BIP44Derivation(cryptocurrency=BitcoinMainnet, account=0, change=False,address=index[0])
bip44_btc.from_path(path=bip44_derivation)
wif_0_44 = bip44_btc.wif()
key = Key(wif=wif_0_44)
balance_1_for_btc = key.get_balance('usd')
addressinput_0_btc = bip44_btc.address()

С иирархией понятно. Идея

После реализации платёжной системы с приёмом биткоин, мне тут же пришла банальная и в то же время интересная идея, что если просто платёжку обернуть в приложение и сделать из этого Bitcoin Wallet. Да ещё и с контролем всех средств пользователей
Всё относительно просто, для интерфейса Qt, заворачиваем наш код в логику и готово.

Кто не понял

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

Давай код!

Начинаем по стандарту PyQt5 с интерфейса

окно регистрацииокно регистрациидомашнее окнодомашнее окно

После интерфейса начнём его описывать.

import sqlite3

import bit
import clipboard
import qrcode as qrcode
import requests
from PyQt5 import QtWidgets
from PyQt5.QtGui import *
from PyQt5.QtWidgets import QDialog, QApplication, QMainWindow, QMessageBox
from PyQt5.uic import loadUi
from bs4 import BeautifulSoup
from pywallet import wallet
from pywallet.utils import *

Импорт нужных нам библиотек.

class LoginScreen(QMainWindow):
    def __init__(self):
        super(LoginScreen, self).__init__()
        loadUi("GUI/atom.ui",self)
        self.passwordline.setEchoMode(QtWidgets.QLineEdit.Password)
        self.registrnow.clicked.connect(self.gotoReg)
        self.Loginone.clicked.connect(self.loginfunction)

    def gotoReg(self):
        Reg = RegScreen()
        widget.addWidget(Reg)
        widget.setCurrentIndex(widget.currentIndex()+1)

    def loginfunction(self):
        password = self.passwordline.text()
        if len(password) == 0:
            self.error.setText("Please input all fields.")
        else:
            db = sqlite3.connect("wallet.db")
            curs = db.cursor()
            curs.execute(f"SELECT * FROM users WHERE Password = '{password}'")
            if not curs.fetchone():
                self.error.setText("Incorrect password")
            else:
                fillprofile = Profile()
                widget.addWidget(fillprofile)
                widget.setCurrentIndex(widget.currentIndex() + 1)

класс входа в приложение, в конструкторе класса подгружаем наш ui и определяем кнопки
далее обычная минимальная функции проверки пароля и входа в систему с использованием Sqlite3.

В идеале

Конечно для продакшена этот проект не подходит, реализация была как пет-проект и в идеале бы конечно ui так не подгружать для каждого отдельный, можно было сделать stackedWidget и по нему переходить, и можно было бы обойтись в таком случае одним классом или двумя, подключить Postgresql какой нибудь, да и вообще софт в стол лучше на плюсах или шарпе.

class Profile(QDialog):
    def __init__(self):
        super(Profile, self).__init__()
        loadUi("GUI/main.ui", self)
        self.btcpricee()
        self.balanceuser()
        self.logout.clicked.connect(self.loginout)
        self.receive.clicked.connect(self.receivebtcaddress)
        self.receive_2.clicked.connect(self.receivebtcaddress)
        self.walletbutton.clicked.connect(self.walletent)
        self.sendd.clicked.connect(self.sendbtc)
        self.sendd_2.clicked.connect(self.sendbtc)
        self.settings.clicked.connect(self.settinges)

    def balanceuser(self):
        try:
            db = sqlite3.connect("wallet.db")
            curs = db.cursor()
            sss = "SELECT btc_address FROM users"
            curs.execute(sss)
            adres = curs.fetchone()
            url = f'https://www.blockchain.com/btc/address/{adres[0]}'
            headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0'}
            Response = requests.get(url, headers=headers)
            wallet = BeautifulSoup(Response.content, 'html.parser')
            convert = wallet.findAll("span", {"class": "sc-16b9dsl-1","class": "ZwupP", "class": "u3ufsr-0", "class": "eQTRKC"})
            rx = convert[6].text
            self.balance.setText(str(rx))
            response = requests.get('https://api.coindesk.com/v1/bpi/currentprice.json')
            data = response.json()
            x = data["bpi"]["USD"]["rate_float"]
            xx = rx.rstrip('BTC')
            us = (x * float(xx))
            self.usdbalance.setText(str(us))
        except:
            self.balance.setText(str('Loading...'))
            self.usdbalance.setText(str('---'))

функция индексирования баланса, достаём из бд адрес и просто парсим его и далее через setText выводим

class RegScreen(QDialog):
    def __init__(self):
        super(RegScreen, self).__init__()
        loadUi("GUI/reg.ui",self)
        self.loginperehod.clicked.connect(self.gotoLogin)
        self.signupreg.clicked.connect(self.registrationfunction)

    def gotoLogin(self):
        Log = LoginScreen()
        widget.addWidget(Log)
        widget.setCurrentIndex(widget.currentIndex() + 1)


    def registrationfunction(self):
        password_reg = self.passwordreg.text()
        repl_password = self.relacepasswordreg.text()
        if repl_password != password_reg:
            self.errorreg1_2.setText("Password does not match")
        else:
            if len(password_reg) == 0:
                self.errorreg1.setText("To register, you need to fill in all the fields")
            elif len(password_reg) < 8:
                self.passerror.setText('Password cannot be less than 8 characters')
            else:
                db = sqlite3.connect('wallet.db')
                curs = db.cursor()

                curs.execute('''CREATE TABLE IF NOT EXISTS users (
                                            Password TEXT,
                                            balance INTEGER,
                                            btc_address,
                                            wif TEXT,
                                            btc_send TEXT
                                            )''')

                db.commit()
                curs.execute("SELECT Password FROM users")
                if curs.fetchone() is None:
                    index = 0
                    seed = ''
                    master_key = wallet.HDPrivateKey.master_key_from_mnemonic(seed)
                    root_keys = wallet.HDKey.from_path(master_key, "m/44'/0'/0'/0")[-1].public_key.to_b58check()
                    xpublic_key = (root_keys)
                    address = Wallet.deserialize(xpublic_key, network='BTC').get_child(index,is_prime=False).to_address()
                    rootkeys_wif = wallet.HDKey.from_path(master_key, f"m/44'/0'/0'/0/{index}")[-1]
                    xprivatekey = rootkeys_wif.to_b58check()
                    wif = Wallet.deserialize(xprivatekey, network='BTC').export_to_wif()
                    curs.execute("INSERT INTO users VALUES (?, ?, ?, ?, ?)", (password_reg, 0, address, wif, 0))
                    img = qrcode.make(address)
                    img.save('GUI/qr.png')
                    db.commit()
                    self.successreg.setText("You have successfully registered!")
                else:
                    error = QMessageBox()
                    error.setWindowTitle('Big request to create an account')
                    error.setText('Sorry, you cannot create another account.')
                    error.setIcon(QMessageBox.Warning)
                    error.setDefaultButton(QMessageBox.Ok)
                    error.exec_()
                    exit()

Класс регистрации с его функциями, как видим при регистрации мы используем ту самую функцию которую я описывал в начале статьи, далее мы заносим наши данные в бд.

Как видите всё достаточно просто, мы взяли биткоин функцию и засунули её в Qt и так же как и с платёжкой мы может получать и отображать баланс и выводить за счёт Wallet Import Format или сокр.WIF и по разному манипулировать монетами ибо все они хранятся на нашем кошельке через нашу сид фразу, храним всё в бд, а управление через простой интерфейс. можно было бы сделать мультивалютность и использовать web3 тогда например генерация данных для ETH выглядела бы так

index = [0, 1, 2]
bip44 = BIP44HDWallet(cryptocurrency=EthereumMainnet)
bip44.from_mnemonic(mnemonic=seedphrase, language="english")
bip44.clean_derivation()

bip44_derivation: BIP44Derivation = BIP44Derivation(cryptocurrency=EthereumMainnet, account=0, change=False,address=index[0])
bip44.from_path(path=bip44_derivation)
private_key_0 = '0x' + bip44.private_key()
addressinput_0 = bip44.address()

Ну и на этом всё.

Что можно вынести из данной статьи, это то что даже самая невзрачная вещь может послужить вам основой большого и крутого проекта. А так же то что нельзя доверять ни одному клиенту криптокошелька не видев его код, быть может именно он так и устроен, под децентролизавной системой криптовалют может скрываться мнимая централизация с утечкой и хранением ваших средств у какого нибудь индуса :-)

Полный исходный код тут

© Habrahabr.ru