--- tags: substrate-中文 --- # Substrate 进阶课第 3 讲 - Substrate Kitties 教程(二)| Jimmy 部份 ## 大纲 - Polkadot-js API - FRAME Sudo 模块讲解 - FRAME treasury 模块讲解 ## 讲在开始之前 - 授人以鱼,不如授之以渔 - 会讲如何在各文档之间穿梭,这是学用 Substrate 最重要的技能 Substrate/Polkadot-JS 文档: - 主要:[substrate.dev](https://substrate.dev) - 教程 tutorials - 基础知识 knowledge base - 进阶菜谱 Recipes - API 文档 Rustdocs - [Polkadot wiki](https://wiki.polkadot.network/) - 纪录着 Polkadot 及 Kusama 网络的基础知识及网络行为 - [Polkadot JS 文档](https://polkadot.js.org/docs/) ## Polkadot-js API 一些基本的,我假设你们都应该懂了,如果未搞清楚,可看回基础课: - `api.tx.<pallet>.<call>` 来发送外部交易 (extrinsics) - `api.consts.<pallet>.<const>` 来拿取 pallet 常数 - `api.query.<pallet>.<name>` 来读取 pallet 存储 ### 读取链上元数据 (metadata) ```javascript const { magicNumber, metadata } = await api.rpc.state.getMetadata(); console.log("Magic number: " + magicNumber); console.log("Metadata: " + metadata.raw); ``` 为什么这个重要?因为你能知道整个 链提供了什么外部交易给客户端使用 ```javascript { magicNumber: 1635018093, metadata: { V12: { modules: [ // { ... } { name: TemplateModule, storage: { prefix: TemplateModule, items: [ { name: Something, modifier: Optional, type: { Plain: u32 }, fallback: 0x00, documentation: [] } ] }, calls: [ { name: do_something, args: [ { name: something, type: u32 } ], documentation: [ An example dispatchable that takes a singles value as a parameter, writes the value to, storage and emits an event. This function must be dispatched by a signed extrinsic. ] }, { name: cause_error, args: [], documentation: [ An example dispatchable that may throw a custom error. ] } ], events: [ { name: SomethingStored, args: [ u32, AccountId ], documentation: [ Event documentation should end with an array that provides descriptive names for event, parameters. [something, who] ] } ], constants: [], errors: [ { name: NoneValue, documentation: [ Error names should be descriptive. ] }, { name: StorageOverflow, documentation: [ Errors should have helpful documentation associated with them. ] } ], index: 8 } ], extrinsic: { version: 4, signedExtensions: [ CheckSpecVersion, CheckTxVersion, CheckGenesis, CheckMortality, CheckNonce, CheckWeight, ChargeTransactionPayment ] } } } } ``` - 这里提到有多少个 pallets (又叫 module),每个 module 的名字,有什么 storage, calls, events, constants, errors - 它的 index 数。出现错误信息时,用来追踪是那个 pallet 发出的 ### 设定自订义类型 如果你看到有以下错误信息, ``` Cannot construct unknown type ... ``` 比如:我直接拿了一位学生截图来用 🙏 ![unknown type 报错](https://i.imgur.com/nwl3cTy.jpg) 那就说明你的链有一自定义类型,但 polakdot-js API 不知道怎么解释它。需要做的是: - 参看:https://polkadot.js.org/docs/api/start/types.extend ```javascript const api = await ApiPromise.create({ provider: wsProvider, types: { KittyIndex: 'u64' } }); ``` 增加 `types` 这个参数 ### 批量查询及订阅 1. **同时发多个查询** 可同时发多个查询,而不是一条一条发 ```javascript // Subscribe to balance changes for 2 accounts, ADDR1 & ADDR2 (already defined) const unsub = await api.query.system.account.multi([ADDR1, ADDR2], (balances) => { const [{ data: balance1 }, { data: balance2 }] = balances; console.log(`The balances are ${balance1.free} and ${balance2.free}`); }); ``` 也可同时发多个不同类型查询 ```javascript // Subscribe to the timestamp, our index and balance const unsub = await api.queryMulti([ api.query.timestamp.now, [api.query.system.account, ADDR] ], ([now, { nonce, data: balance }]) => { console.log(`${now}: balance of ${balance.free} and a nonce of ${nonce}`); }); ``` 以上的开发模式有两点要注意: - 作查询时,传入一个 回调函数 (callback) / 订阅函数。你在这里更新你 react 的 state 的话,就不会出现为什么链上数据改了,而前端没有更新数据的问题。 - `unsub`:这个 `unsub` 是一个函数,用来取消这个订阅的。如果是 react/前端开发,你在 `ComponentWillUnmount()`,或 `useEffect()` 里,就会 call 这个取消订阅函数。整个模式类似以下: ```javascript useEffect(() => { let unsub = null; const asyncFetch = async () => { unsub = await api.query.pallet.storage( param, result => console.log(`Result: ${result}`) ); }; asyncFetch(); return () => { unsub && unsub() } }, [api, keyring]); ``` 2. **交易并订阅事件** ```javascript // Create alice (carry-over from the keyring section) const alice = keyring.addFromUri('//Alice'); // Make a transfer from Alice to BOB, waiting for inclusion const unsub = await api.tx.balances .transfer(BOB, 12345) .signAndSend(alice, (result) => { console.log(`Current status is ${result.status}`); if (result.status.isInBlock) { console.log(`Transaction included at blockHash ${result.status.asInBlock}`); } else if (result.status.isFinalized) { console.log(`Transaction finalized at blockHash ${result.status.asFinalized}`); unsub(); } }); ``` ### keyring 钥匙圈 ```javascript // Import const { Keyring } = require('@polkadot/keyring'); ``` - 这里有几个概念要讲,首先 keypair 钥匙对。一个帐号背后是一对公钥和私钥。 - 这个钥匙对用来你所作的交易签名的。 - 你用你的私钥对一个交易 (可理解为一组信息,一堆 bytes) 作签名。其他人可用你的公钥来验证这个交易为你用私钥签署的 - 签名的方法 polkadot-js API 支持: - ed25519 - sr25519 - ecdsa - 及 ethereum - 而同一对钥匙对,会因应不同的网络,生成出不同的帐号 (AccountID)。也就是说同一对钥匙对,在 Substrate 网络是一个 AccountID, 在 Polkadot 网络则显示为另一组 AccountID, 而在 Kusama 又是另一个。 ```javascript import { Keyring } from '@polkadot/keyring'; // create a keyring with some non-default values specified const keyring = new Keyring(); ``` > 小窍门: 你可访问 polkadot-js App, Developer > Javascript 内,可再加 debugger 与里面的对象物件互动。 这样,默认生成出来是用 `ed25519` 签名法,及为 Substrate 网络的帐号。 ```javascript const keyring = new Keyring({ type: 'sr25519', ss58Format: 2 }); ``` 这样,默认生成的出来是用 `sr25519` 签名法,及为 Kusama 网络的帐号。 ss58Format: - `0`: Polkadot 网络 - `2`: Kusama 网络 - `42`: 一般 Substrate 网络 然后,就可这样加一个帐号: ```javascript const mnemonic = mnemonicGenerate(); // create & add the pair to the keyring with the type and some additional // metadata specified const pair = keyring.addFromUri(mnemonic, { name: 'first pair' }); ``` 最后,拿着这个帐号,你就可对一个交易作签名: ```javascript const txHash = await api.tx.balances .transfer(BOB, 12345) .signAndSend(alice); ``` 参考: - [SS58 地址格式](https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)) - [Polkadot Accounts](https://wiki.polkadot.network/docs/en/learn-accounts) - [ecdsa 签名法](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) ## FRAME Sudo 模块讲解 > 这部份大家听的时候: > > - git clone 这个版本:https://github.com/paritytech/substrate/tree/v2.0.1 > - 打开代码 > - 也打开 Rustdocs 相应页 - [sudo rustdocs 的相应页](https://substrate.dev/rustdocs/v2.0.0/pallet_sudo/index.html) - rustdoc 及 代码之间穿梭 - Call - `sudo` - `sudo_unchecked_weight` - `sudo_as` - 特别的是 `T::Lookup` 是哪里来的? 还记得这结构图 ![Substrate 结构图](https://i.imgur.com/GN96Ful.png) - 回到 runtime 里,runtime 来定义。而 `bin/node/runtime/src/lib.rs#170`: ```rust type Lookup = Indices ``` 而 `Indices` 就是定义在 `bin/node/runtime/src/lib.rs#901`: ```rust Indices: pallet_indices::{Module, Call, Storage, Config<T>, Event<T>} ``` - 这里展示了一个开发模式在一个 pallet 调用另一个 pallet 的函数。 ## FRAME Treasury 模块讲解 - [treasury rustdocs 的相应页](https://substrate.dev/rustdocs/v2.0.0/pallet_treasury/index.html) - `Structs` 部份。有很多 `Instance0`, `Instance1`. 这个是 Instantiable Pallet。在一个 runtime 里,这个模块 (pallet) 能有多个 instance. 看 [Substrate Recipe](https://substrate.dev/recipes/instantiable.html). - [`Call`](https://substrate.dev/rustdocs/v2.0.0/pallet_treasury/enum.Call.html) 部份有不同的 extrinsics. Treasury 讲的就是三个东西,Proposal,Tip,和 Bounty。 ### Proposal 的状态转移 ![Proposal 的状态转移](https://i.imgur.com/4gobLSd.png) ### Tip 的状态转移 ![Tip 的状态转移](https://i.imgur.com/Svje2tK.png) ### Bounty 的状态转移 ![Bounty 的状态转移](https://i.imgur.com/LuJ58Xw.png) ## 作业 前端基于 [kitties-course 已有前端](https://github.com/SubstrateCourse/kitties-hw) 加以下 UX 及功能。这部份共 10 分: 1. 能创建一个毛孩 (**3 分**) 2. 每一个毛孩展示成一张卡片,並显示是不是属于你的 (**4 分**) 3. 可以转让毛孩给另一位用户 (**3 分**)