Python-区块链开发实用指南(三)

187 阅读32分钟

Python 区块链开发实用指南(三)

原文:zh.annas-archive.org/md5/E6FBF7D7A6EED49747FB2B635A55F938

译者:飞龙

协议:CC BY-NC-SA 4.0

第三部分:前端去中心化应用程序

本节是一个实践性的学习指南,教你如何用桌面前端构建类似 Twitter 的应用程序。

接下来的章节将在本节中介绍:

  • 第七章,前端去中心化应用程序

第七章:前端分散式应用程序

在本章中,我们将学习如何编写具有 GUI 前端的分散式应用程序。这是一个桌面分散式应用程序。人们可以使用按钮和文本字段与智能合约进行交互。这比 Truffle 控制台或web3.py脚本更进一步。在编写此 GUI 前端时,我们将学习处理智能合约的最佳实践,包括如何处理延迟以及如何在分散式应用程序中提供附加值。

在本章中,我们将学习以下主题:

  • 设置 Qt GUI 库

  • 安装 Qt for Python

  • Qt for Python 速成课程

  • 为类似 Twitter 的应用程序编写智能合约

  • 为类似 Twitter 的应用程序构建 GUI

设置 Qt GUI 库

Qt 库是一个设计用于创建多平台应用程序的 C++框架。在 Linux 中,该库是 KDE 桌面的基础。它的对应物是 GTK+库,是 Gnome 桌面的基础。然而,Qt 库已经提升了自己的游戏。您可以使用 Qt 库在 Android 和 iOS 上创建应用程序。有时人们错误地认为 Qt 库只能创建 GUI 应用程序。然而,Qt 库还包括非 GUI 库,例如数据存储、多媒体和网络。Qt 库甚至包含一个线程库!过去,您可以免费使用 Qt 库创建开源应用程序。但是,如果您想使用 Qt 库创建专有应用程序,您必须支付许可费。现在,在使用 Qt 库时,您可以有更多的灵活性。

您可以免费使用 Qt 库创建专有应用程序,但只有在为 Qt 库提供重新链接机制、提供许可证副本并明确承认 Qt 使用等情况下才可以。详情请参阅www.qt.io/download

选择 GUI 库

Python 有大量可供选择的 GUI 库。其中包括 Tkinter、Phoenix(wxPython)、PyGObject、PyQt、Qt for Python(PySide2)等。决定为 Python 应用程序使用哪个 GUI 库有时是主观的。

Tkinter 是一个 Python GUI 库,但 UI 不够流畅和精致。PyGObject 只有在您想要在 Linux 中编写桌面应用程序时才是一个不错的选择,因为它使用 GTK+。在 Windows 和 Mac 中也支持 GTK+,但不是本地支持。

我将选择 Qt for Python(www.qt.io/qt-for-python)库。这是 Qt 自己的官方 Python 绑定 Qt 库。不过要小心,还有另一家公司提供的 Qt 库的 Python 绑定。这个库叫做 PyQt,公司是 Riverbank。这两个库都支持最新的 Qt 库,即 Qt 5。

这两个库之间存在差异。如果您想使用 PyQt 编写和销售专有应用程序,必须从 Riverbank 购买许可证。然而,对于 Qt for Python,您不需要这样做,尽管它有自己的一套限制。例如,您必须为 Qt 库提供重新链接机制。有关 PyQt 的书籍已经出版,但在撰写本书时,Qt for Python 还没有相关书籍。

以前,可靠的 Python 绑定 Qt 只能从 Riverbank 获得。然而,在今年中期,Qt 宣布了其最新 Qt 库(Qt 5.11)的 Python 绑定。Qt 非常重视推广这个 Python 绑定。

安装 Qt for Python

首先,您必须安装 Qt 5.11。转到www.qt.io/download。在下载库时,您有两种许可证可供选择——商业和开源。在这种情况下,您必须选择开源选项。

  1. 如果你在 Linux 平台上,你将下载以下文件:qt-unified-linux-x64-3.0.5-online.run

这是一个安装程序。如果你运行它,你会看到以下屏幕:

  1. 点击下一步。然后你将会看到一个登录屏幕。如果你还没有账户,创建一个;这是免费的。之后,通过以下屏幕登录:

  1. 然后,你会看到安装程序的欢迎屏幕。从这里,点击下一步:

  1. 指定你想要安装 Qt 库的位置:

  1. 然后选择 Qt 5.11.2 或最新的稳定版本:

安装程序完成工作之前还需要一段时间。

  1. 一旦 Qt 5 安装过程完成,你可以使用以下命令行安装 Python 绑定到 Qt:
$ python3.6 -m venv qt-venv
$ source qt-venv/bin/activate
(qt-venv) $ pip install PySide2 
  1. 我们将使用这个虚拟环境编写一个智能合约和一个去中心化应用程序。我们需要像往常一样安装populusvyperweb3库:
(qt-venv) $ pip install eth-abi==1.2.2
(qt-venv) $ pip install eth-typing==1.1.0
(qt-venv) $ pip install py-evm==0.2.0a33
(qt-venv) $ pip install web3==4.7.2
(qt-venv) $ pip install -e git+https://github.com/ethereum/populus#egg=populus
(qt-venv) $ pip install vyper
(qt-venv) $ mkdir twitter_like_project
(qt-venv) $ cd twitter_like_project
(qt-venv) $ mkdir tests contracts
(qt-venv) $ cp ../qt-venv/src/populus/populus/assets/defaults.v9.config.json project.json
  1. 将 Vyper 支持添加到project.json中,方法是将"compilation"键的值更改为以下内容:
"compilation": {
    "backend": {
      "class": "populus.compilation.backends.VyperBackend"
    },
    "contract_source_dirs": [
      "./contracts"
    ],
    "import_remappings": []
},

Vyper 的最新版本是 0.1.0b6,它破坏了 Populus。开发者需要一些时间来修复它。如果在你阅读本书时错误还没有被修复,你可以自己修补 Populus。

  1. 使用以下命令检查错误是否已经被修复:
(qt-venv) $ cd qt-venv/src/populus
(qt-venv) $ grep -R "compile(" populus/compilation/backends/vyper.py
 bytecode = '0x' + compiler.compile(code).hex()
 bytecode_runtime = '0x' + compiler.compile(code, bytecode_runtime=True).hex()

在我们的情况下,错误尚未被修复。

  1. 因此,让我们使用以下命令行修补 Populus 以修复错误。确保你仍然在同一个目录下(qt-venv/src/populus):
(qt-venv) $ wget https://patch-diff.githubusercontent.com/raw/ethereum/populus/pull/484.patch
(qt-venv) $ git apply 484.patch
(qt-venv) $ cd ../../../ 

Qt for Python 速成课程

让我们使用我们的 GUI 库编写一个简单的应用程序:

  1. 创建一个名为hello.py的文件:
import sys
from PySide2.QtWidgets import QApplication, QWidget

app = QApplication(sys.argv)
window = QWidget()
window.resize(400, 400)
window.show()
sys.exit(app.exec_())
  1. 然后使用以下命令运行它:
(qt-venv) $ python hello.py

现在你会看到一个空白窗口:

让我们通过这个文件更好地理解 Qt for Python:

import sys
from PySide2.QtWidgets import QApplication, QWidget

sys导入自标准 Python 库。这是必需的,因为我们想要在启动这个 GUI 应用程序脚本时从命令行获取参数。然后我们从PySide2.QtWidgets中导入QApplicationQWidget

什么是PySide2?它来自PySide,这是 Qt 4 的 Python 绑定。PySide2是 Qt 5 的 Python 绑定。PySide于 2009 年由 Nokia(Qt 的前任所有者)发布。此前,诺基亚未能与 Riverbank 达成PyQt许可证 LGPL 的协议。因此,诺基亚决定创建自己的 Python 绑定用于 Qt,并将其命名为PySide。Qt 的所有权从诺基亚转移到了 Qt 公司。Qt 公司决定加强他们的努力来开发这个 Python 绑定用于 Qt,特别是在 Qt 5 发布后。

我们从PySide2.QtWidgets中导入的两个类是QApplicationQWidget

  • QApplication是一个设计用来管理 GUI 应用程序流程和其设置的类。它检查你的桌面配置,比如字体,并将其传递给 GUI。它还理解来自桌面的传入对象,例如当你从文本编辑器复制文本并粘贴到 GUI 应用程序时。在你的 GUI 脚本中只能有一个QApplication
app = QApplication(sys.argv)

我们创建一个QApplication的实例,并传递命令行参数。大多数情况下,你不会使用任何命令行参数。例如,如果你想要告诉 GUI 应用程序使用不同的样式或从右到左显示文本以满足阿拉伯用户的需求,你可以传递命令行参数。

  • 我们从PySide2.QtWidgets中导入的第二个类是QWidget。这是你在创建 GUI 应用程序时将要使用的任何小部件的基类,比如按钮、文本字段、滑块或标签。如果你构造基类,你会得到一个空窗口。这类似于 iOS 中的 UIView。然后我们调整窗口大小:
window = QWidget()
window.resize(400, 400)

使用window.show()方法,我们显示了窗口对象,然后使用app.exec_()进入了主循环。这是QApplication将所有来自桌面的事件分发到 GUI 的地方。我们使用sys.exit()将这个过程包装起来,这样我们就可以从QApplication中获取返回代码:

window.show()
sys.exit(app.exec_())

布局

在添加另一个小部件之前,我们必须了解布局的概念。我们将使用两种布局,即QHBoxLayoutQVBoxLayout。这两种布局足以创建一个 GUI 应用程序。还有其他布局,比如QGridLayoutQFormLayout等等,但我们不需要它们。QHBoxLayoutQVBoxLayout就像 CSS 中的 flexbox。你可以将小部件放在使用QHBoxLayout的容器中,然后所有小部件将被放在一条水平线上。让我们看一个例子。将此脚本命名为hello_horizontal_layout.py

import sys
from PySide2.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton, QLabel

app = QApplication(sys.argv)

hello_button = QPushButton('Hello')
very_label = QLabel('Very Very')
beautiful_button = QPushButton('Beautiful')
world_label = QLabel('World')

layout = QHBoxLayout()
layout.addWidget(hello_button)
layout.addWidget(very_label)
layout.addWidget(beautiful_button)
layout.addWidget(world_label)

window = QWidget()
window.setLayout(layout)
window.resize(200, 200)
window.show()

sys.exit(app.exec_())

我将逐行解释代码。我们首先导入库:

