在上一篇已经介绍了如何搭建好环境了,如果你还没有搭建好环境,请移步NEO区块链-Python编写智能合约(一)环境搭建,本文将介绍如钱包创建和使用,智能合约的开发与部署。
钱包
进入NEO命令行,想了解命令行如何工作,可以在命令行输入help
,查看命令行帮助
NEO cli. Type 'help' to get started
neo> help
quit
help
block {index/hash} (tx)
header {index/hash}
tx {hash}
asset {assetId}
asset search {query}
contract {contract hash}
contract search {query}
notifications {block_number or address}
mem
nodes
state
config debug {on/off}
config sc-events {on/off}
config maxpeers {num_peers}
build {path/to/file.py} (test {params} {returntype} {needs_storage} {needs_dynamic_invoke} {test_params})
load_run {path/to/file.avm} (test {params} {returntype} {needs_storage} {needs_dynamic_invoke} {test_params})
import wif {wif}
import nep2 {nep2_encrypted_key}
import contract {path/to/file.avm} {params} {returntype} {needs_storage} {needs_dynamic_invoke}
import contract_addr {contract_hash} {pubkey}
import multisig_addr {pubkey in wallet} {minimum # of signatures required} {signing pubkey 1} {signing pubkey 2}...
import watch_addr {address}
import token {token_contract_hash}
export wif {address}
export nep2 {address}
open wallet {path}
create wallet {path}
wallet {verbose}
wallet claim (max_coins_to_claim)
wallet migrate
wallet rebuild {start block}
wallet delete_addr {addr}
wallet delete_token {token_contract_hash}
wallet alias {addr} {title}
wallet tkn_send {token symbol} {address_from} {address to} {amount}
wallet tkn_send_from {token symbol} {address_from} {address to} {amount}
wallet tkn_approve {token symbol} {address_from} {address to} {amount}
wallet tkn_allowance {token symbol} {address_from} {address to}
wallet tkn_mint {token symbol} {mint_to_addr} (--attach-neo={amount}, --attach-gas={amount})
wallet tkn_register {addr} ({addr}...) (--from-addr={addr})
wallet tkn_history {token symbol}
wallet unspent
wallet close
withdraw_request {asset_name} {contract_hash} {to_addr} {amount}
withdraw holds # lists all current holds
withdraw completed # lists completed holds eligible for cleanup
withdraw cancel # cancels current holds
withdraw cleanup # cleans up completed holds
withdraw # withdraws the first hold availabe
withdraw all # withdraw all holds available
send {assetId or name} {address} {amount} (--from-addr={addr})
sign {transaction in JSON format}
testinvoke {contract hash} {params} (--attach-neo={amount}, --attach-gas={amount}) (--from-addr={addr})
debugstorage {on/off/reset}
打开钱包
neo-python
项目目录下有个示例的样品钱包neo-privnet.sample.wallet
,我们可以来看一下这个钱包。
open wallet neo-privnet.sample.wallet
然后输入钱包密码coz
,就成功打开钱包了
查看钱包
打开钱包后,输入wallat
,就可以查看钱包的信息
信息很清晰明了,不一一介绍了,有钱包路径,地址(1),余额(2),公钥,索赔(3)等。
synced_balances
余额里有99999000.0NEO和159016.0Gas,关于什么是NEO,什么是Gas请查看NEO 白皮书claims
索赔里有11808.0可用索赔和11703.88296不可用索赔。我们可以所以索赔这11808.0可获索赔到我们的钱包,执行
wallet claim 11808
然后我们再次执行
wallet
查看钱包,就可以看到我们的钱包Gas余额多了11808,并且已经没有可用索赔了。创建新钱包
我们可以使用create wallet {path}
来创建一个新钱包,path是你存放钱包的位置,例如我创建一个钱包
create wallet sww-wallet
然后输入密码,确认密码(这是给你的新钱包设置密码)。
我们已经创建自己的钱包了,可以看到我的钱包里没有任何余额,接下来我们使用neo-privnet.sample.wallet
给我的钱包转账。
转账
我们使用neo-privnet.sample.wallet
给我们的钱包转账,因为创建新钱包后会自动打开新钱包,所以我们要重新打开neo-privnet.sample.wallet
钱包。
open wallet neo-privnet.sample.wallet
然后转账
send neo {address} 10000 # 发送neo
send gas {address} 10000 # 发送gas
{address}
是你的钱包地址,1000是金额。例如我的钱包地址是AS9yCLz2Bw3hrnWdbwedw6fZGvZaFXo8BF
,我给我的钱包转10000NEO和10000Gas:
# 发送 NEO
send neo AS9yCLz2Bw3hrnWdbwedw6fZGvZaFXo8BF 10000
# 发送 Gas
send gas AS9yCLz2Bw3hrnWdbwedw6fZGvZaFXo8BF 10000
转账需要等待区块打包确认,所以需要15-20秒时间。
然后我们切换到我们自己的钱包查看一下余额,可以看到我们的余额已经增加了
钱包就简单介绍到这里。
智能合约
我们在neo-python
下新建一个smart-contracts
文件夹用来放我们的智能合约。
第一个合约:print Hello World
在smart-contracts
下新建一个1-print.py
文件,编辑内容如下:
def Main():
print("Hello World")
Tip:Main()函数是合约执行的入口。
编译
在编译之前,我们先在NEO命令行执行config sc-events on
来打开合约事件的日志。
然后我们执行编译
build smart-contracts/1-print.py test ff ff False False
可以看到打印了
Hello World
。合约编译命令详解:
build {path/to/file.py} (test {params} {returntype} {needs_storage} {needs_dynamic_invoke} {test_params})
- {path/to/file.py}:我们需要编译的合约文件(.py文件)的路径。
- (test ...): 括号里表示的为可选的用于直接测试的参数,如果跟上上test表示编译后直接测试合约,test后面的参数是测试合约用的。
- {params}:合约程序的参数类型(如果有参数的话)。
- {returntype}:合约程序的返回值类型(如果有返回值的话),参数和返回值类型会在下面介绍。
- {needs_storage}:合约程序中是否需要用到存储。
- {needs_dynamic_invoke}:合约执行是否需要特殊条件,动态调用。
- {test_params}:传入合约程序的测试参数(如果有的话)。
params和returntype参数类型介绍:
参数类型 | 参数类型表示值 |
---|---|
Signature | 00 |
Boolean | 01 |
Integer | 02 |
Hash160 | 03 |
Hash256 | 04 |
ByteArray | 05 |
PublicKey | 06 |
String | 07 |
Array | 10 |
InteropInterface | f0 |
void | ff |
我们的1-print.py
合约中,只是print("Hello World")
了一下Hello World
不需要参数(即参数类型为void),也没有返回值(即返回值类型为void),也不需要存储,不需要特殊条件,也不用传入测试参数,所以我们的编译语句为:
build smart-contracts/1-print.py test ff ff False False
NEO支持Java,Python,C#等语言来编写智能合约,NEO本身不能执行Python,Java之类的程序,通过编译器将这些图灵类似的语言编译成.avm文件然后被NEO所使用。可以看到上面我们执行build
的日志里有Saved output to smart-contracts/1-print.avm
,打开smart-contracts
目录会发现多了1-print.avm
文件。
部署
我们将编译后的合约(.avm)部署到NEO区块链上,执行:
import contract smart-contracts/1-print.avm ff ff False False
然后我们需要依次按提示输入
- 合约名Contract Name
- 合约版本Contract Version
- 合约作者Contract Author
- 合约邮箱Contract Email
- 合约描述Contract Description
部署合约到NEO区块链上是需要花费Gas的,所以你还需要输入密码。确认后等待15-20秒时间,等待区块确认。
我们现在已经成功部署hello到我们的区块链。
调用合约
我们需要通过合约的hash值来调用合约,如何查看部署的合约的hash值呢,我们在import
部署的时候已经通过日志打印出来了,我们还可以通过contract search {contract info}
来搜索我们的合约,查看合约的信息,{contract info}
可以是合约的任何信息,例如名称,作者。
contract search hello # 或 contract search sww
可以看到和我们刚部署时的hash是一致的。
然后通过testinvoke {contract hash}
命令来测试调用合约,{contract hash}
就是你的合约的hash值。
testinvoke 0x5f21886e9c5674ef65f3ba787c45c7a4957621cd
可以看到测试调用合约的成功,输入密码以继续,合约将在区块链网络上调用(这是需要消费gas的),然后你需要等待15-20秒,这个时候你就成功在链上执行了你的合约了。
第二个合约:print-and-notify
在smart-contracts
下新建2-print-and-notify.py
文件,编辑内容如下:
def Main():
# Print translates to a `Log` call, and is best used with simple strings for
# development info. To print variables such as lists and objects, use `Notify`.
print("log via print (1)")
Log("normal log (2)")
Notify("notify (3)")
# Sending multiple arguments as notify payload:
msg = ["a", 1, 2, b"3"]
Notify(msg)
print()
是Python内置的打印函数,Log()
和Notify()
同样是打印输出,不同的是Notify()
可以打印object
对象,上面我们用Notify()
打印了一个数组。
编译
因为这个合约没有返回值没有参数不需要存储,所以编译命令如下
build smart-contracts/2-print-and-notify.py test ff ff False False
部署和调用
这里和上面的合约类似,自己尝试一下吧。
第三个合约:calculator
这个合约是做一个计算器,所以有参数有返回值。在smart-contracts
下新建3-calculator.py
文件,编辑内容如下:
def Main(operation, a, b):
if operation == 'add':
return a + b
elif operation == 'sub':
return a - b
elif operation == 'mul':
return a * b
elif operation == 'div':
return a / b
else:
return -1
我们的合约有三个参数,第一个operation
指的是操作:add
,sub
,mul
,div
分别表示加减乘除,否则返回-1,a
和b
表示参加运算的两个整数,返回值为整数。
编译
查上面的表可以知道,我们的输入参数类型为070202
(07表示字符串,02表示整数),返回值类型是02
。所以我们的编译命令是:
build smart-contracts/3-calculator.py test 070202 02 False False add 3 4
后面的 add 3 4
是我传入合约用于编译后测试合约的参数。
编译并测试的结果:
部署
import contract smart-contracts/3-calculator.avm 070202 02 False False
调用合约
我们来执行计算5和6的乘积
testinvoke 0x86d58778c8d29e03182f38369f0d97782d303cc0 mul 5 6
可以看到测试结果与在区块链上执行的结果。
第四个合约:storage
这里我们将要用到存储了,通过key
,value
存储。在smart-contracts
下新建4-storage.py
文件,编辑内容如下:
from boa.interop.Neo.Runtime import Log, Notify
from boa.interop.Neo.Storage import Get, Put, GetContext
def Main():
context = GetContext()
# This is the storage key we use in this example
item_key = 'test-storage-key'
# Try to get a value for this key from storage
item_value = Get(context, item_key)
msg = ["Value read from storage:", item_value]
Notify(msg)
if len(item_value) == 0:
Notify("Storage key not yet set. Setting to 1")
item_value = 1
else:
Notify("Storage key already set. Incrementing by 1")
item_value += 1
# Store the new value
Put(context, item_key, item_value)
msg = ["New value written into storage:", item_value]
Notify(msg)
return item_value
Get(context, key)
是通过上下文使用指定key获取值,Put(context, key, value)
是通过上下文用指定key将value给存储或更新,Delete(context, key)
是通过上下文用删除指定key存储的值。
每次执行合约,我们都会把通过test-storage-key
这个key存储的数加1,如果值不存在,存入1。
编译
因为需要用存储,所以再先执行debugstorage on
打开开发调试环境的存储。
这个合约没有参数但是有返回值,是个整形,并且用到了存储,所以我们的编译命令如下:
build smart-contracts/4-storage.py test ff 02 True False
可以看到我们存储了1。
再次执行
build smart-contracts/4-storage.py test ff 02 True False
可以看到存储了2。
部署
import contract smart-contracts/4-storage.avm ff 02 True False
调用合约
testinvoke 0xec9a9f99b894c333667b008b9df35faaf4536143 # 换成你的合约的hash值
如果短时间内执行了多次合约测试,没有等待区块确认,Test invoke successful
中返回的值可能就没有加1,因为这是测试的结果。当我们等待区块确认后,最终看到的在区块链上执行的结果就是没有问题的。
第五个合约:domain 域名服务
我们的钱包地址是很难记忆的,就像网络中的ip地址一样,所以通常我们在访问网络主机时,都会用形象易记的域名。使用域名访问主机时,会通过DNS服务器,将你访问的域名解析到对应的ip上,所以我们也给钱包地址写一个类似域名服务的合约,让我们通过域名来快速查找钱包地址。
我们的合约应该有以下的基本功能
- 域名注册
- 域名查询
- 删除一个域名
- 转让域名的所有权
在smart-contracts
下新建5-domain.py
文件,编辑内容如下:
from boa.interop.Neo.Runtime import Log, Notify
from boa.interop.Neo.Storage import Get, Put, GetContext
from boa.interop.Neo.Runtime import GetTrigger,CheckWitness
from boa.builtins import concat
def Main(operation, args):
nargs = len(args)
if nargs == 0:
print("No domain name supplied")
return 0
if operation == 'query':
domain_name = args[0]
return QueryDomain(domain_name)
elif operation == 'delete':
domain_name = args[0]
return DeleteDomain(domain_name)
elif operation == 'register':
if nargs < 2:
print("required arguments: [domain_name] [owner]")
return 0
domain_name = args[0]
owner = args[1]
return RegisterDomain(domain_name, owner)
elif operation == 'transfer':
if nargs < 2:
print("required arguments: [domain_name] [to_address]")
return 0
domain_name = args[0]
to_address = args[1]
return TransferDomain(domain_name, to_address)
def QueryDomain(domain_name):
msg = concat("QueryDomain: ", domain_name)
Notify(msg)
context = GetContext()
owner = Get(context, domain_name)
if not owner:
Notify("Domain is not yet registered")
return False
Notify(owner)
return owner
def RegisterDomain(domain_name, owner):
msg = concat("RegisterDomain: ", domain_name)
Notify(msg)
if not CheckWitness(owner):
Notify("Owner argument is not the same as the sender")
return False
context = GetContext()
exists = Get(context, domain_name)
if exists:
Notify("Domain is already registered")
return False
Put(context, domain_name, owner)
return True
def TransferDomain(domain_name, to_address):
msg = concat("TransferDomain: ", domain_name)
Notify(msg)
context = GetContext()
owner = Get(context, domain_name)
if not owner:
Notify("Domain is not yet registered")
return False
if not CheckWitness(owner):
Notify("Sender is not the owner, cannot transfer")
return False
if not len(to_address) != 34:
Notify("Invalid new owner address. Must be exactly 34 characters")
return False
Put(context, domain_name, to_address)
return True
def DeleteDomain(domain_name):
msg = concat("DeleteDomain: ", domain_name)
Notify(msg)
context = GetContext()
owner = Get(context, domain_name)
if not owner:
Notify("Domain is not yet registered")
return False
if not CheckWitness(owner):
Notify("Sender is not the owner, cannot transfer")
return False
Delete(context, domain_name)
return True
代码很简单,就不仔细讲解了,简单梳理下:
- 在
Main(operation, args)
函数中,通过不同的operation
来调用不同的函数进行操作,并进行参数校验。 -
QueryDomain(domain_name)
:进行域名查询操作,通过domain_name
查询所对应的地址,若对应地址存在,则返回查询到的地址,否则返回False
。 -
RegisterDomain(domain_name, owner)
:注册域名,我们应该只能自己当前钱包的地址进行注册等操作,所以使用CheckWitness(owner)
进行核验,核验地址是否为当前钱包所有,然后判断域名是否已经被注册。 -
TransferDomain(domain_name, to_address)
:域名转让,将域名domain_name
转让到新地址to_address
上。首先校验被转让的域名是否已经注册存在,不存在的域名是无法转让的、然后检验新地址是否为当前钱包所有,不是自己的域名是无法转让的、最后检验新地址长度是否合法。 -
DeleteDomain(domain_name)
:删除域名,首先校验域名是否存在,然后校验域名是否为当前钱包所有,否则不能删除。
编译
合约接受一个字符串类型的操作operation
和一个操作所需要的参数数组args
(因为操作不同,参数个数不同,所以使用数组)所以参数类型是0710
,查询会返回一个字节数组ByteArray
,所以返回类型是05
。
build smart-contracts/5-domain.py 0710 05 True False
部署
import contract smart-contracts/5-domain.avm 0710 05 True False
调用合约
- 注册一个域名
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 register ['sww.com', 'ATZE8izvnGGuxLYRZUDPwFr6yqyaBavVWV']
- 查询一个域名
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 query ['sww.com']
可以看到查询结果为
813d340c1416f019fd5cc898dfaacab251c7da48
这和我的地址ATZE8izvnGGuxLYRZUDPwFr6yqyaBavVWV
是完全不一样的,因为合约返回的是一个字节数组,所以我们需要转换一下,可以使用NEO的Perter童鞋写的在线[工具]。(https://peterlinx.github.io/DataTransformationTools/ "工具")可以看到与我的地址一致。
open wallet sww-wallet
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 transfer ['sww.com', 'ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve']
然后我们再查询下
sww.com
,查看sww.com
所对应的地址
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 query ['sww.com']
可以看到
sww.com
所对应的地址是ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve
。
- 删除域名
我们现在删除sww.com
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 delete ['sww.com']
可以看到删除失败了,因为我们已经将
sww.com
转让了,现在我们是无权删除它的。因为现在
sww.com
已经被转让到shiwenwenwallet
钱包下,由于调用合约需要使用Gas。所以我们需要给shiwenwenwallet
先转账一部分Gas,然后切换到shiwenwenwallet
删除sww.com
:
send gas ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve 1000
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 delete ['sww.com']
可以看到删除成功了。我们再次查询
sww.com
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 query ['sww.com']
可以看到提示这个域名未注册,该域名已经不存在了。
结束
我们已经对NEO区块链做了些基本操作了,你应该已经清楚地了解如何将智能合约部署和调用到NEO区块链。