许多正在学习如何编写智能合约的程序员将学习 Solidity 编程语言。有大量的在线教程和书籍可以教你关于坚固性的知识。当与块菌框架相结合时,Solidity 形成了开发智能合约的杀手组合。几乎所有以太坊区块链上的智能合约都是用 Solidity 编程语言编写的。
在本章中,我们将探讨如何编写智能合约。但是,我们不会为此使用 Solidity 编程语言。相反,我们将使用 Vyper 编程语言。
本章将介绍以下主题:
编写智能合约不同于开发普通的 web 应用。当开发一个普通的 web 应用时,座右铭是快速移动并打破事物。开发 web 应用的速度至关重要。如果应用中存在错误,您可以随时在以后升级应用。或者,如果 bug 是灾难性的,您可以在引入修复之前在线修补它或使应用离线。有一个非常流行的词来描述在开发一个普通的敏捷 web 应用时的理想心态。随着需求的变化,您需要灵活地更改软件。
然而,编写智能合同需要不同的心态。智能合同的应用范围可以从编写金融应用到向太空发射火箭。一旦部署了智能合约,就很难修复错误。您无法替换智能合约,因为一旦部署智能合约,它就会被部署。如果编写函数来销毁智能合约,则可以销毁智能合约,但修复错误智能合约的唯一方法是部署一个新的智能合约,在新地址中修复错误,然后将此情况告知所有相关方。但你不能取代智能合约。
因此,理想的情况是在区块链上部署一个没有 bug 的智能合约,或者至少没有恶性 bug。然而,在现实世界中发布的智能合约中仍然会出现 bug。
那么,智能合约中会出现什么样的 bug 呢?第一种会让你的钱消失。假设您正在为首次硬币发行(ICO)编写智能合约。ICO 是通过出售您在以太坊区块链上创建的代币来积累资本。所以基本上,人们用以太购买代币。您可以根据自己的喜好设置价格,例如,1ETH=100 您的代币。这意味着,如果人们付给你 1 英镑,他们将得到你 100 英镑的代币。
您可以引入的第一个错误是,人们可以向您的智能合约发送金钱(以太),但您不能撤回它(您可能忘记实现撤回方法,或者撤回方法有缺陷)。这意味着您可以检查智能合约的余额,以太余额很可能价值 100 万美元,但它将永远卡在那里,没有人能够索赔。
另一个错误可能是您忘记保护销毁智能合约的方法。在以太坊,你会被激励从区块链中移除东西,因为存储成本很高。因此,如果您部署智能合约,您将支付汽油费,因为您的智能合约将被保留。你可以尝试一下,如果你对你的智能合约感到厌倦,你可以毁掉它。为此,以太坊将向您的帐户返还一些汽油。这是为了阻止以太坊区块链的垃圾邮件。因此,回到我们的智能合约漏洞案例,假设您在智能合约中积累了价值 100 万美元的以太,然后有人通过访问一个函数来销毁您的智能合约帐户。在这种情况下,你的乙醚平衡也会被破坏。
最后一种错误是允许黑客窃取你的以太余额并将其移动到他们的帐户。这可能发生在许多不同的情况下。例如,可能您忘记了在提取功能中设置正确的权限,或者提取功能中的权限过于开放。
当然,所有这些错误都可以追溯到程序员的错误。为了避免这些缺陷,一种新的工作诞生了——智能合约审计员,他会审计您的智能合约,以确保它没有缺陷。然而,Vitalik Buterin(以太坊的发明者)随后查看了该工具(本例中为编程语言)并想知道是否可以通过改进工具本身来缓解这种情况。本例中的罪魁祸首是 Solidity 编程语言。Vitalik 认为,Solidity 具有一些强大的功能,但可能会产生 bug。尽管 Solidity 的开发人员有一个改进 Solidity 安全性的计划,但 Vitalik 希望有一些自由来尝试新的视角。维珀由此诞生。
假设您创建了一个具有重要函数的父类。在当前或子类中,使用此函数时不检查其定义。也许父类是由团队中的其他人编写的。程序员有时懒得检查其他文件中的函数定义;它们将在源代码文件中上下滚动以读取代码,但程序员通常不会检查由继承功能启用的其他文件中的代码。
另一个可能使智能合约变得复杂且难以阅读的坚固特性是修饰符,它类似于一个初步功能。以下代码显示了如何在“实体”中使用修改器:
modifier onlyBy(address _account)
{
require(msg.sender == _account, "Sender not authorized.");
_;
}
function withdraw() public onlyBy(owner)
{
//withdraw money;
}
如果我们想使用withdraw()
方法,智能合约将首先执行onlyBy()
修饰符方法。require
短语用于确保msg.sender
(调用此方法)与作为参数发送的account
变量相同。这个例子很简单。你可以在一眨眼之间读懂所有的代码。但是,请考虑这些函数是由多行分隔的,或者甚至在另一个文件中定义的事实。程序员倾向于忽略onlyBy()
方法的定义。
函数重载是编程语言中最强大的功能之一。此功能使您能够发送不同的参数以获得不同的函数,如以下代码所示:
function flexible_function(uint _in) public {
other_balance = _in;
}
function flexible_function(uint _in, uint _in2) public {
other_balance = _in + _in2;
}
function flexible_function(uint _in, uint _in2, uint _in3) public {
other_balance = _in + _in2 - _in3;
}
然而,函数重载特性可能误导程序员,导致他们以不同的意图执行函数。程序员可能只记得flexible_function
函数执行此操作,但可能会天真地执行与flexible_function
不同的函数。
因此,一些聪明的人决定,尽管所有这些特性都使创建一个真正复杂的程序成为可能,但这些特性应限于开发智能合约。也许他们是从那些在飞船上编写程序的人那里得到这个想法的,那里有规则禁止使用 C++的哪些特性。或者,他们可能是因为 java 被创建来取代 C++的原因而激发出来的。在 Java 中,直接操纵内存特性是不可能的。Bjarne Stroustoup(C++的创造者)说 C++是如此强大,C++可以让人们用脚射击自己。
这些聪明的人决定创造一种新的编程语言,它比 Solidity 更简单。Python 是他们的主要灵感来源,因为这种编程语言的语法源自 Python。这种编程语言称为Vyper。在 Vyper 中,诸如继承、函数重载、修饰符等特性都被删除。Vyper 编程语言的创建者认为,删除这些功能可以使智能合约的开发更容易。重要的是,它还使代码更易于阅读。代码读的比写的多得多。考虑到所有这些因素,他们希望程序员在使用 Vyper 编程语言创建智能合约时能够减少 bug。
默认情况下,UbuntuXenial 安装了 Python3.5。Vyper 需要 Python3.6 软件,因此如果你想使用 UbuntuXenial,你需要先安装 Python3.6。一个更新版本的 Ubuntu,比如仿生海狸,已经安装了 Python 3.6。
因此,如果未安装 Python 3.6 软件,则必须首先使用以下命令安装:
$ sudo apt-get install build-essential
$ sudo add-apt-repository ppa:deadsnakes/ppa
$ sudo apt-get update
$ sudo apt-get install python3.6 python3.6-dev
Vyper 所需要的不仅仅是 Python3.6;您还需要安装开发文件python3.6-dev
,然后通过以下步骤为 Python 3.6 创建一个虚拟环境:
virtualenv
工具:$ sudo apt-get install virtualenv
$ virtualenv -p python3.6 vyper-venv
$ source vyper-venv/bin/activate
pip
安装 Vyper,如下所示:(vyper-venv) $ pip install vyper
(vyper-venv) $ vyper --version
0.1.0b6
然后你就准备好踏上旅程的下一步。
现在,让我们用 Vyper 创建一个智能合约。首先,我们将创建一个扩展名为.vy
的文件,并将其命名为hello.vy
,如下所示:
name: public(bytes[24])
@public
def __init__():
self.name = "Satoshi Nakamoto"
@public
def change_name(new_name: bytes[24]):
self.name = new_name
@public
def say_hello() -> bytes[32]:
return concat("Hello, ", self.name)
如果您来自 Solidity 或 Python 背景,您会注意到一个特点:在使用 Vyper 编程语言编写的智能合约中,没有类(如 Python 编程语言中的类),也没有合约(如 Solidity 编程语言中的合约)。但是,有一个initializer
功能。initializer
函数的名称与 Python 编程语言中的名称相同,即__init__
。
在使用 Python 时,您可以在一个文件中创建任意数量的类。在 Vyper 中,规则是每个文件一个智能合约。这里也没有课程或合同;文件本身是一个类。
以下是编译此vyper
文件的方式:
(vyper-venv) $ vyper hello.vy
由此,您将获得以下输出:
这是智能合约的字节码。请记住,要部署智能合约,您需要字节码,但要访问智能合约,您需要abi
。那么你如何得到abi
?可以通过运行以下命令来执行此操作:
(vyper-venv) $ vyper -f json hello.vy
由此,您将获得以下输出:
如果您想在单个编译过程中同时获得abi
和bytecode
,您可以在编译过程中组合这两个标志,如下所示:
(vyper-venv) $ vyper -f json,bytecode hello.vy
这将为您提供以下输出:
那么,您如何将此智能合约部署到以太坊区块链?有几种方法可以做到这一点,但让我们用一种熟悉的方法使用块菌:
truffle``init
初始化,如下所示:$ mkdir hello_project
$ cd hello_project
$ truffle init
truffle-config.js
设置为以下内容:module.exports = {
networks: {
"development": {
network_id: 5777,
host: "localhost",
port: 7545
},
}
};
build
目录,如下所示:$ mkdir -p build/contracts
$ cd build/contracts
Hello.json
文件,如下所示:{
"abi":
"bytecode":
}
abi
或json
填充abi
字段,用编译过程输出的bytecode
填充bytecode
字段。您需要用双引号引用bytecode
值。别忘了在abi
字段和bytecode
字段之间加逗号。这将为您提供类似于以下内容的信息:{
"abi": [{"name": "__init__", "outputs": [], "inputs": [], "constant": false, "payable": false, "type": "constructor"}, {"name": "change_name", "outputs": [], "inputs": [{"type": "bytes", "name": "new_name"}], "constant": false, "payable": false, "type": "function", "gas": 70954}, {"name": "say_hello", "outputs": [{"type": "bytes", "name": "out"}], "inputs": [], "constant": false, "payable": false, "type": "function", "gas": 8020}, {"name": "name", "outputs": [{"type": "bytes", "name": "out"}], "inputs": [], "constant": true, "payable": false, "type": "function", "gas": 5112}],
"bytecode": "0x600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffff...
...
1600101808352811415610319575b50506020610160526040610180510160206001820306601f8201039050610160f3005b60006000fd5b61012861049703610128600039610128610497036000f3"
}
migrations/2_deploy_hello.js
中创建新文件来创建迁移文件以部署此智能合约,如下所示:var Hello = artifacts.require("Hello");
module.exports = function(deployer) {
deployer.deploy(Hello);
};
一切准备就绪后,启动 Ganache!
hello_project
目录中,您可以只运行迁移过程,如下所示:$ truffle migrate
您将看到类似以下内容:
使用 Vyper 编写的智能合约已部署到 Ganache。您的智能合约地址如下:
0x3E9417399786347B6Ab38f59d3f00829d6bba7b8
正如我们之前所做的,您可以使用 Truffle 控制台与智能合约进行交互,如下所示:
$ truffle console
您的智能合约始终命名为Contract
。我们可以使用以下语句访问智能合约:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8")
您将获得一个长输出,在其中可以看到abi
、bytecode
等,如以下屏幕截图所示:
让我们使用以下语句查看智能合约的name
变量的值:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.name.call(); });
'0x5361746f736869204e616b616d6f746f'
您可能会注意到,神秘的输出看起来不像 Satoshi Nakamoto。然而,它实际上是 Satoshi Nakamoto,但却是用十六进制写成的。让我们从神秘的输出中扔掉0x
;这只是一个指示符,表明这个字符串是十六进制的。你现在有了5361746f736869204e616b616d6f746f
字符串。取前两个数字,即53
,并将其转换为十进制数。在 Python 中,可以按如下方式执行此操作:
>>> int(0x53)
83
所以,十进制数是83
。你还记得 ASCII 表吗?这是一个保存十进制数字和字符之间关系的数据表。因此,十进制数字65
代表字符 A(大写 A),十进制数字66
代表字符 B(大写 B)。
那么十进制数字83
的特征是什么?您可以使用 Python 了解以下内容:
>>> chr(83)
'S'
如果您对所有其他十六进制字符执行此操作,其中每个十六进制字符包含两个数字字符,则它将拼写为 Satoshi Nakamoto。
让我们使用以下代码执行此智能合约中的另一个方法:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.say_hello.call(); })
'0x48656c6c6f2c205361746f736869204e616b616d6f746f'
那个神秘的输出只是Hello, Satoshi Nakamoto
。
让我们按如下方式更改名称:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.change_name(web3.utils.fromAscii("Vitalik Buterin"), { from: "0x6d3eBC3000d112B70aaCA8F770B06f961C852014" }); });
您将获得以下内容作为输出:
from
字段中的值取自 Ganache 中的一个帐户。您只需查看 Ganache 窗口,然后选择您喜欢的任何帐户。
我们不能直接向change_name
方法发送字符串;我们必须首先使用web3.utils.fromAscii
方法将其转换为十六进制字符串。
现在这个名字改了吗?让我们看看。运行以下命令:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.name.call(); });
'0x566974616c696b204275746572696e'
是的,名字已经改了。如果您将十六进制字符串转换为 ASCII 字符串,您将得到 Vitalik Buterin。
让我们看看我们的智能合同:
name: public(bytes[24])
@public
def __init__():
self.name = "Satoshi Nakamoto"
@public
def change_name(new_name: bytes[24]):
self.name = new_name
@public
def say_hello() -> bytes[32]:
return concat("Hello, ", self.name)
请看第一行:
name: public(bytes[24])
字节数组基本上是一个字符串。名为name
的变量的数组类型为bytes
或string
。其能见度为public
。如果要将其设置为private
,则只需省略 public 关键字,如下所示:
name: bytes[24]
现在,看下面几行:
@public
def __init__():
self.name = “Satoshi Nakamoto”
如果您来自 Python 背景,那么您将认识 Python decorator 函数。Vyper 中有四个:
@public
意味着您可以作为用户执行此方法(就像您在上一章的 Truffle 控制台中所做的那样)。@private
表示只有同一智能合约中的其他方法才能访问此方法。您不能以用户身份调用该方法(在 Truffle 控制台中)。@payable
表示您可以发送一些以太到该方法。@const
表示此方法不应修改智能合约的状态。这意味着执行此方法不会花费太多成本。这就像读取公共变量的值。回到__init__()
方法,您可以像这样向该方法传递一个参数:
i: public(uint256)
@public
def __init__(int_param: uint256):
self.i = int_param
部署智能合约时不要忘记发送参数。在我们的例子中,我们在 Truffle 软件中使用迁移,因此将您的迁移文件2_deploy_hello.js
修改如下:
var Hello = artifacts.require("Hello");
module.exports = function(deployer) {
deployer.deploy(Hello, 4);
};
让我们继续了解智能合约的以下几行,以了解public
方法:
@public
def change_name(new_name: bytes[24]):
self.name = new_name
此方法修改智能合约的状态,即name
变量。这会引起汽油。
让我们转到智能合约的下一行,了解如何在public
方法中返回值:
@public
def say_hello() -> bytes[32]:
return concat("Hello, ", self.name)
concat
是组合字符串的内置函数。参见https://vyper.readthedocs.io/en/latest/built-in-functions.html 获取内置功能的完整列表。
必须小心右箭头指示的方法的返回值(→). 您可以将其设置为长度不足的字节数组。例如,请查看以下代码:
@public
def say_hello() -> bytes[28]:
return concat("Hello, ", self.name)
在这种情况下,它将无法编译,尽管“Hello,Satoshi Nakamoto”肯定少于 28 个字符。字符串的长度为 23 个字符;但是,您必须记住,self.name
定义为bytes[24]
,而Hello,
的长度为 7 个字符。因为 24+7 是 31 个字符,所以必须将其设置为更大的数组。
由于此方法不会改变此智能合约的状态,您可以在此方法之上添加@const
,如下所示:
@public
@const
def say_hello() -> bytes[32]:
return concat("Hello, ", self.name)
让我们创建一个更复杂的智能合约,并将其命名为donation.vy
,如下所示。您可以参考下面的 GitLab 链接以获取完整代码:https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_03/donation.vy :
struct DonaturDetail:
sum: uint256(wei)
name: bytes[100]
time: timestamp
donatur_details: public(map(address, DonaturDetail))
...
...
@public
def withdraw_donation():
assert msg.sender == self.donatee
send(self.donatee, self.balance)
像以前一样编译和部署智能合约。如果重用项目目录,请不要忘记删除build/contracts
目录中的所有文件,并重新启动 Ganache。
请看以下几行:
struct DonaturDetail:
sum: uint256(wei)
name: bytes[100]
time: timestamp
让我们逐一讨论 Vyper 数据类型:
DonaturDetail.name = "marie curie"
uint256(wei)
。这是指可容纳的特定量的乙醚。如你所知,1 乙醚是 10000000000000000 微(18 个零)。要保存这么大的数量,需要特定的数据类型。timestamp
数据类型。这是为了保存时间值而设计的。0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
。这可能是帐户或智能合约的地址。如果您想知道地址数据类型是什么样子,可以在下面的屏幕截图中查看 Ganache。帐户地址是地址数据类型的一个示例。您可以使用以下数据类型向变量发送以太:map
数据类型。这就像一本字典。简单的地图如下所示:simple_map: map(address, uint256)
这里,键是address
,值是uint256
。以下是如何将值填充到此映射:
self.simple_map[0x9049386D4d5808e0Cd9e294F2aA3d70F01Fbf0C5] = 10
如果您习惯于 Python 中的 dictionary 数据类型,那么这个映射数据类型会有一个扭曲:您不能迭代这个映射。因此,不要期望在 Vyper 中迭代具有映射数据类型的变量,就像在 Python 中使用dictionary
数据类型的变量一样。您可以通过查看以下代码了解其工作原理:
for key in self.simple_map:
// do something with self.simple_map[key]
以太坊虚拟机(EVM不跟踪具有映射数据类型的变量的所有键。在 Python 中,可以从具有 dictionary 数据类型的变量中获取所有键,如以下代码所示:
self.simple_map.keys()
但在 Vyper 中不能这样做。
如果访问不存在的键,它将返回值数据类型的默认值。在我们的例子中,如果我们这样做,我们会得到0
,如下代码所示:
self.simple_map[0x1111111111111111111111111111111111111111] => 0
如果您从未为0x1111111111111111111111111111111111111111
键设置值,或者您将其设置为0
值,则没有区别。如果您要跟踪这些键,则需要将它们保存在单独的数组中。映射数据类型类似于 Python 中的默认字典,如以下代码所示:
>>> from collections import defaultdict
>>> d = defaultdict(lambda: 0, {})
>>> d['a']
0
>>> d['a'] = 0
>>> d['a']
0
那么,回到我们定义的第二个变量,让我们看看下面的代码:
donatur_details: public(map(address, DonaturDetail))
此代码显示地址到包含wei
、string
和timestamp
数据类型的结构的映射。我们希望使用此数据类型记录捐赠者的姓名、捐赠金额和捐赠时间。
请看以下几行:
donaturs: public(address[10])
这是一个大小为10
的地址数组。
让我们看看下面的几行,了解如何在智能合同中保持所有者的帐户:
donatee: public(address)
uint256
或int128
。请注意,uint256
和uint256(wei)
是不同的。uint256 和 int128 之间的区别在于 int128 数据类型可以包含零、正数和负数。uint256 数据类型只能容纳零和正数,但其上限高于 int128。以下代码将保存启动此智能合约的人的地址:
index: int128
这是为了记录有多少捐赠者捐款。请注意,它没有公共修饰符。这意味着您无法从 Truffle 控制台访问变量。
让我们来看一看这个方法:
@public
def __init__():
self.donatee = msg.sender
在每个方法中,都有特殊的对象。其中之一是msg
。您可以通过msg.sender
访问访问此方法的账号。您还可以通过msg.value
找到醚的数量(在wei
中)。在以下代码中,我们希望保存此智能合约的启动器地址:
@payable
@public
def donate(name: bytes[100]):
assert msg.value >= as_wei_value(1, "ether")
assert self.index < 10
self.donatur_details[msg.sender] = DonaturDetail({
sum: msg.value,
name: name,
time: block.timestamp
})
self.donaturs[self.index] = msg.sender
self.index += 1
此处,@payable
表示该方法接受以太支付。assert
短语类似于 Python 编程语言中的assert
。如果条件为false
,则该方法的执行将中止。在assert
行之后,我们只需将self.donatur_details
地图和msg.sender
键设置为DonaturDetail
结构。在结构内部,您使用指示当前时间的block.timestamp
设置时间的属性。as_wei_value
短语是一个内置函数。因为我们必须在这个智能合约中使用 wei 单元来处理以太支付,所以使用这个内置功能是一个好主意。如果不是,则必须使用大量的零,如下所示:
assert msg.value >= 1000000000000000000
智能合约的最后几行将是一种向donatee
账户提取捐赠的方法,如下代码所示:
@public
def withdraw_donation():
assert msg.sender == self.donatee
send(self.donatee, self.balance)
此处,self.balance
表示此智能合约中累积的所有以太。send
短语是一个内置函数,用于将资金转移到第一个参数,在本例中为donatee
。
让我们在块菌控制台中测试这个智能合约。确保将方法中的地址更改为智能合约的地址。您可以通过truffle migrate
命令获取,如下所示:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.donatee.call(); });
'0xb105f01ce341ef9282dc2201bdfda2c26903da77'
这是 Ganache 中的第一个帐户,如以下屏幕截图所示:
让我们从 Ganache 的第二个帐户捐赠 2 台乙醚,如下所示:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.donate(web3.utils.fromAscii("lionel messi"), {from: "0x6d3eBC3000d112B70aaCA8F770B06f961C852014", value: 2000000000000000000}); });
现在从 Ganache 的第三个账户捐赠 3.5 乙醚,如下所示:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.donate(web3.utils.fromAscii("taylor swift"), {from: "0x9049386D4d5808e0Cd9e294F2aA3d70F01Fbf0C5", value: 3500000000000000000}); });
现在使用以下代码查看捐赠者的捐赠:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.donatur_details__sum.call("0x9049386D4d5808e0Cd9e294F2aA3d70F01Fbf0C5"); });
<BN: 30927f74c9de0000>
访问结构属性的方法是在donatur_details
结构后使用两个下划线。您将地图的键放入call
函数中。如果你想知道<BN: 30927f74c9de0000>
中的30927f74c9de0000
是什么意思,那不是内存的位置,而是十六进制格式的数字。由于数字非常大(BN 是大数字的缩写),EVM 必须以十六进制格式显示数字,如下所示:
truffle(development)> web3.utils.toBN(15);
<BN: f>
truffle(development)> web3.utils.toBN(9);
<BN: 9>
truffle(development)> web3.utils.toBN(100);
<BN: 64>
truffle(development)> web3.utils.toBN(3500000000000000000);
<BN: 30927f74c9de0000>
如果您查看 Ganache,第二个和第三个帐户已经损失了一些钱,如以下屏幕截图所示:
那么,让我们使用以下代码撤回捐赠:
truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.withdraw_donation({from: "0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77"}); });
看看你的加纳什。在我的例子中,第一个帐户有 105.48 ETH,如以下屏幕截图所示:
Vyper 具有捐赠智能合约中未使用的其他数据类型,如下表所示:
bool
:此数据类型类似于普通布尔值。它包含真值或假值,如以下代码所示:bull_or_bear: bool = True
decimal
:该数据类型类似于 Python 中的float
或double
,如下代码所示:half_of_my_heart: decimal = 0.5
bytes32
:该数据类型类似于bytes32
,具有特殊性。如果值的长度小于 32 字节,则将用零字节填充。因此,如果您将messi
值(5 个字符/字节)设置为bytes32
数据类型变量(如下代码所示),它将变为messi\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
:goat: bytes32 = convert('messi', bytes32)
Constant
:此数据类型声明后不能更改:GOAT: constant(bytes[6]) = 'messi'
不同于 C++编程语言,在未初始化变量可以具有垃圾值的情况下,VyPEL 编程语言中的所有未初始化变量都具有默认值。默认整数数据类型值为0
。默认的布尔数据类型值为false
。
您已经使用了内置函数,例如send
、assert
、as_wei_value
、concat
和convert
。但是,还有其他有用的功能,例如:
slice
:slice
短语为字节数据类型。它用于从字符串中获取子字符串等任务,如以下代码所示:first_name: bytes[10] = slice(name, start=0, len=10)
len
:此函数用于获取值的长度,如下代码所示:length_of_name: int128 = len(name)
selfdestruct
:此功能用于销毁智能合约,如下代码所示。此参数是此智能合约将其以太网发送到的地址:selfdestruct(self.donatee)
ceil
:此函数用于将整数舍入到上限,如下代码所示:round_heart: int128 = ceil(half_of_my_heart)
floor
:此函数用于将整数舍入到下限,如下代码所示:round_heart: int128 = floor(half_of_my_heart)
sha3
:这是一个内置的哈希函数,如下代码所示:secret_hash: bytes32 = sha3('messi')
Vyper 支持事件。您可以将方法中的事件广播给此事件的任何订阅者。例如,当人们使用智能合约捐赠以太时,您可以播放捐赠活动。要声明事件,可以使用以下语句:
Donate: event({_from: indexed(address), _value: uint256(wei)})
然后,在我们的donate
方法中,您可以在捐赠交易发生后广播事件,如下代码所示:
@public
def donate(name: bytes[100]):
log.Donate(msg.sender, msg.value)
我们将在后面的章节中更多地讨论事件。
你知道你的智能合约不必在外面孤独吗?您的智能合约可以与区块链上的其他智能合约交互。
地址数据类型不仅用于普通帐户,还可用于智能合约帐户。因此,智能合约可以通过捐赠智能合约向我们的捐赠对象捐赠以太!
重新启动你的 Ganache;我们将重新启动区块链。还记得你的hello.vy
Vyper 文件吗?我们希望使用自定义名称部署我们的Hello
智能合约。
我们的迁移文件migrations/2_deploy_hello.js
还是一样的,如下代码所示:
var Hello = artifacts.require("Hello");
module.exports = function(deployer) {
deployer.deploy(Hello);
};
再次编译您的hello.vy
文件以获取接口和字节码。打开我们的合同 JSON 文件,build/contracts/Hello.json
文件。清除所有内容并替换为以下代码:
{
"contractName": "Hello",
"abi": <your Hello smart contract's interface>,
"bytecode": "<your Hello smart contract's bytecode>"
}
您必须为您的智能合约命名,因为这一次,您将部署两个智能合约。如果您没有为智能合约命名,它将有一个默认名称Contract
。如果您只想部署一个智能合约,这不是问题。
然后,对于您的donation.vy
,编辑它,并将以下代码行(以粗体突出显示)添加到代码文件中(请参阅以下 GitLab 链接中的代码文件,以获取处donation.vy
的完整代码文件)https://gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_03/donation.vy :
struct DonaturDetail:
sum: uint256(wei)
name: bytes[100]
time: timestamp
contract Hello():
def say_hello() -> bytes[32]: constant
donatur_details: public(map(address, DonaturDetail))
...
...
@public
def withdraw_donation():
assert msg.sender == self.donatee
send(self.donatee, self.balance)
@public
@constant
def donation_smart_contract_call_hello_smart_contract_method(smart_contract_address: address) -> bytes[32]:
return Hello(smart_contract_address).say_hello()
请注意粗体显示的更改。这些更改是您声明要与之交互的智能合约接口的方式;声明契约对象和要与之交互的方法。您不需要知道say_hello
方法的实现,只需要知道接口(即它期望的参数和返回值)。
然后调用外部智能合约的donation_smart_contract_call_hello_smart_contract_method
方法。发送地址作为合同对象的参数,并像往常一样调用该方法。如果您已经知道要与之交互的智能合约的地址,则可以对其进行硬编码。但我使用参数,因为我还不知道Hello
智能合约的地址。
使用以下代码,为我们升级的Donation
智能合约migrations/3_deploy_donation.js
创建另一个迁移文件:
var Donation = artifacts.require("Donation");
module.exports = function(deployer) {
deployer.deploy(Donation);
};
编译您的donation.vy
并获取智能合约的接口和字节码。
然后,使用以下代码,为我们的Donation
智能合约build/contracts/Donation.json
创建另一个合约 JSON 文件:
{
"contractName": "Donation",
"abi": <your Donation smart contract's interface>,
"bytecode": "<your Donation smart contract's bytecode>"
}
运行迁移。您可能需要使用--reset
标志,如下所示:
$ truffle migrate --reset
您将获得以下输出:
注意Donation
智能合约的地址和Hello
智能合约的地址。Donation
智能合约的地址为0x98Db4235158831BF9133faC1c4e1829021ecEB67
,而Hello
智能合约的地址为0xBc932d934cfE859F9Dc903fdd5DE135F32EbC20E
。你的可能不一样。
按如下方式运行块菌控制台:
$ truffle console
现在我们的智能合约不再孤独,如下代码所示:
truffle(development)> Donation.at("0x98Db4235158831BF9133faC1c4e1829021ecEB67").then(function(instance) { return instance.donation_smart_contract_call_hello_smart_contract_method.call("0xBc932d934cfE859F9Dc903fdd5DE135F32EbC20E"); } );
'0x48656c6c6f2c205361746f736869204e616b616d6f746f'
智能合约之间交互的一个用例是创建一个分散的交换智能合约。假设您的祖母启动了一个名为 power grid token 的代币智能合约,您的叔叔启动了一个名为 Wi-Fi access token 的代币智能合约。您可以创建与电网令牌和 Wi-Fi 接入令牌交互的智能合约。在您的智能合约中,您可以创建一个方法来支持这两个代币之间的交易;你只需要得到他们智能合约的地址和接口。当然,你还需要写出交易的逻辑。
您可以创建一个脚本来编译 Vyper 代码,而不是使用命令行实用程序。确保您位于包含hello.vy
和donation.vy
的同一目录中。创建一个名为compiler.vy
的脚本,如下所示:
import vyper
import os, json
filename = 'hello.vy'
contract_name = 'Hello'
contract_json_file = open('Hello.json', 'w')
with open(filename, 'r') as f:
content = f.read()
current_directory = os.curdir
smart_contract = {}
smart_contract[current_directory] = content
format = ['abi', 'bytecode']
compiled_code = vyper.compile_codes(smart_contract, format, 'dict')
smart_contract_json = {
'contractName': contract_name,
'abi': compiled_code[current_directory]['abi'],
'bytecode': compiled_code[current_directory]['bytecode']
}
json.dump(smart_contract_json, contract_json_file)
contract_json_file.close()
如果您使用以下命令执行此脚本,您将获得一个可与 Truffle 一起使用的Hello.json
文件,如下代码所示:
(vyper-venv) $ python compiler.py
现在,让我们一点一点地学习脚本。首先,导入Vyper
库和一些 Python 标准库,这样我们就可以编写一个 JSON 文件,如下所示:
import vyper
import os, json
您需要一个 Vyper 文件、要为智能合约指定的名称以及输出 JSON 文件。以下代码将执行此任务:
filename = 'hello.vy'
contract_name = 'Hello'
contract_json_file = open('Hello.json', 'w')
使用以下代码行获取 Vyper 文件的内容:
with open(filename, 'r') as f:
content = f.read()
然后创建一个 dictionary 对象,其中键是 Vyper 文件的路径,值是 Vyper 文件的内容,如下所示:
current_directory = os.curdir
smart_contract = {}
smart_contract[current_directory] = content
要编译 Vyper 代码,只需使用vyper
模块中的compile_codes
方法,如下所示:
format = ['abi', 'bytecode']
compiled_code = vyper.compile_codes(smart_contract, format, 'dict')
compile_codes
方法的第一个参数是一个字典,其中包含指向路径的关键点和表示字符串中 Vyper 代码的值。第二个参数是format
,它由接口和字节码组成。第三个参数是可选的。如果你使用'dict'
,你会得到一本字典。如果不给出第三个参数,那么将得到一个数组。让我们看看下面的代码:
smart_contract_json = {
'contractName': contract_name,
'abi': compiled_code[current_directory]['abi'],
'bytecode': compiled_code[current_directory]['bytecode']
}
因为我们使用'dict'
作为第三个参数,所以我们得到了 dictionary 对象的结果。结果的关键是我们到 Vyper 文件的路径。从技术上讲,您可以将其设置为任何您喜欢的字符串。一些开发人员使用文件路径来区分分散在项目目录中的 Vyper 文件。
最后一段代码用于将结果写入输出 JSON 文件:
json.dump(smart_contract_json, contract_json_file)
contract_json_file.close()
通过以编程方式编译 Vyper 代码,您可以在 Vyper 之上构建一个框架。在本书后面的章节中,您将使用一个名为 Populus 的框架来编译和部署 Vyper 文件。但是您可能想要构建一个更好的框架,或者您可以构建一个 Vyper集成开发环境(IDE),比如 JetBrains IDE,但不是针对 Vyper 编程语言。
Vyper 不像 Python 那么自由;你必须接受一些限制。要克服这些限制,你需要与它们和睦相处,或者你需要释放你的创造力。这里有一些关于如何做到这一点的提示。
第一个限制是数组必须具有固定大小。在 Python 中,您可能非常习惯于拥有一个可以随心所欲扩展的列表,如以下代码所示:
>>> flexible_list = []
>>> flexible_list.append('bitcoin')
>>> flexible_list.append('ethereum')
>>> flexible_list
['bitcoin', 'ethereum']
Vyper 中没有这样的东西。你必须声明你的数组有多大。然后,必须使用整数变量来跟踪已插入此固定大小数组的项目数。您在Donation
智能合约中使用了此策略。
如果您渴望拥有一个无限大小的数组,有一种方法可以实现这一点。可以使用带有整数作为键的映射数据类型。您仍然使用整数变量来跟踪已插入此映射数据类型变量的项目数,如以下代码所示:
infinite_array_of_strings: map(uint256, bytes[100])
index: int128
但是由于infinite_array_of_strings
是一种映射数据类型,您有责任保护该变量不受非整数键的影响。
第二个限制是映射数据类型不能接受复合数据类型作为键。因此,不能将映射数据类型或结构数据类型作为键。但它可以接受映射数据类型或结构数据类型作为值,如下代码所示:
mapping_of_mapping_of_mapping: map(uint256, map(uint256, map(uint256, bytes[10])))
如果要使用 struct 作为映射数据类型变量的键,可以首先序列化它们。例如,如果要使用两个字符串作为映射数据类型变量的键,则可以连接这些字符串以生成映射数据类型变量的键,如以下代码所示:
friend1_str: bytes32 = convert(friend1, bytes32)
friend2_str: bytes32 = convert(friend2, bytes32)
key: bytes[100] = concat(friend1_str, friend2_str)
dating[key] = True
也可以使用嵌套数组,如下所示:
dating[friend1_address][friend2_address] = True
哪种方法更好取决于具体情况和您的偏好。
第三个限制是 Vyper 编程语言无法访问真实世界。因此,不要在智能合约中想象以下情况:
nba_final_winner = nba.get_json_winner('2019/2020')
在本章中,我们学习了如何使用 Vyper 编程语言编写智能合约。首先,我们安装了 Vyper 编译器。然后我们制定了一个智能合同。通过这样做,我们了解了 Vyper 编程语言的大部分特性,包括函数装饰器、初始化函数和函数权限修饰符。还有一些数据类型,如地址、整数、时间戳、映射、数组和字节数组(字符串)。我们学习了如何将 Vyper 源代码编译成智能合约,然后使用 Truffle 工具将其部署到 Ganache。我们还通过 Truffle 控制台与智能合约进行交互。
在下一章中,我们将学习web3.py
。这是构建去中心应用的第一步。