Version: next

使用 Muta 框架从零开发一条 Dex 专有链

我们的目标是开发一条链上挂单、链上撮合、链上成交的简易 dex 专有链,旨在通过 step by step 的流程,帮助开发者熟悉 Muta 框架,学会如何使用框架开发自己的区块链。

在开始本教程之前,开发者需要先学习 Service 开发指南

我们按照 Service 开发指南 中提到的,使用 Muta 框架开发自己的区块链流程,来开发这条 dex 专有链:

  1. 思考自己链的专属需求,确定需要哪些 Service
  2. 如果需要的 Service 有现成的,可以直接复用;如果没有,可以自己开发
  3. 将这些 Service 接入框架,编译运行!

1. 思考需要的 Service

我们一共需要 2 个 Service,除了 Dex Service 外,由于 Dex 链需要有进行交易的资产,所以还需要一个 Asset Service。 Asset Service 除了常见的发行、转账、查询功能外,还需要一个锁定资产功能。 因为用户发起挂单交易时,需要锁定用户资产,确保成交时有足够的余额来完成交易。 Dex 订单成交时,需要修改用户资产余额,所以 Asset Service 需要提供修改余额接口,并且该接口只能由 Dex Service 调用,无法被用户直接调用。 我们先将 Asset Service 的对其他 Service 接口定义如下:

pub trait AssetFacade {
fn lock(&mut self, ctx: ServiceContext, payload: ModifyBalancePayload) -> ServiceResponse<()>;
fn unlock(&mut self, ctx: ServiceContext, payload: ModifyBalancePayload)
-> ServiceResponse<()>;
fn add_value(
&mut self,
ctx: ServiceContext,
payload: ModifyBalancePayload,
) -> ServiceResponse<()>;
fn sub_value(
&mut self,
ctx: ServiceContext,
payload: ModifyBalancePayload,
) -> ServiceResponse<()>;
}

然后我们定义 Asset Service 对外的接口,以及内部方法。标有 [read] , [write] 的方法为对外方法。没有标记的方法为内部方法。

#[cycles(210_00)]
#[write]
fn create_asset(
&mut self,
ctx: ServiceContext,
payload: CreateAssetPayload,
) -> ServiceResponse<Asset> ;
#[cycles(100_00)]
#[read]
fn get_asset(&self, ctx: ServiceContext, payload: GetAssetPayload) -> ServiceResponse<Asset> ;
#[cycles(100_00)]
#[read]
fn get_balance(
&self,
ctx: ServiceContext,
payload: GetBalancePayload,
) -> ServiceResponse<GetBalanceResponse>;
#[cycles(210_00)]
#[write]
fn transfer(&mut self, ctx: ServiceContext, payload: TransferPayload) -> ServiceResponse<()>;
fn _add_value(&mut self, payload: &ModifyBalancePayload) -> ServiceResponse<()>;
fn _sub_value(&mut self, payload: &ModifyBalancePayload) -> ServiceResponse<()>;

Dex Service 包含的功能有:

  1. 增加交易对
  2. 查询交易对
  3. 发起挂单交易(买或卖)
  4. 每个 block 执行结束后,匹配订单并成交
  5. 查询订单状态

功能 1、2、3、5 可由用户调用 Servcie 接口触发:

#[cycles(210_00)]
#[write]
fn add_trade(&mut self, ctx: ServiceContext, payload: AddTradePayload) -> ServiceResponse<()>;
#[read]
fn get_trades(&self, _ctx: ServiceContext) -> ServiceResponse<GetTradesResponse>;
#[cycles(210_00)]
#[write]
fn order(&mut self, ctx: ServiceContext, payload: OrderPayload) -> ServiceResponse<()> ;
#[read]
fn get_order(
&self,
_ctx: ServiceContext,
payload: GetOrderPayload,
) -> ServiceResponse<GetOrderResponse> ;

功能 4 由 #[hook_after] 自动触发:

#[hook_after]
fn match_and_deal(&mut self, params: &ExecutorParams);

2. 开发 Asset Service,Dex Service

新建一个 cargo 项目,在 dependency 中依赖必要的 muta 组件,可以参看源代码仓库

muta-tutorial-dex 目录结构如下:

./muta-tutorial-dex
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── config
│ ├── chain.toml
│ └── genesis.toml
├── rust-toolchain
├── services
│ └── asset
│ │ ├── Cargo.toml
│ │ └── src
│ │ └── lib.rs
│ └── dex
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── src
└── main.rs

可以看到,目录主要包含 config,services 和 src 三个子目录:

  • config:链的配置信息
  • services:包含链的所有 service
  • src:这条链的 bin 目录,在 main.rs 中,我们将 services 接入 muta library,并启动整条链

注意:services 目录中并不包含了一个 [metadata service] ,但是 muta 链启动的时候,需要提供一个 metadata service 。 我们需要在 ServiceMapping 中将 muta package 内置的 metadata service 注册进去。

