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}

ShellCopy

打开钱包

neo-python项目目录下有个示例的样品钱包neo-privnet.sample.wallet,我们可以来看一下这个钱包。

open wallet neo-privnet.sample.wallet

BashCopy

然后输入钱包密码coz,就成功打开钱包了

查看钱包

打开钱包后,输入wallat,就可以查看钱包的信息

信息很清晰明了,不一一介绍了,有钱包路径,地址(1),余额(2),公钥,索赔(3)等。

  • synced_balances余额里有99999000.0NEO和159016.0Gas,关于什么是NEO,什么是Gas请查看NEO 白皮书
  • claims索赔里有11808.0可用索赔和11703.88296不可用索赔。我们可以所以索赔这11808.0可获索赔到我们的钱包,执行
wallet claim 11808

ShellCopy

然后输入密码coz

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

创建新钱包

我们可以使用create wallet {path}来创建一个新钱包,path是你存放钱包的位置,例如我创建一个钱包

create wallet sww-wallet

BashCopy

然后输入密码,确认密码(这是给你的新钱包设置密码)。

我们已经创建自己的钱包了,可以看到我的钱包里没有任何余额,接下来我们使用neo-privnet.sample.wallet给我的钱包转账。

转账

我们使用neo-privnet.sample.wallet给我们的钱包转账,因为创建新钱包后会自动打开新钱包,所以我们要重新打开neo-privnet.sample.wallet钱包。

open wallet neo-privnet.sample.wallet

BashCopy

然后转账

send neo {address} 10000 # 发送neo
send gas {address} 10000 # 发送gas

BashCopy

{address}是你的钱包地址,1000是金额。例如我的钱包地址是AS9yCLz2Bw3hrnWdbwedw6fZGvZaFXo8BF,我给我的钱包转10000NEO10000Gas

# 发送 NEO
send neo AS9yCLz2Bw3hrnWdbwedw6fZGvZaFXo8BF 10000
# 发送 Gas
send gas AS9yCLz2Bw3hrnWdbwedw6fZGvZaFXo8BF 10000

BashCopy

转账需要等待区块打包确认,所以需要15-20秒时间。

然后我们切换到我们自己的钱包查看一下余额,可以看到我们的余额已经增加了

钱包就简单介绍到这里。


智能合约

我们在neo-python下新建一个smart-contracts文件夹用来放我们的智能合约。

第一个合约:print Hello World

smart-contracts下新建一个1-print.py文件,编辑内容如下:

def Main():
    print("Hello World")

PythonCopy

Tip:Main()函数是合约执行的入口。

编译

在编译之前,我们先在NEO命令行执行config sc-events on来打开合约事件的日志。
然后我们执行编译

build smart-contracts/1-print.py test ff ff False False

BashCopy


可以看到打印了Hello World
合约编译命令详解:

build {path/to/file.py} (test {params} {returntype} {needs_storage} {needs_dynamic_invoke} {test_params})

BashCopy

  • {path/to/file.py}:我们需要编译的合约文件(.py文件)的路径。
  • (test …): 括号里表示的为可选的用于直接测试的参数,如果跟上上test表示编译后直接测试合约,test后面的参数是测试合约用的。
  • {params}:合约程序的参数类型(如果有参数的话)。
  • {returntype}:合约程序的返回值类型(如果有返回值的话),参数和返回值类型会在下面介绍。
  • {needs_storage}:合约程序中是否需要用到存储。
  • {needs_dynamic_invoke}:合约执行是否需要特殊条件,动态调用。
  • {test_params}:传入合约程序的测试参数(如果有的话)。

paramsreturntype参数类型介绍:

参数类型参数类型表示值
Signature00
Boolean01
Integer02
Hash16003
Hash25604
ByteArray05
PublicKey06
String07
Array10
InteropInterfacef0
voidff

我们的1-print.py合约中,只是print("Hello World")了一下Hello World不需要参数(即参数类型为void),也没有返回值(即返回值类型为void),也不需要存储,不需要特殊条件,也不用传入测试参数,所以我们的编译语句为:

build smart-contracts/1-print.py test ff ff False False

BashCopy

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

BashCopy

然后我们需要依次按提示输入

  • 合约名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

BashCopy

