Python 加密货币钱包详解

在本章中,您将学习如何构建桌面加密货币钱包。您仍将使用相同的 GUI 库 Qt for Python 或 PySide2 来创建桌面应用。这个加密货币钱包可以发送以太和 ERC20 代币。在构建此加密货币钱包之前,您将了解 PySide2 库的高级功能,例如选项卡、组合框、大小策略,以及添加拉伸以控制布局中小部件的分布。最重要的是,您将把测试集成到应用中。

在本章中,我们将介绍以下主题:

本章要求读者对 PySide2 库有一些了解。如果您还没有阅读第 7 章前端去中心应用,请先阅读,因为本章是以该章为基础的。如果您已经熟悉了使用PySide2构建 GUI,您就具备了构建桌面加密货币钱包的必要技能,至少从用户界面UI角度来看是如此。但是,您构建的应用将对用户产生不和谐。例如,如果在水平布局中合并了一个按钮,并且该按钮是水平布局中唯一的小部件,则在调整具有水平布局的窗口的大小时,该按钮将向右和向左拉伸。如果这不是你想要的,你需要一种方法来告诉按钮保持它的宽度。

因此,让我们从PySide2库中学习其他功能,例如选项卡、大小策略和网格布局,这样我们就有了使应用的 UI 更具吸引力的技能。我们的应用将不会赢得苹果最佳设计奖*,但至少对用户来说不会那么刺耳。

此外,在第 7 章前端去中心应用中,我们忽略了测试。由于加密货币钱包应用是一个处理人们金钱的应用,因此错误代价高昂。因此,我们需要在用户之前捕获任何错误。因此,我们应该为加密货币钱包编写适当的测试。但是,我们将重点测试加密货币钱包的 UI 部分。我们不会把重点放在测试内部方法上。换句话说,我们的测试将是集成测试。

Install the Qt library if you haven't already done so. Please read Chapter 7Frontend Decentralized Application, for guidance on how to do this. After doing so, create a virtual environment for your project using the following command:

$ virtualenv -p python3.6 wallet-venv
$ source wallet-venv/bin/activate
(wallet-venv) $ pip install PySide2
(wallet-venv) $ pip install web3==4.7.2

我们还希望安装一个测试库来测试我们的应用,这可以通过以下命令完成:

(wallet-venv) $ pip install pytest-qt

现在所有的库都已经设置好了,让我们编写一个简单的应用来测试它。

