3. 智能合约开发
3.1. Go合约
3.1.1. 概述
本项目为go示例合约,实现了对key/value对的增删查改的功能
3.1.2. 开发环境
go版本:1.17+
IDE:Goland或IDEA
在拥有智能合约开发环境后,基于gotest合约demo,进行go语言开发。
Demo目录如图


3.1.3. 编写合约
对合约的编写主要是在example/gotest/src里
state.go
state.go存放合约使用的存储定义和数据结构定义。
主要是set存数据和get取数据,对于要存储的数据类型、对应的key都可以自行定义。
例如:Dome中定义了item结构体,存储item和取出item方法。

storage.Set 传入[]byte类型的key , value. Demo里saveitem方法将item转化为json格式作为value进行存储 , 可根据自己的需求进行变换。
storage.Get 传入[]byte类型的key, 返回value.
关于storage的方法还有Range,Remove(删除), 详情查看std/imports.go
msg.go
msg.go存放合约接收消息的定义。
根据合约需求定义不同的结构体类型, 里面必须定义InstantiateMsg, ExecuteMsg。
MigrateMsg QueryMsg 每个结构体里的内容也是根据需求自定义,但需要对应后面的执行。
示例:

例如,想对合约一次传入2个Item,可以这样定义:

contract.go
contract.go存放合约各方法的实现
合约结构体必须包含Instantiate合约方法,用于合约初始化时执行相应逻辑

Migrate 升级合约

合约结构体必须包含Execute合约方法,用于合约执行时处理收到相应消息类型

Execute函数相当于一个分流器,根据传入的参数去执行不同的函数,每类消息里的case需要跟msg.go里的定义相对应,下面要执行的函数根据业务需求来写,需要返回的结构体对的response。
Demo中的executeCreatea函数示例:
func executeCreate(deps *std.Deps, _ types.Env, _ types.MessageInfo, create *Create) (*types.Response, error) {
err := SaveItem(deps.Storage, create.Item)
if err != nil {
return nil, err
}
return &types.Response{}, nil
}
合约调用合约写法:
设置和调用合约所需操作的数据结构

示例操作

合约结构体必须包含Query合约方法,用于合约查询时处理收到相应消息类型。

Query函数与Execute类似,根据传入的参数去执行不同的函数,每类消息里的case需要跟msg.go里的定义相对应,下面要执行的函数根据业务需求来写,需要返回的结构体对的response。
合约本质上是对链存储的操作。

合约操作原语包含对Storage的增删查改,Storage的定义如上所示。
常用接口如下:
1.func (s ExternalStorage) Get(key []byte) (value []byte) {} :数据查询接口
2.func (s ExternalStorage) Range(start, end []byte, order Order) (iter Iterator) {} :范围查询接口
3.func (s ExternalStorage) Set(key, value []byte) {} : 数据存储接口
4.func (s ExternalStorage) Remove(key []byte) {} :数据删除接口
合约与链的交互
获取链相关信息
Env:定义了此合约运行所在的区块链环境的状态,必须只包含受信任的数据 - Tx 本身中没有尚未验证的数据。Env在传递到wasm合约之前由json转换为byte切片。
包含:Block (区块信息,含有高度、时间、链ID)
Contract(合约信息,含合约地址)
Transaction(交易信息,含有唯一链的事务标识符,以供查询)
获取当前区块高度:
env.Block.Height
获取区块生成时间:
env.Block.Time
MessageInfo: 包含执行者的地址和一起发送到合同的资金金额的信息。
获取交易发送者地址:
types.MessageInfo.Sender
Response: 当instantiate/execute/migrate成功时的返回值。
包含:Message(直接来自contract,是对操作的请求。)
Data
Attributes(日志事件的属性)
Events(事件)
其中messages用于跨模块与跨合约操作,Attributes用于写日志,data用于返回值供链使用
示例如下:
跨合约调用
func (r Response) AddWasmMsg(contractName string, msg []byte) Response {
m := CosmosMsg{
Wasm: &WasmMsg{
Execute: &ExecuteMsg{
ContractAddr: contractName,
Msg: msg,
},
},
}
sm := NewSubMsg(m)
r.Messages = append(r.Messages, sm)
return r
}
将合约的EventAttribute打印到Attributes
func (r Response) AddAttribute(key string, value string) Response {
m := EventAttribute{
key,
value,
}
r.Attributes = append(r.Attributes, m)
return r
}
将b返回给链,链可以获取到b进行其他操作
func (r Response) SetData(b []byte) Response {
r.Data = b
return r
}
Deps:传递给contract的可变入口点的依赖。
包含:Storage(提供对数据持久化层的读写访问)
Api(Api提供对常见实用工具的访问,如地址解析和验证。)
Querier(用于查询其他合同的信息。)
3.1.4. 编译合约
1. 编译生成json文件

点击执行小绿三角,执行完毕后可以查看xx_tinyjson.go是否生成更新
2.编译合约
进入Makefile文件,执行gotest语句,或者在终端输入该条指令执行。[注:${CURDIR)换成当前目录]

根据网络情况稍等一段时间后,合约编译完成结果如下所示:

编译完成的文件默认在gotest下,目录下,扩展名wasm

3.2. Rust合约
3.2.1. 概述
本项目为Rust示例合约,实现了对key/value对的增删查改的功能。
3.2.2. 开发环境
Rust版本:1.55+
IDE:Clion或IDEA
3.2.3. 合约项目目录说明
src目录:存放合约代码。lib.rs中导入其它文件。需要编写的文件为contract.rs、msg.rs、state.rs。
contract.rs存放合约各方法的实现。msg.rs存放合约接收消息的定义。state.rs存放合约使用的存储定义和数据结构定义。
在Cargo.toml中添加项目要导入的依赖。
3.2.4. 合约主体
智能合约模块支持灵活定义合约方法并对外暴露,以提供区块链模块执行,具体步骤如下:
3.2.4.1. 在msg.rs中必须定义合约接收的消息类型
pub struct InstantiateMsg {} // 合约实例化时调用
pub enum ExecuteMsg { // 合约执行时可调用的方法
Create { item: Item },
}
pub enum QueryMsg { // 合约查询时可调用的方法
Find { key: String },
}
pub struct MigrateMsg {} // 合约升级时调用
3.2.4.2. 在state.rs中定义合约用到的存储和数据结构
引入存储的定义
pub const ITEM_KEY: &[u8] = b"item";
不同数据类型的存储通过前缀区分
pub static STUDENT_KEY: &[u8] = b"student";
读写存储的定义:
pub fn item_store(storage: &mut dyn Storage) -> Bucket<Item> {
bucket(storage, ITEM_KEY)
}//写权限的存储访问
pub fn item_store_read(storage: &dyn Storage) -> ReadonlyBucket<Item> {
bucket_read(storage, ITEM_KEY)
}//读权限的存储访问
在contract.rs中可以通过如下方式调用:
let item_option = item_store_read(deps.storage).may_load(key.as_bytes())?;//查询
item_store(deps.storage).save(item.key.as_bytes(), &item)?;//存储
引入自定义结构体的定义
pub struct Item {
pub key: String,
pub value: String,
}
3.2.4.3. 在contract.rs中写合约各消息类型的实现方法
合约中必须包含instantiate合约方法,用于合约初始化时执行相应逻辑,#[entry_point]表明该方法是合约的一个入口访问点
#[entry_point]
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: InstantiateMsg,
) -> StdResult<Response> {
Ok(Response::default())
}
合约中必须包含execute合约方法,用于合约执行时处理收到相应消息类型,#[entry_point]表明该方法是合约的一个入口访问点
#[entry_point]
pub fn execute(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
match msg {
ExecuteMsg::Create { item } => create(deps, item),
}
}
合约中必须包含query合约方法,用于合约查询时处理收到相应消息类型,#[entry_point]表明该方法是合约的一个入口访问点
#[entry_point]
pub fn query(
deps: Deps,
_env: Env,
msg: QueryMsg,
) -> StdResult<QueryResponse> {
match msg {
QueryMsg::Find { key } => find(deps, key),
}
}
合约中必须包含migrate合约方法,用于合约升级化时执行相应逻辑,#[entry_point]表明该方法是合约的一个入口访问点
#[entry_point]
pub fn migrate(
_deps: DepsMut,
_env: Env,
_msg: MigrateMsg,
) -> StdResult<Response> {
Ok(Response::default())
}
3.2.4.4. 实现方法
合约本质上是对链存储的操作。
合约操作原语包含对store的增删查改。store的定义如上所示。
常用接口如下:
store(&mut deps.storage).may_load(key.as_bytes()): 数据查询接口
store(&mut deps.storage).save(key.as_bytes(), &value): 数据存储接口
store(&mut deps.storage).remove(key.as_bytes()): 数据删除接口
Err(StdError::generic_err("")): 返回错误接口
使用方法示例具体如下:
fn create(deps: DepsMut, item: Item) -> StdResult<Response> {
let existed_value = store(&mut deps.storage).may_load(key.as_bytes())?;
if existed_value.is_some() {
return Err(StdError::generic_err("The key already existed"));
}
store(&mut deps.storage).save(key.as_bytes(), &value)?;
Ok(Response::default())
}
3.2.4.5. 合约与链的交互
获取链相关信息
合约实现方法中包含参数Env,其结构如下所示
pub struct Env {
pub block: BlockInfo,
pub transaction: Option<TransactionInfo>,
pub contract: ContractInfo,
}
pub struct BlockInfo {
pub height: u64,
pub time: Timestamp,
pub chain_id: String,
}
pub struct TransactionInfo {
pub index: u32,
}
pub struct ContractInfo {
pub address: Addr,
}
合约实现方法中包含参数MessageInfo,其结构如下所示
pub struct MessageInfo {
pub sender: Addr,
pub funds: Vec<Coin>,
}
如在合约方法中获取合约调用者地址可以如下操作
let sender = info.sender.to_string();
合约方法返回消息类型定义如下
pub struct Response<T = Empty> {
pub messages: Vec<SubMsg<T>>,
pub attributes: Vec<Attribute>,
pub events: Vec<Event>,
pub data: Option<Binary>,
}
其中messages用于跨模块与跨合约操作
attributes用于写日志
data用于返回值供链使用,示例如下
跨合约调用
let cross_msg = CrossMsg { create: CrossParam { item } };
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: "test".to_string(),
msg: to_binary(&cross_msg).unwrap(),
funds: vec![],
})))
将合约create函数名和attribute_value变量打印到log
Ok(Response::new()
.add_attribute("create", attribute_value))
将result返回给链,链可以获取到result进行其他操作
Ok(Response::new().set_data(result.as_bytes()))
3.2.5. 官方库支持
智能合约由Rust官方编译支持,支持除浮点数、随机数、时间外的大部分Rust官方库与特性
3.2.6. 第三方库支持
支持如schemars、serde、snafu、p256、hex、pem、x509等
3.2.7. 编译
基于合约Demo,进行Rust语言开发,Demo为IDEA的Rust工程
安装Rust环境
[https://www.rust-lang.org/zh-CN/tools/install]
安装wasm编译插件
rustup target add wasm32-unknown-unknown
将合约编译成wasm文件
cargo build --release --target wasm32-unknown-unknown
编译完的wasm文件在target/wasm32-unknown-unknown/release/test.wasm路径
3.2.8. 合约部署
命令行部署
合约部署详情查看
xccli tx wasm instantiate -h
举例
xccli tx wasm instantiate test.wasm rust 2 "{}" test --from jack --gas="800000" --gas-adjustment="1.2" -y
Java-SDK:见SDK示例org/xbl/xchain/sdk/example中,RustContract相关测试。
3.2.9. 合约调用
命令行
命令说明:
xccli tx wasm execute -h
举例:
PUT='{"create":{"item":{"key":"key1","value":"value1"}}}'
xccli tx wasm execute test "$PUT" --from jack --gas="800000" -y
Java-SDK:见SDK示例org/xbl/xchain/sdk/example中,RustContract相关测试。
3.2.10. 合约查询
命令行
命令说明:
xccli query wasm -h
举例
QUERY='{"find":{"key":"key1"}}'
xccli query wasm contract-state smart test "$QUERY"
3.2.11. 合约升级
命令行
命令说明:
xccli tx wasm migrate -h
举例:
xccli tx wasm migrate test test1.wasm 2 "{}" --from jack --gas="800000" --gas-adjustment="1.2" -y
3.2.12. 查看交易状态
xccli query tx txhash
其中txhash用交易返回的txhash替换
Java-SDK:见SDK示例org/xbl/xchain/sdk/example中,RustContract相关测试。