可以看到和我们刚部署时的hash是一致的。
然后通过testinvoke {contract hash}命令来测试调用合约,{contract hash}就是你的合约的hash值。

testinvoke 0x5f21886e9c5674ef65f3ba787c45c7a4957621cd

BashCopy

可以看到测试调用合约的成功,输入密码以继续,合约将在区块链网络上调用(这是需要消费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)

PythonCopy

print()是Python内置的打印函数,Log()Notify()同样是打印输出,不同的是Notify()可以打印object对象,上面我们用Notify()打印了一个数组。

编译

因为这个合约没有返回值没有参数不需要存储,所以编译命令如下

build smart-contracts/2-print-and-notify.py test ff ff False False

BashCopy

部署和调用

这里和上面的合约类似,自己尝试一下吧。

第三个合约: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

PythonCopy

我们的合约有三个参数,第一个operation指的是操作:addsubmuldiv分别表示加减乘除,否则返回-1,ab表示参加运算的两个整数,返回值为整数。

编译

查上面的表可以知道,我们的输入参数类型为070202(07表示字符串,02表示整数),返回值类型是02。所以我们的编译命令是:

build smart-contracts/3-calculator.py test 070202 02 False False add 3 4

PythonCopy

后面的 add 3 4是我传入合约用于编译后测试合约的参数。
编译并测试的结果:

部署

import contract smart-contracts/3-calculator.avm 070202 02 False False

BashCopy

调用合约

我们来执行计算5和6的乘积

testinvoke 0x86d58778c8d29e03182f38369f0d97782d303cc0 mul 5 6

BashCopy


可以看到测试结果与在区块链上执行的结果。

第四个合约: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

PythonCopy

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

BashCopy


可以看到我们存储了1。
再次执行

build smart-contracts/4-storage.py test ff 02 True False

BashCopy

可以看到存储了2。

部署

import contract smart-contracts/4-storage.avm ff 02 True False

BashCopy

调用合约

 testinvoke 0xec9a9f99b894c333667b008b9df35faaf4536143 # 换成你的合约的hash值

BashCopy

如果短时间内执行了多次合约测试,没有等待区块确认,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

PythonCopy

代码很简单,就不仔细讲解了,简单梳理下:

  • 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

BashCopy

部署

import contract smart-contracts/5-domain.avm 0710 05 True False

BashCopy

调用合约

  1. 注册一个域名
 testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 register ['sww.com', 'ATZE8izvnGGuxLYRZUDPwFr6yqyaBavVWV']
 ```
[![](https://sww-wordpress.oss-cn-beijing.aliyuncs.com/2018/07/domain-register-1.png)](https://sww-wordpress.oss-cn-beijing.aliyuncs.com/2018/07/domain-register-1.png)
2. 查询一个域名
```bash
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 query ['sww.com']

BashCopy


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

可以看到与我的地址一致。
3. 转让域名
我创建了一个新钱包,地址为ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve

我们测试将域名转让到此地址

open wallet sww-wallet
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 transfer ['sww.com', 'ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve']

BashCopy


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

testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 query ['sww.com']

BashCopy



可以看到sww.com所对应的地址是ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve
4. 删除域名
我们现在删除sww.com

testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 delete ['sww.com']

BashCopy


可以看到删除失败了,因为我们已经将sww.com转让了,现在我们是无权删除它的。
因为现在sww.com已经被转让到shiwenwenwallet钱包下,由于调用合约需要使用Gas。所以我们需要给shiwenwenwallet先转账一部分Gas,然后切换到shiwenwenwallet删除sww.com

send gas ASPkJuDuNKbQCAVHkmFCGQLBwCJbLnpxve 1000
testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 delete ['sww.com']

BashCopy


可以看到删除成功了。我们再次查询sww.com

testinvoke 0x37c7ed02c81dbe6109e7b45b8fbbf43f585a71d2 query ['sww.com']

BashCopy


可以看到提示这个域名未注册,该域名已经不存在了。

结束

我们已经对NEO区块链做了些基本操作了,你应该已经清楚地了解如何将智能合约部署和调用到NEO区块链。

原文:https://blog.gallifrey.cn/?p=439


NEOFANS 微博:https://www.weibo.com/neofanscommunity

NEOFANS Telegram群:t.me/NEOfansCN


发表评论

Top