Substrate 前端开发系列 - 1/2
看了这专栏之前几篇文章后,相信各位对用 Substrate 作开发已经有了基本认识。可以把节点跑起来,也能写出能完成个别功能的 runtime 出来,甚至跑起几个节点形成一个网络出来。但终端用户始终不会直接与这区块链网络互动。现在我们需要搭建一个前端,借着它用户才能与这网络互动。
所以接下来我们会介绍如何利用 Substrate 生态中的 Polkadot-JS API (下面简称 JS API) 来使前端与Substrate 节点交互。这项目名称虽然写着 Polkadot,但其实它可以连接到所有基于 Substrate 开发的节点。
本篇文章是前端系列的第一篇,先深入探讨以 JS API 来连接到 Substrate 节点并与之交互。内容适合任何前端框架,甚至如果你要打造一个 Node.js 的中间件来订阅 Substrate 节点事件 (events) 也可以,JS API 也允许你这么做。如果你的前端打算是用 React 打造,请留意我们的下篇,讲述如何在 Substrate Front-end Template 的基础上打造你的前端,它把 Polkadot-JS API 封装在 React 的组件内来使用。
接下来,我们假设你在本机已能跑起 Substrate (还没做这步的小伙伴可参考这里)。并且 Substrate 的 web socket 端口设在默认的 localhost:9944
。
首先在你的 JS 项目中添加 JS API 的库
yarn add @polkadot/api
我们建议使用 yarn
作你的项目包管理工具。
然后在开始要与 Subtrate 网络互动前创建一个 api
对象如下:
// 引入
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.transactionByteFee.toNumber());
在这里,注意我们是用 ES2015 的 JS 准则来写的,所以用 import
来引用外部的库及支持 async
/ await
这些功能。做了以上的操作后,你可从 api
这对象取得所有需要的与 Substrate 交互的函数及常量。
接下来,下面是取得链上数据的例子。
// 初始化 `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 存取项的读取函数。基本原则就是:
api.query.
这里的函数只会对链上数据做出简单的读取操作。因为这是需要和 Substrate 节点实时交互,会是一个异步操作,返回一个 Promise,然后用 await 等待结果。
做前端开发时,你有时不但需要在载入网页那一刻取得链上的数据,随着这些链上数据在变更,可能你也需要动态变更页面上的内容。这也是为什么我们一开始连接时,不是用简单的 http API 请求,而是 WebSocket 连接。
在以上取得用户余额的例子中,你也可以传入一个回调函数。这样取得用户余额以外,每次当这余额数值变更时也会回调过来。
// 订阅着该数值
const unsub = await api.query.balances.freeBalance(ADDR, balance => {console.log(`balance of ${balance}`);
});
用这方法的话,返回的将会是他的取消订阅函数。当你不再需要监听这数值时,就呼叫这函数。
JS API 也有个便捷的方法,可一次过订阅多个链上数值。例子如下:
const unsub = await api.queryMulti([// 一个 getter functionapi.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 数组, 回调函数)
。
方法跟读取链上数据差不多。在取得 api
接口后,可以用 api.const.
来获取。以下是一些例子。
// 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 中能识别的值。这点我们会在自定义结构里进一步讲解。
这部份的操作会直接变更链上的数据,而且都需要有个用户/主体对呼叫的函数作出签署,所以我们称之为外部交易。以下例子是用户 Alice
打款 12345 个单位货币到另一帐户:
// ...// 一个模拟地址
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.
返回的是一个 hash 值,代表这个交易被记录到区块链上。但这并不意味交易已经顺利执行。所以接下来我们可监听事件 (events) 来确定交易已完成(或报错)。另外你可能也留意我们还没有提 alice
究竟是个怎么样的值。这部份留到 帐号管理/签署部份来谈。
用以下的方法,我们就可监听自己提交的交易。方法和上面订阅链上数据类似:
// 从 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
: 此事件传出的参数,是一个数组。接下来,我们详细说明上文 alice
这用户对象是如何取得的。这当然不是一条字符串地址。若然如此,那么任何人都可以冒充另一个人发交易给其他人。
要创建用户对象,首先要加入 @polkadot/keyring
库到项目中:
yarn add @polkadot/keyring
然后在 JS 代码里:
// 引入
import { Keyring } from '@polkadot/api';// 初始化 api
// const api = await ...;// api 完成初始化后,再创建 keyring 对象。
const keyring = new Keyring({ type: 'sr25519' });
跟着你可以用以下几种方法创建出有公钥及私钥的用户帐号:
// 从 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');
跟着你可以这样签署信息和核实信息:
// 引入一些帮助函数
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 这操作。
你也可用类似链上数据查询的方法,来订阅链上发出的所有事件。方法如下:
// 创建 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 行),然后对我们所关注的事件作处理。
当 Substrate 把数据返回至 JS 时,并不会返回元数据 (meta-data)的。所以当 Substrate 网络内有自定义类型时,我们相应也要在前端把这些类型的定义作为参数输入,以至 JS API 在收到这些数据时,可重构回这些对象。另外要注意一点是 Substrate 节点是用 Rust 编写的。Rust 的数据类型和 JS 也没有一对一对应。所以也有一个库 @polkadot/types 把 Rust 的基本数据类型封装成不同的 JS 对象。
当我们要处理自定类型时,首先在项目里载入这个库,
yarn add @polkadot/types
然后,在初始化 api
对象时,可传入一个 types
的参数,说明前端会触及到的所有自定义数据类型。例子如下:
const api = await ApiPromise.create({...,types: {Price: {dollars: 'u32',cents: 'u32',currency: 'Vec
});
在以上例子里,我们定义了一个 Price 的类型。里面有三个属性 dollars
, cents
, currency
. 分别是 u32
, u32
, Vec
. 这些数据类型都是 Substrate Node 那边定义的。所以这方面的信息事先要 Substrate Runtime 开发那边告诉前端开发的。而 @polkadot/types
就对 u32
, u8
, Vec
等 Rust 类型在 JS 进行了封装,创建成一个独立对象。里面有函数可以进一步对数据进行处理。如 toJSON()
,toString()
,isEmpty(), toNumber()
等。
今天这篇文章描述了如何在 JS 建立一个前端与 Substrate 网络交互。主要透过 Polkadot-JS API 来进行。你可以透过 JS API 读取及订阅链上数据及常量,提交外部交易,生成用户帐户并用它来签署交易等。
其实,这里也只是概括性地描述了 Polkadot JS 的功能。更详细的内容可在它的官方文档里看到。另外,你也可现在试一下实际用 JS API 建成的前端是怎样的。下一步你可以:
本篇读后有什么意见,欢迎在下方留言。对了,如果这篇文章你已读到这里,可能你会有兴趣再知道两个消息: