![1d00b26828c4562954d16faafa5cc4dd.png](https://img-blog.csdnimg.cn/img_convert/1d00b26828c4562954d16faafa5cc4dd.png)
文章内容持续更新......
前文《【Substrate入门】在Runtime添加一个模块》中已经添加了一个很简洁的模块,仅仅是让我们有一个直观的认识,在实际项目开发中,还会有更复杂的一些开发,比如RPC API,本文旨在介绍一个典型的复杂的模块——智能合约模块pallet-contracts
,它覆盖了开发环节中可能遇到的棘手问题,有助于我们更深入地理解pallet的开发。
本文目标
掌握将一个复杂的pallet加入到runtime,以便后续我们能灵活自主地将官方造的轮子(pallet)加入到链当中。
操作步骤
- 导入智能合约pallet
- 添加智能合约pallet到runtime
- 更新外节点
- 升级runtime
前提条件
本文的操作是基于substrate-node-template v2.0.0和前端界面substrate-front-end-template构建的环境,如果我们还没有构建好,请参考之前的文章【Substrate入门】搭建第一条substrate链
开始吧!
一、导入智能合约pallet
先预览一下我们即将要开发的涉及到的文件与目录:
substrate-node-template
|
+-- runtime
| |
| +-- Cargo.toml <-- 在包管理文件中将合约模块加入到依赖项中
| |
| +-- build.rs
| |
| +-- src
| |
| +-- lib.rs <-- 多处需要修改
|
+-- pallets
|
+-- scripts
|
+-- node <-- 多处需要修改
|
+-- ...
1、首先将pallet-contracts
加入到Cargo.toml中,打开runtime/Cargo.toml
文件:
[dependencies]
#--snip--
pallet-contracts = { version = '2.0.0', default_features = false }
pallet-contracts-primitives = { version = '2.0.0', default_features = false }
与其他pallet一样,智能合约pallet也有一个std功能,在同一个文件中找到并添加:
[features]
default = ['std']
std = [
#--snip--
'pallet-contracts/std',
'pallet-contracts-primitives/std',
#--snip--
]
简单地理解一下std与no_std的区别:有些嵌入式设备或者底层开发时没有文件或者网络系统,rust作为一个系统编程语言,既可以做系统级别的开发,也可以支持应用层的开发(如读写文件),std里包含了常用的应用层开发的功能,而no_std则没有这些功能,在cargo编译的时候可以指定是否加载std。在substrate开发中,基本上每个pallet都需要std的支持。
检查一下依赖项是否能编译通过:
SKIP_WASM_BUILD=1 cargo check
二、添加智能合约pallet到runtime
1、打开文件runtime/src/lib.rs
,添加如下代码:
// These time units are defined in number of blocks.
/* --snip-- */
/*** 添加这一段 开始 ***/
// Contracts price units.
pub const MILLICENTS: Balance = 1_000_000_000;
pub const CENTS: Balance = 1_000 * MILLICENTS;
pub const DOLLARS: Balance = 100 * CENTS;
/*** 添加这一段 结束 ***/
impl pallet_timestamp::Trait for Runtime {
/* --snip-- */
}
/*** 添加这一段 开始 ***/
parameter_types! {
pub const TombstoneDeposit: Balance = 16 * MILLICENTS;
pub const RentByteFee: Balance = 4 * MILLICENTS;
pub const RentDepositOffset: Balance = 1000 * MILLICENTS;
pub const SurchargeReward: Balance = 150 * MILLICENTS;
}
impl pallet_contracts::Trait for Runtime {
type Time = Timestamp;
type Randomness = RandomnessCollectiveFlip;
type Currency = Balances;
type Event = Event;
type DetermineContractAddress = pallet_contracts::SimpleAddressDeterminer<Runtime>;
type TrieIdGenerator = pallet_contracts::TrieIdFromParentCounter<Runtime>;
type RentPayment = ();
type SignedClaimHandicap = pallet_contracts::DefaultSignedClaimHandicap;
type TombstoneDeposit = TombstoneDeposit;
type StorageSizeOffset = pallet_contracts::DefaultStorageSizeOffset;
type RentByteFee = RentByteFee;
type RentDepositOffset = RentDepositOffset;
type SurchargeReward = SurchargeReward;
type MaxDepth = pallet_contracts::DefaultMaxDepth;
type MaxValueSize = pallet_contracts::DefaultMaxValueSize;
type WeightPrice = pallet_transaction_payment::Module<Self>;
}
/*** 添加这一段 结束 ***/
将智能合约pallet加载到自启动区construct_runtime!
打开runtime/src/lib.rs
, 搜索construct_runtime!
construct_runtime!(
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
/* --snip-- */
Contracts: pallet_contracts::{Module, Call, Config, Storage, Event<T>}, <-- 添加这一行
}
);
检查一下runtime是否能编译通过:
SKIP_WASM_BUILD=1 cargo check -p node-template-runtime
公开合约API
打开runtime/Cargo.toml
[dependencies]
#--snip--
pallet-contracts-rpc-runtime-api = { version = '0.8.0', default-features = false } <-- 添加这一行
同一文件中找到并添加:
[features]
default = ['std']
std = [
#--snip--
'pallet-contracts-rpc-runtime-api/std', <-- 添加这一行
]
我们需要将返回类型添加到runtime,将它与其他use语句一起添加:
use pallet_contracts_rpc_runtime_api::ContractExecResult;
现在,我们准备实现智能合约的Runtime API,搜索impl_runtime_apis!
,在其中添加如下代码:
impl_runtime_apis! {
/* --snip-- */
/*** 添加这一段 开始 ***/
impl pallet_contracts_rpc_runtime_api::ContractsApi<Block, AccountId, Balance, BlockNumber>
for Runtime
{
fn call(
origin: AccountId,
dest: AccountId,
value: Balance,
gas_limit: u64,
input_data: Vec<u8>,
) -> ContractExecResult {
let (exec_result, gas_consumed) =
Contracts::bare_call(origin, dest.into(), value, gas_limit, input_data);
match exec_result {
Ok(v) => ContractExecResult::Success {
flags: v.flags.bits(),
data: v.data,
gas_consumed: gas_consumed,
},
Err(_) => ContractExecResult::Error,
}
}
fn get_storage(
address: AccountId,
key: [u8; 32],
) -> pallet_contracts_primitives::GetStorageResult {
Contracts::get_storage(address, key)
}
fn rent_projection(
address: AccountId,
) -> pallet_contracts_primitives::RentProjectionResult<BlockNumber> {
Contracts::rent_projection(address)
}
}
/*** 添加这一段 结束 ***/
}
为了确保每一步的修改不会出现错误,再次检查一下runtime是否能编译通过:
SKIP_WASM_BUILD=1 cargo check -p node-template-runtime
二、更新外节点
到这一步,我们已经完成了向Runtime中添加智能合约pallet的工作。现在,我们将注意力转向外部节点,该外部节点通常需要一些相应的更新。对于智能合约pallet,我们将添加自定义RPC API和创始配置。
添加RPC API扩展
公开了正确的运行时API之后,现在我们可以将RPC添加到节点的服务中,以调用该运行时API。因为我们现在在外部节点上工作,所以我们不需要构建no_std,也不必维护专用std功能。
打开node/Cargo.toml
文件,添加如下依赖项:
[dependencies]
jsonrpc-core = '15.0.0'
structopt = '0.3.8'
#--snip--
# *** 添加下面两行 ***
pallet-contracts = '2.0.0'
pallet-contracts-rpc = '0.8.0'
Substrate提供了一个RPC与我们的节点进行交互,但是,默认情况下,它不包含对智能合同pallet的访问。要与该pallet交互,我们必须扩展现有的RPC并添加智能合约pallet及其API。
打开文件node/src/rpc.rs
, 添加如下代码:
use node_template_runtime::{opaque::Block, AccountId, Balance, Index, BlockNumber};
use pallet_contracts_rpc::{Contracts, ContractsApi};
/// Instantiate all full RPC extensions.
pub fn create_full<C, P>(
deps: FullDeps<C, P>,
) -> jsonrpc_core::IoHandler<sc_rpc::Metadata> where
/* --snip-- */
C: Send + Sync + 'static,
C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, AccountId, Index>,
/*** 添加下面一行 ***/
C::Api: pallet_contracts_rpc::ContractsRuntimeApi<Block, AccountId, Balance, BlockNumber>,
/* --snip-- */
{
/* --snip-- */
// Extend this RPC with a custom API by using the following syntax.
// `YourRpcStruct` should have a reference to a client, which is needed
// to call into the runtime.
// `io.extend_with(YourRpcTrait::to_delegate(YourRpcStruct::new(ReferenceToClient, ...)));`
/*** 添加这一段 开始 ***/
io.extend_with(
ContractsApi::to_delegate(Contracts::new(client.clone()))
);
/*** 添加这一段 结束 ***/
io
}
创世配置
并不是所有pallet都具有创世配置,但是如果有的话,我们可以查看开发文档。例如:pallet_contracts::GenesisConfig文档描述了我们需要为智能合约pallet定义的所有字段。
创世配置是在node/src/chain_spec.rs
文件中进行修改。我们需要修改此文件以ContractsConfig在顶部包含类型和合约价格单位:
use node_template_runtime::ContractsConfig;
然后在testnet_genesis函数中,我们需要将合同配置添加到返回的GenesisConfig对象中,如下所示:
/// Configure initial storage state for FRAME modules.
fn testnet_genesis(
wasm_binary: &[u8],
initial_authorities: Vec<(AuraId, GrandpaId)>,
root_key: AccountId,
endowed_accounts: Vec<AccountId>,
enable_println: bool, // <-- 修改这一行,去除前面的下划线
) -> GenesisConfig {
GenesisConfig {
/* --snip-- */
/*** 添加这一段 开始 ***/
pallet_contracts: Some(ContractsConfig {
current_schedule: pallet_contracts::Schedule {
enable_println,
..Default::default()
},
}),
/*** 添加这一段 结束 ***/
}
}
三、升级Runtime
现在,我们可以编译并运行具有智能合同模块的节点了:
WASM_BUILD_TOOLCHAIN=nightly-2020-10-05 cargo build --release
编译完成后,在开发模式下启动临时节点:
./target/release/node-template --dev --tmp
我们看到节点能正常启动起来就说明链升级成功了。
添加其他FRAME中的pallet
在本文中,我们专门介绍了如何导入智能合约pallet,但是每个pallet都会有所不同。不用担心,我们可以随时参考演示的Substrate节点的Runtime,该Runtime几乎包括FRAME中的每个pallet。在Substrate节点的runtime/Cargo.toml
文件中,我们可以看到每个不同pallet的导入示例,在lib.rs文件中,我们将找到如何将每个pallet添加到Runtime,基本上复制粘贴到我们的Runtime即可。
进一步学习
- 关于在自己的程序包中编写Runtime pallet的极简教程
- 接下来,我们的节点就可以运行智能合约了,学习有关Substrate ink!智能合约语言
- Substrate Recipes提供了有关编写运行时API和自定义RPC的详细教程,如本文中探讨的内容
- 了解Chain Spec文件以自定义我们的Genesis配置。
小结
待小结
参考:
添加一个模块 · Substrate Developer Hubsubstrate.dev![cd06e1ee645052336c1e8ed48b8b7789.png](https://img-blog.csdnimg.cn/img_convert/cd06e1ee645052336c1e8ed48b8b7789.png)
![c81b664f6780053d5f4dd48d24150a50.png](https://img-blog.csdnimg.cn/img_convert/c81b664f6780053d5f4dd48d24150a50.png)