import sys
from PySide2.QtWidgets import QApplication, QWidget, QHBoxLayout, QPushButton, Qlabel

除了我们在之前的脚本中导入的类之外,我们还导入了QHBoxLayout(水平布局)和两个小部件(QPushButtonQLabel):

app = QApplication(sys.argv)

hello_button = QPushButton('Hello')
very_label = QLabel('Very Very')
beautiful_button = QPushButton('Beautiful')
world_label = Qlabel('World')

我们初始化了QApplication,然后是四个小部件(两个按钮和两个标签)。QPushButtonQLabel的第一个参数是它们的标签字符串。在这里,我们只是构建了四个小部件,但是还没有显示它们:

layout = QHBoxLayout()
layout.addWidget(hello_button)
layout.addWidget(very_label)
layout.addWidget(beautiful_button)
layout.addWidget(world_label)

上面的代码使用QHBoxLayout类构建了水平布局。然后我们将填充布局,使用我们之前构建的四个小部件。在这种情况下,hello_button将是布局中最左边的小部件,而world_label将是最右边的小部件。然后,我们构建了一个window对象,并告诉它使用我们的水平布局:

window = QWidget()
window.setLayout(layout)
window.resize(200, 200)

这个语句将把我们的小部件放在窗口内:

window.show()

sys.exit(app.exec_())

然后,我们显示窗口,执行QApplication实例,并使用以下命令运行它:

(qt-venv) $ python hello_horizontal_layout.py

然后你将会看到以下结果,从左到右显示了四个小部件:

要获得垂直布局,创建另一个脚本并将其命名为hello_vertical_layout.py

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

app = QApplication(sys.argv)

hello_button = QPushButton('Hello')
very_label = QLabel('Very Very')
beautiful_button = QPushButton('Beautiful')
world_label = QLabel('World')

layout = QVBoxLayout()
layout.addWidget(hello_button)
layout.addWidget(very_label)
layout.addWidget(beautiful_button)
layout.addWidget(world_label)

window = QWidget()
window.setLayout(layout)
window.resize(200, 200)
window.show()

sys.exit(app.exec_())

在这里,你可以使用QVBoxLayout而不是HBoxLayout来获得垂直布局。使用以下命令运行脚本:

(qt_venv) $ python hello_vertical_layout.py

然后你将会看到四个小部件从上到下显示在窗口中:

如果你想要结合水平和垂直布局,你可以将水平布局嵌入到垂直布局中,反之亦然。为此,请创建一个名为hello_vertical_horizontal_layout.py的脚本。请参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_07/crash_course_qt_for_python/hello_vertical_horizontal_layout.py

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

app = QApplication(sys.argv)

hello_button = QPushButton('Hello')
very_label = QLabel('Very Very')
beautiful_button = QPushButton('Beautiful')
world_label = QLabel('World')

...
...

window = QWidget()
window.setLayout(horizontal_layout)
window.resize(200, 200)
window.show()

sys.exit(app.exec_())

这里需要注意的是,你需要使用addLayout方法从一个布局中添加一个嵌套布局:

vertical_layout = QVBoxLayout()
vertical_layout.addWidget(vertical_hello_button)
vertical_layout.addWidget(vertical_very_label)
vertical_layout.addWidget(vertical_beautiful_button)
vertical_layout.addWidget(vertical_world_label)

horizontal_layout = QHBoxLayout()
horizontal_layout.addWidget(hello_button)
horizontal_layout.addWidget(very_label)
horizontal_layout.addLayout(vertical_layout)

在这里要注意!你使用addLayout方法将一个布局嵌入到另一个布局中,而不是addWidget

运行以下脚本以查看嵌套布局:

(qt-venv) $ python hello_vertical_horizontal_layout.py

执行此操作将会给你以下的屏幕:

其他小部件

让我们创建一个脚本来展示更多种类的小部件。首先,将脚本命名为hello_varieties.py。请参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_07/crash_course_qt_for_python/hello_varieties.py

脚本的内容可以在以下代码块中查看:

import sys
from PySide2.QtWidgets import (QApplication,
                               QWidget,
                               QVBoxLayout,
                               QHBoxLayout,
                               QGroupBox,
                               QPushButton,
                               QLabel,
                               QSpinBox,
                               QLineEdit,
                               QRadioButton,
                               QComboBox)

...
...

window = QWidget()
window.setLayout(layout)
window.show()

sys.exit(app.exec_())

让我们逐行解析代码。我们首先使用以下行导入库:

import sys
from PySide2.QtWidgets import (QApplication,
                               QWidget,
                               QVBoxLayout,
                               QHBoxLayout,
                               QGroupBox,
                               QPushButton,
                               QLabel,
                               QSpinBox,
                               QLineEdit,
                               QRadioButton,
                               QComboBox)

在这里,我们导入了许多新类型的小部件,例如QGroupBoxQSpinBoxQLineEditQRadioButtonQComboBox。Qt 中有很多小部件,我们只是没有时间讨论所有的小部件。然后,我们初始化所有小部件:

button = QPushButton('Button')
label = QLabel('Label')
spinbox = QSpinBox()
lineedit = QLineEdit()
radio_button1 = QRadioButton('Option 1')
radio_button2 = QRadioButton('Option 2')
radio_button3 = QRadioButton('Option 3')
combo_box = QComboBox()
combo_box.addItems(["Bitcoin", "Ethereum", "Monero", "Ripple"])

这些是新类型的小部件。让我们讨论这些新小部件及其独特属性。QSpinBox是一个用于选择数字的框。您可以通过单击上箭头或下箭头来旋转此小部件。QLineEdit是一个文本字段,而QRadioButton是一个单选按钮,QComboBox是一个选择框。然后,我们初始化水平布局和垂直布局:

vlayout = QVBoxLayout()
vlayout.addWidget(button)
vlayout.addWidget(radio_button1)
vlayout.addWidget(radio_button2)
vlayout.addWidget(radio_button3)
vlayout.addWidget(spinbox)

hlayout = QHBoxLayout()
hlayout.addWidget(lineedit)
hlayout.addWidget(label)
hlayout.addWidget(combo_box)

我们将一些小部件放入垂直布局中,并将其他一些小部件放入水平布局中。然后,我们需要将这些布局放入一个更大的容器中:

top_groupbox = QGroupBox('Top')
top_groupbox.setLayout(vlayout)
bottom_groupbox = QGroupBox('Bottom')
bottom_groupbox.setLayout(hlayout)

我们现在使用QGroupBox类创建了两个分组框。第一个分组框有一个垂直布局,而另一个分组框有一个水平布局。分组框类似于带有边框的容器。这类似于 HTML 中的<div>

layout = QVBoxLayout()
layout.addWidget(top_groupbox)
layout.addWidget(bottom_groupbox)

window = QWidget()
window.setLayout(layout)
window.show()

sys.exit(app.exec_())

为了容纳这两个分组框,我们需要另一个布局。在这里,我们使用垂直布局。其余部分与往常一样。我们构建一个窗口,给它一个布局,然后在启动QApplication实例之前显示它。

运行以下脚本以查看许多不同类型的小部件:

(qt_venv) $ python hello_varieties.py

然后,您将看到一个看起来像这样的屏幕:

回调

此时,我们已经创建了许多小部件和布局。现在我们需要给一个小部件一个工作要做,我的意思是当按钮被点击时我们想要按钮做什么。当前,如果点击按钮,按钮将不会执行任何操作。

让我们创建一个简单的脚本来解释如何为按钮的点击事件提供回调。将其命名为hello_connect_simple.py

脚本包含以下代码行:

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

app = QApplication(sys.argv)

hello_button = QPushButton("Hello")
world_label = QLabel("Sun")

layout = QVBoxLayout()
layout.addWidget(hello_button)
layout.addWidget(world_label)

def set_text_in_world_label():
    world_label.setText("World")

hello_button.connect(QtCore.SIGNAL('clicked()'), set_text_in_world_label)

window = QWidget()
window.setLayout(layout)
window.resize(200, 200)
window.show()

sys.exit(app.exec_())

让我们逐行查看这段代码:

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

除了您导入的通常库之外,您还必须导入QtCoreQtCore提供了小部件进行通信的一种方式。因此,如果您想要使按钮能够与标签通信,比如“在我被点击后,请将您的标签设置为此字符串”,您需要导入QtCore。然后创建小部件并将小部件放入布局中:

app = QApplication(sys.argv)

hello_button = QPushButton("Hello")
world_label = QLabel("Sun")

layout = QVBoxLayout()
layout.addWidget(hello_button)
layout.addWidget(world_label)

您实例化QApplication类,创建一个按钮和一个标签,然后将小部件放入垂直布局中。

def set_text_in_world_label():
    world_label.setText("World")
    hello_button.connect(QtCore.SIGNAL('clicked()'), set_text_in_world_label)

在上述代码行中,我们创建一个函数来设置标签中的文本。setTextQLabel的一个用于更改标签的方法。在下一行中,我们将按钮的clicked信号连接到此函数。这意味着如果我们点击QPushButtonset_text_in_world_label将被执行。QPushButton除了clicked之外还有其他信号,例如pressedreleased。然后,我们创建一个窗口小部件,并使用以下代码显示它:

window = QWidget()
window.setLayout(layout)
window.resize(200, 200)
window.show()

sys.exit(app.exec_())

运行脚本以测试我们创建的回调:

(qt-venv) $ python hello_connect.py

运行脚本后,您将得到以下屏幕:

然后,如果您点击按钮,标签小部件将更改文本:

按钮没有参数发送到回调函数。但是另一个小部件可能有参数发送到回调函数。

让我们创建一个脚本来说明这个选项。将脚本命名为hello_connect_param.py。脚本的内容可以在以下代码块中查看:

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

app = QApplication(sys.argv)

hello_line_edit = QLineEdit()
world_label = QLabel("")

layout = QVBoxLayout()
layout.addWidget(hello_line_edit)
layout.addWidget(world_label)

def set_world_label(text):
    world_label.setText(text.upper())
    hello_line_edit.textChanged.connect(set_world_label)

window = QWidget()
window.setLayout(layout)
window.resize(200, 200)
window.show()

sys.exit(app.exec_())

关注这些行,看看我们如何在小部件的回调中使用参数:

def set_world_label(text):
    world_label.setText(text.upper())
    hello_line_edit.textChanged.connect(set_world_label)

我们的回调函数有一个参数。行编辑小部件的textChanged信号有一个参数。当我们更改行编辑小部件中的文本时,文本将被发送到回调函数。

