NEO区块链-Python编写智能合约(二)合约开发

在上一篇已经介绍了如何搭建好环境了,如果你还没有搭建好环境,请移步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

然后输入密码coz


然后我们再次执行wallet查看钱包,就可以看到我们的钱包Gas余额多了11808,并且已经没有可用索赔了。
image

创建新钱包

我们可以使用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,我给我的钱包转10000NEO10000Gas

# 发送 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}:传入合约程序的测试参数(如果有的话)。

paramsreturntype参数类型介绍:

参数类型 参数类型表示值
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指的是操作:addsubmuldiv分别表示加减乘除,否则返回-1,ab表示参加运算的两个整数,返回值为整数。

编译

查上面的表可以知道,我们的输入参数类型为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

这里我们将要用到存储了,通过keyvalue存储。在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

调用合约

  1. 注册一个域名
 testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 register ['sww.com', 'ATZE8izvnGGuxLYRZUDPwFr6yqyaBavVWV']

  1. 查询一个域名
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 query ['sww.com']


可以看到查询结果为813d340c1416f019fd5cc898dfaacab251c7da48这和我的地址ATZE8izvnGGuxLYRZUDPwFr6yqyaBavVWV是完全不一样的,因为合约返回的是一个字节数组,所以我们需要转换一下,可以使用NEO的Perter童鞋写的在线[工具]。(https://peterlinx.github.io/DataTransformationTools/ "工具")
image

可以看到与我的地址一致。

  1. 转让域名
    我创建了一个新钱包,地址为ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve
    image

    我们测试将域名转让到此地址
open wallet sww-wallet
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 transfer ['sww.com', 'ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve']


然后我们再查询下sww.com,查看sww.com所对应的地址

testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 query ['sww.com']


image

可以看到sww.com所对应的地址是ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve

  1. 删除域名
    我们现在删除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区块链。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容