对区块链了解的同学可以直接阅读Provider模块。无特别说明本文区块链特指以太坊。
前言
从本质上来讲,我们可以将区块链看作一个大型的分布式状态管理器。与一般的状态管理工具相比,区块链特点是存储在区块链上的状态是无法篡改(得益于加密算法、共识算法、点对点网络),且所有状态的变更更加容易追溯(区块链数据结构);
状态管理
从一个前端开发的角度来看状态管理可能是如下入所示:
用户操作视图界面,视图界面中的事件分发行为,行为修改状态,状态最终再呈现在页面上,形成一个状态流转的闭环;
现在我们来稍微变换一下,看看我们是怎么跟区块链交互的:
两者之间做了如下变更:
- 将 user 改成了
account,从广义角度来讲这两者指代的含义是相同的,都是用户,从具象上来说,一个指代的操作用户,也就是操作web页面的人;一个指代的经过密码学转化之后的公、私钥、地址所形成的集合,具体指的是在区块链上操作状态变更的主体。 - 将 action 改成了
transction,虽然名称变了,但是做的事情却是一摸一样,都是用于改变状态,区块链中改变链上状态的行为被称之为交易(transaction)。 - View改成了Browser,这边不重要,主要指的是dapp(去中心化应用)的客户端。
account(账户)和transaction(交易)
从上图中我们可以看到,要想改变区块链中的状态,有两个必备条件:
account(账户),用于执行状态变更操作;transaction(交易),改变/获取链上状态;
在以太坊中账户又分为两类,外部账户和合约账户,这部分的知识会在后续文章中做详细讲解。本文主要介绍以太坊是怎么让各个客户端进行统一的状态变更。
Provider
在实际进行dapp开发的过程中,我们可以通过多种方式或工具进行链上交互,如通过web3.js、ethers.js等 npm 库,MetaMask、coinbase Wallet等钱包工具,或者直接通过访问JSON-RPC服务(不推荐)与以太坊客户端进行直接交互。
为了更好的规范各类工具以及钱包暴露访问以太坊API的时候的不兼容问题,也为了更好的 dapp 生态,以太坊规定了一个基本的API格式提供给以太坊开发者,就是Provider,Provider本质上是一个 Javascript Object。
Provider 的命名很好理解,可以类比成React中的Provider,通过Provider提供的属性或者方法,Consumer可以很方便的获取到全局状态以及改变全局状态的方法
- provider => transaction 提供者通过对交易的封装提供给消费者很好的使用体验
- consumer => account 消费者通过 provider 更简单的进行状态的修改
Provider对象主要包含 Reqeust 和 Event,任何实现了以下API的js对象都可以称之为以太坊应用的Provider,详细规则可查看EIP-1193。
request(请求)
request 方法旨在作为远程过程调用(Remote Procedure call, rpc)的一个与传输和协议无关的包装器函数;通过具体的RPC方法(EIP-1474)来进行链上数据的获取与状态修改。
// 请求参数
interface RequestArguments {
readonly method: string;
readonly params?: readonly unknown[] | object;
}
// 调用方式
Provider.request(args: RequestArguments): Promise<unknown>;
请求参数由两部分组成 method 和 params ,既然 request 函数是一个JSON-PRC请求的封装,那也就是可以简单的理解为我们是去访问以太坊客户端上的一个远程函数,调用函数就需要函数名(method)以及参数(params)。通过调用以太坊客户端上的函数,我们可以得到链上信息(Getter)或者改变链上状态(Setter)
可以看到 request 函数返回的是一个 Promise,当JSON-PRC请求错误或其他内部错误的时候,需要抛出异常。对于JSON-RPC异常,其具体类型如下:
interface ProviderRpcError extends Error {
code: number;
data?: unknown;
}
Provider Error
| Status Code | Name | Description |
|---|---|---|
| 4001 | User Rejected Request | 用户拒绝了该请求 |
| 4100 | Unauthorized | 当前请求的方法未被授权 |
| 4200 | Unsupported Method | Provider 当前请求的方法不支持 |
| 4900 | Disconnected | Provider 没有链接到任何区块链客户端 |
| 4901 | Chain Disconnected | Provider 没有链接到所请求的区块链客户端 |
request 示例
获取当前区块链客户端上账户的信息
provider
.request({ method: 'eth_accounts' })
.then((accounts) => {
console.log(`Accounts:\n${accounts.join('\n')}`);
})
.catch((error) => {
console.error(
`Error fetching accounts: ${error.message}.
Code: ${error.code}. Data: ${error.data}`
);
});
Event(事件)
以太坊规定,Provider必须实现如下两个方法来进行事件处理:
- on 监听事件
- removeListener 移除监听
message
message 事件是用来处理其他事件未覆盖到任一消息的事件:
// 消息类型
interface ProviderMessage {
readonly type: string;
readonly data: unknown;
}
// 调用方式
Provider.on('message', listener: (message: ProviderMessage) => void): Provider;
Subscriptions
如果Provider支持以太坊RPC订阅消息,如eth_subscribe方法,当接收到以太坊订阅消息的时候就必须要触发订阅事件:
// 订阅消息的消息类型如下
interface EthSubscription extends ProviderMessage {
readonly type: 'eth_subscription';
readonly data: {
readonly subscription: string;
readonly result: unknown;
};
}
// 调用方式
Provider.on('message', listener: (message: EthSubscription) => void): Provider;
connect
当Provider能链接上以太坊客户端上的JSON-PRC服务时,必须触发 connenct 事件,分别有如下两种场景:
- 在初始化后第一次链接上以太坊客户端
- 在断开链接(disconnenct)后再次连上以太坊客户端
// 返回chainId
interface ProviderConnectInfo {
readonly chainId: string;
}
// 调用方式
Provider.on('connect', listener: (connectInfo: ProviderConnectInfo) => void): Provider;
chainId:由于公链环境中区块链众多,不同的链有不同的作用(生产、测试),同时公链的类型也很多,为了区分不同的链以便开发人员识别引入了chainId字段。可以通过ChainList查看不同链的chainId。详细讲解可查看EIP-155
disconnect
Provider在断开跟以太坊网络链接时需要触发要触发
Provider.on('disconnect', listener: (error: ProviderRpcError) => void): Provider;
chainChanged
Provider在切换以太坊网络的时候触发,并给出新的chainId
Provider.on('chainChanged', listener: (chainId: string) => void): Provider;
accountsChanged
Provider当能读取到链上项目信息的时候,必须要触发 accountsChanged 事件。当通过eth_accounts方法获知账户变化的时候触发
Provider.on('accountsChanged', listener: (accounts: string[]) => void): Provider;
总结
Provider API由一个请求(request)和5个事件(event)组成。request 方法和 message 事件是独立的,剩余4个 event 可以分成两组:
- 更改
Provider发送JSON-RPC接口的能力:- connect
- disconnect
- 任何dapp程序都必须处理的以太坊客户端上的状态更改:
- chainChanged
- accountsChanged
有了Provider后,我们的状态管理流程图就变成如下所示了