创建一个名为advanced_course_qt的目录。我们可以把所有的教程文件放在这里。命名第一个脚本button_and_label.py并使用以下代码为该按钮创建一个按钮和一个标签(完整代码请参阅以下 GitLab 链接上的代码文件:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_09/advanced_course_qt/button_and_label.py

from PySide2.QtWidgets import QWidget, QApplication, QLabel, QPushButton, QVBoxLayout
from PySide2.QtCore import Qt
import sys

class ButtonAndLabel(QWidget):

...
...

if __name__ == "__main__":

    app = QApplication(sys.argv)
    button_and_label = ButtonAndLabel()
    button_and_label.show()
    sys.exit(app.exec_())

运行前面的代码以查看此应用的功能。该应用由一个按钮和一个标签组成:

如果单击该按钮,标签上的文本将更改,如以下屏幕截图所示:

让我们测试一下这个应用。将测试命名为test_button_and_label.py并将其放在同一目录中。对测试应用使用以下代码块:

from button_and_label import ButtonAndLabel
from PySide2 import QtCore

def test_button_and_label(qtbot):
    widget = ButtonAndLabel()
    qtbot.addWidget(widget)

    assert widget.label.text() == "label: before clicked"

    qtbot.mouseClick(widget.button, QtCore.Qt.LeftButton)

    assert widget.label.text() == "label: after clicked

使用以下命令运行测试:

(wallet-venv) $ pytest test_button_and_label.py

Be aware that the (wallet-venv) $ python test_button_and_label.pycommand is a negligible error often used to run the test. Don't fall for it!

在这个测试脚本中,我们导入我们的widget类。然后,我们创建一个名称以test_开头的测试方法。此方法有一个名为qtbot的参数。请勿更改其名称。qtbot是一个特殊名称,不得更改。在这个方法中,qtbot可以用来与widget类交互。

首先,我们实例化一个要测试的widget类。然后,我们使用qtbot中的addWidget方法添加widget实例:

qtbot.addWidget(widget)

然后,在点击按钮之前,我们在label变量上测试text

assert widget.label.text() == "label: before clicked"

如您所见,我们可以从widget访问label。这是可能的,因为我们使用以下代码在button_and_label.py中声明了label变量:

self.label = QLabel("label: before clicked")

如果您在button_and_label.py中使用以下代码声明标签:

label = QLabel("label: before clicked")

那么您将无法从测试中的widget实例访问label。当然,您可以通过创建一个变量来保存标签的文本来避免这种情况。但是,为了测试标签的文本,将label设置为widget实例属性是最简单的事情。您将在所有进一步的测试中使用此策略。简而言之,如果您想测试小部件(例如标签、按钮或组合框),请将其widget作为其父小部件实例的属性。然后,我们继续讨论如何单击按钮小部件:

qtbot.mouseClick(widget.button, QtCore.Qt.LeftButton)

要在测试过程中单击按钮,请使用qtbot中的mouseClick方法。qtbotmouseClick方法的第一个参数是按钮小部件,或接受点击事件的东西。第二个参数是用于检测鼠标单击事件性质的选项。这种情况下的测试只接受左键单击。

以下代码用于在单击按钮后测试和显示标签文本:

assert widget.label.text() == "label: after clicked"

在构建 GUI 应用时,有时我们必须显示对象列表。在我们的加密货币钱包中,列表可以存放帐户。因此,让我们为该场景编写一个测试。但是,首先,我们必须创建一个脚本来显示对象列表。将脚本命名为button_and_list.py并为脚本使用以下代码块(完整代码请参阅以下 GitLab 链接上的代码文件:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_09/advanced_course_qt/button_and_list.py

from PySide2.QtWidgets import QWidget, QApplication, QLabel, QPushButton, QVBoxLayout
from PySide2.QtCore import Qt
import sys

class ButtonAndList(QWidget):

...
...

if __name__ == "__main__":

    app = QApplication(sys.argv)
    button_and_list = ButtonAndList()
    button_and_list.show()
    sys.exit(app.exec_())

运行脚本以查看应用的显示方式。以下显示单击按钮之前的屏幕截图:

下面显示了单击该按钮的结果:

这里只有一个按钮,如果您单击它,就会出现一个新的标签,上面的文字仅为1。如果再次单击该按钮,底部将出现一个新标签,其文本为2,以此类推。

单击按钮后显示的新标签是垂直框布局的一部分。这意味着我们需要将垂直框布局设置为小部件实例的属性,以便我们可以在测试中访问它。

让我们为这个 GUI 脚本编写一个测试,如下面的代码块所示,并将其命名为test_button_and_list.py

from button_and_list import ButtonAndList
from PySide2 import QtCore

def test_button_and_list(qtbot):
    widget = ButtonAndList()
    qtbot.addWidget(widget)

    qtbot.mouseClick(widget.button, QtCore.Qt.LeftButton)
    qtbot.mouseClick(widget.button, QtCore.Qt.LeftButton)
    qtbot.mouseClick(widget.button, QtCore.Qt.LeftButton)

    label_item = widget.v_layout.takeAt(2)
    assert label_item.widget().text() == "3"

    label_item = widget.v_layout.takeAt(1)
    assert label_item.widget().text() == "2"

    label_item = widget.v_layout.takeAt(0)
    assert label_item.widget().text() == "1"

正如我们在前面的代码块中看到的,在第三次执行qtbotmouseClick方法后,我们使用以下代码从垂直框布局中抓取标签:

label_item = widget.v_layout.takeAt(2)

我们通过takeAt方法获取小部件的子小部件。我们在本例中使用的参数是2。这意味着我们要抓住第三个孩子,最后一个。然后,我们使用以下代码测试小部件的文本:

assert label_item.widget().text() == "3"

让我们创建一个更复杂的场景。到目前为止,我们测试的所有内容都在一个窗口内,但是如果我们有一个输入对话框呢?我们如何测试对话框?

让我们创建一个有对话框的 GUI 脚本,并将其命名为button_and_dialog.py:(完整代码请参考以下 GitLab 链接上的代码文件:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_09/advanced_course_qt/button_and_dialog.py

from PySide2.QtWidgets import QWidget, QApplication, QLabel, QPushButton, QVBoxLayout, QInputDialog, QLineEdit
from PySide2.QtCore import Qt
import sys

class ButtonAndDialog(QWidget):

...
...

if __name__ == "__main__":

    app = QApplication(sys.argv)
    button_and_dialog = ButtonAndDialog()
    button_and_dialog.show()
    sys.exit(app.exec_())

运行代码以查看应用。下面有一个按钮和空白:

单击按钮,将出现一个对话框,然后您应在输入对话框中键入任何文本,然后单击确定:

您输入的文本将显示在按钮下方:

让我们看看下面代码块中的另一个测试脚本,以便了解如何处理涉及两个不同窗口的流程。在本测试方法中,除了qtbot之外,我们还有一个参数,称为monkeypatch,将测试文件命名为test_button_and_dialog.py

from button_and_dialog import ButtonAndDialog
from PySide2.QtWidgets import QInputDialog
from PySide2 import QtCore

def test_button_and_dialog(qtbot, monkeypatch):
    widget = ButtonAndDialog()
    qtbot.addWidget(widget)

    monkeypatch.setattr(QInputDialog, 'getText', lambda *args: ("New Text", True))
    qtbot.mouseClick(widget.button, QtCore.Qt.LeftButton)

    assert widget.label.text() == "New Text"

monkeypatch用于覆盖对话框输入。这意味着在测试中启动对话框时,QInputDialoggetText方法将返回一个("New Text", True)元组。还记得QInputDialog的 API 吗?这将返回一个元组。这个元组包含两个参数:我们在对话框中键入的文本,以及单击“确定”还是“取消”按钮。

QInputDialoggetText方法接受四个参数:此对话框所基于的窗口实例、标题、输入字段前的标签以及输入字段的类型。当您在输入字段中键入文本,例如To the moon!并单击确定按钮时,它返回一个元组,由字符串To the moon!和您是否单击确定按钮的boolean值组成:

new_text, ok = QInputDialog.getText(self, "Write A Text", "New Text:", QlineEdit.Normal)

但是,monkeypatch对该方法进行了修补,因此在测试中不会启动任何对话框。我们绕过他们。这就好像启动对话框行被替换为以下代码行:

new_text, ok = ("New Text", True)

对于所有这些测试,我们总是使用按钮类型的小部件来启动一些东西(更改标签上的文本)。让我们使用另一种类型的小部件来更改标签,如以下代码块所示,并将脚本命名为combobox_and_label.py

from PySide2.QtWidgets import QWidget, QApplication, QLabel, QComboBox, QVBoxLayout
from PySide2.QtCore import Qt
import sys

class ComboBoxAndLabel(QWidget):

    def __init__(self):
        super(ComboBoxAndLabel, self).__init__()

        self.combobox = QComboBox()
        self.combobox.addItems(["Orange", "Apple", "Grape"])
        self.combobox.currentTextChanged.connect(self.comboboxSelected)

        self.label = QLabel("label: before selecting combobox")

        layout = QVBoxLayout()
        layout.addWidget(self.combobox)
        layout.addWidget(self.label)

        self.setLayout(layout)

    def comboboxSelected(self, value):
        self.label.setText(value)

if __name__ == "__main__":

    app = QApplication(sys.argv)
    combobox_and_label = ComboBoxAndLabel()
    combobox_and_label.show()
    sys.exit(app.exec_())

此 GUI 脚本使用 combobox 更改标签上的文本。它使用标签的选定选项中的文本设置标签上的文本。运行脚本以查看其显示方式:

现在,让我们创建一个测试脚本来测试这个组合框小部件,并将其命名为test_combobox_and_label.py

from combobox_and_label import ComboBoxAndLabel
from PySide2 import QtCore

def test_combobox_and_label(qtbot):
    widget = ComboBoxAndLabel()
    qtbot.addWidget(widget)

    assert widget.label.text() == "label: before selecting combobox"

    qtbot.keyClicks(widget.combobox, "Grape")

    assert widget.label.text() == "Grape"

我们在这里可以采取的关键点是,我们将combobox的所选选项更改为qtbot的方式:

qtbot.keyClicks(widget.combobox, "Grape")

方法的名称不直观;它接受两个参数。第一个是小部件,在本例中是组合框。第二个是组合框中的选项文本。此keyClicks方法不仅仅用于在组合框中选择选项。它还可以用于在行编辑中键入文本。只需将 line edit 小部件放在第一个参数中。

此测试知识足以测试我们的加密货币钱包。在我们开始构建加密货币钱包之前,让我们先了解一下PySide2的一些其他功能,包括网格布局、选项卡和大小策略。

我们想了解的第一件事是伸展运动。我们知道如何将小部件添加到框布局(垂直或水平)。但是,我们可以在一定程度上配置如何分发添加到方框布局中的这些小部件。我们是否应该拉伸窗口小部件,将窗口小部件放在水平布局的顶部,让空间吞噬其余部分?

让我们创建一个脚本来解释框布局中的小部件分发配置,并将脚本命名为add_stretch.py(完整代码请参阅以下 GitLab 链接上的代码文件:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_09/advanced_course_qt/add_stretch.py

from PySide2.QtWidgets import QFrame, QLabel, QWidget, QApplication, QPushButton, QHBoxLayout, QVBoxLayout, QSizePolicy, QSizePolicy
from PySide2.QtCore import Qt
import sys

class AddStretch(QWidget):

...
...

if __name__ == "__main__":

    app = QApplication(sys.argv)
    widget = AddStretch()
    widget.resize(500, 500)
    widget.show()
    sys.exit(app.exec_())

运行脚本以查看其外观:

如果您将拉伸添加到垂直容器的末尾,它会将小部件推到垂直容器的开头,并让其余部分成为空白。如果您在开始时添加拉伸,它会将小部件推到垂直容器的末尾,让其余部分成为一个空白。如果不添加任何拉伸,小部件将在布局中均匀分布。

就应用的功能而言,它没有任何区别。但是,如果选择正确的选项,它可以使 UI 更具吸引力。

我们总是使用方框布局(垂直方框布局或水平方框布局)。在大多数情况下,方框布局就足够了。但是,有时您希望使用更复杂的布局。Qt 的网格布局比长方体布局更强大。

让我们创建一个脚本来探索网格布局的威力,并将脚本命名为create_grid_window.py(完整代码请参阅以下 GitLab 链接上的代码文件:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_09/advanced_course_qt/create_grid_window.py

from PySide2.QtWidgets import QWidget, QApplication, QLabel, QGridLayout
from PySide2.QtCore import Qt
import sys

class GridWindow(QWidget):

...
...

if __name__ == "__main__":

    app = QApplication(sys.argv)
    gridWindow = GridWindow()
    gridWindow.show()
    sys.exit(app.exec_())

运行脚本以查看网格布局如何管理其子窗口:

网格类似于表格或电子表格。将小部件添加到由行和列组成的表中,而不是向具有水平布局的行或具有垂直布局的列添加小部件。

如果要将小部件添加到第一行和第一列,请使用以下语句:

layout.addWidget(label, 0, 0)

第一个参数表示一行。第二个参数表示一列。因此,如果要将小部件添加到第二行和第一列,请使用以下语句:

layout.addWidget(label, 1, 0)

网格布局的addWidget方法接受可选的第三和第四个参数。第三个参数表示希望此小部件扩展到多少行。第四个参数表示希望此小部件扩展到多少列:

layout.addWidget(label, 1, 1, 2, 2)

如果拉伸窗口,将看到类似于以下屏幕截图的内容:

看看标签 G。它最多可以延伸到两行两列。

现在,让我们讨论一下如果我们增加包含小部件的父窗口的大小,小部件会发生什么情况。小部件是否应该随之调整大小?小部件是否应该保持静止,并允许边距变宽?您可以使用大小策略决定调整配置的大小。让我们创建一个名为button_with_sizepolicy.py的脚本来演示配置此策略的大小(完整代码请参阅以下 GitLab 链接上的代码文件:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_09/advanced_course_qt/button_with_sizepolicy.py

from PySide2.QtWidgets import QWidget, QApplication, QPushButton, QVBoxLayout, QSizePolicy
from PySide2.QtCore import Qt
import sys

class ButtonWithSizePolicy(QWidget):

...
...

if __name__ == "__main__":

    app = QApplication(sys.argv)
    button_with_size_policy_widget = ButtonWithSizePolicy()
    button_with_size_policy_widget.resize(500, 200)
    button_with_size_policy_widget.show()
    sys.exit(app.exec_())

运行脚本以查看每个按钮在不同大小策略下的显示方式:

然后,尝试调整窗口大小以了解大小策略配置:

QSizePolicy.Maximum表示小部件不能大于大小提示,在这种情况下不能大于按钮的内容。如果希望按钮保持其原始大小,请使用此大小策略。QSizePolicy.Preferred表示它更喜欢一个尺寸提示,但它可以更大或更小。QSizePolicy.Expanding表示小部件应尽可能扩展。QSizePolicy.Minimum表示小部件可以扩展,但不能小于大小提示。QSizePolicy.MinimumExpanding表示小部件不能小于大小提示,但它会尽可能地扩展。

In creating a GUI application, most of the time you would not put all of your functionalities/widgets in a single window. Otherwise, the window would be bigger than the screen resolution of the monitor.

您可以启动一个带有按钮的对话框来保存更多功能/小部件。这当然有效。但你真正想要的是一个控制器。在 Qt 中,您有StackView。StackView 可以包含多个窗口,但一次最多显示一个窗口。

我们不会直接使用 StackView。相反,我们使用选项卡式视图。选项卡式视图在后台使用 StackView。让我们创建一个脚本来使用选项卡式视图,并将其命名为tabbed_window.py

from PySide2.QtWidgets import QTabWidget, QApplication, QWidget
import sys
from button_and_label import ButtonAndLabel

class TabbedWindow(QTabWidget):

    def __init__(self, parent=None):
        super(TabbedWindow, self).__init__(parent)
        widget1 = QWidget()
        self.widget2 = ButtonAndLabel()
        widget3 = QWidget()
        self.addTab(widget1, "Tab 1")
        self.addTab(self.widget2, "Tab 2")
        self.addTab(widget3, "Tab 3")

if __name__ == "__main__":

    app = QApplication(sys.argv)
    tabbedWindow = TabbedWindow()
    tabbedWindow.show()
    sys.exit(app.exec_())

此选项卡式窗口有三个选项卡。每个选项卡都包含一个小部件。第二个选项卡甚至包含一个我们在单独的脚本中创建的小部件,button_and_label.py。这个小部件位于第二个选项卡中,有一个按钮和一个标签。要向选项卡式窗口添加选项卡,请使用addTab方法。第一个参数是小部件,第二个参数是选项卡的标题。

运行脚本以查看选项卡式视图的工作方式。在下面的屏幕截图中,我们看到选项卡 1:

在下面的屏幕截图中,我们看到选项卡 2 和来自button_and_label.py的小部件:

现在您已经了解了 Qt for Python 库的其他功能,让我们开始构建一个桌面加密货币钱包。由于这是一个复杂的应用,我们不应该将所有内容都放在一个文件中;相反,我们将把它分成许多文件。我们甚至将许多文件分离到不同的目录中。我们还希望将此应用保持在足够基础的状态,以供教程使用。因此,我们不会在这个应用中添加很多功能。这个加密货币钱包可以创建新帐户,将以太发送到另一个帐户,并观看 ERC20 代币,以便我们以后可以将一些代币发送到另一个帐户。但是,它不会具有您期望从正确的加密货币钱包中获得的完整功能。

首先,让我们使用以下命令创建项目目录及其内部目录:

$ mkdir wallet
$ mkdir wallet/icons
$ mkdir wallet/images
$ mkdir wallet/tests
$ mkdir wallet/tools
$ mkdir wallet/wallet_threads
$ mkdir wallet/wallet_widgets

主应用、主库及其配置文件放在主目录中,即wallet目录。一些用于增加应用 UI 的图标放在icons目录中。化身图像放在images目录中。测试文件放在tests目录中。与区块链和 UI 无关的库文件放在tools目录中。线程类放在wallet_threads目录中。最后,主窗口小部件的子窗口小部件被放入wallet_widgets目录中。

让我们在wallet中创建一个区块链接口代码,并将脚本命名为blockchain.py。此文件负责连接到区块链。其职责包括检查帐户余额、获取本地帐户、发送交易和获取令牌信息。通过将所有区块链功能放在一个类或文件中,我们可以更容易地调试问题、测试实现和开发功能。转到https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/tree/master/chapter_09/wallet 并参考本节的blockchain.py代码文件。

该区块链类有 10 种与区块链交互的方法。此外,它还具有 ERC20 令牌的通用json接口。

让我们逐行讨论这个区块链类文件:

from web3 import Web3, IPCProvider
from web3.exceptions import ValidationError
from populus.utils.wait import wait_for_transaction_receipt
from collections import namedtuple
from os.path import exists
import json

SendTransaction = namedtuple("SendTransaction", "sender password destination amount fee")
TokenInformation = namedtuple("TokenInformation", "name symbol totalSupply address")

导入所需的库之后,我们创建两个命名元组。那么,我们为什么要创建这些命名元组呢?基本上,我们这样做是为了避免错误。加密货币钱包中出现错误代价高昂。

假设您具有以下功能:

def send_transaction(sender, password, destination, amount, fee):
    // the code to create transaction

您可以按如下方式执行该函数:

send_transaction("427af7b53b8f56adf6f13932bb17c42ed2a53d04", “password”, "6ad2ffd2e08bd73f5c50db60fdc82a58b0590b99", 3, 2)

如果交换发送方和目的地,在最坏的情况下,会出现未处理的异常,程序会停止,因为私钥与发送方不匹配。但是,如果您交换金额和费用怎么办?在这种情况下,您将小额款项发送给费用非常高的人。有很多方法可以避免这个错误。例如,可以使用以下代码块中给出的关键字参数,也可以使用命名元组:

send_transaction(SendTransaction(sender="0xaksdfkas", password="password", destination="0xkkkkkk", amount=3, fee=2))

现在,让我们转到 ERC20 代币智能合约的json接口。当我们想要发送以太时,这不是必需的:仅当我们想要发送代币时才需要:

true = True
false = False
erc20_token_interface = [
            {
                "anonymous": false,
                "inputs": [
                    {
                        "indexed": true,
                        "name": "_from",
                        "type": "address"
                    },
                    {
                        "indexed": true,
                        "name": "_to",
                        "type": "address"
                    },
                    {
                        "indexed": false,
                        "name": "_value",
                        "type": "uint256"
                    }
                ],
                "name": "Transfer",
                "type": "event"
            },
...

如您所知,为了与智能合约交互,您需要智能合约的json接口(abi。您可能想知道我们是如何获得这个json接口的。这是通过 ERC20 令牌智能合约的编译输出实现的。名称、十进制数和符号是什么并不重要。只要接口来自满足 ERC20 标准的智能合约,我们就应该得到正确的接口。为了简化问题,我决定将接口与Blockchain类放在同一个文件中。但是,您可以将接口放在json文件中,然后将json文件加载到Blockchain类文件中。然后,我们继续讨论Blockchain类的定义:

class Blockchain:

    tokens_file = 'tokens.json'

    def __init__(self):
        self.w3 = Web3(IPCProvider('/tmp/geth.ipc'))

在这里,我们开始Blockchain课程。在其初始化方法中,我们构造一个w3变量来连接区块链。我们通过 IPC 提供商硬编码与区块链的连接。例如,如果您使用HTTPProvider,或者使用不同的IPC文件路径,则可以更改此配置。tokens_file变量是保存我们监视的所有令牌的文件。

让我们看看下面的代码行:

    def get_accounts(self):
        return map(lambda account: (account, self.w3.fromWei(self.w3.eth.getBalance(account), 'ether')), self.w3.eth.accounts)

我们使用w3.eth.accounts获取所有本地账户,然后使用w3.eth.getBalance获取每个账户的余额。本地帐户是您在本地节点中创建的帐户。通常,文件保存在keystore目录中。

以下代码用于在本地区块链节点中创建新帐户:

    def create_new_account(self, password):
        return self.w3.personal.newAccount(password)

帐户文件将使用我们提供的密码进行加密。要查看私钥,我们需要使用密码解密帐户文件。但是,除了出于备份目的外,这不是必需的。

使用以下代码从地址获取余额:

    def get_balance(self, address):
        return self.w3.fromWei(self.w3.eth.getBalance(address), 'ether')

余额在魏。然后,我们把魏的平衡转换成以太的平衡。

以下代码块旨在获得 ERC20 令牌的余额,但不是以太的余额:

    def get_token_balance(self, account_address, token_information):
        try:
            token_contract = self.w3.eth.contract(address=token_information.address, abi=erc20_token_interface)
            balance = token_contract.functions.balanceOf(account_address).call()
        except ValidationError:
            return None
        return balance

首先,我们得到接受两个参数的契约对象——智能契约的地址和 json 接口。如果您还记得您在以太坊中Chapter 8创建令牌时学到的内容,ERC20 令牌需要有一个balanceOf方法。此方法的目的是从帐户地址获取令牌余额。

以下代码块用于创建发送以太网的事务:

    def create_send_transaction(self, tx):
        nonce = self.w3.eth.getTransactionCount(tx.sender)
        transaction = {
          'from': tx.sender,
          'to': Web3.toChecksumAddress(tx.destination),
          'value': self.w3.toWei(str(tx.amount), 'ether'),
          'gas': 21000,
          'gasPrice': self.w3.toWei(str(tx.fee), 'gwei'),
          'nonce': nonce
        }

        tx_hash = self.w3.personal.sendTransaction(transaction, tx.password)
        wait_for_transaction_receipt(self.w3, tx_hash)

首先,获取nonce,然后构造一个事务对象。要使用密码而不是私钥发送此事务,您需要使用来自w3.personal对象的sendTransaction方法。然后,等待交易确认。

在了解了涉及发送以太的事务之后,让我们继续看下面的代码块,这是一种创建发送 ERC20 令牌的事务的方法:

    def create_send_token_transaction(self, tx, token_information):
        nonce = self.w3.eth.getTransactionCount(tx.sender)
        token_contract = self.w3.eth.contract(address=token_information.address, abi=erc20_token_interface)
        transaction = token_contract.functions.transfer(tx.destination, int(tx.amount)).buildTransaction({
                  'from': tx.sender,
                  'gas': 70000,
                  'gasPrice': self.w3.toWei(str(tx.fee), 'gwei'),
                  'nonce': nonce
              })

        tx_hash = self.w3.personal.sendTransaction(transaction, tx.password)
        wait_for_transaction_receipt(self.w3, tx_hash)

首先,获取nonce,然后构造一个契约对象。然后,调用这个智能合约对象的transfer方法。请记住,ERC20 代币需要有一个transfer方法来转移代币硬币,该方法接受两个参数:目的地和代币硬币的数量。然后,您通过在此方法中构建一个事务来执行此方法,然后将其从w3.personal对象传递给sendTransaction方法。最后,我们等待该交易得到确认。

以下代码块用于从令牌智能合约获取信息:

    def get_information_of_token(self, address):
        try:
            token_contract = self.w3.eth.contract(address=address, abi=erc20_token_interface)
            name = token_contract.functions.name().call()
            symbol = token_contract.functions.symbol().call()
            total_supply = token_contract.functions.totalSupply().call()
        except ValidationError:
            return None
        token_information = TokenInformation(name=name.decode('utf-8'),
                                             symbol=symbol.decode('utf-8'),
                                             totalSupply=total_supply,
                                             address=address)
        return token_information

首先,我们创建一个契约对象。然后,为了获取名称、符号和总供应量,我们从智能合同中访问namesymboltotalSupply方法。因为名称和符号是字节对象,所以我们需要将其解码为字符串。我们将此信息包装在一个名为TokenInformation的元组中。

*以下代码是将令牌信息字典包装到名为tuple的文件中的一种方便方法:

    def get_token_named_tuple(self, token_dict, address):
        return TokenInformation(name=token_dict['name'],
                                totalSupply=token_dict['total_supply'],
                                symbol=token_dict['symbol'],
                                address=address)

以下代码用于从配置文件中获取我们正在监视的所有令牌:

    def get_tokens(self):
        tokens = {}
        if exists(self.tokens_file):
            with open(self.tokens_file) as json_data:
                tokens = json.load(json_data)
        return tokens

有很多代币智能合约,但我们只想使用其中的一些。因此,我们将与这些令牌智能合约相关的信息保存到一个json文件中。然后,我们转到文件的最后一行,它正在构造一个Blockchain类实例:

blockchain = Blockchain()

我们这样做是为了任何导入此模块的文件都可以直接获得区块链对象,而两个不同的文件可以获得相同的对象。这类似于单例模式。

现在,让我们编写线程对象以访问区块链。在区块链中创建交易时,您通常希望使用线程或非阻塞功能。因此,每当我们想要广播一个事务时,我们都会使用这些线程类。这些线程类将使用我们前面描述的区块链对象。

使用以下代码块在wallet_threads目录中创建balance_thread.py文件:

from PySide2.QtCore import QThread, Signal
from time import sleep
from blockchain import blockchain

class BalanceThread(QThread):

    get_balance_transaction = Signal(map)

    def __init__(self, parent=None):
        super(BalanceThread, self).__init__(parent)
        self.quit = False

    def kill(self):
        self.quit = True

    def run(self):
        while True:
            sleep(2)
            if self.quit:
                break
            accounts = blockchain.get_accounts()
            self.get_balance_transaction.emit(accounts)

该线程类未在区块链中创建任何交易;它的目的是读取每个帐户中的以太余额。那么,为什么我们需要一个线程来读取余额呢?读天平应该快吗?想象你启动加密货币钱包,你看到你的余额是 10 以太。然后,有人送你一些以太。你希望你的平衡能尽快反映出来,对吗?这就是这条线的目的;它将每 2 秒检查每个帐户的余额。kill方法用于关闭应用并停止线程工作。这不是强制性的,但如果不这样做,您将收到一条恼人的警告,说明在关闭应用时,应用已被销毁,而线程仍在运行。

现在,让我们在wallet_threads目录中创建另一个线程类,并将其命名为send_thread.py

from PySide2.QtCore import QThread, Signal
from blockchain import blockchain

class SendThread(QThread):

    send_transaction = Signal()

    def __init__(self, parent=None):
        super(SendThread, self).__init__(parent)

    def prepareTransaction(self, tx):
        self.tx = tx

    def run(self):
        blockchain.create_send_transaction(self.tx)
        self.send_transaction.emit()

这个线程类的目的是调用区块链对象的create_send_transaction方法。在运行线程之前,我们需要使用名为SendTransactiontuple参数调用该线程类的prepareTransaction方法。

现在,让我们在wallet_threads目录中创建另一个线程类,并将其命名为send_token_thread.py

from PySide2.QtCore import QThread, Signal
from blockchain import blockchain

class SendTokenThread(QThread):

    send_token_transaction = Signal()

    def __init__(self, parent=None):
        super(SendTokenThread, self).__init__(parent)

    def prepareTransaction(self, tx, token_information):
        self.tx = tx
        self.token_information = token_information

    def run(self):
        blockchain.create_send_token_transaction(self.tx, self.token_information)
        self.send_token_transaction.emit()

这类似于SendThread类。这个线程的目的是调用create_send_token_transaction方法,它这次接受两个参数,一个元组名为SendTransaction,另一个元组名为TokenInformation

现在,让我们了解什么是 identicon 库。identicon 库的目的是基于特定字符串的散列生成自定义化身图像(例如分形)。如果您登录到 StackOverflow 并且没有设置您的个人资料图像,那么您的化身将由 identicon 库生成。

屏幕截图如下所示:

或者它会像这样出现:

这是可选的。没有这些头像,我们的加密货币钱包可以正常运行。这只是为了增加用户界面的趣味性。

下载文件 https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_09/wallet/tools/identicon.py 进入tools目录。这是 Shin Adachi 的杰作。我对它进行了修改,使其能够与 Python 3 一起工作。你不必理解这个文件;将其视为第三方库。

然后,在tools目录中创建一个文件,使用以下代码块使用该库,并将其命名为util.py

from os.path import isdir, exists
from os import mkdir
from tools.identicon import render_identicon

def render_avatar(code):
    code = int(code, 16)
    img_filename = '/github/python/bc/img/%08x.png' % code
    if exists(img_filename):
        return img_filename
    img = render_identicon(code, 24)
    if not isdir('images'):
        mkdir('images')
    img.save(img_filename, 'PNG')
    return img_filename

基本上,此方法可以使用帐户地址呈现化身图像。这使得应用更具吸引力。因此,当你创建一个帐户时,你会得到一个根据你的地址而独特的化身。

然后,在icons文件夹中下载一些图标。您需要其中两个:ajax-loader.gifcopy.svg。您可以从免费图标网站下载copy.svg。任何显示复制操作的图标都可以。然后,您可以从下载ajax-loader.gifhttp://ajaxload.info/

让我们使用以下代码块创建主应用。这是我们加密货币钱包的主要入口。命名为wallet.py

from PySide2.QtWidgets import QTabWidget, QApplication
import sys

from wallet_widgets.account_widget import AccountWidget
from wallet_widgets.send_widget import SendWidget
from wallet_widgets.token_widget import TokenWidget

class WalletWidget(QTabWidget):

    def __init__(self, parent=None):
        super(WalletWidget, self).__init__(parent)
        self.account_widget = AccountWidget()
        self.send_widget = SendWidget()
        self.token_widget = TokenWidget()
        self.addTab(self.account_widget, "Account")
        self.addTab(self.send_widget, "Send")
        self.addTab(self.token_widget, "Token")

    def killThreads(self):
        self.account_widget.kill()

if __name__ == "__main__":

    app = QApplication(sys.argv)
    wallet_widget = WalletWidget()
    wallet_widget.show()
    return_app = app.exec_()
    wallet_widget.killThreads()
    sys.exit(return_app)

WalletWidget是一个选项卡式窗口。有三个选项卡:

  • 第一个选项卡用于保存帐户小部件。此小部件负责管理帐户(列出帐户并创建新帐户)。
  • 第二个选项卡设计用于保存一个小部件,用户可以使用该小部件创建一个事务来发送以太网或 ERC20 令牌。任何与发送以太或令牌有关的操作都在这个小部件中完成。
  • 第三个选项卡用于保存令牌小部件。此小部件负责监视 ERC20 令牌。监视 ERC20 令牌意味着从 ERC20 自定义令牌智能合约获取信息,并使这些令牌能够在发送事务小部件中使用。

这三个小部件将在稍后讨论的其他文件中定义。

killThreads方法是可选的。如果不使用此方法,则在关闭应用后将收到警报,因为应用创建的线程尚未完成其业务。

现在,让我们在这个选项卡式窗口的第一个选项卡中创建第一个小部件。将文件放入wallet_widgets目录中,并将其命名为account_widget.py。然后,您将从以下链接获得完整的代码文件:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/tree/master/chapter_09/wallet/wallet_widgets

如前所述,此小部件将显示在钱包选项卡窗口的第一个选项卡中。在此选项卡中,您将获取列表帐户并创建新的帐户功能。

使用以下代码从PySide2导入多种类型的小部件和类:

from PySide2.QtWidgets import (QWidget,
                               QGridLayout,
                               QVBoxLayout,
                               QHBoxLayout,
                               QPushButton,
                               QLabel,
                               QInputDialog,
                               QLineEdit,
                               QToolTip,
                               QApplication,
                               QSizePolicy)
from PySide2.QtCore import Slot, SIGNAL, QSize
from PySide2.QtGui import QPixmap, QIcon, QCursor, QClipboard
from time import sleep
from blockchain import blockchain
from tools.util import render_avatar
from wallet_threads.balance_thread import BalanceThread

我们还导入了blockchain对象和render_avatar方法等。此外,我们将使用balance_thread实例,这是一个更新我们帐户余额的线程。

使用以下代码块创建一个按钮,允许我们在小部件中创建帐户:

class AccountWidget(QWidget):

    balance_widgets = {}

    def __init__(self, parent=None):
        super(AccountWidget, self).__init__(parent)

        self.create_account_button = QPushButton("Create Account")
        self.create_account_button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        self.connect(self.create_account_button, SIGNAL('clicked()'), self.createNewAccount)

        self.accounts_layout = QVBoxLayout()

        accounts = blockchain.get_accounts()

        for account, balance in accounts:
            self._addAccountToWindow(account, balance)

        layout = QGridLayout()

        layout.addWidget(self.create_account_button, 0, 0)
        layout.addLayout(self.accounts_layout, 1, 0)

        self.setLayout(layout)

        self.balance_thread = BalanceThread()
        self.balance_thread.get_balance_transaction.connect(self._updateBalances)
        self.balance_thread.start()

所有这些账户都将放在accounts_layout垂直方框布局中。我们从区块链对象获取所有本地账户,然后使用addAccountToWindow方法将该账户放入账户布局。之后,我们将按钮和accounts_layout放在主布局中。最后,我们将BalanceThread线程实例的插槽连接到_updateBalances方法并运行线程。

使用以下代码启动输入对话框并请求密码:

    @Slot()
    def createNewAccount(self):
        password, ok = QInputDialog.getText(self, "Create A New Account",
                 "Password:", QLineEdit.Normal)
        if ok and password != '':
            new_account = blockchain.create_new_account(password)
            self._addAccountToWindow(new_account, 0, resize_parent=True)

这里,我们称之为blockchain对象的create_new_account方法。新帐户的地址将发送到_addAccountToWindow方法,该方法将在垂直框布局中包含新帐户信息。

接下来,我们使用以下代码块将帐户地址复制到剪贴板:

    def copyAddress(self, address):
        QToolTip.showText(QCursor.pos(), "Address %s has been copied to clipboard!" % address)
        clipboard = QApplication.clipboard()
        clipboard.setText(address)

在这里,我们获取剪贴板对象并将内容复制到其中。因此,在每个帐户信息中,都会有一个连接到此方法的按钮。但是,首先,我们将在工具提示中显示此复制操作的信息。Qcursor.pos()是我们鼠标的位置。使用QtoolTipshowText方法显示工具提示。

有四个主要的小部件:帐户地址标签、复制帐户地址的按钮、帐户标签余额和化身图像。为了显示化身图像,我们可以使用标签。但我们使用的不是setText方法,而是setPixmap方法,如下代码块所示:

    def _addAccountToWindow(self, account, balance, resize_parent=False):
        wrapper_layout = QVBoxLayout()
        account_layout = QHBoxLayout()
        rows_layout = QVBoxLayout()
        address_layout = QHBoxLayout()
        account_label = QLabel(account)
...
...
        avatar.setPixmap(pixmap)
        account_layout.addWidget(avatar)
        account_layout.addLayout(rows_layout)
        wrapper_layout.addLayout(account_layout)
        wrapper_layout.addSpacing(20)
        self.accounts_layout.addLayout(wrapper_layout)

        if resize_parent:
            sizeHint = self.sizeHint()
            self.parentWidget().parentWidget().resize(QSize(sizeHint.width(), sizeHint.height() + 40))

setPixmap接受Qpixmap对象。如果resize_parent为真,那么我们将增加窗户的高度。我们使用名为parentWidget的方法访问主窗口,即选项卡式窗口。必须将其链接并调用两次,如self.parentWidget().parentWidget()。第一个父窗口小部件是堆栈视图。选项卡式小部件是使用堆栈视图构建的。

使用以下代码调用BalanceThread实例的kill()方法:

    def kill(self):
        self.balance_thread.kill()
        sleep(2)

这将告诉线程停止其任务。

线程实例使用下一种方法更新余额:

    @Slot()
    def _updateBalances(self, accounts):
        for account, balance in accounts:
            self.balance_widgets[account].setText('Balance: %.5f ethers' % balance)

balance_widgets[account]持有特定账户的余额标签。

第二个小部件是SendWidget。在wallet_widgets目录中创建一个名为send_widget.py的文件。此小部件负责从 ERC20 令牌发送以太或硬币。有关此部分的完整代码,请转到以下 GitLab 链接:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/tree/master/chapter_09/wallet/wallet_widgets

此小部件是选项卡式窗口中最复杂的。在这个小部件中,我们需要选择发送者的帐户,然后根据该帐户,我们需要显示与该帐户的 ERC20 令牌相关的以太或硬币余额。余额是否显示在以太或 ERC20 令牌中取决于此小部件的另一部分是否选择了以太坊或 ERC20 令牌。我们还需要添加一行编辑,以便人们可以填写目的地地址。此外,我们需要一种选择费用的方法,因为有时,人们不介意支付更高的费用,以便更快地处理交易。然后,有一个按钮启动一个输入对话框,请求密码,以便我们可以创建一个事务。

要从PySide2库导入小部件和类,请使用以下代码块:

from PySide2.QtWidgets import (QWidget,
                               QGridLayout,
                               QVBoxLayout,
                               QHBoxLayout,
                               QPushButton,
                               QLabel,
                               QInputDialog,
                               QLineEdit,
                               QToolTip,
                               QComboBox,
                               QApplication,
                               QSlider,
                               QSizePolicy)
from PySide2.QtCore import Slot, SIGNAL, QSize, Qt
from PySide2.QtGui import QPixmap, QMovie, QPalette, QColor
from os.path import isdir, exists
from os import mkdir
from tools.util import render_avatar
from blockchain import blockchain, SendTransaction
from wallet_threads.send_thread import SendThread
from wallet_threads.send_token_thread import SendTokenThread

我们还导入了其他东西,例如渲染化身的工具、与区块链交互的方法以及创建交易和检索令牌信息的线程类。

使用以下代码初始化SendWidget类:

class SendWidget(QWidget):

    tokens_file = 'tokens.json'

    def __init__(self, parent=None):
        super(SendWidget, self).__init__(parent)

        self.token_name = 'Ethereum'

        self.setupSenderSection()
        self.setupDestinationSection()
        self.setupTokenSection()
        self.setupProgressSection()
        self.setupSendButtonSection()
        self.setupFeeSection()

        self.send_thread = SendThread()
        self.send_thread.send_transaction.connect(self.sendTransactionFinished)
        self.send_token_thread = SendTokenThread()
        self.send_token_thread.send_token_transaction.connect(self.sendTransactionFinished)

        layout = QGridLayout()

        layout.addLayout(self.sender_layout, 0, 0)
        layout.addLayout(self.destination_layout, 0, 1)
        layout.addLayout(self.progress_layout, 1, 0, 1, 2, Qt.AlignCenter)
        layout.addLayout(self.token_layout, 2, 0)
        layout.addLayout(self.send_layout, 2, 1)
        layout.addLayout(self.slider_layout, 3, 0)

        self.setLayout(layout)

tokens_file保存tokens.json文件。此配置文件包含我们监视的所有 ERC20 令牌。token_name最初设置为Ethereum,因为默认情况下,我们的加密货币钱包应该处理以太坊交易,而不是 ERC20 代币。在这个小部件中,我们可以发送以太或自定义令牌。然后,我们调用六种方法来建立六个内部布局。此小部件由六个布局组成。发件人布局用于选择发件人的帐户。目的地布局是一个用于保存交易目的地帐户的字段。默认情况下隐藏的进度布局用于显示仅发送事务后仍在确认事务。令牌布局用于选择是发送 ERC20 令牌还是以太。此外,发送布局用于按住发送按钮,滑块布局用于按住滑块以选择交易费用。我们还创建了两个线程实例,第一个用于发送以太,第二个用于发送 ERC20 令牌。对于主布局,我们使用网格布局。之所以使用这种布局,是因为它更容易布置小部件。

以下代码块是一种方法,可用于设置发件人布局部分以创建事务小部件:

    def setupSenderSection(self):
        accounts = blockchain.get_accounts()

        sender_label = QLabel("Sender")
        sender_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)

        self.balance_label = QLabel("Balance: ")
        self.balance_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)

        self.avatar = QLabel()

        self.sender_combo_box = QComboBox()
        self.sender_items = []
        for account, balance in accounts:
            self.sender_items.append(account)
        self.sender_combo_box.addItems(self.sender_items)
        self.sender_combo_box.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        self.sender_combo_box.currentTextChanged.connect(self.filterSender)

        first_account = self.sender_items[0]
        self.filterSender(first_account)
        self.setAvatar(first_account, self.avatar)

        self.sender_layout = QVBoxLayout()
        sender_wrapper_layout = QHBoxLayout()
        sender_right_layout = QVBoxLayout()
        sender_right_layout.addWidget(sender_label)
        sender_right_layout.addWidget(self.sender_combo_box)
        sender_right_layout.addWidget(self.balance_label)
        sender_wrapper_layout.addWidget(self.avatar)
        sender_wrapper_layout.addLayout(sender_right_layout)
        sender_wrapper_layout.addStretch()

        self.sender_layout.addLayout(sender_wrapper_layout)
        self.sender_layout.addStretch()

在这里,您有一个组合框来选择本地帐户、化身图像和余额标签。如果您更改组合框的值,这将自动更改平衡标签上的文本和化身图像。

以下代码块是用于设置目标布局部分的方法:

    def setupDestinationSection(self):
        self.destination_layout = QVBoxLayout()

        destination_label = QLabel("Destination")
        destination_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)

        self.destination_line_edit = QLineEdit()
        self.destination_line_edit.setFixedWidth(380);
        self.destination_line_edit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)

        self.destination_layout.addWidget(destination_label)
        self.destination_layout.addWidget(self.destination_line_edit)
        self.destination_layout.addStretch()

此方法主要保存行编辑。您可以在此编辑行中粘贴或键入目标地址。

以下代码块是设置令牌布局部分的方法:

    def setupTokenSection(self):
        token_label = QLabel("Token")
        token_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)

        token_combo_box = QComboBox()

        tokens = blockchain.get_tokens()
        first_token = 'Ethereum'
        items = [first_token]
        self.token_address = {'Ethereum': '0xcccccccccccccccccccccccccccccccccccccccc'}
        self.token_informations = {}

        for address, token_from_json in tokens.items():
            token_information = blockchain.get_token_named_tuple(token_from_json, address)
            self.token_informations[token_information.name] = token_information
            self.token_address[token_information.name] = token_information.address
            items.append(token_information.name)

        self.amount_label = QLabel("Amount (in ethers)")

        token_combo_box.addItems(items)
        token_combo_box.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        token_combo_box.currentTextChanged.connect(self.filterToken)

        self.token_avatar = QLabel()

        self.filterToken(first_token)
        token_address = self.token_address[first_token]
        self.setAvatar(token_address, self.token_avatar)

        self.token_layout = QVBoxLayout()
        token_wrapper_layout = QHBoxLayout()
        token_right_layout = QVBoxLayout()
        token_right_layout.addWidget(token_label)
        token_right_layout.addWidget(token_combo_box)
        token_wrapper_layout.addWidget(self.token_avatar)
        token_wrapper_layout.addLayout(token_right_layout)
        token_wrapper_layout.addStretch()
        self.token_layout.addLayout(token_wrapper_layout)

此部分包含令牌的化身、选择以太坊或其他 ERC20 令牌的组合框,以及 ERC20 令牌的总供应量。如果我们更改 combobox 的值,它将更改化身和总供应标签。令牌的化身来自令牌智能合约的地址。但是,以太坊没有地址,因为它是平台本身。因此,对于以太坊,我们使用以下虚拟地址:0xcccccccccccccccccccccccccccccccccccccccc

以下代码块是用于设置进度布局部分的方法:

    def setupProgressSection(self):
        self.progress_layout = QHBoxLayout()
        progress_vertical_layout = QVBoxLayout()
        progress_wrapper_layout = QHBoxLayout()
        self.progress_label = QLabel()
        movie = QMovie('icons/ajax-loader.gif')
        self.progress_label.setMovie(movie)
        movie.start()
        self.progress_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        self.progress_description_label = QLabel()
        self.progress_description_label.setText("Transaction is being confirmed. Please wait!")
        self.progress_description_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        progress_wrapper_layout.addWidget(self.progress_label)
        progress_wrapper_layout.addWidget(self.progress_description_label)
        progress_vertical_layout.addLayout(progress_wrapper_layout, 1)
        self.progress_layout.addLayout(progress_vertical_layout)
        self.sendTransactionFinished()

基本上,这是一个用来表明交易正在确认的标签。在本节中,有一个标签用于显示装载活动指示器。首先,我们初始化接受gif文件的QMovie对象。然后,通过调用该标签的setMovie方法,将该Qmovie设置为标签。

以下代码块是设置小部件的发送布局部分以创建事务的方法:

    def setupSendButtonSection(self):
        self.send_layout = QVBoxLayout()
        self.amount_line_edit = QLineEdit()
        self.send_button = QPushButton("Send")
        self.send_button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        self.send_button.clicked.connect(self.sendButtonClicked)
        pal = self.send_button.palette()
        pal.setColor(QPalette.Button, QColor(Qt.green))
        self.send_button.setAutoFillBackground(True)
        self.send_button.setPalette(pal)
        self.send_button.update()
        self.send_layout.addWidget(self.amount_label)
        self.send_layout.addWidget(self.amount_line_edit)
        self.send_layout.addWidget(self.send_button)

此部分用于保持连接到回调的发送按钮。此发送按钮是定制的,通过使用背景色使其看起来更具吸引力。更改按钮颜色的方法很简单:

使用以下代码从按钮获取调色板对象,然后将颜色设置为该调色板对象:

        pal = self.send_button.palette()
        pal.setColor(QPalette.Button, QColor(Qt.green))

这里,我们使用预定义的颜色。

以下代码块用于创建滑块和标签,指示我们在滑块中选择的值:

    def setupFeeSection(self):
        self.slider_layout = QVBoxLayout()
        fee_label = QLabel("Fee")
        self.fee_slider = QSlider(Qt.Horizontal)
        self.fee_slider.setRange(1, 10)
        self.fee_slider.setValue(3)
        self.fee_slider.valueChanged.connect(self.feeSliderChanged)
        self.gwei_label = QLabel()
        self.feeSliderChanged(3)
        self.slider_layout.addWidget(fee_label)
        self.slider_layout.addWidget(self.fee_slider)
        self.slider_layout.addWidget(self.gwei_label)

滑块的用途是选择交易费用。如果您选择更高的费用,交易处理速度将更快。

以下代码块用于选择以太坊或 ERC20 令牌:

    def filterToken(self, token_name):
        address = self.token_address[token_name]
        token_information = None
        if token_name != 'Ethereum':
            token_information = self.token_informations[token_name]
            self.amount_label.setText("Amount")
        else:
            self.amount_label.setText("Amount (in ethers)")
        self.updateBalanceLabel(token_name, self.sender_account, token_information)
        self.setAvatar(address, self.token_avatar)
        self.token_name = token_name

这是在更改令牌组合框的值时将执行的回调。我们在这里更新以太余额或帐户令牌。完成此操作后,我们更改令牌的化身。我们还更新代币的总供应量。

以下代码块用于选择发件人帐户:

    def filterSender(self, account_address):
        self.sender_account = account_address
        token_information = None
        if self.token_name != 'Ethereum':
            token_information = self.token_informations[self.token_name]
        self.updateBalanceLabel(self.token_name, account_address, token_information)
        self.setAvatar(account_address, self.avatar)

这是如果我们更改 sender 组合框的值将执行的回调。在这里,我们更新帐户的以太或代币余额,然后根据地址更改帐户的化身。

以下代码块是用于将帐户余额设置为标签的方法:

    def updateBalanceLabel(self, token_name, account_address, token_information=None):
        if token_name == 'Ethereum':
            self.balance_label.setText("Balance: %.5f ethers" % blockchain.get_balance(account_address))
        else:
            self.balance_label.setText("Balance: %d coins" % blockchain.get_token_balance(account_address, token_information))

在这个updateBalanceLabel方法中,如果我们正在使用以太坊,我们将使用blockchain对象中的get_balance方法为balance_label设置文本。如果我们使用的是 ERC20 代币,我们将使用blockchain中的get_token_balance方法。

以下代码块是用于设置化身的方法:

    def setAvatar(self, code, avatar):
        img_filename = render_avatar(code)
        pixmap = QPixmap(img_filename)
        avatar.setPixmap(pixmap)

此方法用于设置令牌和帐户的化身。

以下代码块是更改费用滑块值时将执行的回调:

    def feeSliderChanged(self, value):
        self.gwei_label.setText("%d GWei" % value)
        self.fee = value

以下代码块是我们单击“发送”按钮时将执行的方法:

    def sendButtonClicked(self):
        password, ok = QInputDialog.getText(self, "Create A New Transaction",
                 "Password:", QLineEdit.Password)
        if ok and password != '':
            self.progress_label.setVisible(True)
            self.progress_description_label.setVisible(True)
            tx = SendTransaction(sender=self.sender_account,
                                 password=password,
                                 destination=self.destination_line_edit.text(),
                                 amount=self.amount_line_edit.text(),
                                 fee=self.fee)
            token_information = None
            if self.token_name != 'Ethereum':
                token_information = self.token_informations[self.token_name]
                self.send_token_thread.prepareTransaction(tx, token_information)
                self.send_token_thread.start()
            else:
                self.send_thread.prepareTransaction(tx)
                self.send_thread.start()

在这里,我们将被要求在输入对话框中提供密码。如果我们点击 Ok,那么我们将把进度标签和加载活动指示器设置为可见。我们构造一个名为SendTransaction的元组,然后将其发送到线程类对象,这些对象处理以太坊或 ERC20 令牌的发送事务。最后,我们运行线程。

以下代码块用于在事务完成时隐藏进度标签(加载指示器):

    def sendTransactionFinished(self):
        self.progress_label.setVisible(False)
        self.progress_description_label.setVisible(False)

此方法将在完成作业后由线程实例调用(通过发送以太或硬币作为 ERC20 令牌)。

最后一个小部件是令牌小部件。此小部件负责监视 ERC20 令牌。在wallet_widgets目录中创建token_widget.py。转到本节中为完整代码文件提供的以下 GitLab 链接:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/tree/master/chapter_09/wallet/wallet_widgets

最后一个小部件位于主小部件的第三个选项卡中。这里的目的是监视 ERC20 令牌并列出所有已监视的 ERC20 令牌。有一个按钮用于启动输入对话框,有一个按钮用于询问 ERC20 智能合约令牌的地址,然后有一个垂直布局,显示所有 ERC20 令牌:

from PySide2.QtWidgets import (QWidget,
                               QGridLayout,
                               QVBoxLayout,
                               QHBoxLayout,
                               QPushButton,
                               QLabel,
                               QInputDialog,
                               QLineEdit,
                               QToolTip,
                               QComboBox,
                               QApplication,
                               QSlider,
                               QSizePolicy)
from PySide2.QtCore import Slot, SIGNAL, QSize, Qt
from PySide2.QtGui import QPixmap, QMovie, QPalette, QColor
from os.path import isdir, exists
from os import mkdir
from time import sleep
import json
from tools.util import render_avatar
from blockchain import blockchain, SendTransaction, TokenInformation

像往常一样,我们导入了很多东西,比如渲染化身工具、用于从区块链建立令牌相关信息的区块链对象,以及用于处理文件系统的大量库。除此之外,我们还从PySide2导入 UI 类,例如许多类型的小部件,以及将回调附加到小部件的类。除了 UI 类之外,我们还从 PySide2 导入非 UI 类,例如slotsignal

将以下代码块用于初始化方法:

class TokenWidget(QWidget):

    tokens_file = 'tokens.json'

    def __init__(self, parent=None):
        super(TokenWidget, self).__init__(parent)

        self.watch_token_button = QPushButton("Watch Token")

        tokens = blockchain.get_tokens()

...
...

        self.watch_token_button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        self.connect(self.watch_token_button, SIGNAL('clicked()'), self.watchNewToken)

        layout.addWidget(self.watch_token_button, 0, 0)
        layout.addLayout(self.tokens_layout, 1, 0)

        self.setLayout(layout)

在这个初始化方法中,我们创建一个链接到watchNewToken方法的按钮,然后创建一个垂直框布局来保存所有令牌信息。我们还声明了持有tokens.json配置文件的tokens_file对象。此文件跟踪所有 ERC20 令牌相关信息。

使用以下代码块为每条令牌信息创建化身图像、令牌名称标签、令牌符号标签和令牌总供应标签:

    def _addTokenToWindow(self, token_information, resize_parent=False):
        wrapper_layout = QVBoxLayout()
        token_layout = QHBoxLayout()
        rows_layout = QVBoxLayout()
        token_label = QLabel(token_information.name)
...
...
        if resize_parent:
            sizeHint = self.size()
            self.parentWidget().parentWidget().resize(QSize(sizeHint.width(), sizeHint.height() + 100))

如果resize_parent为真,这意味着我们通过对话框添加令牌信息。换句话说,我们要求父窗口增加其高度。如果resize_parentfalse,则表示从一开始就调用此方法。

以下代码块是用于通过对话框请求智能合约地址的方法:

    @Slot()
    def watchNewToken(self):
        address, ok = QInputDialog.getText(self, "Watch A New Token",
                 "Token Smart Contract:", QLineEdit.Normal)
        if ok and address != '':
            token_information = blockchain.get_information_of_token(address)
            self._addTokenToWindow(token_information, resize_parent=True)
            token_data = {}
            if exists(self.tokens_file):
                with open(self.tokens_file) as json_data:
                    token_data = json.load(json_data)
            token_data[token_information.address] = {'name': token_information.name,
                                                     'symbol': token_information.symbol,
                                                     'total_supply': token_information.totalSupply}
            with open(self.tokens_file, 'w') as outfile:
                json.dump(token_data, outfile)

如果用户确认智能合约的地址,我们使用blockchain对象的get_information_of_token方法获取令牌信息。然后将该令牌的信息放入垂直框布局中。稍后,我们将令牌的信息保存在 json 文件中。这样做是为了在重新启动应用时可以加载此令牌信息。

在启动加密货币钱包之前,请确保先运行专用链,然后在此专用链中部署一个或两个 ERC20 智能合约。您可以使用第 8 章以太坊创建令牌中的 ERC20 智能合约源代码。执行此操作后,使用以下命令运行桌面加密货币钱包:

(wallet-venv) $ python wallet.py

您将获得以下屏幕截图所示的最终输出:

在前面的屏幕截图中,我们看到 Account 选项卡显示每个帐户的余额。请确保您至少有两个帐户。如果没有,请单击“创建帐户”按钮从此选项卡创建一个帐户。

以下屏幕截图显示了“发送”选项卡,我们可以在其中将以太发送到我们选择的任何帐户:

在第二个选项卡中,尝试发送以太。交易确认前需要一段时间。因此,请尝试将 ERC20 令牌发送到另一个帐户(但必须首先在第三个选项卡中添加 ERC20 令牌),如以下屏幕截图所示:

最后,在第三个选项卡中,尝试观看令牌智能合约。单击“监视令牌”按钮时,将智能合约地址的地址放入对话框:

您的令牌将反映在第二个选项卡中。

让我们为这个 GUI 应用编写测试。这些测试不应详尽无遗。我们将创建三个测试,每个选项卡一个。我们不会为应用的非 UI 部分创建测试。本节只是演示如何测试 UI 应用。

第一个选项卡的第一个测试是帐户小部件测试。将测试命名为test_account.py并保存在tests目录中,下面的代码块就是测试脚本:

import sys, os
sys.path.append(os.path.realpath(os.path.dirname(__file__)+"/.."))

from wallet import WalletWidget
from PySide2.QtWidgets import QInputDialog
from PySide2 import QtCore

def test_account(qtbot, monkeypatch):
    wallet = WalletWidget()
    qtbot.addWidget(wallet)

    old_accounts_amount = wallet.account_widget.accounts_layout.count()

    monkeypatch.setattr(QInputDialog, 'getText', lambda *args: ("password", True))
    qtbot.mouseClick(wallet.account_widget.create_account_button, QtCore.Qt.LeftButton)

    accounts_amount = wallet.account_widget.accounts_layout.count()
    assert accounts_amount == old_accounts_amount + 1

    wallet.killThreads()

在这个测试中,我们在单击按钮、启动对话框、填写密码,然后单击确定之前,测试 accounts 布局有多少子级。然后,我们在创建新帐户后再次检查子帐户的数量。这个数字应该增加一个。对于此测试,我们修补对话框以使其更易于测试。

This test is not comprehensive. We did not test the fail case. I will leave that as an exercise for the reader.

第二个选项卡的测试是发送事务小部件测试。将测试文件命名为test_send.py并保存在tests目录中。测试脚本在下面的代码块中给出(完整代码参见下面 GitLab 链接中的代码文件:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_09/wallet/tests/test_send.py

import sys, os
sys.path.append(os.path.realpath(os.path.dirname(__file__)+"/.."))
from time import sleep

from wallet import WalletWidget
from PySide2.QtWidgets import QInputDialog
from PySide2 import QtCore

...
...

    qtbot.keyClicks(wallet.send_widget.sender_combo_box, second_account)
    balance_of_second_account = int(float(wallet.send_widget.balance_label.text().split()[1]))

    assert balance_of_second_account - old_balance_of_second_account == 10

    wallet.killThreads()

在这个测试中,我们在组合框中检查第二个帐户的余额。第二个帐户将是目标帐户。在这里,我们读取标签上的余额,然后将组合框的值更改回第一个帐户,即发送者。之后,我们将目标帐户的地址设置为目标行编辑。然后,我们在金额行编辑中设置以太数量,并单击发送按钮,但请记住,我们需要修补输入对话框。最后,我们等待大约 20 秒,然后再次将 account combobox 的值更改为第二个帐户。我们从标签中检索余额,然后比较旧值和新值之间的差值,即 10 以太。

第三个选项卡的测试用于测试令牌小部件。将其命名为test_token.py并保存在tests目录中。此测试的测试脚本在以下代码块中给出:

import sys, os
sys.path.append(os.path.realpath(os.path.dirname(__file__)+"/.."))

from wallet import WalletWidget
from PySide2.QtWidgets import QInputDialog
from PySide2 import QtCore

def test_token(qtbot, monkeypatch):
    wallet = WalletWidget()
    qtbot.addWidget(wallet)

    old_tokens_amount = wallet.token_widget.tokens_layout.count()

    address = None
    with open('address.txt') as f:
        address = f.readline().rstrip()

    monkeypatch.setattr(QInputDialog, 'getText', lambda *args: (address, True))
    qtbot.mouseClick(wallet.token_widget.watch_token_button, QtCore.Qt.LeftButton)

    tokens_amount = wallet.token_widget.tokens_layout.count()
    assert tokens_amount == old_tokens_amount + 1

    wallet.killThreads()

首先,我们在address.txt文件中加载令牌智能合约的地址,因为我们不想在测试文件中硬编码它。该策略与 account 小部件测试中的策略相同。我们检查垂直框布局有多少子项。完成后,我们单击按钮,启动一个对话框,填写智能合约的地址,然后单击确定。接下来,我们再次检查垂直框布局有多少子项。这个数字应该再增加 1。

Like I said, this test is actually not complete. We should test the token information as well. However, this test is a good start.

可以使用以下命令运行前面的测试:

(wallet-venv) $ pytest tests

您现在已经创建了桌面加密货币钱包。然而,这个钱包还不完整。加密货币钱包是一个巨大的主题,它的变化如此频繁,以至于一本书可以单独就这个主题编写。您还可以在加密货币钱包应用中实现其他功能,例如此交易已确认的区块数。在我们的应用中,我们只等待一个事务,但是一些用户可能希望先确认几个块。如果仅用一个区块确认交易,则有可能用更长的区块替换交易。但是,在 12 个块之后,块中的事务非常安全且不可逆,如下链接所述:https://ethereum.stackexchange.com/questions/319/what-number-of-confirmations-is-considered-secure-in-ethereum

我们的加密货币钱包是一款纯加密货币钱包。但是,您也可以向我们的加密货币钱包添加与钱包功能无关的其他功能。例如,Mist cryptocurrency 钱包不仅仅是一个钱包;它也是一个分散的应用浏览器。它还可以编译智能合约的源代码并将其部署到区块链。

如果你想制作一个成熟的加密货币钱包,你应该实现很多功能。许多想法包括生成二维码、导出加密私钥的选项、导入私钥、使用种子短语生成帐户、验证输入以及短时间记住密码。

在这里,我们正在构建一个桌面加密货币钱包。桌面应用可以拥有大量的内存和存储空间。但是,如果您正在构建一个移动加密货币钱包,则情况就不同了。例如,比特币桌面加密货币钱包可以在本地访问完整节点。然而,你不能在手机上放置一个完整的比特币节点,因为它太大了。当然,您可以将完整的比特币节点放在云端,让移动加密货币钱包应用访问该节点。然而,大多数人不想在云上设置完整的节点。因此,比特币移动加密货币钱包的开发者通常使用简化支付验证SPV)。这样,比特币移动加密货币钱包就不需要在手机上存储完整的比特币节点。

如果你想建立一个加密货币钱包或为现有的加密货币钱包捐款,你需要记住两件事:安全性和用户体验UX

加密货币钱包处理货币,因此您需要确保其安全。安全是一个复杂的话题,我们将在这里简要讨论。

不要仅仅因为你可以安装第三方库;每个库都是另一个向量攻击。在应用中绑定第三方库时要谨慎。我们的加密货币钱包使用来自以太坊 GitHub 的库,如 web3.py 和 Populus。这应该没问题,因为它们是核心库。我们也使用 Qt 公司的PySide2库。这个库是必须的,因为没有 GUI 库,就不可能有 GUI 应用。我们还使用第三方库生成 identicon 化身图像。我们在这里需要小心。该库是一个单独的文件,我已经完全阅读了它,以确保没有隐藏的恶意软件。因此,我可以自信地将其集成到我们的应用中。

在声明交易已完成之前,使用最少数量的确认。多少确认足够好取决于您的威胁和风险建模。通常情况下,12 次确认会使撤销交易变得不切实际。Mist 钱包使用 12 次确认,而 ZCash 钱包使用 10 次确认。

您还可以在加密货币钱包中创建帐户时强制用户创建好密码,因为大多数用户都倾向于使用坏密码创建帐户。但是在这里要小心;你不想太烦他们。

如果一个应用非常安全,但很难使用,那么它是没有用的。因此,我们需要减少对用户的威胁。比特币的创造者中本聪(Satoshi Nakamoto)在构建该软件时对用户体验进行了大量思考。以前,人们使用 base64 格式将二进制文件转换为文本。然而,Satoshi 使用 base58 表示比特币地址。Base58 与 base64 类似,但没有在打印时引起混淆的字符,如 I(大写字母 I)和 l(小写字母 l)。

Zcash 发布了一份密码货币钱包设计的用户体验指南,可在以下链接中找到:https://zcash.readthedocs.io/en/latest/rtd_pages/ux_wallet_checklist.html 。并不是所有的东西都可以在这里实现,因为 Zcash 有一个以太坊没有的私有事务。但是,其他建议可以实施;例如,市场信息。不管喜欢与否,人们将加密货币的价格与法定货币挂钩,向人们展示 1 乙醚的市场价格是一个好主意。如果网络拥挤,您也应该通知用户。您可以建议用户等待或增加交易费用。

如果您构建 iOS 加密货币钱包,则应遵循苹果人机界面指南。如果您正在构建 Android 加密货币钱包,则应遵循材料设计指南。选择字体和颜色时要小心。在设计加密货币钱包时,您应该进行用户访谈。用户体验是一个广泛的主题。平衡用户体验和安全性是一门微妙的艺术。构建加密货币钱包时,不应忽略用户体验。

在本章中,我们已经熟悉了PySide2的选项卡式视图、大小策略和网格布局。然后,我们还学习了如何测试 Qt 应用。接下来,我们开始构建一个桌面加密货币钱包。我们将应用分为许多部分:区块链、线程、小部件、identicon 工具和测试。加密货币钱包的区块链部分基于web3Populus库,其目的是在区块链中读取和创建交易。创建交易时,线程是 UI 部分和区块链对象之间的中间人。identicon 工具用于基于特定字符串(通常是帐户地址或令牌智能合约地址)创建化身图像。小部件部分是一个有三个选项卡的选项卡式小部件。第一个选项卡是 account 小部件,第二个选项卡是 sending transaction 小部件,第三个选项卡是 token 小部件。最后,我们为这个应用创建了测试。

在下一章中,我们将开始学习一个超出区块链技术范围的主题。这种技术称为 IPFS。它仍然是分散技术的一部分,但该技术将克服与区块链技术相关的弱点;换句话说,它的存储是昂贵的。**

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

代码精进之路 -〔范学雷〕

消息队列高手课 -〔李玥〕

RPC实战与核心原理 -〔何小锋〕

检索技术核心20讲 -〔陈东〕

编译原理实战课 -〔宫文学〕

Redis核心技术与实战 -〔蒋德钧〕

手把手带你写一个Web框架 -〔叶剑峰〕

技术领导力实战笔记 2022 -〔TGO 鲲鹏会〕

手把手教你落地DDD -〔钟敬〕