这行代码hello_line_edit.textChanged.connect(set_world_label)相当于hello_line_edit.connect(QtCore.SIGNAL('textChanged(QString)'), set_world_label)

运行脚本以测试具有参数的回调:

(qt_venv) $ python hello_connect_param.py

当您在行编辑中输入时,标签小部件中的文本也会更改:

我们使用回调是因为我们希望在将文本设置到标签小部件之前对其进行操作。但是,如果你想直接将文本参数从行编辑小部件发送到标签小部件,你不必创建一个专门的函数,你可以使用“信号”和“槽”。

为此,删除回调,然后将line hello_line_edit.textChanged.connect(set_world_label)更改为hello_line_edit.connect(QtCore.SIGNAL('textChanged(QString)'), world_label, QtCore.SLOT('setText(QString)'))

当你在行编辑中输入文本时,你的文本将直接显示在标签中。

你也可以将那一行更改为QtCore.QObject.connect(hello_line_edit, QtCore.SIGNAL('textChanged(QString)'), world_label, QtCore.SLOT('setText(QString)'))

我希望你能看到这里的模式。如果小部件 A 想要与小部件 B 通信,小部件 A 将使用“信号”与小部件 B 的“槽”连接。当然,你也可以将“槽”部分替换为一个自定义的回调,通过它调用小部件 B 的 API。

这个“信号”和“槽”概念是通用的,不仅限于 GUI 小部件。这意味着你可以定义自定义的“信号”和自定义的“槽”。

让我们创建一个脚本来说明这个新概念:

import sys
from PySide2 import QtCore

@QtCore.Slot(str)
def slot_func(param):
    print(param)

class Simple(QtCore.QObject):
    signal = QtCore.Signal(str)

simple = Simple()
simple.signal.connect(slot_func)
simple.signal.emit("Hello World")

让我们解剖这个脚本。我们导入了库:

import sys
from PySide2 import QtCore

槽和信号功能来自QtCore类。你可以使用Slot装饰器创建一个函数:

@QtCore.Slot(str)
def slot_func(param):
    print(param)

你也可以创建一个QObject的子类来创建一个“信号”对象:

class Simple(QtCore.QObject):
    signal = QtCore.Signal(str)

simple = Simple()
simple.signal.connect(slot_func)

你必须实例化这个类。“信号”必须是实例的一部分,而不是一个类。然后你可以将它与一个“槽”函数连接如下:

simple.signal.emit("Hello World")

最后一部分是将参数从“信号”发送到“槽”。

运行脚本以测试回调是否仍然有效:

(qt-venv) $ python hello_custom_signal_slot.py
Hello World

线程

要在 Qt 中使用线程,我们可以使用QtCore中的QThread类。希望你能看到这里有一个模式。QtCore除了创建小部件之外还有许多功能。线程很重要,因为在构建去中心化应用程序时,我们将不得不等待相当长的时间来确认交易。根据你在以太坊中的燃气和交通慷慨程度,等待时间可能从几分钟到半个小时不等。我们不希望 GUI 应用程序冻结一分钟,更不用说半个小时了。

让我们创建一个简单的脚本来演示如何使用QThread创建一个线程。将脚本命名为hello_thread.py

from PySide2 import QtCore
import time

class SimpleThread(QtCore.QThread):
    def __init__(self, parent=None):
        super(SimpleThread, self).__init__(parent)

    def run(self):
        time.sleep(2) # simulating latency in network
        print("world")

simple_thread = SimpleThread()
simple_thread.start()

print("hello")
simple_thread.wait()

然后,你将在编写run方法内部的类中对QThread进行子类化。要使用这个线程类,你需要实例化这个类,然后调用start方法。然后你可以在主应用程序中做所有你想做的事情,同时等待线程类完成它的工作。如果你想等待线程类完成工作,你可以从线程类中调用它的wait方法。

线程是一个广泛的主题。我们还没有讨论互斥锁或线程安全,但对于我们的去中心化应用程序来说,我们不需要讨论这些。

有些人对在他们的应用程序中使用线程感到恐惧。另一种选择是使用asyncio库使用单线程并发代码。这完全没问题,但在这种情况下,我们将使用线程。

我们已经知道如何使用 Qt 库来构建 Python 桌面应用程序的所有必要知识。在编写 GUI 去中心化应用程序之前,我们需要编写一个智能合约,因为没有这个,就不会有去中心化应用程序。

编写 Twitter 样应用程序的智能合约

我们现在要构建一个抗审查的类似 Twitter 的应用程序。这意味着即使智能合约的所有者也无法删除用户的推文。这个类似 Twitter 的应用程序非常简单;没有关注、点赞或转发的选项;它只包括推文。此外,用户无法删除他们的推文,推文必须少于 32 个字节,这甚至比 Twitter 原始推文限制还要短!

现在我们回到我们的 Populus 项目,修改我们的project.json文件。将这个添加到project.json中:

"ganache": {
   "chain": {
     "class": "populus.chain.ExternalChain"
   },
   "web3": {
     "provider": {
       "class": "web3.providers.HTTPProvider",
       "settings": {
         "endpoint_uri": "http://localhost:7545"
       }
     }
   },
   "contracts": {
     "backends": {
       "JSONFile": {"$ref": "contracts.backends.JSONFile"},
       "ProjectContracts": {
         "$ref": "contracts.backends.ProjectContracts"
       }
     }
   }
 }

这是智能合约。在twitter_like_projects/contracts目录中创建一个TwitterOnBlockchain.vy文件*:*

struct Tweet:
    messages: bytes32[10]
    index: int128

tweets: public(map(address, Tweet))

@public
def write_a_tweet(tweet: bytes32):
    assert self.tweets[msg.sender].index < 10

    index: int128 = self.tweets[msg.sender].index
    self.tweets[msg.sender].messages[index] = tweet
    self.tweets[msg.sender].index += 1

这是一个非常简短的智能合约。

我们从结构数据类型变量声明和地址到该结构数据类型变量的映射开始:

struct Tweet:
    messages: bytes32[10]
    index: int128

tweets: public(map(address, Tweet))

tweets变量是从地址到messagesindex数组的映射数据类型。messages数组的长度为 10 个bytes32数据类型变量,这意味着每个账户或地址在这个智能合约中最多可以有 10 条推文。index是指向messages数组的指针。每次创建一条推文,index增加 1,这样它就可以指向messages数组中的下一个槽。

@public
def write_a_tweet(tweet: bytes32):
    assert self.tweets[msg.sender].index < 10
    index: int128 = self.tweets[msg.sender].index
    self.tweets[msg.sender].messages[index] = tweet
    self.tweets[msg.sender].index += 1

write_a_tweet函数是创建一条推文的方法。这只是将一条推文插入到messages数组中,并加入一些保护措施以避免越界错误。

测试

这是对这个智能合约的测试。您可以将测试文件保存在tests/test_twitter_on_blockchain.py中。有关完整代码,请参考以下 GitLab 链接中的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_07/twitter_on_blockchain/tests/test_twitter_on_blockchain.py

以下代码块显示了代码:

import pytest
import eth_tester

def test_initial_condition(web3, chain):
    twitter_on_blockchain, _ = chain.provider.get_or_deploy_contract('TwitterOnBlockchain')
    assert twitter_on_blockchain.functions.tweets__index(web3.eth.coinbase).call() == 0

...
...

twitter_on_blockchain.functions.tweets__messages(web3.eth.coinbase,0).call()[:len(tweet)] == tweet
    assert twitter_on_blockchain.functions.tweets__messages(web3.eth.coinbase,1).call()[:len(tweet2)] == tweet2

这个测试确保index从零开始,并且在用户在这个智能合约中发推文后会增加一。它还检查推文是否保留在messages数组中。

让我们思考一下。如果你看看智能合约,所有者无法审查任何人的推文。所有者甚至无法删除智能合约,因此政府或黑手党都无法向智能合约的所有者施加压力来审查推文。将这种情况与应用程序所有者使用传统网络应用程序的情况进行比较。应用程序所有者可以通过从数据库中删除条目来审查推文。另外,政府或黑手党可以向应用程序所有者施加压力来审查推文,或者政府或黑手党可以关闭托管服务器。在区块链的情况下,这意味着人们需要关闭一半的 10,000 个节点才能破坏以太坊区块链,以摧毁智能合约。

这是区块链特有的一种特性——抗审查。这并不意味着它不能被审查,但很难被审查。为了关闭这个智能合约,政府必须禁止以太坊网络本身。另外,他们可以在所有公民的计算机上安装间谍软件,并确保他们不访问这个智能合约。此外,抗审查并不意味着它保护隐私。政府仍然可以弄清楚谁写了什么。

要部署智能合约,运行 Ganache,编译智能合约,然后使用以下命令将智能合约部署到 Ganache:

(qt-venv) $ populus deploy --chain ganache TwitterOnBlockchain

固定装置

在我们开发一个去中心化的 GUI 应用之前,让我们先写一个脚本来在智能合约中放一些固定数据。固定数据就像样本数据。这样可以让应用的开发体验更加愉快。将脚本命名为fixtures.py。完整的代码请参考以下 GitLab 链接中的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_07/dapp/fixtures.py

from web3 import Web3, HTTPProvider
from populus.utils.wait import wait_for_transaction_receipt
w3 = Web3(HTTPProvider('http://localhost:7545'))

private_keys = ['dummy',
                '59e31694256f71b8d181f47fc67914798c4b96990e835fc1407bf4673ead30e2',
                'ac1e6abbe002699fbef756a2cbc2bf8c03cfac97adee84ce32f198219be94788']

...
...
        txhash = w3.eth.sendRawTransaction(signed.rawTransaction)
        wait_for_transaction_receipt(w3, txhash)

让我们逐行解析这个脚本。我们从web3中导入Web3类和HTTPProvider类。我们使用HTTPProvider是因为我们想使用 Ganache。然后我们从populus库中导入wait_for_transaction_receipt

from web3 import Web3, HTTPProvider
from populus.utils.wait import wait_for_transaction_receipt

w3 = Web3(HTTPProvider('http://localhost:7545'))

然后我们有了前三个账户的私钥:

private_keys = ['dummy',
                '59e31694256f71b8d181f47fc67914798c4b96990e835fc1407bf4673ead30e2',
                'ac1e6abbe002699fbef756a2cbc2bf8c03cfac97adee84ce32f198219be94788']

我们不需要第一个账户的私钥或启动智能合约的账户;我们只需要第二个和第三个账户:

