使用 Muta 框架从零开发一条 Dex 专有链
我们的目标是开发一条链上挂单、链上撮合、链上成交的简易 dex 专有链,旨在通过 step by step 的流程,帮助开发者熟悉 Muta 框架,学会如何使用框架开发自己的区块链。
在开始本教程之前,开发者需要先学习 Service 开发指南
我们按照 Service 开发指南 中提到的,使用 Muta 框架开发自己的区块链流程,来开发这条 dex 专有链:
- 思考自己链的专属需求,确定需要哪些 Service
- 如果需要的 Service 有现成的,可以直接复用;如果没有,可以自己开发
- 将这些 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 包含的功能有:
- 增加交易对
- 查询交易对
- 发起挂单交易(买或卖)
- 每个 block 执行结束后,匹配订单并成交
- 查询订单状态
功能 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 开发指南,相信读者对如何开发 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 lock
、fn unlock
、fn add_value
、fn 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_service
和 fn 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 有可能发生变动