Plugin Developer Guide
This guide intends to provide the necessary context for developing plugins for web3.js.
Feel free to explore some of the already built plugins and/or use this template to start with development of your Web3.js plugin.
To provide type safety and IntelliSense for your plugin users, please refer to the Setting Up Module Augmentation section for how to augment the Web3Context
module to enable typing features for your plugin.
Plugin Dependencies
At the minimum, your plugin should depend on web3
package version 4.0.2
. This will allow your plugin class to extend the provided Web3PluginBase
abstract class. However, web3
shouldn't be listed as a regular dependency, instead it should be listed in your plugin's package.json
as a peer dependency.
It is important to note that the plugin name should be structured as @<organization>/web3-plugin-<name>
or web3-plugin-<name>
.
{
"name": "web3-plugin-custom-rpc-methods",
"version": "0.1.0",
"peerDependencies": {
"web3": ">= 4.0.2 < 5"
}
}
When your users install your plugin, this will allow the package manager to make use of the user installed web3
if available and if the version satisfies the version constraints instead of installing its own version of web3
.
Add New Transaction Type
Furthermore, you have the flexibility to expand your range of transaction types, enhancing compatibility with the web3.js
library.
// create new TransactionType class which extends BaseTransaction class
import { BaseTransaction } from 'web3-eth-accounts';
const TRANSACTION_TYPE = 15;
class SomeNewTxTypeTransaction extends BaseTransaction {
// ...
}
// create new plugin and add `SomeNewTxTypeTransaction` to the library
import { Web3EthPluginBase } from 'web3';
class SomeNewTxTypeTransactionPlugin extends Web3PluginBase {
public pluginNamespace = 'someNewTxTypeTransaction';
public constructor() {
super();
TransactionFactory.registerTransactionType(TRANSACTION_TYPE, SomeNewTxTypeTransaction);
}
}
Extending Web3PluginBase
Your plugin class should extend
the Web3PluginBase
abstract class. This class extends
Web3Context and when the user registers your plugin with a class, your plugin's Web3Context
will point to the module's Web3Context
giving your plugin access to things such as user configured requestManager and accountProvider.
import { Web3PluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3PluginBase { ... }
Extending Web3EthPluginBase
In addition to Web3PluginBase
, you can choose to extend Web3EthPluginBase
which will provide the Ethereum JSON RPC API interface, which packages such as Web3Eth
use, as a generic to your plugin's requestManager
, giving it type support for the Ethereum JSON RPC spec. This would be the recommended approach if your plugin makes Ethereum JSON RPC calls directly to a provider using web3's provided requestManager
.
import { Web3EthPluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3EthPluginBase { ... }
pluginNamespace
After extending the Web3PluginBase
class, your plugin will need a public
pluginNamespace
property that configures how your plugin will be accessed on the class, which your plugin was registered with. In the following example, the pluginNamespace
is set to customRpcMethods
, so when the user registers the plugin they will access your plugin as follows:
The following represents your plugin code:
// custom_rpc_methods_plugin.ts
import { Web3PluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3PluginBase {
public pluginNamespace = 'customRpcMethods';
public someMethod() {
return 'someValue';
}
}
The following represents the plugin user's code:
// 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();
Using the Inherited Web3Context
Below is an example of CustomRpcMethodsPlugin
making use of this.requestManager
which will have access to an Ethereum provider if one was configured by the user. In the event that no provider
was set by the user, the below code will throw a ProviderError if customRpcMethod
was to be called:
import { Web3PluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3PluginBase {
public pluginNamespace = 'customRpcMethods';
public async customRpcMethod() {
return this.requestManager.send({
method: 'custom_rpc_method',
params: [],
});
}
}
Below depicts a plugin user's code that does not configure an Ethereum provider, resulting in a thrown ProviderError when calling customRpcMethod
:
// registering_a_plugin.ts
import { Web3Context } from 'web3';
import { CustomRpcMethodsPlugin } from './custom_rpc_methods_plugin';
const web3Context = new Web3Context();
web3Context.registerPlugin(new CustomRpcMethodsPlugin());
// The following would result in a thrown ProviderError when
// the plugin attempts to call this.requestManager.send(...)
await web3Context.customRpcMethods.customRpcMethod();
Thrown ProviderError:
ProviderError: Provider not available. Use `.setProvider` or `.provider=` to initialize the provider.
Providing an API Generic to Web3PluginBase
If needed, you can provide an API type (that follows the Web3ApiSpec pattern) as a generic to Web3PluginBase
that will add type hinting to the requestManager
when developing your plugin. In the below code, this is the CustomRpcApi
type that's being passed as 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],
});
}
}
Using web3.js Packages within Your Plugin
Overriding Web3Context
's .link
Method
There currently exists an issue with certain web3.js packages not correctly linking their Web3Context
with the context of the class the user has registered the plugin with. As mentioned in the issue, this can result in a bug where a plugin instantiates an instance of Contract
(from web3-eth-contract
) and attempts to call a method on the Contract
instance (which uses the requestManager
to make a call to the Ethereum provider), resulting in a ProviderError even though the plugin user has set a provider and it should be available to the plugin.
A workaround for this issue is available, below is an example of it:
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);
}
/**
* This method overrides the inherited `link` method from
* `Web3PluginBase` to add a configured `RequestManager`
* to the Contract instance when `Web3.registerPlugin`
* is called.
*
* @param parentContext - The context to be added to the instance of `ChainlinkPlugin`,
* and by extension, the instance of `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,
);
}
}
The workaround is overwriting the inherited link
method (inherited from Web3PluginBase
which inherits it from Web3Context
) and explicitly calling .link
on the Contract
instance. The parentContext
will get passed when the user calls registerPlugin
, it will be the context of the class the user is registering your plugin with.
The following is the workaround, and will probably need to be done for any instantiated web3.js package your plugin uses that makes use of Web3Context
:
public link(parentContext: Web3Context) {
super.link(parentContext);
// This workaround will ensure the context of the Contract
// instance is linked to the context of the class the
// plugin user is registering the plugin with
this._contract.link(parentContext);
}
Plugin Middleware
Middleware allows plugins to intercept network interactions and inject custom logic. There are two types of plugin middleware: request middleware and transaction middleware. In both cases, the middleware is implemented as a new class and registered with the plugin in the plugin's link
method. Keep reading to learn how to add middleware to a plugin.
Request Middleware
Request middleware allows plugins to modify RPC requests before they are sent to the network and modify RPC responses before they are returned to Web3.js for further internal processing. Request middleware must implement the RequestManagerMiddleware
interface, which specifies two functions: processRequest
and processResponse
. Here is a simple example of request middleware that prints RPC requests and responses to the console:
export class RequestMiddleware<API> implements RequestManagerMiddleware<API> {
public async processRequest<ParamType = unknown[]>(
request: JsonRpcPayload<ParamType>,
): Promise<JsonRpcPayload<ParamType>> {
const reqObj = { ...request } as JsonRpcPayload;
console.log('Request:', reqObj);
return Promise.resolve(reqObj as JsonRpcPayload<ParamType>);
}
public async processResponse<
Method extends Web3APIMethod<API>,
ResponseType = Web3APIReturnType<API, Method>,
>(response: JsonRpcResponse<ResponseType>): Promise<JsonRpcResponse<ResponseType>> {
const resObj = { ...response };
console.log('Response:', resObj);
return Promise.resolve(resObj);
}
}
To add request middleware to a plugin, use the Web3RequestManager.setMiddleware
method in the plugin's link
method as demonstrated below:
public link(parentContext: Web3Context): void {
parentContext.requestManager.setMiddleware(new RequestMiddleware());
super.link(parentContext);
}
Transaction Middleware
Transaction middleware allows plugins to modify transaction data before it is sent to the network. Transaction middleware must implement the TransactionMiddleware
interface, which specifies one function: processTransaction
. Here is a simple example of transaction middleware that prints transaction data to the console:
export class TxnMiddleware implements TransactionMiddleware {
public async processTransaction(
transaction: TransactionMiddlewareData,
): Promise<TransactionMiddlewareData> {
const txObj = { ...transaction };
console.log('Transaction data:', txObj);
return Promise.resolve(txObj);
}
}
To add transaction middleware to a plugin, use the Web3Eth.setTransactionMiddleware
method in the plugin's link
method as demonstrated below:
public link(parentContext: Web3Context): void {
(parentContext as any).Web3Eth.setTransactionMiddleware(
new TxnMiddleware()
);
super.link(parentContext);
}
Setting Up Module Augmentation
In order to provide type safety and IntelliSense for your plugin when it's registered by the user, you must augment the Web3Context
module. In simpler terms, you will be making TypeScript aware that you are modifying the interface of the class Web3Context
, and any class that extends it, to include the interface of your plugin (i.e. your plugin's added methods, properties, etc.). As a result, your plugin object will be accessible within a namespace of your choice, which will be available within any Web3Context
object.
A good tutorial that further explains Module Augmentation, in general, can be found here.
Module Augmentation
When registering a plugin, you're adding additional methods and/or classes to the module's interface and TypeScript needs a little help understanding what's going to be available within the module after the plugin is registered.
// custom_rpc_methods_plugin.ts
import { Web3PluginBase } from 'web3';
export class CustomRpcMethodsPlugin extends Web3PluginBase {
public pluginNamespace = 'customRpcMethods';
public someMethod() {
return 'someValue';
}
}
// Module Augmentation
declare module 'web3' {
// Here is where you're adding your plugin's
// class inside Web3Context class
interface Web3Context {
customRpcMethods: CustomRpcMethodsPlugin;
}
}
Important points to consider
- By augmenting
Web3Context
(and, by extension, all the classes that extend it), your plugin's interface will show up in things like IntelliSense for all Web3 modules that extendWeb3Context
, even if your plugin isn't registered. This is something worth making your users aware of, as they'll only be able to use your plugin if they register it with a Web3 class instance using.registerPlugin
.
The following represent what your plugin users would see, when they use the plugin CustomRpcMethodsPlugin
, without calling .registerPlugin
:
The above screenshot shows IntelliSense thinking .customRpcMethods.someMethod
is available to call on the instance of Web3
, regardless if the plugin user registered or did not register CustomRpcMethodsPlugin
.
But, the user who does not call .registerPlugin
, before accessing your plugin, would face an error. And you need to make it clear for them that they need to call .registerPlugin
, before they can access any plugin functionality.
-
The
registerPlugin
method exists on theWeb3Context
class, so any class thatextends Web3Context
has the ability to add your plugin's additional functionality to its interface. So, by augmentingWeb3Context
to include your plugin's interface, you're essentially providing a blanket augmentation that adds your plugin's interface to all Web3 modules that extendWeb3Context
(i.e.web3
,web3-eth
,web3-eth-contract
, etc.). -
The value of the
pluginNamespace
, that we usedcustomRpcMethods
for it in our sample code, MUST have the exact same name at 2 places: The first place is in the augmentation. And the second is the value of the publicpluginNamespace
inside your plugin class.So, for example, kindly notice using
customRpcMethods
in the next 2 snippets:Module Augmentation:
// code written by the plugin **developer**
declare module 'web3' {
// Here is where you're adding your plugin inside Web3Context
interface Web3Context {
customRpcMethods: CustomRpcMethodsPlugin;
}
}
Your the plugin class:
// code written by the plugin **developer**
export class CustomRpcMethodsPlugin extends Web3PluginBase {
public pluginNamespace = 'customRpcMethods';
//...
}
This is because .registerPlugin
will use the pluginNamespace
property provided by the plugin as the property name when it registers the plugin with the class instance that the plugin user will call .registerPlugin
on:
// code written by the plugin **user**
const web3 = new Web3('http://127.0.0.1:8545');
web3.registerPlugin(new CustomRpcMethodsPlugin());
// Now customRpcMethods (i.e. the pluginNamespace) is available
// on the instance of Web3
web3.customRpcMethods;
Complete Example
You may find it helpful to reference a complete example for developing and using a web3 plugin. The Web3.js Chainlink Plugin repository provides an excellent example which you can check out.