true = True
false = False
abi = [{
         "constant": false,
         "gas": 71987,
         "inputs": [{
                      "name": "tweet",
                      "type": "bytes32"
                   }],
         "name": "write_a_tweet",
         "outputs": [],
         "payable": false,
         "type": "function"
       },
...
...
         "payable": false,
         "type": "function"
       }]

这是你可以从智能合约的编译中得到的abi

with open('address.txt', 'r') as f:
    address = f.read().rstrip("\n")

TwitterOnBlockchain = w3.eth.contract(address=address, abi=abi)

你将智能合约的地址放在address.txt文件中。在通过 Populus 部署智能合约时,你会收到这个地址。

然后,根据智能合约的地址和abi构建一个智能合约对象:

for i in range(1, 3):
    for j in range(1, 11):
        nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(w3.eth.accounts[i]))
        txn = TwitterOnBlockchain.functions.write_a_tweet(b'Tweet ' + str(j).encode('utf-8')).buildTransaction({'gas': 70000, 'gasPrice': w3.toWei('1', 'gwei'), 'nonce': nonce })
        signed = w3.eth.account.signTransaction(txn, private_key=private_keys[i])
        txhash = w3.eth.sendRawTransaction(signed.rawTransaction)
        wait_for_transaction_receipt(w3, txhash)

对于每个账户,你创建 10 条推文。每条推文都是像'Tweet 1','Tweet 2'这样的。这些固定数据使我们能够在不必先创建推文的情况下检查推文。这对检查应用的功能是有益的。

为 Twitter 类似应用构建 GUI 前端

现在让我们构建一个去中心化的 GUI 应用。将脚本命名为twitter_dapp.py。完整的代码请参考以下 GitLab 链接中的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_07/dapp/twitter_dapp.py

from PySide2 import QtCore, QtWidgets
import web3
from web3 import Web3, HTTPProvider
from populus.utils.wait import wait_for_transaction_receipt

w3 = Web3(HTTPProvider('http://localhost:7545'))

...
...

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    twitter_dapp = TwitterDapp()
    twitter_dapp.show()
    sys.exit(app.exec_())

让我们逐行解析这个脚本。在这里,我们导入必要的类并将我们的web3对象设置为 Ganache,它在本地主机的端口7545上提供智能合约。

from PySide2 import QtCore, QtWidgets
import web3
from web3 import Web3, HTTPProvider
from populus.utils.wait import wait_for_transaction_receipt

w3 = Web3(HTTPProvider('http://localhost:7545'))
true = True
false = False

abi = [{
         "constant": false,
         "gas": 71987,
         "inputs": [{
                      "name": "tweet",
                      "type": "bytes32"
                   }],
         "name": "write_a_tweet",
         "outputs": [],
         "payable": false,
         "type": "function"
       },
...
...
         "payable": false,
         "type": "function"
       }]

然后,我们像往常一样定义abi

with open('address.txt', 'r') as f:
    address = f.read().rstrip("\n")

TwitterOnBlockchain = w3.eth.contract(address=address, abi=abi)

然后,我们通过提供智能合约的地址和abi来创建一个智能合约对象。

这是一个辅助函数,用于从字符串中移除'\x00'

def strip_x00_from_tweet(tweet):
    null_index = tweet.find(b'\x00')
    return tweet[:null_index]

记住,我们在智能合约中的推文数据类型是bytes32,它会一直添加\x00直到达到 32 个字节。因此,如果你将一个长度为 5 个字节的'messi'字符串设置为这个bytes32变量,它将变成'messi\x00\x00\x00\x00...\x00'。这个实用函数将从字符串中去掉\x00,这样我们就可以得到'messi'。然后,我们进入线程类的定义:

class Web3ReadTweetsThread(QtCore.QThread):
    fetched_posts = QtCore.Signal(list)
    account = ''

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

    def setAccount(self, account):
        self.account = account

    def run(self):
        try:
            index = TwitterOnBlockchain.functions.tweets__index(self.account).call()
        except web3.exceptions.ValidationError:
            return
        tweets = []
        for i in range(index):
            tweet = TwitterOnBlockchain.functions.tweets__messages(self.account, i).call()
            tweets.append(tweet.decode('utf-8'))
        self.fetched_posts.emit(tweets)

这是一个线程类,旨在从区块链中读取数据。但是读取数据不应该很快吗,因为我们使用的是 Ganache,不需要涉及矿工?如果你的区块链节点和 GUI 前端脚本在同一台计算机上,那么这个线程类是不需要的。但是,如果你将区块链节点放在云上呢?也许你的本地计算机没有足够的空间,这就是你这样做的原因。这种设置会在你的程序中引入延迟,所以最好使用一个线程来等待来自云端区块链的结果。

fetched_posts是一个信号,将把推文列表发送到我们稍后将定义的槽中。在run方法中,我们首先获取索引,这样我们就知道推特发布了多少条。由于我们从智能合约中得到的是一个字节,所以在将推文发送到slot函数之前,我们首先对其进行解码。

以下代码是一个线程类,我们用它来创建智能合约中的交易。要使用这个线程类,我们需要一个私钥。请记住,我们需要用私钥签署交易。然而,我们还需要地址以便获取 nonce。我们也可以要求用户输入地址,但这将是多余的。我们可以通过使用account = w3.eth.account.privateKeyToAccount('0x'+self.private_key)方法从私钥中派生地址:

class Web3WriteATweetThread(QtCore.QThread):
    write_a_tweet = QtCore.Signal()
    private_key = ''
    tweet = ''

...
...

w3.eth.sendRawTransaction(signed.rawTransaction)
        wait_for_transaction_receipt(w3, txhash)
        self.write_a_tweet.emit()

获取地址后,我们用这行获取nonce

nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(account.address))

然后像往常一样构建交易,签署交易,并发送交易到区块链。然后,我们等待交易确认。之后,我们向slot函数发出信号,稍后我们将定义该函数。

class TwitterDapp(QtWidgets.QWidget):
    private_key = '0x0'
    account = ''
    bookmark_file = 'bookmark.txt'
    addresses = []

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

...
...
        self.web3_write_a_tweet_thread.write_a_tweet.connect(self.successfullyWriteATweet)

现在,是时候创建一个 GUI 了。我们设置窗口标题,子类化QWidget,并设置私人分组框,我们将从用户那里请求私钥。我们将添加一个写入按钮小部件,并配置一个用于显示推文的分组框。然后准备一个用于显示书签的分组框,并将它们全部添加到垂直布局中。此外,我们创建了两个线程实例。对于每个线程实例,我们将它们的信号连接到slot函数:

    def createPrivateKeyGroupBox(self):
        self.private_key_group_box = QtWidgets.QGroupBox("Account")
        self.private_key_field = QtWidgets.QLineEdit()
        self.welcome_message = QtWidgets.QLabel()

        layout = QtWidgets.QFormLayout()
        layout.addRow(QtWidgets.QLabel("Private key:"), self.private_key_field)
        button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok)
        button_box.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self.checkPrivateKey)
        layout.addRow(button_box)
        layout.addRow(self.welcome_message)

        self.private_key_group_box.setLayout(layout)

在这个方法中,我们创建一个分组框,放置行编辑器以接收私钥。我们还放置一个标签,用于在输入有效的私钥时显示通知。在这里,我们使用QFormLayout,而不是QHBoxLayoutQVBoxLayoutQFormLayout非常适用于创建两列布局。第一列通常用作标签的容器,而第二列通常用作输入的容器,例如行编辑器。因此,使用这个布局,我们使用addRow方法,它接受一个或两个参数。如果参数数量为两个,则参数将是左列上的小部件和右列上的小部件。然后我们添加一个私钥标签和一个行编辑器,然后是一个确定对话框按钮和一个通知标签。对于对话框按钮,我们将点击信号连接到一个名为checkPrivateKey的方法。

在接下来的方法中,我们在水平布局中创建一个按钮。我们将点击信号连接到一个名为writeANewTweet的方法:

    def createWritingTweetGroupBox(self):
        self.tweet_button = QtWidgets.QPushButton("Write a new tweet")
        self.tweet_button.setMaximumSize(200,40)
        self.write_button_layout = QtWidgets.QHBoxLayout()
        self.write_button_layout.addWidget(self.tweet_button)
        self.connect(self.tweet_button, QtCore.SIGNAL('clicked()'), self.writeANewTweet)

在接下来的方法中,我们创建一个分组框来显示推文。首先有一个行编辑器来接受推特的地址。这旁边有一个按钮用于获取推文和将地址加入书签。回调函数将稍后定义。我们还在布局内放置了一个布局来显示所有推文:

    def createTweetsGroupBox(self):
        self.tweets_group_box = QtWidgets.QGroupBox("Tweets")
        self.account_address = QtWidgets.QLineEdit()
        self.fetch_button = QtWidgets.QPushButton("Fetch")
        self.add_to_bookmark_button = QtWidgets.QPushButton("Bookmark it!")

...
...
        self.tweets_main_layout.addLayout(account_address_layout)
        self.tweets_main_layout.addSpacing(20)
        self.tweets_main_layout.addLayout(self.tweets_layout)
        self.tweets_group_box.setLayout(self.tweets_main_layout)

在接下来的方法中,我们创建一个分组框来显示书签。这个书签布局只是一个普通的垂直布局。我们从文本文件中读取书签,然后处理这些行。基本上,处理是从文本行中删除换行符:

    def createBookmarkGroupBox(self):
        self.bookmark_group_box = QtWidgets.QGroupBox("Bookmark")
        self.bookmark_layout = QtWidgets.QVBoxLayout()
        self.bookmark_group_box.setLayout(self.bookmark_layout)

        with open(self.bookmark_file) as f:
            addresses = f.readlines()
        self.addresses = list(map(lambda x: x.rstrip(), filter(lambda x: len(x) > 1, addresses)))
        self.fillBookmark()

这是用于获取推文的slot函数。它使用线程来完成任务。

    @QtCore.Slot()
    def fetchTweets(self):
        account = self.account_address.displayText()
        self.web3_read_tweets_thread.setAccount(account)
        self.web3_read_tweets_thread.start()

这是slot函数,它将推特的地址添加到书签文本文件中。在将书签添加到书签文本文件后,它会更新书签布局:

    @QtCore.Slot()
    def bookmarkAddress(self):
        account = self.account_address.displayText()
        if account:
            self.addresses.append(account)
            self.addresses = list(set(addresses))
        with open(self.bookmark_file, 'w') as f:
            for address in self.addresses:
                f.write(address)
        self.fillBookmark()

