写点什么

从前端开发角度理解如何与 Substrate 协作

作者:彭亚伦
  • 2022 年 6 月 30 日
  • 本文字数:6213 字

    阅读完需:约 20 分钟

从前端开发角度理解如何与Substrate协作

前端与 Substrate 区块链交互

当我们说前端与区块链交互, 我们在说什么?


这里首先需要理解 2 个知识点:


  • 区块链(blockchain), 准确地说指的是一种数据存储结构, 即以链(chain)的形式串联起来的一堆区块(block)

  • 区块链网络, 一种分布式网络, 网络中的每一个节点(node), 都是一台装有并运行节点客户端(node-clint)的计算机, 一个完整的节点客户端(node-client) 包含一个区块链存储数据结构, 和用于跟其它节点通讯以及达成共识的外围程序.


具体如下图:



区块链上每个节点都"实时"与其它节点保持同步一致, 内部维护区块链数据必须全网一致, 这就是所谓达成**"共识".**


因此, 一个单独的完整区块链节点, 其具备的功能, 与任意其它节点具备的功能是一致的, 这里我们假定该网络中所以节点都是可信的.


💡 因此, 所谓与区块链交互, 从前端开发的角度来说, 就是与区块链网络中的某一个(或者一组)节点进行交互.


从前端的角度来说, 区块链网络中的某个节点, 就类似传统的开发中的"后端" server.


当然, 作为官方的前端 app, 区块链网络应提供一组可信的,性能优越的机器作为交互节点.

Substrate 的基本结构 (可选阅读)

这一部分是可以跳过, 主要是为了让前端的伙伴了解一下后端大致开发的结构, 不看也没关系 😂


我们的 cambio network 是基于 substrate 开发, substrate 的一个重要特性就是模块化. 一个区块链可能包含很多功能, 比如治理/投票/质押/众筹等等, 这些模块在 substrate 内部被称为 "pallet".


一个典型的 pallet 的结构都是固定的, 包含以下:


  • 常量constants, 比如质押模块可能会设定一个 min_staking_amount 常量, 用于规定最小质押额度, 常量经常用于定义pallet的配置(Configuration)

  • 储存值storage, 这个是区块链数据中的主要成分. 一个palletstorages, 就代表了这个pallet要往链上存储什么数据.

  • 还是以质押为例, 比如当某个用户质押了一笔金额, 那个这个pallet就会将一个(AccountId, Staking_amount, Block_number)元组存入链上. 这个元组就称为一个storage, storage有几种形式:

  • 单值形式: 单个的类型值, 比如数字, 元组, 数组集合等

  • map键值对: 即一个 Key:Value 形式

  • D-map 双键值对: 一个 (Key1, Key2):Value 的形式

  • N-Map键值对: 即多键的形式

  • 以上的形式都在 Polkadot.js 里有对应的处理 API.

  • Call调用: 即pallet的方法, 也就是pallet对外的执行接口, (内部接口不在讨论之列), 比如质押模块可能提供:

  • Staking(AccountId, Amount) 用于用户质押一定金额的Call;

  • Revoke_Staking(AccountId, Amount)用于撤回之前质押的Call;

  • 一个 Call 在波卡系中, 也被称为一个**Extrinsic, 调用并执行一次Call, 有时也被称为"发起一笔交易".**

  • Event事件, 当一个 Call 执行后会可能会对执行情况以事件的形式报告出来, 比如质押模块中Staking Call 执行后, 如果成功可能会触发一个"StakingSuccessed(AccountId, Amount, Block_number)" 事件, 其事件中包含用户 id, 金额, 和写入的区块值.

  • Error错误, 当 Call 执行出错, 则会类似事件一样报错, 同样以 Staking Call 为例子, 如果用户余额只有 10, 但是却去质押 100, 那么执行中会报错" InsufficientBalance(AccountId, Balance)".


大致这 5 个元素在 Pallet 的关系和作用如下:


