GearFans

V1

2023/04/03阅读:25主题:默认主题

Gear 智能合约开发常用技术 1|Gear Wiki

提示

本篇文章介绍与 Gear 智能合约相关技术,帮助你更好的了解 Gear。

可执行函数

程序是 Gear 组件的主要单元。程序代码存储为不可变的 Wasm 二进制文件(blob)。 每个程序都有一个固定的内存,在消息处理之间持续存在(所谓的静态区域)。

基础结构

任何程序最多可以包含 3 个入口点,它们在程序生命周期中执行各种功能:init()handle()handle_reply()。 它们都是可选的,但任何程序都需要至少有一个方法:init()handle()

Gear 协议引入的另一个特殊系统入口点是 handle_signal()。如果有必要的通知(信号)与程序消息相关的某些事件已经发生,它允许系统与程序通信。

init()

init()方法是可选的,在程序初始化时只被调用一次。并处理传入的 init payload,如果有的话。

#[no_mangle]
extern "C" fn init() {
    // execute this code during contract initialization
}

handle()

handle()方法(同样可选)将在每次程序收到传入的消息时被调用。在这种情况下,payload将被处理。

#[no_mangle]
extern "C" fn handle() {
    // execute this code during explicitly incoming message
}

handle_reply()

在 Gear 程序中对信息的回复,使用handle_reply函数进行处理。

#[no_mangle]
extern "C" fn handle_reply() {
    // execute this code during handling reply on the previously sent message
}

Gear 库

Gear Protocol 的库 gstd 为开发智能合约提供了所有必要和完善的功能和方法。

通过 prelude 导入相似的类型

gstd的默认 prelude 模块列出了 Rust 自动导入每个程序中的东西。它重新导入了默认的 std 模块和 traits。在 Rust 的 Gear 程序中,std 可以安全地替换为 gstd

更多详细内容请看 https://docs.gear.rs/gstd/prelude/index.html

消息处理

Gear Protocol 允许用户和程序通过消息与其他用户和程序进行交互。消息可以包含在 payload,它能够在消息执行期间处理。通过模块 msg,我们可以与消息交互:

use gstd::msg;

消息处理只能在定义的函数 init()handle()hadle_reply() 内进行。它们还定义了处理此类消息的上下文。

  • 获取当前正在处理的消息的 payload 并对其进行解码:
#![no_std]
use gstd::{msg, prelude::*};

#[no_mangle]
extern "C" fn handle() {
    let payload_string: String = msg::load().expect("Unable to decode `String`");
}
  • 使用payload回复消息:
#![no_std]
use gstd::msg;

#[no_mangle]
extern "C" fn handle() {
    msg::reply("PONG", 0).expect("Unable to reply");
}
  • 向用户发送消息:
#![no_std]
use gstd::{msg, prelude::*};

#[no_mangle]
extern "C" fn handle() {
    // ...
    let id = msg::source();
    let message_string = "Hello there".to_string();
    msg::send(id, message_string, 0).expect("Unable to send message");
}

关于 msg 的更多用法,请看 https://docs.gear.rs/gstd/msg/index.html

执行信息

程序可以通过使用 exec 模块获取有关当前执行上下文的一些有用信息:

use gstd::exec;
  • 在区块时间戳到达指定日期后发送回复消息:
#![no_std]
use gstd::{exec, msg};

#[no_mangle]
extern "C" fn handle() {
    // Timestamp is in milliseconds since the Unix epoch
    if exec::block_timestamp() >= 1672531200000 {
        msg::reply(b"Current block has been generated after January 01, 2023", 0)
            .expect("Unable to reply");
    }
}
  • 获得一个程序的余额:
#![no_std]
use gstd::exec;

#[no_mangle]
extern "C" fn handle() {
    // Get self value balance in program
    let my_balance = exec::value_available();
}

关于 syscalls 的更多用法,请看 https://docs.gear.rs/gstd/exec/index.html

在合约内部进行调试

gstd::debug 提供在程序执行期间调试合约的能力:

#![no_std]
use gstd::{debug, msg, prelude::*};

#[no_mangle]
extern "C" fn handle() {
    let payload_string: String = msg::load().expect("Unable to decode `String`");
    debug!("{:?} received message: ", payload_string);
}

Mailbox

当程序向用户发送消息时,这条消息会被放在用户的 mailbox 里。实际上,mailbox 是一个专门的存储器,用于保存从程序中收到的信息。

用户可以通过订阅 UserMessageSent 事件来检测收到的信息

在使用 IDEA 网站时,应该进入 https://idea.gear-tech.io/mailbox。

备注

一条信息在 mailbox 中是要收费的。因此,信息在 mailbox 中的存在时间是有限的。 我们来探讨一下用户对 mailbox 信息的可能反应。

用户对信息进行回复

程序可以向用户发送一条信息,并等待回复。用户可以使用 send_reply extrinsic 来回复。与消息相关的余额被转移到用户的账户,信息被从 mailbox 中删除,而新的回复信息被添加到信息队列中。

用户从 mailbox 的信息中获取余额

如果 mailbox 中的消息具有关联的余额,则用户可以使用 claim_value extrinsic 声明它。余额被转移到用户的帐户,消息从 mailbox 中删除。

用户忽略 mailbox 中的消息

一条消息在其 gas 限制内对 mailbox 中的每一个区块都要收费。如果消息没有明确的 gas 限制,则从源头的限制中借用 gas(例如,启动执行的行为者)。

当信息的 gas 耗尽时,信息将从 mailbox 中删除,并将相关转账金额转回给信息发送者。

State 函数

存储数据

Gear 智能合约的持久化数据的存储方式与传统程序相同,不需要初始化外部存储。

// ...
// describe state structure
#[derive(TypeInfo, Decode, Encode, Clone)]
pub struct Wallet {
    pub id: ActorId,
    pub person: String,
}

// declare and initialize the state
static mut WALLETS: Vec<Wallet> = Vec::new();

如果用 Rust 或其他面向对象的语言编程,你应该对大多数类型都很熟悉。然而,在 Gear 上开发合约时,ActorId 类型是一个新内容。

一级引用如下:

信息

ActorId 是一个特殊的类型,代表一个 32 字节的数组,并定义了 Gear Protocol 中所有的 ID

State 函数

为了显示合约状态信息(类似于view函数),使用 state() 函数。它可以立即读取合约状态(例如,余额)。读取状态是一个免费函数,不需要消耗任何 gas。

要返回 State 使用:

#[no_mangle]
extern "C" fn state() {
    msg::reply(unsafe { WALLETS.clone() }, 0).expect("Failed to share state");
}

默认情况下,state() 函数返合约的整体状态。

自定义程序读取状态

此外,你可以创建自己的程序来读取状态。这个包装器将允许你为客户端实现自定义函数,而不依赖于主程序。

这有很多优点,例如,即使程序改变了,也始终能够读取状态(只要输入或输出的类型没有改变)。或者你正在基于一个已经存在的程序创建服务,你需要一些自己的函数来从状态中获得你自己的数据块。

为此,我们需要创建一个独立的程序并在 metawasm 特征中描述必要的功能。例如:

// ...
use gmeta::metawasm;

#[metawasm]
pub trait Metawasm {
    type State = Vec<Wallet>;

    fn all_wallets(state: Self::State) -> Vec<Wallet> {
        state
    }

    fn first_wallet(state: Self::State) -> Option<Wallet> {
        state.first().cloned()
    }

    fn last_wallet(state: Self::State) -> Option<Wallet> {
        state.last().cloned()
    }
}

更多复杂的例子:

// ...
use gmeta::metawasm;

#[metawasm]
pub trait Metawasm {
    type State = Vec<Wallet>;

    fn wallet_by_id(id: Id, state: Self::State) -> Option<Wallet> {
        state.into_iter().find(|w| w.id == id)
    }

    fn wallet_by_person(person: String, state: Self::State) -> Option<Wallet> {
        state.into_iter().find(|w| w.person == person)
    }
}

要构建 meta.wasm,需要在项目根目录下设置 build.rs 文件:

fn main() {
    gear_wasm_builder::build_metawasm();
}

元数据

元数据是一种接口映射,有助于将一组字节转换为可理解的结构,并展示出该结构的用途。元数据决定了所有传入和传出数据的编码/解码方式。

元数据允许 dApp 的各个部分--智能合约和客户端(JavaScript)相互理解并交互数据。

要描述元数据接口,请使用 gmeta

use gmeta::{InOut, Metadata};

pub struct ProgramMetadata;

// 一定要描述所有的类型。
// 但如果程序中缺少任一入口,请使用 () 代替;
// 如 `type Signal` 所示

impl Metadata for ProgramMetadata {
    type Init = InOut<MessageInitIn, MessageInitOut>;
    type Handle = InOut<MessageIn, MessageOut>;
    type Others = InOut<MessageAsyncIn, Option<u8>>;
    type Reply = InOut<String, Vec<u16>>;
    type Signal = ();
    type State = Vec<Wallet>;
}

正如我们所看到的,元数据使你能够确定合约在每个端点的输入/输出的预期数据格式:

  • Init - init() 函数的输入/输出类型。
  • Handle -handle()函数的输入/输出类型。
  • Others- 在异步交互的情况下,main()函数的输入/输出类型。
  • Reply - 使用 handle_reply 函数执行的消息的输入/输出类型。
  • Signal - 只描述在处理系统信号时从程序中输出的类型。
  • State - 查询的状态的类型

生成元数据

要生成元数据,需要在项目文件夹的根目录下添加 build.rs 文件:

// build.rs
// Where ProgramMetadata is your metadata structure

use meta_io::ProgramMetadata;

fn main() {
    gear_wasm_builder::build_with_metadata::<ProgramMetadata>();
}

作为智能合约编译的结果,将生成一个 meta.txt 文件。这个元数据文件可用于与该智能合约互动的用户界面应用程序。文件的内容看起来像一个十六进制字符串:

01000000000103000000010500000001090000000102000000010d000000010f0000000111000000000112000000a9094c00083064656d6f5f6d6574615f696f344d657373616765496e6974496e0000080118616d6f756e74040108753800012063757272656e6379080118537472696e6700000400000503000800000502000c083064656d6f5f6d6574615f696f384d657373616765496e69744f7574000008013465786368616e67655f72617465100138526573756c743c75382c2075383e00010c73756d04010875380000100418526573756c740804540104044501040108084f6b040004000000000c457272040004000001000014083064656d6f5f6d6574615f696f244d657373616765496e000004010869641801084964000018083064656d6f5f6d6574615f696f084964000008011c646563696d616c1c010c75363400010c68657820011c5665633c75383e00001c000005060020000002040024083064656d6f5f6d6574615f696f284d6573736167654f7574000004010c7265732801384f7074696f6e3c57616c6c65743e00002804184f7074696f6e040454012c0108104e6f6e6500000010536f6d6504002c00000100002c083064656d6f5f6d6574615f696f1857616c6c6574000008010869641801084964000118706572736f6e300118506572736f6e000030083064656d6f5f6d6574615f696f18506572736f6e000008011c7375726e616d65080118537472696e670001106e616d65080118537472696e6700003400000238003800000504003c083064656d6f5f6d6574615f696f384d6573736167654173796e63496e0000040114656d707479400108282900004000000400004404184f7074696f6e04045401040108104e6f6e6500000010536f6d650400040000010000480000022c00

验证元数据

为了验证程序的元数据,可以使用metahash()函数。它允许在链上验证元数据。

#[no_mangle]
// It returns the Hash of metadata.
// .metahash is generating automatically while you are using build.rs
extern "C" fn metahash() {
    let metahash: [u8; 32] = include!("../.metahash");
    msg::reply(metahash, 0).expect("Failed to share metahash");
}

数据序列化/反序列化

为了优化数据在网络上的发送和接收方式,Gear 使用 parity-scale-codec,这是 SCALE 编解码器的 Rust 实现。这个编解码器被 Substrate 节点的内部 runtime 所使用。SCALE 是一种轻量级的编码格式,能够实现数据的序列化和反序列化。使用 SCALE 对数据进行编码(和解码),它非常适用于资源受限的执行环境,如区块链运行时间和低功耗、低内存设备。

在程序中使用 SCALE codec,Cargo.toml 需要添加以下内容:

[dependencies]

// ...
codec = { package = "parity-scale-codec", version = "3.1.2", default-features = false }
use codec::{Decode, Encode};

#[derive(Encode, Decode)]
enum MyType {
    MyStruct { field: ... },
    ...
}

信息

我们只需要在使用 gstd 的包装方法时使用 Encode 和 Decode 特性,例如 msg::sendmsg::replysend_for_reply 等。像 send_bytereply_bytes 这样的方法,我们操作的是字节数组,所以不需要进行解码/编码。

更多的内容请看 SCALE Codec。

https://github.com/paritytech/parity-scale-codec

scale-info

scale-info 是一个描述 Rust 类型的库,提供有关可编码 SCALE 类型结构的信息。

这些第三方工具 (例如 UI 客户端) 提供了关于它们如何能够解码不受语言影响的类型的信息。Gear 程序使用 scale-info 的接口称为 metadata。它为所有必要的入口点定义了输入和输出类型,并允许合约和客户端相互理解。

信息

如何在合约中使用 metadata,请看链接

在项目中使用 scale-info

[dependencies]

// ...
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }

更多的内容请看 scale-info。 https://github.com/paritytech/scale-info

关于 GearFans

Gear 是波卡生态的计算组件,GearFans 是 Gear 爱好者社区。

  • 官网:https://gear-tech.io/
  • Twitter:https://twitter.com/gear_techs
  • 中文 Twitter:https://twitter.com/gear_fans
  • Vara Twitter:https://twitter.com/VaraNetwork
  • Vara Twitter CN:https://twitter.com/VaraNetwork_CN
  • GitHub:https://github.com/gear-tech
  • Discord:https://discord.com/invite/7BQznC9uD9
  • Medium:https://medium.com/@gear_techs
  • Telegram 群:https://t.me/gear_tech
  • Telegram 中文群:https://t.me/Gear_CN
  • Telegram 中文开发群:https://t.me/gear_dev_cn
  • Vara Telegram CN:https://t.me/varanetwork_cn

分类:

后端

标签:

区块链

作者介绍

GearFans
V1

Gear 是波卡生态的计算组件。