impl ServiceMapping for DefaultServiceMapping {
fn get_service<SDK: 'static + ServiceSDK, Factory: SDKFactory<SDK>>(
&self,
name: &str,
factory: &Factory,
) -> ProtocolResult<Box<dyn Service>> {
let service = match name {
"asset" => Box::new(Self::new_asset(factory)?) as Box<dyn Service>,
"metadata" => Box::new(Self::new_metadata(factory)?) as Box<dyn Service>,
"dex" => Box::new(Self::new_dex(factory)?) as Box<dyn Service>,
_ => panic!("not found service"),
};
Ok(service)
}
fn list_service_name(&self) -> Vec<String> {
vec!["asset".to_owned(), "metadata".to_owned(), "dex".to_owned()]
}
}

编写 Asset Service

学习完 [Service 开发指南][service_dev],相信读者对如何开发 asset service 已经有了一定的想法,并且能够阅读 asset service 源码。这里就不复述相关内容,仅向读者说明一些需要注意的地方:

代码结构

Service 的组件定义在 lib.rs 中,组件需要用到的数据结构,如输入输出参数(TransferPayload)、事件类型(TransferEvent)、存储类型(Asset)定义在 types.rs 中。

创世配置

Asset Service 通过 fn init_genesis 方法,注册了 Muta Tutorial Token,该 token 信息将包含在创世块的世界状态里:

// lib.rs
#[genesis]
fn init_genesis(&mut self, payload: InitGenesisPayload) {
let asset = Asset {
id: payload.id,
name: payload.name,
symbol: payload.symbol,
supply: payload.supply,
issuer: payload.issuer.clone(),
};
self.assets.insert(asset.id.clone(), asset.clone())?;
let balance = Balance {
current: payload.supply,
locked: 0,
};
self.sdk.set_account_value(&asset.issuer, asset.id, balance)
}
// types.rs
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct InitGenesisPayload {
pub id: Hash,
pub name: String,
pub symbol: String,
pub supply: u64,
pub issuer: Address,
}

该方法的输入参数 InitGenesisPayload,定义在 muta-tutorial-dex/config/genesis.toml 文件中,该文件包含所有 service 的创世配置信息:

# config/genesis.toml
[[services]]
name = "asset"
payload = '''
{
"id": "0xf56924db538e77bb5951eb5ff0d02b88983c49c45eea30e8ae3e7234b311436c",
"name": "Muta Tutorial Token",
"symbol": "MTT",
"supply": 1000000000,
"issuer": "0xf8389d774afdad8755ef8e629e5a154fddc6325a"
}
'''

框架在创建创世块时,会读取该配置并调用 fn init_genesis 方法。

接口权限

Asset Service 的 fn lockfn unlockfn add_valuefn sub_value 接口方法,只能被 Dex Service 调用,无法被用户直接调用。在 Asset Service 中定义了 ADMISSION_TOKEN,通过检验在 ServiceContext 中的 extra 字段是否包含该令牌进行权限控制。

const ADMISSION_TOKEN: Bytes = Bytes::from_static(b"dex_token");

编写 Dex Service

Dex Service 源码可以在 这里 找到,注意事项同上。

3. 将 Service 接入框架,编译运行!

前面已经提到,这部分工作将在 src 目录的 main 文件中完成。脚手架下载的 main 文件已经帮我们实现了绝大部分代码,所以这部分工作将变得非常简单。

在模版代码中,定义了一个 struct DefaultServiceMapping 结构体,并为该结构体实现了 trait ServiceMapping,框架通过 trait ServiceMapping 可以获取到所有 service 实例,从而将开发者定义的 service 接入框架底层组件。

trait ServiceMapping 包含两个方法,一个 fn get_service 用来根据 service 名称获取 service 实例,另一个 fn list_service_name 用来获取所有 service 名称。

需要注意的是,框架将使用在 fn list_service_name 方法中 service 名称排列的顺序,依次调用 service 中 #[genesis]#[hook_before]#[hook_after] 标记的方法。

我们需要做的,仅仅是把 fn get_servicefn list_service_name 方法中的 service 集合,替换成我们 services 目录中包含的 service 集合:

impl ServiceMapping for DefaultServiceMapping {
fn get_service<SDK: 'static + ServiceSDK, Factory: SDKFactory<SDK>>(
&self,
name: &str,
factory: &Factory,
) -> ProtocolResult<Box<dyn Service>> {
let service = match name {
"asset" => Box::new(Self::new_asset(factory)?) as Box<dyn Service>,
"metadata" => Box::new(Self::new_metadata(factory)?) as Box<dyn Service>,
"dex" => Box::new(Self::new_dex(factory)?) as Box<dyn Service>,
_ => panic!("not found service"),
};
Ok(service)
}
fn list_service_name(&self) -> Vec<String> {
vec!["asset".to_owned(), "metadata".to_owned(), "dex".to_owned()]
}
}

到这里,所有的开发工作就完成了,运行 cargo run 编译并启动 dex 链。

通过浏览器打开 http://localhost:8000/graphiql ,即可与 dex 链进行交互,graphiql 的使用方法参见文档

注意:由于框架正在持续的开发过程中,所以框架的 api 有可能发生变动