Pallet_staking: {  {    Constants:  { //不可变的常量, 节点启动时就写入链上,除非整个链升级否则无法更改
min_staking_amount: u32 ..... } Storages: { // 可变存储量, 等价于整个链的各种state, 是链的主要数据 // 在运行中通过Call中的方法来CRUD // 任何时刻的state, 都会被区块链以区块的形式存储下来, // 因此, 在区块链中所有state的历史变化都是可查的 Record: (Acc, u32, u64), .... } Calls: { //类似一个实例化的interface, 主要用于操作Storage // 一般会返回一个Result枚举类型, T或者Error的一种, // T是泛型, 类似typescript中的Any, 但实际实例化时, 会给T确定一个类型 Staking(Acc, u32) -> Result<T, Error> .... }
Events: { //本质是一个枚举, Call运行中或运行完毕dispatch一个或者多个事件 // 虽然不是强制, 但是Call运行完毕dispatch一个事件则是最佳做法 //类似subquery等数据工具, 就非常依赖链上的Event来进行历史采集 StakingSuccessed(Record), .....
}
Errors: { // 本质也是一个枚举, 一个Call要么只能返回一个T // 或者dispatch一个Event, 要么只能返回Error InsufficientBalance(Acc, Balance) }
}
复制代码


总结一下:


  1. Storage是链上主要的数据, 代表链从创世区块以来的状态, 每个区块都记录着零个或者多个 storage 的最新状态

  2. 外部通过调用Call中定义的方法, 来 CRUD 这些Storage

  3. Call中的方法在操作Storage时, 可能会报错Error, 也可能会发出一个事件Event

  4. Constants是初始化就定义好的不可变常量, 常常用于定义pallet的配置, 除非升级链, 否则不会变动.


如果用一句话综合一下, 一个Pallet要实现的就是:


💡 在 Constants 设定的配置条件下, 使用 Call 去操作 Storage, 来修改链的状态, 中间可能触发 Error 或者 Event


当然除了以上 5 个, pallet还会包含其他内容, 但基本对于前端是无须知道的.

前端如何与 substrate 节点通讯

一个完整的 substrate 开发的区块链节点客户端会暴露一组 JSON-RPC API 接口, 可以使用 HTTP 或者 WebSocket 方式来访问.


这个 JSON-RPC API 暴露的数据接口内容, 每个不同链都不一样, 但都遵循规定的格式和约束, 就是所谓metadata. 一个链的metadata包含了区块链网络给外部访问的所以接口和数据.


这些数据和接口, 通常可以用一个 json 文件来描述, 一个典型的metadata结构如下:


{  "magicNumber": 1635018093,  "metadata": {    "v13": {      "modules": [        ....  // 主要关注的部分, 详细请见下文      ],      "extrinsic": {        "version": 4,        "signedExtensions": [          "CheckSpecVersion",          "CheckTxVersion",          "CheckGenesis",          "CheckMortality",          "CheckNonce",          "CheckWeight",          "ChargeTransactionPayment"        ]      }    }  }}
复制代码


其中前端主要关注的就是 "modules"部分, 这里的"modules", 就是指的是前面所说的 pallets.


一个典型的module内容, 就包含了前面所说的pallet的 5 大要素:


        {          "name": "TemplateModule",          "**storage**": {            "prefix": "TemplateModule",            "items": [              {                "name": "Something",                "modifier": "Optional",                "type": {                  "plain": "u32"                },                "fallback": "0x00",                "docs": []              }            ]          },          "**calls**": [            {              "name": "do_something",              "args": [                {                  "name": "something",                  "type": "u32"                }              ],              "docs": [                " 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": [],              "docs": [                " An example dispatchable that may throw a custom error."              ]            }          ],          "**events**": [            {              "name": "SomethingStored",              "args": [                "u32",                "AccountId"              ],              "docs": [                " Event documentation should end with an array that provides descriptive names for event",                " parameters. [something, who]"              ]            }          ],          "**constants**": [],          "**errors**": [            {              "name": "NoneValue",              "docs": [                " Error names should be descriptive."              ]            },            {              "name": "StorageOverflow",              "docs": [                " Errors should have helpful documentation associated with them."              ]            }          ],          "index": 8        },
复制代码


这里有一个完整的metadata文件.


meta.json


可以看到, 一个module内就包含了每个 pallet 的名称,记录着每个palletstorage, calls, events, constants, errors.


所以, 读取到这个链的metadata, 就会知道这 Substrate 链提供了什么接口可供调用。当然, 对于我们自己开发而言, 只需知道必要的几个 Call/Storage/Event/Error 即可.

Polkadot.js 交互基础

polkadot.js 是波卡官方提供用来与 substrate 链交互的 js 库, 对各种类型,结构和调用进行了封装, 具体来说, 有 3 个主要的基础封装:


  • api.tx.<pallet>.<call> 来调用Call, tx 是 transaction 的缩写, 即交易, 调用一个 Call, 通常也被称为”发送一个交易”, 一般而言, 交易都是需要签名的, 签名环节请见后文

  • api.consts.<pallet>.<const> 来读取 pallet 中的常量

  • api.query.<pallet>.<name> 来读取 pallet 存储storage


大致上, polkadot.js 与链上交互分为几个步骤:


  1. 连接到节点


    const { ApiPromise, WsProvider } = require('@polkadot/api');        // Construct    const wsProvider = new WsProvider('ws://127.0.0.1:9944');    // 如没有运行 node-template,也可试连到波卡主网上: `wss://rpc.polkadot.io`.    const api = await ApiPromise.create({ provider: wsProvider });
复制代码


  1. 读取元数据, 一般性这只是一次性的可选动作, 后端完成后, 一般会自己导出一个 json 文件给到前端使用, 如果前端自行读取, 则:


    const { magicNumber, metadata } = await api.rpc.state.getMetadata();        console.log("Magic number: " + magicNumber);    console.log("Metadata: " + metadata.raw);
复制代码


  1. 基础业务逻辑处理

  2. 读取常量:


    // api.consts.<pallet 名称>.<常量名称>. 比如:    const main = async() => {      const existentialDeposit = await api.consts.balances.existentialDeposit    }
复制代码


读取`storage`:
复制代码


    // api.query.<pallet 名称>.<存储名称>. 比如:    const main = async() => {      const acct = await api.query.system.account(alice.address);    }
复制代码


//调用`call`( 或者说 **发送交易**)      await api.tx.balances.transfer(dest.address, amt)      .signAndSend(src, res => {        console.log(`Tx status: ${res.status}`);      });

复制代码


  1. 批量处理

  2. 同时发多个查询可同时发多个查询,而不是一条一条发


        // 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}`);        });
复制代码


可以批量调用多个:


        // 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 这个取消订阅函数。整个模式类似以下:
复制代码


        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]);
复制代码


订阅事件


        // 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, 类似于 hardcode 在代码中的方式

  • 通过钱包应用或者插件


对于一个 web app 来说, 主要是通过钱包插件, 波卡中则是统一的 Polkadotjs-dapp 插件. 文档: https://polkadot.js.org/docs/extension

基本流程

首先需要安装 @polkadot/extension-dapp , 钱包里账户进入页面的方式被称为 “Inject”, 在前端中, 所有账户的提供方, 比如 polkadotjs 钱包, 狐狸钱包, 或者是页面自己新建的账户, 这些来源方统一称为”Provider”,


  1. 请求钱包授权, 只有获得钱包授权, 才能从钱包把账户”Inject”到页面中供 js 调用


import {  web3Accounts,  web3Enable,  web3FromAddress,  web3ListRpcProviders,  web3UseRpcProvider} from '@polkadot/extension-dapp';
// 这一步在所有动作之前完成, const allInjected = await web3Enable('cambio network');
if (extensions.length === 0) { // 如果钱包插件没有安装, 或者没能获得授权, 应该做处理 return;}
复制代码


  1. 列出钱包中的账户



const allAccounts = await web3Accounts()
复制代码



本文为 SEP Creation 原创组文章, 作者彭亚伦


未经许可, 请勿转载.

IN RUST WE TRUST 🇨🇳🌞🌑🌦🌬💫✨

发布于: 刚刚阅读数: 3
用户头像

彭亚伦

关注

A Rustacean and Substrate Evangelist 2021.01.25 加入

A Rustacean, and Substrate Evangelist, member of CRVA (RISC-V)

评论

发布
暂无评论
从前端开发角度理解如何与Substrate协作_Substrate_彭亚伦_InfoQ写作社区