除了 web3.js 标准库之外,从 4.0.2 开始 web3.js 支持插件,它为最终用户添加了特定的功能。这些额外的功能可以是特定契约的包装器、额外的 RPC 方法包装器,甚至可以扩展 web3.js 方法的逻辑。本指南旨在为开发 web3.js 插件提供必要的上下文信息。
请随意探索一些已构建的插件和 / 或使用这个模板来开始开发你的 Web3.js 插件。
为了为插件用户提供类型安全性和 IntelliSense,请参考设置模块增强部分,了解如何扩展 Web3Context
模块以为插件启用类型特性。
插件依赖项
最少,你的插件应该依赖于 web3
包的 4.0.2
版本。这将允许你的插件类扩展提供的 Web3PluginBase
抽象类。但是,web3
不应该被列为常规依赖项,而是应该在你的插件的 package.json
中作为对等依赖项列出。
需要注意的是,插件名称应该结构化为@<organization>/web3-plugin-<name>
或web3-plugin-<name>
。
{
"name": "web3-plugin-custom-rpc-methods",
"version": "0.1.0",
"peerDependencies": {
"web3": ">= 4.0.2 < 5"
}
}
当用户安装你的插件时,这将允许包管理器在用户已安装的 web3
可用且版本满足版本约束的情况下使用用户安装的 web3
,而不是安装自己的 web3
版本。
扩展 Web3PluginBase
你的插件类应该 extend
(继承)Web3PluginBase
抽象类。此类 extends
Web3Context 当用户使用类注册你的插件时,你的插件的 Web3Context
将指向模块的 Web3Context
,从而使你的插件可以访问用户配置的 requestManager 和 accountProvider 等内容。
import { Web3PluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3PluginBase { ... }
扩展 Web3EthPluginBase
除了 Web3PluginBase
,你还可以选择扩展 Web3EthPluginBase
,它将提供Ethereum JSON RPC API 接口,这是一些包(例如 Web3Eth
)使用的,作为你的插件的 requestManager
的泛型,为Ethereum JSON RPC 规范提供类型支持。如果你的插件直接使用 web3 提供的 requestManager
向提供程序发出 Ethereum JSON RPC 调用,这将是推荐的方法。
import { Web3EthPluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3EthPluginBase { ... }
插件命名空间
在扩展 Web3PluginBase
类之后,你的插件将需要一个 public
的 pluginNamespace
属性,该属性配置了在用户注册插件的类上如何访问你的插件。在下面的例子中,pluginNamespace
被设置为 customRpcMethods
,因此当用户注册插件时,他们将以以下方式访问你的插件:
以下是你的插件代码:
// custom_rpc_methods_plugin.ts
import { Web3PluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3PluginBase {
public pluginNamespace = 'customRpcMethods';
public someMethod() {
return 'someValue';
}
}
以下是插件用户的代码:
// registering_a_plugin.ts
import { Web3Context } from 'web3';
import { CustomRpcMethodsPlugin } from './custom_rpc_methods_plugin';
const web3Context = new Web3Context('http://127.0.0.1:8545');
web3Context.registerPlugin(new CustomRpcMethodsPlugin());
await web3Context.customRpcMethods.someMethod();
使用继承的 Web3Context
下面是 CustomRpcMethodsPlugin
利用 this.requestManager
的示例,该属性将访问 Ethereum 提供程序(如果用户已配置提供程序)。如果用户没有设置提供程序,则在调用 customRpcMethod
时,以下代码将抛出一个 ProviderError:
import { Web3PluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3PluginBase {
public pluginNamespace = 'customRpcMethods';
public async customRpcMethod() {
return this.requestManager.send({
method: 'custom_rpc_method',
params: []
});
}
}
以下是不配置 Ethereum 提供程序的插件用户代码,当调用 customRpcMethod
时,将抛出 ProviderError:
// registering_a_plugin.ts
import { Web3Context } from 'web3'
import { CustomRpcMethodsPlugin } from './custom_rpc_methods_plugin';
const web3Context = new Web3Context();
web3Context.registerPlugin(new CustomRpcMethodsPlugin());
// 当插件尝试调用 this.requestManager.send(...) 时,以下操作将抛出 ProviderError
await web3Context.customRpcMethods.customRpcMethod();
抛出的 ProviderError:
ProviderError: Provider not available. Use `.setProvider` or `.provider=` to initialize the provider.
为 Web3PluginBase 提供 API 泛型
如果需要,你可以为 Web3PluginBase
提供一个 API 类型(遵循 Web3ApiSpec模式),作为泛型传递给 Web3PluginBase
,它将为 requestManager
提供类型提示,从而在开发插件时为其提供类型提示。在下面的代码中,CustomRpcApi
是传递给 Web3PluginBase<CustomRpcApi>
的类型。
import { Web3PluginBase } from 'web3';
type CustomRpcApi = {
custom_rpc_method_with_parameters: (
parameter1: string,
parameter2: number
) => string
}
export class CustomRpcMethodsPlugin extends Web3PluginBase<CustomRpcApi> {
public pluginNamespace = 'customRpcMethods';
public async customRpcMethodWithParameters( parameter1: string, parameter2: number) {
return this.requestManager.send({
method: 'custom_rpc_method_with_parameters',
params: [parameter1, parameter2]
})
}
}
在你的插件中使用 web3.js 包
当前存在一个问题,即某些 web3.js 包未能正确将它们的 Web3Context
链接到用户注册插件的类的上下文。正如在问题中所述,这可能导致一个 bug,其中插件实例化一个 Contract
(来自 web3-eth-contract
)的实例并尝试在 Contract
实例上调用方法(它使用 requestManager
调用 Ethereum 提供程序的方法),从而导致 ProviderError 错误,尽管插件用户已设置了提供程序,并且它应该对插件可用。
该问题的解决方法如下所示:
import { Contract, ContractAbi, Web3Context, Web3PluginBase, types, utils } from 'web3';
import { ERC20TokenAbi } from './ERC20Token';
export class ContractMethodWrappersPlugin extends Web3PluginBase {
public pluginNamespace = 'contractMethodWrappersPlugin';
private readonly _contract: Contract<typeof ERC20TokenAbi>;
public constructor(abi: ContractAbi, address: types.Address) {
super();
this._contract = new Contract(abi, address);
}
/**
* 此方法覆盖了继承的 `link` 方法,以在 `Web3.registerPlugin` 被调用时,为 `Contract` 实例添加一个配置好的 `RequestManager`。
*
* @param parentContext - 要添加到 `ChainlinkPlugin` 实例(从而添加到 `Contract` 实例)的上下文。
*/
public link(parentContext: Web3Context) {
super.link(parentContext);
this._contract.link(parentContext);
}
public async getFormattedBalance<ReturnFormat extends types.DataFormat>(
address: types.Address,
returnFormat?: ReturnFormat
) {
return utils.format(
{ eth: 'unit' },
await this._contract.methods.balanceOf(address).call(),
returnFormat ?? types.DEFAULT_RETURN_FORMAT
);
}
}
这个解决方法是覆盖继承的 link
方法(从 Web3PluginBase
继承的,它又从 Web3Context
继承而来),并明确调用 Contract
实例上的 .link
方法。当用户调用 registerPlugin
时,将传递 parentContext
,它将是用户正在注册插件的类的上下文。
这个解决方法将覆盖继承的 link
方法(从 Web3PluginBase
继承,它又继承自 Web3Context
),并明确调用 Contract
实例上的 .link
方法。当用户调用 registerPlugin
时,将传递 parentContext
,它将是用户正在注册插件的类的上下文。
以下是解决方法,并且可能需要对插件使用的任何实例化的 web3.js 包进行相同的操作,这些包使用 Web3Context
:
public link(parentContext: Web3Context) {
super.link(parentContext);
// 这个解决方法将确保 Contract 实例的上下文与插件用户正在注册插件的类的上下文相链接
this._contract.link(parentContext);
}
设置模块增强
为了为插件注册时提供类型安全性和 IntelliSense,你必须扩充 Web3Context
模块。简单来说,你将告诉 TypeScript,你正在修改 Web3Context
类的接口,并在其中包括你的插件的接口(即你的插件的添加方法、属性等)。结果,你的插件对象将以你选择的命名空间的形式可用,并将在任何 Web3Context
对象中可用。
关于模块增强的更多信息,可以参考这里的教程。
模块增强
在注册插件时,你正在向模块的接口中添加额外的方法和 / 或类,并且 TypeScript 需要一些帮助来理解在插件注册后模块中会有哪些内容可用。
// custom_rpc_methods_plugin.ts
import { Web3PluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3PluginBase {
public pluginNamespace = 'customRpcMethods';
public someMethod() {
return 'someValue';
}
}
// 模块增强
declare module 'web3' {
// 这里是在 Web3Context 类内添加你的插件的地方
interface Web3Context {
customRpcMethods: CustomRpcMethodsPlugin;
}
}
需要考虑的重要点
- 通过扩展
Web3Context
(及其所有扩展它的类),你的插件的接口将出现在所有扩展Web3Context
的 Web3 模块(即web3
、web3-eth
、web3-eth-contract
等)的 IntelliSense 中。即使
你的插件未注册,这是值得让用户知道的事情,因为他们只有在使用 .registerPlugin
时才能使用你的插件。
这是值得让用户知道的事情,因为他们只有在使用 .registerPlugin
时才能使用你的插件。以下是你的插件用户 在不调用 .registerPlugin
的情况下会看到的内容:
上面截图显示了 .customRpcMethods.someMethod
可以在 Web3
实例上调用的智能提示,无论插件用户是否注册了 CustomRpcMethodsPlugin
。但是,如果用户在访问插件功能之前没有调用 .registerPlugin
,那么他们会遇到错误。你需要明确告诉他们在访问插件的任何功能之前,他们需要调用 .registerPlugin
。
registerPlugin
方法存在于Web3Context
类上,因此任何extends Web3Context
的类都可以将你的插件的额外功能添加到其接口中。因此,通过扩充Web3Context
以包括你的插件的接口,实际上你提供了一个通用的扩充,将你的插件的接口添加到所有扩展Web3Context
的 Web3 模块上(例如web3
、web3-eth
、web3-eth-contract
等)。- 我们在示例代码中使用
customRpcMethods
作为插件的pluginNamespace
的名称,请注意在两个地方使用相同的名称:第一个地方是在模块增强中。第二个地方是在你的插件类中的公共pluginNamespace
属性的值。
所以,例如,请注意在下面的两个代码片段中使用 customRpcMethods
:
模块增强:
// 由 **插件开发者**编写的代码
declare module 'web3' {
// 这里是在 Web3Context 类内添加你的插件
interface Web3Context {
customRpcMethods: CustomRpcMethodsPlugin
}
}
你的插件类:
// 由 **插件开发者**编写的代码
export class CustomRpcMethodsPlugin extends Web3PluginBase {
public pluginNamespace = 'customRpcMethods';
...
}
这是因为 .registerPlugin
将使用插件提供的 pluginNamespace
属性作为属性名称,当它将插件注册到 插件用户 将调用 .registerPlugin
的类实例上时:
// 由 **插件用户**编写的代码
const web3 = new Web3('http://127.0.0.1:8545');
web3.registerPlugin(new CustomRpcMethodsPlugin());
// 现在 customRpcMethods(即 pluginNamespace)在 Web3 实例上是可用的
web3.customRpcMethods;
完整示例
你可能会发现参考一个完整的 web3 插件开发和使用示例是有帮助的。Web3.js Chainlink Plugin 存储库提供了一个非常好的示例,可以参考一下。
评论 (0)