--- tags: substrate-中文 --- # 用 Polkadot-JS API 轻松搭建 Substrate 前端 *Substrate 前端开发系列 - 1/2* ## 前言 看了这专栏之前几篇文章后,相信各位对用 Substrate 作开发已经有了基本认识。可以把节点跑起来,也能写出能完成个别功能的 runtime 出来,甚至跑起几个节点形成一个网络出来。但终端用户始终不会直接与这区块链网络互动。现在我们需要搭建一个前端,借着它用户才能与这网络互动。 所以接下来我们会介绍如何利用 Substrate 生态中的 [Polkadot-JS API](https://polkadot.js.org/api/) (下面简称 JS API) 来使前端与Substrate 节点交互。这项目名称虽然写着 Polkadot,但其实它可以连接到所有基于 Substrate 开发的节点。 本篇文章是前端系列的第一篇,先深入探讨以 JS API 来连接到 Substrate 节点并与之交互。内容适合任何前端框架,甚至如果你要打造一个 Node.js 的中间件来订阅 Substrate 节点事件 (events) 也可以,JS API 也允许你这么做。如果你的前端打算是用 [React](https://reactjs.org/) 打造,请留意我们的下篇,讲述如何在 [Substrate Front-end Template](https://github.com/substrate-developer-hub/substrate-front-end-template) 的基础上打造你的前端,它把 Polkadot-JS API 封装在 React 的组件内来使用。 接下来,我们假设你在本机已能跑起 Substrate (还没做这步的小伙伴可[参考这里](https://zhuanlan.zhihu.com/p/67580341))。并且 Substrate 的 web socket 端口设在默认的 `localhost:9944`。 ## 连接到开发节点 首先在你的 JS 项目中添加 JS API 的库 ```bash yarn add @polkadot/api ``` 我们建议使用 `yarn` 作你的项目包管理工具。 然后在开始要与 Subtrate 网络互动前创建一个 `api` 对象如下: ```javascript= // 引入 import { ApiPromise, WsProvider } from '@polkadot/api'; // 创建 api 对象 const wsProvider = new WsProvider('ws://localhost:9944'); const api = await ApiPromise.create({ provider: wsProvider }); // 简单测试-读取常量 console.log(api.consts.balances.transferFee.toNumber()); ``` 在这里,注意我们是用 ES2015 的 JS 准则来写的,所以用 `import` 来引用外部的库及支持 `async` / `await` 这些功能。做了以上的操作后,你可从 `api` 这对象取得所有需要的与 Substrate 交互的函数及常量。 ## 读取链上数据 (Queries) 接下来,下面是取得链上数据的例子。 ```javascript= // 初始化 `api` 对象 const api = ...; // 取得链上的时间戳 const now = await api.query.timestamp.now(); // 一个模拟地址 const ADDR = '5DTestUPts3kjeXSTMyerHihn1uwMfLj8vU8sqF7qYrFabHE'; // 取得用户地址中的余额 const balance = await api.query.balances.freeBalance(ADDR); const nonce = await api.query.system.accountNonce(ADDR); console.log(`${now}: balance of ${balance} and a nonce of ${nonce}`); ``` 读取链上数据数据的方法是用 `api.query` 而 `query` 后的名字则是当连到 Substrate 节点时动态建成的,取决于连接的 Substrate 网络加载了什么模块 (pallets),这些模块里的存取项 (storage),及其对应的读取函数 (getter function)。[这里可了解更多 Substrate 存取项的读取函数](https://substrate.dev/rustdocs/master/frame_support/macro.decl_storage.html)。基本原则就是: ``` api.query.<pallet 名字>.<getter function 名字>; ``` 这里的函数只会对链上数据做出简单的读取操作。因为这是需要和 Substrate 节点实时交互,会是一个异步操作,返回一个 Promise,然后用 await 等待结果。 ## 订阅链上数据的变化 (Subscription) 做前端开发时,你有时不但需要在载入网页那一刻取得链上的数据,随着这些链上数据在变更,可能你也需要动态变更页面上的内容。这也是为什么我们一开始连接时,不是用简单的 http API 请求,而是 WebSocket 连接。 在以上取得用户余额的例子中,你也可以传入一个回调函数。这样取得用户余额以外,每次当这余额数值变更时也会回调过来。 ```javascript= // 订阅着该数值 const unsub = await api.query.balances.freeBalance(ADDR, balance => { console.log(`balance of ${balance}`); }); ``` 用这方法的话,返回的将会是他的取消订阅函数。当你不再需要监听这数值时,就呼叫这函数。 JS API 也有个便捷的方法,可一次过订阅多个链上数值。例子如下: ```javascript= const unsub = await api.queryMulti([ // 一个 getter function api.query.timestamp.now, // 另一个 getter function,及所需参数 [api.query.balances.freeBalance, ADDR], [api.query.system.accountNonce, ADDR], ], ([now, balance, nonce]) => { // 回调函数 console.log(`${now}: balance of ${balance} and a nonce of ${nonce}`); }); ``` 就是用 `api.queryMulti(queries 数组, 回调函数)`。 ## 查询节点常量 (Constants) 方法跟读取链上数据差不多。在取得 `api` 接口后,可以用 `api.const.<pallet 名称>.<pallet 常量>.toNumber();` 来获取。以下是一些例子。 ```javascript= // babe 组件内的常量 console.log(api.consts.babe.epochDuration.toNumber()); // balances 组件内的常量 console.log(api.consts.balances.creationFee.toNumber()); console.log(api.consts.balances.transferFee.toNumber()); ``` 值得注意两点,第一,常量在 `api` 连接到节点时已取得,所以它们是直接返回,不需要以 Promise 方式异步返回。第二,尽管这常量是一个数字, 但返回时 JS API 帮我们封装到一个对象中,要用 `toNumber()` 从这对象取出在 JS 中能识别的值。这点我们会在自定义结构里进一步讲解。 ## 提交外部交易 (Extrinsics) 这部份的操作会直接变更链上的数据,而且都需要有个用户/主体对呼叫的函数作出签署,所以我们称之为外部交易。以下例子是用户 `Alice` 打款 12345 个单位货币到另一帐户: ```javascript= // ... // 一个模拟地址 const recipient = '5DTestUPts3kjeXSTMyerHihn1uwMfLj8vU8sqF7qYrFabHE'; // Sign and send a transfer from Alice to Bob const txHash = await api.tx.balances .transfer(recipient, 12345) .signAndSend(alice); // 显示交易 hash 码 console.log(`Submitted with hash ${txHash}`); ``` 其使用方法是: ``` api.tx.<pallet 名字>.<extrinsics 名字>(参数1, ...); ``` 返回的是一个 hash 值,代表这个交易被记录到区块链上。但这并不意味交易已经顺利执行。所以接下来我们可监听事件 (events) 来确定交易已完成(或报错)。另外你可能也留意我们还没有提 `alice` 究竟是个怎么样的值。这部份留到 [帐号管理/签署](/crzAAlhRSpuRk2edOcy1Xg#%E5%B8%90%E5%8F%B7%E7%AE%A1%E7%90%86%E7%AD%BE%E7%BD%B2-Keyring)部份来谈。 用以下的方法,我们就可监听自己提交的交易。方法和上面订阅链上数据类似: ```javascript= // 从 Alice 提交一个交易给另一用户 const unsub = await api.tx.balances .transfer(recipient, 12345) .signAndSend(alice, ({ eventRecords = [], status }) => { console.log(`Current status is ${status.type}`); if (status.isInBlock) { console.log(`Transaction included at blockHash ${status.asInBlock}`); } else if (status.isFinalized) { console.log(`Transaction finalized at blockHash ${status.asFinalized}`); unsub(); } }); ``` 我们把回调函数放在 `signAndSend` 的最后参数内。这回调函数会返回以下对象包含两个属性。 - `status`: 是一个 enum,可以是 `InBlock`。用 `isInBlock` 来查询 (返回 `true` 或 `false`),代表交易已写在区块内,但未被最后确认。另外也可以是 `Finalized`。用 `isFinalized` 来查询。代表交易已被最后确认。 - `eventRecords`: 放着 `Event Record` 对象的数组。每个 `Event Record` 有着以下属性: - `phase`: 这事件在哪个阶段触发 - `event`: 对象,有着以下属性 - `section`: 触发此事件的 pallet 名字。 - `method`: 此事件在 pallet 中的名字。 - `data`: 此事件传出的参数,是一个数组。 ## 帐号管理/签署 (Keyring) 接下来,我们详细说明上文 `alice` 这用户对象是如何取得的。这当然不是一条字符串地址。若然如此,那么任何人都可以冒充另一个人发交易给其他人。 要创建用户对象,首先要加入 [` @polkadot/keyring`](https://github.com/polkadot-js/common/tree/master/packages/keyring) 库到项目中: ```bash yarn add @polkadot/keyring ``` 然后在 JS 代码里: ```javascript= // 引入 import { Keyring } from '@polkadot/api'; // 初始化 api // const api = await ...; // api 完成初始化后,再创建 keyring 对象。 const keyring = new Keyring({ type: 'sr25519' }); ``` 跟着你可以用以下几种方法创建出有公钥及私钥的用户帐号: ```javascript= // 从 mnemonic 来生成,建议方法: const PHRASE = 'entire material egg meadow latin bargain dutch coral blood melt acoustic thought'; const newPair = keyring.addFromUri(PHRASE); // 只限开发时使用,即运行 Substrate 节点时加了 `--dev` 参数: const alice = keyring.addFromUri('//Alice', { name: 'Alice default' }); // 用 32 位的 16 进制数字生成 const hexPair = keyring.addFromUri('0x1234567890123456789012345678901234567890123456789012345678901234'); // 最后你也可用不多于 32位的字符串生成。少于 32位值的前面会加上空格。 const strPair = keyring.addFromUri('Peter'); ``` 跟着你可以这样签署信息和核实信息: ```javascript= // 引入一些帮助函数 import { stringToU8a, u8aToHex } from '@polkadot/util'; // 创建信息 const message = stringToU8a('a testing message'); // 签署信息 const signature = alice.sign(message); // 核实信息 const isValid = alice.verify(message, signature); ``` 当你使用 `signAndSend()` 提交外部交易时,內里已自动对交易信息作 sign 这操作。 ## 读取及订阅网络内的事件 (Events) 你也可用类似链上数据查询的方法,来订阅链上发出的所有事件。方法如下: ```javascript= // 创建 api // const api = await ...; api.query.system.events(events => { events.forEach(record => { // 遍历所有事件记录 const { event, phase } = record; const types = event.typeDef; // 过滤掉我们不关注的事件 const eventName = `${event.section}:${ event.method }:: (phase=${phase.toString()})`; if (filter.includes(eventName)) return; // 从这里开始,对我们关注的事件作进一步的处理 // ... }); }); ``` 上面是查询 `System` 模块内读取所有 `events`。回调函数中,过滤掉我们不关注的事件 (第 15 行),然后对我们所关注的事件作处理。 ## 收听自定义类型 (Custom Types) 当 Substrate 把数据返回至 JS 时,并不会返回元数据 (meta-data)的。所以当 Substrate 网络内有自定义类型时,我们相应也要在前端把这些类型的定义作为参数输入,以至 JS API 在收到这些数据时,可重构回这些对象。另外要注意一点是 Substrate 节点是用 Rust 编写的。Rust 的数据类型和 JS 也没有一对一对应。所以也有一个库 [@polkadot/types](https://github.com/polkadot-js/api/tree/master/packages/types/src/interfaces) 把 Rust 的基本数据类型封装成不同的 JS 对象。 当我们要处理自定类型时,首先在项目里载入这个库, ```bash yarn add @polkadot/types ``` 然后,在初始化 `api` 对象时,可传入一个 `types` 的参数,说明前端会触及到的所有自定义数据类型。例子如下: ```javascript const api = await ApiPromise.create({ ..., types: { Price: { dollars: 'u32', cents: 'u32', currency: 'Vec<u8>', } } }); ``` 在以上例子里,我们定义了一个 Price 的类型。里面有三个属性 `dollars`, `cents`, `currency`. 分别是 `u32`, `u32`, `Vec<u8>`. 这些数据类型都是 Substrate Node 那边定义的。所以这方面的信息事先要 Substrate Runtime 开发那边告诉前端开发的。而 `@polkadot/types` 就对 `u32`, `u8`, `Vec<u8>` 等 Rust 类型在 JS 进行了封装,创建成一个独立对象。里面有函数可以进一步对数据进行处理。如 `toJSON()`,`toString()`,`isEmpty(), toNumber()` 等。 ## 小结 今天这篇文章描述了如何在 JS 建立一个前端与 Substrate 网络交互。主要透过 Polkadot-JS API 来进行。你可以透过 JS API 读取及订阅链上数据及常量,提交外部交易,生成用户帐户并用它来签署交易等。 其实,这里也只是概括性地描述了 Polkadot JS 的功能。更详细的内容可在它的[官方文档](https://polkadot.js.org/api/)里看到。另外,你也可现在试一下实际用 JS API 建成的前端是怎样的。下一步你可以: - 尝试 [Substrate Front-end Template](https://substrate.dev/substrate-front-end-template/)。这个前端是用 React 和 Polkadot-JS API 打造的,连接到 Parity 运营的 Substrate 开发节点上,注意的是后台(链上)数据每若干小时就会重置。我们这系列的第二篇文章也会重点谈一下这个 Front-end Template。 - 尝试 [Polkadot-JS App](https://polkadot.js.org/apps/)。这是官方开发的 Polkadot / Substrate 的前端,功能非常强大。可对 Polkadot 及Substrate 网络作出不同类型的查询,及实时看到现在的链上状态及更新。 因为功能强大,刚开始可能要花点时间了解 App 里不同的界面,也需要对 Substrate 内部结构有些认识才行。 本篇读后有什么意见,欢迎在下方留言。对了,如果这篇文章你已读到这里,可能你会有兴趣再知道两个消息: - Parity 在亚洲正招聘开发推广及工程师,[详情看这里,欢迎报名](https://mp.weixin.qq.com/s/K6oTeLAiKDBiwYBO6-UByw)。 - 如果你和小伙伴们有个主意,或已经开始在 Substrate/Polkadot 生态中打造一个产品/平台出来,也可报名我们的 [Substrate Bootcamp](http://bootcamp.web3.foundation/zh-cn),截止报名日期为 3月15日。