在这个方法中,我们启动一个只有一个行编辑器的输入对话框。获取输入后,我们在运行之前将输入发送到线程类:

    @QtCore.Slot()
    def writeANewTweet(self):
        text, ok = QtWidgets.QInputDialog.getText(self, "Write a new tweet", "Tweet:", QtWidgets.QLineEdit.Normal, "")
        if ok and text != '':
            self.web3_write_a_tweet_thread.setPrivateKey(self.private_key)
            self.web3_write_a_tweet_thread.setTweet(text)
            self.web3_write_a_tweet_thread.start()

这是一个回调函数,用于检查私钥是否有效。如果有效(地址可以从私钥中派生),我们将通知标签设置为成功:

    def checkPrivateKey(self):
        self.private_key = self.private_key_field.displayText()
        try:
            self.account = w3.eth.account.privateKeyToAccount('0x'+self.private_key)
        except ValueError:
            QtWidgets.QMessageBox.warning(self, 'Error', 'Private key is invalid.')
            return
        self.welcome_message.setText('Welcome, ' + self.account.address + '!')
        self.private_key_field.clear()

这两个方法用于填充推文到推文布局中。首先,我们清除垂直布局中的推文。我们可以使用takeAt方法从布局中获取每个小部件。没有方法可以删除布局的所有子小部件;我们必须逐个迭代它们。如果小部件存在,我们可以删除它。清除布局中的所有推文后,我们使用addWidget方法填充布局以显示新的推文。处理书签时也使用了相同的策略:

    def clearTweetsField(self):
        while True:
            label_item = self.tweets_layout.takeAt(0)
            if label_item is None:
                break
            else:
                label_item.widget().close()

    def fillPosts(self, posts):
        self.clearTweetsField()
        for post in posts:
            label_field = QtWidgets.QLabel(post)
            self.tweets_layout.addWidget(label_field)

在接下来的两种方法中使用相同的策略。我们通过逐个迭代并删除小部件来清除书签布局中的书签小部件。然后,我们通过addWidget方法在书签布局中填充书签。处理推文和书签的一个区别是,我们配置了书签部分中的标签,以便可以使用鼠标进行选择。负责此操作的方法是setTextInteractionFlags方法。

参数是标签的当前标志,该标志与Qt.TextSelectableByMouse标志掩码。如果您在不使用前一个值掩码的情况下使用此代码,label_field.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse),则可以删除标签小部件具有的所有当前标签选择配置标志。大多数情况下,您不希望这样做:

    def clearBookmarkField(self):
        while True:
            label_item = self.bookmark_layout.takeAt(0)
            if label_item is None:
                break
            else:
                label_item.widget().close()

    def fillBookmark(self, addresses):
        self.clearBookmarkField()
        for address in addresses:
            label_field = QtWidgets.QLabel(address)
            label_field.setTextInteractionFlags(label_field.textInteractionFlags() | QtCore.Qt.TextSelectableByMouse)
            self.bookmark_layout.addWidget(label_field)

这是设置标签小部件中成功消息的回调:

    def successfullyWriteATweet(self):
        self.welcome_message.setText('You have successfully written a new tweet!')

最后,我们启动 GUI 前端:

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    twitter_dapp = TwitterDapp()
    twitter_dapp.show()
    sys.exit(app.exec_())

在 Ganache 中输入第二个地址以获取执行固定脚本时设置的推文:

输入私钥并单击确定:

单击“撰写新推文”按钮。它将启动对话框。然后输入您的推文并单击确定:

输入新推文并确认后,您将收到成功消息。然后,您可以单击按钮(收藏!)将地址保存到bookmarks文本文件中:

如果您想为智能合约或去中心化应用程序构建前端,需要牢记两件事。首先,要优雅地处理阻塞功能(向区块链发送交易并等待),可以使用线程或单线程协程(使用asyncio库)。其次,虽然智能合约处理核心事务(审查抵抗消息),但您可以为去中心化应用程序增加更多价值。在我们的示例应用程序中,增加的价值是书签实用程序。您可以在智能合约中添加书签功能吗?当然可以。但是,如果将功能放在智能合约中,就会失去隐私,因为每个人都可以阅读您的书签。

简而言之,由于智能合约的限制,您无法将每个功能都放在智能合约中。因此,如果要使用机器学习添加情感分析,最好在智能合约之外进行此操作。

总结

在本章中,我们学习了如何为智能合约构建 GUI 前端,以及如何使用 Qt for Python 或PySide2库,该库利用了 Qt 5 库。我们抽出时间学习如何使用 Qt for Python 构建 GUI 应用程序。从一个简单的应用程序开始,我们了解了布局、各种小部件、信号和插槽以及线程,最终了解了如何使用PySide2库。我们构建了我们的智能合约,以简单的方式模拟 Twitter,享受这种类似 Twitter 应用程序的审查抵抗。接下来,我们构建了 GUI 前端。此 GUI 应用程序使用了迄今为止学到的所有概念。我们使用线程连接到区块链,以便 GUI 不会冻结。我们还通过在 GUI 应用程序中添加书签功能来增加附加值。

在下一章中,您将学习如何创建 ERC20 代币。这是使以太坊在开发人员中受欢迎的原因。代币可以在某些环境中用作货币的替代品。

第四部分:加密货币和钱包

本节是一个实践学习指南,教你如何发行自己的加密货币并构建处理它的钱包。

本节将涵盖以下章节:

  • 第八章,在以太坊中创建代币

  • 第九章,加密货币钱包

第八章:在以太坊中创建代币

在本章中,您将学习如何在以太坊上创建代币。代币有各种用途;它可以是一个社区的本地货币,它可以代表一个实物商品,它可以是游戏中的虚拟货币,或者它可以是忠诚度积分。有了这个代币,您可以构建一个新的加密货币。虽然以太坊本身就是一种加密货币,但您可以在其上构建一个新的加密货币。以太坊使得创建新代币变得更加容易,这推动了 2017 年许多新加密货币的诞生。

本章将涵盖以下主题:

  • 如何创建一个简单的代币智能合约

  • ERC 20(以太坊代币标准)

  • 如何出售您的代币

  • 如何自定义您的代币

代币智能合约

使用 Vyper 在以太坊上创建代币很容易。在构建代币之前,让我们遵循初始步骤准备我们的开发环境。

首先确保您已经安装了 geth,并且geth程序在$PATH环境变量中(这意味着您可以在不输入完整路径的情况下调用geth):

$ virtualenv -p python3.6 token-venv
$ source token-venv/bin/activate
(token-venv) $ pip install eth-abi==1.2.2
(token-venv) $ pip install eth-typing==1.1.0
(token-venv) $ pip install py-evm==0.2.0a33
(token-venv) $ pip install web3==4.7.2
(token-venv) $ pip install -e git+https://github.com/ethereum/populus#egg=populus
(token-venv) $ pip install vyper
(token-venv) $ mkdir token_project
(token-venv) $ cd token_project
(token-venv) $ mkdir tests contracts
(token-venv) $ cp ../token-venv/src/populus/populus/assets/defaults.v9.config.json project.json

通过将compilation键的值更改为以下内容,向project.json添加 Vyper 支持:

"compilation": {
    "backend": {
      "class": "populus.compilation.backends.VyperBackend"
    },
    "contract_source_dirs": [
      "./contracts"
    ],
    "import_remappings": []
},

Vyper 的最新版本是 0.1.0b6,它破坏了 Populus。开发者需要一些时间来解决这个问题。如果到您阅读本书时错误仍未被修复,您可以自己修补 Populus。

首先,使用以下命令检查错误是否已被修复:

(token-venv) $ cd voting-venv/src/populus
(token-venv) $ grep -R "compile(" populus/compilation/backends/vyper.py
 bytecode = '0x' + compiler.compile(code).hex()
 bytecode_runtime = '0x' + compiler.compile(code, bytecode_runtime=True).hex()

在我们的案例中,错误尚未被修复。因此,让我们修补 Populus 以修复错误。确保您仍然在同一个目录(token-venv/src/populus)中:

(token-venv) $ wget https://patch-diff.githubusercontent.com/raw/ethereum/populus/pull/484.patch
(token-venv) $ git apply 484.patch
(token-venv) $ cd ../../../

token_project目录中,运行以下命令:

(token-venv) $ populus chain new localblock

然后,使用init_chain.sh脚本初始化私有链:

(token-venv) $ ./chains/localblock/init_chain.sh

编辑chains/localblock/run_chain.sh并将--ipcpath的值更改为/tmp/geth.ipc

然后,运行区块链:

(voting-venv) $ ./chains/localblock/run_chain.sh

现在,编辑project.json文件。chains有一个包含四个键的对象:testertempropstenmainnet。向此对象添加一个名为localblock的键,并将其值添加到此对象中:

    "localblock": {
      "chain": {
        "class": "populus.chain.ExternalChain"
      },
      "web3": {
        "provider": {
          "class": "web3.providers.ipc.IPCProvider",
        "settings": {
          "ipc_path":"/tmp/geth.ipc"
        }
       }
      },
      "contracts": {
        "backends": {
          "JSONFile": {"$ref": "contracts.backends.JSONFile"},
          "ProjectContracts": {
            "$ref": "contracts.backends.ProjectContracts"
          }
        }
      }
    }

Mist

在第二章中,智能合约基础,和第三章中,使用 Vyper 实现智能合约,您使用 Truffle 控制台与智能合约进行交互。现在,我们将使用另一种软件与智能合约进行交互。我们将使用的软件是Mist,一个以太坊钱包。

为此,请访问github.com/ethereum/mist/releases。如果您使用 Linux Ubuntu,下载Mist-linux64-0-11-1.deb文件。

确保geth已经安装,并且geth的路径在$PATH环境变量中,这样您就可以从任何地方调用geth

然后,按照以下步骤安装 Mist:

$ dpkg -i Mist-linux64-0-11-1.deb

接下来,按照以下步骤运行应用程序:

$ ethereumwallet --rpc /tmp/geth.ipc

注意--rpc /tmp/geth.ipc标志。我们使用这个标志是因为我们使用在/tmp/geth.ipc中定义的套接字文件运行私有区块链。

运行应用程序后,您将看到这个屏幕。您可以通过点击“添加账户”按钮创建一个以太坊区块链账户:

就像您在geth控制台中所做的那样,您需要提供密码来创建一个新账户:

一旦您的钱包账户创建完成,您可以通过点击“发送”选项卡,从第一个账户向这个新账户发送一些钱:

然后,您可以在“TO”字段中插入您的新账户地址。不要忘记包括您想发送的以太币数量,然后点击窗口底部的“发送”按钮:

您必须填写密码以创建交易。默认账户的密码可以在chains/localblock/password文件中找到:

很快,您的交易将在区块链中得到确认:

让我们在token_project/contracts目录中创建一个简单的代币智能合约:

balances: public(map(address, uint256))

@public
def __init__():
    self.balances[msg.sender] = 10000

@public
def transfer(_to: address, _amount: uint256) -> bool:
    assert self.balances[msg.sender] >= _amount

    self.balances[msg.sender] -= _amount
    self.balances[_to] += _amount

    return True

这个智能合约数字化地创建了 10,000 个代币,并将它们全部分配给智能合约的所有者。然后,所有者可以使用transfer方法将代币转发给其他账户。

与创建传统网络应用程序中的简单代币相比,这个简单的智能合约是特殊的,因为一旦部署了这个智能合约,所有者无论多么绝望都无法更改代币数量。如果所有者刚刚执行了transfer方法将一些代币转移到另一个账户的地址,他们就无法再拿回来了。其他人在与之交互之前可以验证智能合约中的游戏规则。

将这个与我们在传统的网络应用程序中创建的简单代币进行比较。一旦您在这里创建了 10,000 个代币,您可以通过更新数据库中的代币数量来改变代币的数量。您可以随意更改规则,这会让其他想要与这个应用程序交互的人处于不利地位。

您还可以添加一个方法来增加智能合约中代币的数量:

balances: public(map(address, uint256))
owner: address

@public
def __init__():
    self.balances[msg.sender] = 10000
    self.owner = msg.sender

@public
def transfer(_to: address, _amount: uint256) -> bool:
    assert self.balances[msg.sender] >= _amount

    self.balances[msg.sender] -= _amount
    self.balances[_to] += _amount

    return True

@public
def mint(_new_supply: uint256):
    assert msg.sender == self.owner
    self.balances[msg.sender] = _new_supply

看一下mint方法。这可以用来增加所有者账户中的代币数量。

不同之处在于,您无法在部署智能合约后更改游戏规则。如果您部署了这个版本的智能合约,人们可以要求源代码和编译器的版本来验证代码。如果您不提供源代码,人们可能会对您的智能合约失去兴趣。如果您提供了源代码,人们可以检查并看到您有一个可以随时增加代币的方法。他们是否接受这个规则取决于他们自己,但至少通过智能合约,有透明度。

以下代码块是对这个简单代币智能合约的测试;这个测试并不全面,但它可以让你有一个良好的开始。对于代币智能合约来说,测试的重要性是至关重要的。首先,将测试命名为test_simple_token.py,并将其放在token_project/tests目录中。请参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_08/token_project/tests/test_simple_token.py

import pytest
import eth_tester

def test_balance(web3, chain):
    simple_token, _ = chain.provider.get_or_deploy_contract('SimpleToken')

...
...

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        simple_token.functions.transfer(web3.eth.coinbase, 10).transact({'from': account2})

让我们使用 Mist 将我们的简单代币智能合约(第一个,没有mint函数)部署到区块链上。

要做到这一点,点击 CONTRACTS 选项卡,然后点击 DEPLOY NEW CONTRACT 按钮:

在部署合约屏幕上,选择要从中部署的账户,然后单击 CONTRACT BYTE CODE 选项卡。在那里插入我们简单代币智能合约的字节码。当然,首先编译智能合约的源代码,然后可以从token_project/build/contracts.json中获取我们简单代币的字节码。找到bytecode键的值,并将该值复制到 CONTRACT BYTE CODE 选项卡中,不包括双引号。完成后,单击 DEPLOY 按钮(在下面的截图中看不到;您需要向下滚动),然后像往常一样在密码对话框中填写密码:

很快,您的智能合约创建将在区块链中得到确认。

然后,要与智能合约交互,请单击“合同”选项卡,然后单击“监视合同”按钮。然后会出现对话框。填写智能合约的地址,然后填写智能合约本身的名称。您可以在此处使用任何名称。接下来,在 JSON INTERFACE 字段中填写智能合约的json接口。您可以从token_project/build/contracts.json(在contracts.json文件中,找到abi键的值)中获取json接口。观看智能合约的表单如下屏幕所示:

现在,您的简单代币智能合约将出现在屏幕上。点击“简单代币”标签,进入此智能合约的屏幕:

您可以使用Balances方法(Mist 喜欢大写方法)读取智能合约所有者的余额。在“Arg 0 - address”字段中插入所有者帐户的地址,然后按 Enter。从中,您将得到 10000 作为输出。

现在,让我们转移一些代币。选择一个功能。现在只有一个功能:Transfertransfer方法的大写版本)。在“执行自”字段中选择帐户 1,在“到”字段中选择帐户 2 的地址,并在“金额”字段中插入 20。最后,点击“执行”按钮:

在检查目标帐户余额之前,请等待确认。将目标地址输入“余额”字段,然后按 Enter。这将给您输出20

ERC20

很可能,您已经听说过 ERC20。当有新的加密货币问世时,通常会出现的第一个问题是——它是 ERC20 代币吗?人们对 ERC20 代币的错误假设是,他们认为它是基于以太坊的加密货币。从技术上讲,这是正确的,但这并不是全部。ERC20 是以太坊中用于创建代币的标准。我们刚刚创建的简单代币并不满足 ERC20 标准。是的,它是数字代币智能合约,但它不是 ERC20 代币。ERC20 是我们在 2017 年看到新加密货币数量增加的许多原因之一。但是,ERC20 并不是在以太坊上创建代币的要求。

要创建 ERC20 代币,您必须实现以下方法:

function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
event Transfer(address indexed _from, address indexed _to, uint256 _value)
event Approval(address indexed _owner, address indexed _spender, uint256 _value)

这些方法使用 Solidity 语法。以下是可选方法,您也可以实现:

function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)

这个 ERC20 代币有什么特别之处?在创建代币智能合约时是否有义务?为什么我们不能在不满足 ERC20 标准的情况下创建数字代币?

实际上,您不必遵循此标准;没有法律强制您创建 ERC20 代币。例如,ERC20 标准希望您告诉用户totalSupply方法中的代币总量。但是,您可以创建一个名为taylorSwiftIsTheBest的方法来返回代币的总供应量,然后您可以创建一个文档来解释这个方法。

但是,如果您遵循 ERC20 代币标准,将会有一些优势:

  • 首先,它使用户更容易审计您的智能合约。

  • 第二,您的 ERC20 代币将被以太坊钱包(例如我们刚刚使用的Mist和 Metamask)自动识别(以太坊钱包是Firefox/Opera/Chrome插件)。

  • 第三,这样做可以更容易地让加密货币交易所列出您的代币。基本上,这会让每个人的生活更轻松。

然而,你应该把 ERC20 标准视为指导,而不是严格的法律。你不必 100%遵循 ERC20 标准。并非所有建立在以太坊之上的热门代币都是 100%符合 ERC20 标准的。一个例子是 Golem 代币智能合约。它没有实现approve方法,等等。你可以在以下链接阅读 Golem 代币智能合约的源代码:etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d#readContract

话虽如此,让我们创建一个 ERC20 代币。这段代码是从 Vyper 项目的官方示例修改而来的(github.com/ethereum/vyper/blob/master/examples/tokens/ERC20.vy)。请参考以下 GitLab 链接中的代码文件获取完整修改后的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_08/token_project/contracts/ERC20Token.vy

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})

...
...

@public
@constant
def allowance(_owner: address, _spender: address) -> uint256:
    return self.allowed[_owner][_spender]

让我们逐行描述这个智能合约:

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})

你必须定义两种类型的事件,TransferApproval。如果愿意,你可以定义更多的事件。Transfer事件用于在代币转移时使用,这样智能合约的用户可以订阅此事件。Approval事件用于在你批准某个账户的支出金额时使用:

name: public(bytes[10])
symbol: public(bytes[3])
totalSupply: public(uint256)
decimals: public(uint256)
balances: map(address, uint256)
allowed: map(address, map(address, uint256))

有六个变量。第一个变量是代币的名称。我使用了bytes[10]作为数据类型,因为我的代币名称少于 10 个字节。可以随意更改长度。第二个变量是代币的符号;名称和符号是不同的。例如,以太坊加密货币的名称是以太坊,但符号是 ETH。通常,符号的字符长度为 3。第三个变量是totalSupply,即代币的总数量,第四个变量是小数位。你知道 1 比特币等于 100,000,000 聪,所以比特币算法中总共会创建 21,000,000 比特币。因此,我们可以说比特币算法中的总供应量是 2,100,000,000,000,000。小数位是 8(因为 1 比特币等于 100,000,000,或 10⁸)。此外,第五个变量是balances。这是用于跟踪账户地址余额的变量。最后一个变量是allowed,这是一个嵌套映射。它旨在跟踪已获批准从另一个账户余额中支出以太币的账户。如果这还不清楚,我们稍后会深入讨论。现在,我们将继续讨论此 ERC20 代币智能合约的初始化方法,如下代码块所示:

@public
def __init__():
    _initialSupply: uint256 = 1000
    _decimals: uint256 = 3
    self.totalSupply = _initialSupply * 10 ** _decimals
    self.balances[msg.sender] = self.totalSupply
    self.name = 'Haha Coin'
    self.symbol = 'HAH'
    self.decimals = _decimals
    log.Transfer(ZERO_ADDRESS, msg.sender, self.totalSupply)

这是我们在智能合约中初始化代币的方式。我们在空中创建代币,然后将所有代币的硬币交给所有者。然后,我们设置代币的名称和符号以及小数位数。最后,我们发出转移事件。地址是ZERO_ADDRESS is 0x000000000000000000000000000000000000000。这表明智能合约的所有者已经从无处转移了硬币:

@public
@constant
def balanceOf(_owner: address) -> uint256:
    return self.balances[_owner]

这个方法用于返回特定账户的余额:

@public
def transfer(_to: address, _amount: uint256) -> bool:
    assert self.balances[msg.sender] >= _amount
    self.balances[msg.sender] -= _amount
    self.balances[_to] += _amount
    log.Transfer(msg.sender, _to, _amount)

    return True

这是用于转移代币的方法。首先,确保发送者的余额足够支出。然后,只需从发送者的余额中减去转移过程的数量,并将该金额添加到目标的余额中。不要忘记使用事件记录此交易:

@public
def transferFrom(_from: address, _to: address, _value: uint256) -> bool:
    assert _value <= self.allowed[_from][msg.sender]
    assert _value <= self.balances[_from]

    self.balances[_from] -= _value
    self.allowed[_from][msg.sender] -= _value
    self.balances[_to] += _value
    log.Transfer(_from, _to, _value)

    return True

@public
def approve(_spender: address, _amount: uint256) -> bool:
    self.allowed[msg.sender][_spender] = _amount
    log.Approval(msg.sender, _spender, _amount)

    return True

让我们先看一下approve方法。因此,每个账户都有一个名为allowed的映射,但这个映射是用来做什么的呢?这是每个账户如何让其他账户花费它的钱。例如,假设有 5 个账户——A 账户、B 账户、C 账户、D 账户和 E 账户。

A 账户有 50 个硬币的余额和一个名为allowed的映射变量,其中包括 B 账户、C 账户、D 账户和 E 账户的键。这个映射的值如下:

B 账户→3 个硬币

C 账户→7 个硬币

D 账户→2 个硬币

E 账户→3 个硬币

这意味着最多,B 账户可以从 A 账户的余额中花费 3 个硬币,C 账户最多可以从 A 账户的余额中花费 7 个硬币,D 账户最多可以从 A 账户的余额中花费 2 个硬币,E 账户最多可以从 A 账户的余额中花费 3 个硬币。

approve方法内部,我们有以下行:

self.allowed[msg.sender][_spender] = _amount

为了确保 B 账户可以从 A 账户的余额中花费 3 个硬币,A 账户调用了approve方法,_spender设置为 B 账户的地址,_amount设置为 3 个硬币。不要忘记记录这个批准。

然后,如果 B 账户想要花费 3 个或全部 3 个硬币,B 账户可以调用transferFrom方法,如下所示:

assert _value <= self.allowed[_from][msg.sender]

transferFrom方法内的第一个断言是确保 B 账户不花费超过 3 个硬币。

assert _value <= self.balances[_from]

我们确保 A 账户的余额至少有 3 个硬币,如下所示:

    self.balances[_from] -= _value
    self.allowed[_from][msg.sender] -= _value
    self.balances[_to] += _value
    log.Transfer(_from, _to, _value)

然后,我们从 A 账户的余额和 A 账户对 B 账户的津贴中减去花费的金额。之后,我们增加目标账户的余额。不要忘记记录这笔交易作为Transfer事件。

最后一个方法是检查这个津贴的映射:

@public
@constant
def allowance(_owner: address, _spender: address) -> uint256:
    return self.allowed[_owner][_spender]

这是为了确定 B 账户可以从 A 账户的余额中花费多少。

此时,您可能会问这个方法的意义是什么。如果我们想允许 B 账户代表 A 账户花费一些以太币,为什么我们不直接将硬币的数量转移到 B 账户呢?例如,如果 A 账户允许 B 账户从 A 账户的余额中花费 5 个硬币,这意味着 A 账户让 B 账户直接从 A 账户的余额中向 B 账户的地址发送 5 个硬币。那么,为什么 A 账户不直接向 B 账户的地址发送 5 个硬币以节省麻烦呢?如果 B 账户改变主意,他们可以退还代币的数量给 A 账户。

通常情况下,我们不允许普通账户代表我们花费。但是,我们允许智能合约这样做。有许多有效的原因,我们希望允许智能合约花费我们的钱。其中一个原因是去中心化交易智能合约的情况,您希望让去中心化交易智能合约出售您的代币。假设您创建了 1,000 个 HHH 硬币,然后您想在去中心化交易中出售其中一些。因此,您允许这个去中心化交易代表您花费一些硬币。也许您批准去中心化交易代表您花费 30 个 HHH 硬币。这个去中心化交易可以尝试代表您出售 30 个 HHH 硬币。您不会允许去中心化交易智能合约访问您余额中的所有硬币,只是因为您只想出售 30 个 HHH 硬币。尽管您可以审计智能合约,以确保智能合约不会尝试窃取您的硬币,但最好有另一层安全性。approve方法就是这样一层。

编译您的代币智能合约并部署它,就像以前一样。

完成后,转到“合同”选项卡,然后单击“WATCH TOKEN”按钮。在“添加代币”窗口中,插入您的 ERC20 智能合约的地址。然后,其他字段,如“TOKEN NAME”、“TOKEN SYMBOL”和“DECIMAL PLACES OF SMALLEST UNIT”将自动填充。这就是我们应该实现 ERC20 标准的原因之一:

添加代币后,您应该在合同屏幕上看到 HAHA COIN 标签:

然后,在发送标签中,选择 ERC20 智能合约的所有者,然后选择 Haha Coin 而不是以太币。向账户 2 发送 1,000 个 Haha 代币,然后等待确认:

现在,在同一个标签页中选择账户 2;您会看到账户 2 现在有 1,000 个 Haha 代币:

所有与 ERC20 兼容的钱包,如 Mist 和 MetaMask,都可以轻松识别和与您的代币进行交互。加密货币交易背后的人也不会在将您的 ERC20 代币整合到他们的交易所中遇到任何技术困难。想象一下,如果您不遵循 ERC20 标准,并且将一个方法命名为tailorSwiftTransferCoin来转移代币。在这种情况下,您必须为用户构建一个自定义钱包,然后他们才能与您的代币进行交互。

这是对这个 ERC20 代币的测试。请记住,这个测试并不全面。请参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_08/token_project/tests/test_erc20_token.py

import pytest
import eth_tester

def test_balance(web3, chain):
    erc20_token, _ = chain.provider.get_or_deploy_contract('ERC20Token')

    token_name = erc20_token.functions.name().call()
    token_symbol = erc20_token.functions.symbol().call()
    decimals = erc20_token.functions.decimals().call()
    total_supply = erc20_token.functions.totalSupply().call()
    balance = erc20_token.functions.balanceOf(web3.eth.coinbase).call()

...
...

    assert balance_account1 == 999990
    assert balance_account2 == 0
    assert balance_account3 == 10
    assert allowance == 90

除了转账方法,这个测试还测试了approvaltransferFrom方法。

出售代币

现在我们有了一个代币,是时候出售这些代币了。我们希望用以太币出售我们的自定义代币。与比特币平台相比,在以太坊平台上创建一个众售代币非常容易。您已经知道如何在智能合约中创建一个方法来接受以太币。您还知道如何增加一些账户的代币余额。要出售代币,您必须将这两件事结合起来。就是这样。

这是首次代币发行ICO)的核心。以太坊的货币很有价值。尽管以太币的价格波动,但 1 个以太币的价值大约是 100 美元。人们会用真钱购买一些以太币,但不会购买我们的自定义代币。为了使我们的自定义代币有价值,我们首先必须使其有用,或者至少使其看起来有用。但要做到这一点,我们需要资本。那么为什么不将我们的代币(比如 60%)出售给早期采用者呢?然后,他们可以用以太币购买我们的自定义代币。然后,我们可以在将其转换为法定货币之前提取以太币,这样我们就可以雇佣更多的程序员并租用办公室来开发我们的新加密货币。这就是基本的想法。当然,因为 ICO 涉及大量资金,它也会吸引捕食者。

这是众售代币智能合约。这与我们之前的 ERC20 代币智能合约源代码相同,但有些许变化。将这个智能合约命名为CrowdSaleToken.vy,并保存在token_project/contracts目录中。请参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_08/token_project/contracts/CrowdSaleToken.vy

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})
Payment: event({_buyer: indexed(address), _value: uint256(wei)})

name: public(bytes[10])
symbol: public(bytes[3])
totalSupply: public(uint256)
decimals: public(uint256)
balances: map(address, uint256)
ethBalances: public(map(address, uint256(wei)))
allowed: map(address, map(address, uint256))

...
...

@public
@constant
def allowance(_owner: address, _spender: address) -> uint256:
    return self.allowed[_owner][_spender]

让我们逐行讨论这个智能合约:

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})
Payment: event({_buyer: indexed(address), _value: uint256(wei)})

我们在另外两个事件之上添加了一个名为Payment的事件。当有人用以太币购买代币时,将调用此事件:

name: public(bytes[10])
symbol: public(bytes[3])
totalSupply: public(uint256)
decimals: public(uint256)
balances: map(address, uint256)
ethBalances: public(map(address, uint256(wei)))
allowed: map(address, map(address, uint256))

beneficiary: public(address)
minFundingGoal: public(uint256(wei))
maxFundingGoal: public(uint256(wei))
amountRaised: public(uint256(wei))
deadline: public(timestamp)
price: public(uint256(wei))
fundingGoalReached: public(bool)
crowdsaleClosed: public(bool)

我们添加了一些新变量,包括ethBalancesbeneficiaryminFundingGoalmaxFundingGoalamountRaiseddeadlinepricefundingGoalReachedcrowdsaleClosed

ethBalances是一个变量,用于跟踪买家在这个智能合约中花费了多少以太币。我们想跟踪这个数字,因为如果我们无法筹集到必要的金额,我们想要退还以太币。beneficiary是一个用于跟踪谁启动了这个智能合约的变量。这个beneficiary变量是唯一能够提取以太币的账户。

minFundingGoalmaxFundingGoal是设计为确保筹集的金额大于最低筹资目标且低于最高筹资目标的变量。

amountRaised是一个变量,用于跟踪在这个智能合约中筹集了多少以太币。

deadline是一个用于跟踪众筹过程截止日期的变量。

price是我们希望以太币价值我们数字代币的方式。这个变量旨在回答买家用 1 个以太币可以获得多少个我们的代币。

fundingGoalReached是一个布尔变量,表示我们的智能合约是否已经达到了目标。

crowdsaleClosed是一个布尔变量,表示我们的智能合约是否仍在接收购买过程。我们将从初始化方法开始:

@public
def __init__():
    _initialSupply: uint256 = 100
    _decimals: uint256 = 2
    self.totalSupply = _initialSupply * 10 ** _decimals
    self.name = 'Haha Coin'
    self.symbol = 'HAH'
    self.decimals = _decimals
    self.beneficiary = msg.sender
    self.balances[msg.sender] = self.totalSupply
    self.minFundingGoal = as_wei_value(30, "ether")
    self.maxFundingGoal = as_wei_value(50, "ether")
    self.amountRaised = 0
    self.deadline = block.timestamp + 3600 * 24 * 100 # 100 days
    self.price = as_wei_value(1, "ether") / 100
    self.fundingGoalReached = False
    self.crowdsaleClosed = False

这个智能合约中的总代币供应量为 10,000。我们希望至少筹集 30 个以太币,最多筹集 50 个以太币。

截止日期设置为智能合约部署在区块链上后的 100 天。block.timestamp大致是当前时间,或者包含此智能合约代码的区块确认的时间。

1 个代币的价格设定为0.01以太币。这意味着 1 个以太币可以购买 100 个我们的代币。然后,我们进入智能合约中的默认函数:

@public
@payable
def __default__():
    assert msg.sender != self.beneficiary
    assert self.crowdsaleClosed == False
    assert self.amountRaised + msg.value < self.maxFundingGoal
    assert msg.value >= as_wei_value(0.01, "ether")
    self.ethBalances[msg.sender] += msg.value
    self.amountRaised += msg.value
    tokenAmount: uint256 = msg.value / self.price
    self.balances[msg.sender] += tokenAmount
    self.balances[self.beneficiary] -= tokenAmount
    log.Payment(msg.sender, msg.value)

这是用户用来购买代币的方法。__default__是一个默认的回退函数。如果有人没有在智能合约上执行方法并支付以太币,将执行此函数。实际上,我们不必使用默认函数来接受付款。您可以使用标准方法,就像您在以前的智能合约中所做的那样。我们只是使用默认函数来解释这个概念。

在这个付款方法中,我们确保买家不是受益人,众筹仍在进行,并且用以太币发送到此方法的金额不超过 50 个以太币的最高筹资目标。最后,每次购买行为必须至少为 0.01 以太币。然后,我们增加该买家的以太币余额,并增加筹集的以太币金额。然后,我们通过将以太币金额除以 1 个代币的价格来检查他们购买的代币数量。

最后,我们必须增加该买家的代币余额,并减少智能合约所有者的代币余额。不要忘记记录此事件。然后,我们进入可以检查是否达到目标的方法:

@public
def checkGoalReached():
    assert block.timestamp > self.deadline
    if self.amountRaised >= self.minFundingGoal:
        self.fundingGoalReached = True
    self.crowdsaleClosed = True

首先,我们确保只有在截止日期过去后才能成功执行此方法。如果筹集的金额超过最低筹资目标,我们将fundingGoalReached变量设置为true。最后,我们将crowdsaleClosed变量设置为true

为了简单起见,我们只检查block.timestamp变量是否大于截止日期。然而,区块中的时间戳可以填入矿工喜欢的任何内容;它不一定是区块确认时的当前时间。但是,如果矿工将过去的时间戳作为block.timestamp的值,所有其他矿工都会拒绝它。同样,如果矿工将未来时间戳(例如,一年后)作为block.timestamp的值,所有其他矿工也会拒绝它。为了使截止日期检查过程更安全,您必须将其与block.number结合起来,以检查自从智能合约启动以来已确认了多少个区块。然后,我们进入智能合约的管理者可以提取在智能合约中累积的以太币的方法:

@public
def safeWithdrawal():
    assert self.crowdsaleClosed == True
    if self.fundingGoalReached == False:
        if msg.sender != self.beneficiary:
            if self.ethBalances[msg.sender] > 0:
                self.ethBalances[msg.sender] = 0
                self.balances[self.beneficiary] += self.balances[msg.sender]
                self.balances[msg.sender] = 0
                send(msg.sender, self.ethBalances[msg.sender])
    if self.fundingGoalReached == True:
        if msg.sender == self.beneficiary:
            if self.balance > 0:
                send(msg.sender, self.balance)

safeWithdrawal方法的运行方式不同,取决于是否达到了筹资目标。在上述方法内部,我们确保众筹已经关闭。如果未达到筹资目标,我们确保每个买家都可以取回他们的以太币。如果达到了筹资目标,我们确保受益人可以从智能合约中提取所有以太币。其他方法的剩余部分与先前的智能合约相同。但是,我们添加了一些断言来确保这些方法只能在众筹结束后执行。

以下代码块是对这个众筹智能合约的测试。请参考以下 GitLab 链接中的代码文件获取完整的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_08/token_project/tests/test_crowd_sale_token.py

import pytest
import eth_tester
import time

def test_initialization(web3, chain):
    crowd_sale_token, _ = chain.provider.get_or_deploy_contract('CrowdSaleToken')

...
...

    assert abs(beforeCrowdsaleEthBalanceAccount2 - afterCrowdsaleEthBalanceAccount2 - web3.toWei('40', 'ether')) < web3.toWei('1', 'gwei')
    assert abs(afterCrowdsaleEthBalanceAccount1 - beforeCrowdsaleEthBalanceAccount1 - web3.toWei('40', 'ether')) < web3.toWei('1', 'gwei')

特别关注test_withdrawaltest_refund,尤其是这些行:

    # move forward 101 days
    web3.testing.timeTravel(int(time.time()) + 3600 * 24 * 101)
    web3.testing.mine(1)

我们不想等待 100 天,我们想在测试中操纵时钟,使其相信已经到达截止日期(从现在开始的 101 天)。因此,我们假装已经过去了 101 天,然后我们确认了 1 个区块。因此,智能合约内部的block.timestamp变量将是从现在开始的 101 天。

稳定币

您已经创建了一个可以自主出售的数字代币。但是,您不应该局限于通用代币。您可以通过添加更多方法来使您的代币智能合约更具创意。您应该添加哪些方法取决于您的智能合约的目的。在游戏中用作货币的代币智能合约将具有不同的方法,而在供应链跟踪系统中使用的代币智能合约将具有不同的方法。

让我们创建一个稳定币智能合约。这是一个与法定货币(如美元)挂钩的代币智能合约。我们还希望这个智能合约成为一个银行,作为所有者,我们可以冻结账户。

我们可以基于 ERC 20 代币智能合约进行工作。我们只需要添加三种方法——一个冻结账户的方法,一个添加代币的方法,以及一个丢弃代币的方法。

这是智能合约。您可以参考以下 GitLab 链接中的代码文件获取完整的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_08/token_project/contracts/StableCoin.vy

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})
Freeze: event({_account: indexed(address), _freeze: bool})

name: public(bytes[10])
symbol: public(bytes[3])
totalSupply: public(uint256)
decimals: public(uint256)
balances: map(address, uint256)
allowed: map(address, map(address, uint256))
frozenBalances: public(map(address, bool))
owner: public(address)

...
...

@public
@constant
def allowance(_owner: address, _spender: address) -> uint256:
    return self.allowed[_owner][_spender]

让我们逐行讨论智能合约:

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})
Freeze: event({_account: indexed(address), _freeze: bool})

我们需要在上述代码中添加另一个事件,用于冻结账户操作。

我们在 ERC20 代币智能合约的变量之上添加了两个新变量,即frozenBalancesowner

name: public(bytes[10])
symbol: public(bytes[3])
totalSupply: public(uint256)
decimals: public(uint256)
balances: map(address, uint256)
allowed: map(address, map(address, uint256))
frozenBalances: public(map(address, bool))
owner: public(address)

frozenBalances是一个映射变量,用于跟踪已被冻结的账户。owner是一个变量,用于跟踪智能合约的所有者。

在这个初始化方法中,我们将owner变量设置为启动这个智能合约的账户:

@public
def __init__():
    _initialSupply: uint256 = 1000
    _decimals: uint256 = 3
    self.totalSupply = _initialSupply * 10 ** _decimals
    self.balances[msg.sender] = self.totalSupply
    self.name = 'Haha Coin'
    self.symbol = 'HAH'
    self.decimals = _decimals
    self.owner = msg.sender
    log.Transfer(ZERO_ADDRESS, msg.sender, self.totalSupply)

在接下来的方法中,我们确保只有所有者可以调用此方法:

@public
def freezeBalance(_target: address, _freeze: bool) -> bool:
    assert msg.sender == self.owner
    self.frozenBalances[_target] = _freeze
    log.Freeze(_target, _freeze)

    return True

然后,我们设置了frozenBalances映射变量的值。真值表示账户被冻结。不要忘记调用Freeze事件。

下一个方法是增加硬币:

@public
def mintToken(_mintedAmount: uint256) -> bool:
    assert msg.sender == self.owner
    self.totalSupply += _mintedAmount
    self.balances[msg.sender] += _mintedAmount
    log.Transfer(ZERO_ADDRESS, msg.sender, _mintedAmount)

    return True

我们增加了总供应量和所有者账户的余额。

接下来的方法旨在销毁硬币:

@public
def burn(_burntAmount: uint256) -> bool:
    assert msg.sender == self.owner
    assert self.balances[msg.sender] >= _burntAmount
    self.totalSupply -= _burntAmount
    self.balances[msg.sender] -= _burntAmount
    log.Transfer(msg.sender, ZERO_ADDRESS, _burntAmount)

    return True

这与上一种方法类似,但其目的是减少总供应量中的硬币数量。你为什么要销毁硬币?有许多有效的原因。假设您正在使用这个智能合约跟踪美元。假设您口袋里有 1 万美元。因此,智能合约中的总供应量为 10,000(1 个硬币等于 1 美元),但有一天,您的 10,000 美元中有 2,000 美元被小偷偷走了。为了确保智能合约的一致性,您可以使用这种方法销毁 2,000 个硬币。

其他方法与 ERC20 代币智能合约下完全相同,只有两个例外:transfertransferFrom。在这些方法中,我们有额外的断言:

assert self.frozenBalances[msg.sender] == False

以下代码块是对这个智能合约的测试。您可以参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_08/token_project/tests/test_stable_token.py

import pytest
import eth_tester
import time

def test_initialization(web3, chain):
    stable_coin, _ = chain.provider.get_or_deploy_contract('StableCoin')

...
...

    new_total_supply = stable_coin.functions.totalSupply().call()
    assert new_total_supply == 999900

总结

在本章中,您已经学会了如何在以太坊上创建代币。您使用了 Mist 作为以太坊钱包,部署了合约并与代币智能合约进行交互。然后,您通过创建某些方法的实现来实现 ERC 20 标准的代币智能合约。您还看到了这些标准方法如何帮助 Mist 识别您的代币。然后,您创建了一种方法来出售代币以获取以太币。您在这个智能合约中设置了截止日期,然后使用时间旅行方法来模拟智能合约的测试中过期的截止日期。最后,您添加了其他方法来冻结和解冻其他账户。您使用的示例是将硬币与实际资产(如法定货币)挂钩的稳定币。

在下一章中,您将创建一个可以处理以太币和 ERC20 代币的加密货币钱包。

进一步阅读