diff --git a/docs/docs/guides/web3_plugin_guide/assets/custom_rpc_method.png b/docs/docs/guides/web3_plugin_guide/assets/custom_rpc_method.png new file mode 100644 index 00000000000..961e865db1b Binary files /dev/null and b/docs/docs/guides/web3_plugin_guide/assets/custom_rpc_method.png differ diff --git a/docs/docs/guides/web3_plugin_guide/assets/custom_rpc_method_with_parameters.png b/docs/docs/guides/web3_plugin_guide/assets/custom_rpc_method_with_parameters.png new file mode 100644 index 00000000000..cee4153b0de Binary files /dev/null and b/docs/docs/guides/web3_plugin_guide/assets/custom_rpc_method_with_parameters.png differ diff --git a/docs/docs/guides/web3_plugin_guide/index.md b/docs/docs/guides/web3_plugin_guide/index.md new file mode 100644 index 00000000000..1e9d28759ef --- /dev/null +++ b/docs/docs/guides/web3_plugin_guide/index.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 1 +sidebar_label: 'Web3 Plugins' +--- + +# Web3.js Plugins Guide + +In addition to the Web3.js standard libraries, plugins add specific functionality to the end user. This extra functionality could be wrappers around specific contracts, additional RPC method wrappers, or could even extend the logic of Web3.js methods. + +## Before Getting Started + +### Module Augmentation + +In order to provide typing support for the registered plugin, the plugin user must [augment the module](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) they're registering the plugin with. In simpler terms, we must make TypeScript aware that we are modifying a module's (i.e. a package such as `web3` or `web3-eth`) interface with additional methods, properties, and/or classes. A good tutorial that further explains the topic can be found [here](https://www.digitalocean.com/community/tutorials/typescript-module-augmentation). + +The `registerPlugin` method exists on the `Web3Context` class, so any class that `extends` `Web3Context` has the ability to add on the plugin's additional functionality to it's interface. Because of this, the burden of module augmentation falls on the plugin user as Web3.js and the plugin author are unaware of the module the end user is calling `registerPlugin` on. + +#### Web3.js Example + +The following is an example plugin that adds additional RPC method wrappers: + +```typescript +// custom_rpc_methods_plugin.ts +import { Web3PluginBase } from 'web3-core'; + +type CustomRpcApi = { + custom_rpc_method: () => string; + custom_rpc_method_with_parameters: (parameter1: string, parameter2: number) => string; +}; + +export class CustomRpcMethodsPlugin extends Web3PluginBase { + public pluginNamespace = 'customRpcMethods'; + + public async customRpcMethod() { + return this.requestManager.send({ + method: 'custom_rpc_method', + params: [], + }); + } + + public async customRpcMethodWithParameters(parameter1: string, parameter2: number) { + return this.requestManager.send({ + method: 'custom_rpc_method_with_parameters', + params: [parameter1, parameter2], + }); + } +} +``` + +In the below example, the end user is registering the above `CustomRpcMethodsPlugin` with an instance of `Web3Context`: + +```typescript +// registering_a_plugin.ts +import { Web3Context } from 'web3-core'; + +import { CustomRpcMethodsPlugin } from './custom_rpc_methods_plugin'; + +declare module 'web3-core' { + interface Web3Context { + customRpcMethods: CustomRpcMethodsPlugin; + } +} + +const web3Context = new Web3Context('http://127.0.0.1:8545'); +web3Context.registerPlugin(new CustomRpcMethodsPlugin()); +``` + +From the above code, the following is the module augmentation that's required to be declared by the plugin user: + +```typescript +declare module 'web3-core' { + interface Web3Context { + customRpcMethods: CustomRpcMethodsPlugin; + } +} +``` + +Now after augmenting the `Web3Context` interface from the `web3-core` module, we can have the below type safe code: + +##### `web3Context.customRpcMethods.customRpcMethod` + +![custom rpc method](./assets/custom_rpc_method.png 'web3Context.customRpcMethods.customRpcMethod') + +##### `web3Context.customRpcMethods.customRpcMethodWithParameters` + +![custom rpc method with parameters](./assets/custom_rpc_method_with_parameters.png 'web3Context.customRpcMethods.customRpcMethodWithParameters') diff --git a/docs/docs/guides/web3_plugin_guide/plugin_authors.md b/docs/docs/guides/web3_plugin_guide/plugin_authors.md new file mode 100644 index 00000000000..45e51f753c8 --- /dev/null +++ b/docs/docs/guides/web3_plugin_guide/plugin_authors.md @@ -0,0 +1,212 @@ +--- +sidebar_position: 0 +sidebar_label: 'Plugin Authors' +--- + +# Web3.js Plugin Author's Guide + +This guide intends to provide the necessary context for developing plugins for Web3.js. + +## Before Getting Started + +It's highly recommended you as the plugin author understand the limitations of TypeScript's module augmentation as described in the [main plugin guide](/docs/guides/web3_plugin_guide/), so you can communicate to your users that they are responsible for augmenting the class interface they register your plugin with if they desire to have type support when using your plugin. Ideally this could be solved for by the plugin author, or better yet Web3.js, but so far a better solution is unknown - if you have any ideas, please [create an issue](https://github.com/web3/web3.js/issues/new/choose) and help us improve Web3.js' UX. + +## Plugin Dependencies + +At the minimum, your plugin should depend on the `4.x` version of `web3-core`. This will allow your plugin class to extend the provided `Web3PluginBase` abstract class. However, `web3-core` shouldn't be listed as a regular dependency, instead it should be listed in your plugin's `package.json` as a [peer dependency](https://nodejs.org/en/blog/npm/peer-dependencies/): + +```json +{ + "name": "web3-plugin-custom-rpc-methods", + "version": "0.0.1", + "peerDependencies": { + "web3-core": ">= 4.0.1-alpha.0 < 5" + } +} +``` + +When your users install your plugin, this will allow the package manager to make use of the user installed `web3-core` if available and if the version satisfies the version constraints instead of installing it's own version of `web3-core`. + +## Extending `Web3PluginBase` + +Your plugin class should `extend` the `Web3PluginBase` abstract class. This class `extends` [Web3Context](/api/web3-core/class/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](/api/web3-core/class/Web3Context#requestManager) and [accountProvider](/api/web3-core/class/Web3Context#accountProvider). + +```typescript +import { Web3PluginBase } from 'web3-core'; + +export class CustomRpcMethodsPlugin extends Web3PluginBase { ... } +``` + +### `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 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: + +```typescript +// custom_rpc_methods_plugin.ts +import { Web3PluginBase } from 'web3-core'; + +export class CustomRpcMethodsPlugin extends Web3PluginBase { + public pluginNamespace = 'customRpcMethods'; + + public someMethod() { + return 'someValue'; + } +} +``` + +The following represents the plugin user's code: + +```typescript +// registering_a_plugin.ts +import { Web3Context } from './web3_export_helper'; +import { CustomRpcMethodsPlugin } from './custom_rpc_methods_plugin'; + +declare module 'web3-core' { + interface Web3Context { + customRpcMethods: CustomRpcMethodsPlugin; + } +} + +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](/api/web3-errors/class/ProviderError) if `customRpcMethod` was to be called: + +```typescript +import { Web3PluginBase } from 'web3-core'; + +export class CustomRpcMethodsPlugin extends Web3PluginBase { + public pluginNamespace = 'customRpcMethods'; + + public async customRpcMethod() { + return this.requestManager.send({ + method: 'custom_rpc_method', + params: [], + }); + } +} +``` + +Below is representing a plugin user's code that does not configure an Ethereum provider, resulting in a thrown [ProviderError](/api/web3-errors/class/ProviderError): + +```typescript +// registering_a_plugin.ts +import { Web3Context } from './web3_export_helper'; +import { CustomRpcMethodsPlugin } from './custom_rpc_methods_plugin'; + +declare module 'web3-core' { + interface Web3Context { + customRpcMethods: CustomRpcMethodsPlugin; + } +} + +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](/api/web3-errors/class/ProviderError): + +```bash +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](/api/web3-types#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` + +```typescript +import { Web3PluginBase } from 'web3-core'; + +type CustomRpcApi = { + custom_rpc_method_with_parameters: (parameter1: string, parameter2: number) => string; +}; + +export class CustomRpcMethodsPlugin extends Web3PluginBase { + 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](https://github.com/web3/web3.js/issues/5492) 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](/api/web3-errors/class/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: + +```typescript +import { Web3Context, Web3PluginBase } from 'web3-core'; +import { ContractAbi } from 'web3-eth-abi'; +import Contract from 'web3-eth-contract'; +import { Address } from 'web3-types'; +import { DataFormat, DEFAULT_RETURN_FORMAT, format } from 'web3-utils'; + +import { ERC20TokenAbi } from './ERC20Token'; + +export class ContractMethodWrappersPlugin extends Web3PluginBase { + public pluginNamespace = 'contractMethodWrappersPlugin'; + + private readonly _contract: Contract; + + public constructor(abi: ContractAbi, address: Address) { + super(); + this._contract = new Contract(abi, address); + } + + /** + * This method overrides the inherited `link` method from `Web3PluginBase` + * to add to a configured `RequestManager` to our 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( + address: Address, + returnFormat?: ReturnFormat, + ) { + return format( + { eth: 'unit' }, + await this._contract.methods.balanceOf(address).call(), + returnFormat ?? 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`: + +```typescript +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); +} +``` diff --git a/docs/docs/guides/web3_plugin_guide/plugin_users.md b/docs/docs/guides/web3_plugin_guide/plugin_users.md new file mode 100644 index 00000000000..2f139534866 --- /dev/null +++ b/docs/docs/guides/web3_plugin_guide/plugin_users.md @@ -0,0 +1,177 @@ +--- +sidebar_position: 1 +sidebar_label: 'Plugin Users' +--- + +# Web3.js Plugin User's Guide + +This guide intends to provide the necessary context for registering plugins with Web3.js packages. + +## Before Getting Started + +It's highly recommended you as a plugin user understand the limitations of TypeScript's module augmentation as described in the [main plugin guide](/docs/guides/web3_plugin_guide/), so you can get the most out of TypeScript's type safety while using Web3.js plugins. + +## Installing the Plugin + +Unless otherwise mentioned by the plugin author, installing a plugin should be as simple as `yarn add plugin-name`. This should add the plugin as a dependency within your `package.json` and the plugin should be available to import within your code. + +```json +{ + "name": "your-package-name", + "version": "0.0.1", + "dependencies": { + "web3-plugin": "0.0.1" + } +} +``` + +## Registering the Plugin + +The `.registerPlugin` method is what we're going to be using to add a plugin to an instance of a class sourced from Web3.js' modules (i.e. `Web3` or `Web3Eth`). This method only exists on classes that extend `Web3Context`, so it may not be available on every class you import from a Web3.js package. + +Below are a couple examples of registering the following `SimplePlugin` with various classes imported from various Web3.js packages: + +This is an example plugin being used for demonstration purposes: + +```typescript +import { Web3PluginBase } from 'web3-core'; + +export class SimplePlugin extends Web3PluginBase { + public pluginNamespace = 'simplePlugin'; + + public simpleMethod() { + return 'simpleValue'; + } +} +``` + +:::caution +The following code does not include the [module augmentation](/docs/guides/web3_plugin_guide/#module-augmentation) necessary to provide type safety and hinting when using a registered plugin, please refer to the [Setting Up Module Augmentation](/docs/guides/web3_plugin_guide/plugin_users#setting-up-module-augmentation) section for how to augment the `Web3` module to enable typing features for a plugin. +::: + +```typescript +import Web3 from 'web3'; +import SimplePlugin from 'web3-plugin'; + +const web3 = new Web3('http://127.0.0.1:8545'); +web3.registerPlugin(new SimplePlugin()); + +// @ts-expect-error Property 'simplePlugin' does not exist on type 'Web3' +web3.simplePlugin.simpleMethod(); +``` + +#### Why is `@ts-expect-error` Required + +`// @ts-expect-error Property 'simplePlugin' does not exist on type 'Web3'` is required in order for TypeScript to compile this code; This is due to the lack of module augmentation. `.simplePlugin` is not a part of the standard interface of the `Web3` class that TypeScript is made aware of when we import `Web3`. Module augmentation is us telling TypeScript, + +_"Hey, we're modifying the `Web3` class interface to include the interface of `SimplePlugin`"_ + +and TypeScript will be able to infer the interface of `SimplePlugin` so that we no longer receive an error when calling `web3.simplePlugin.simpleMethod();` + +## Setting Up Module Augmentation + +This section of the guide will delve deeper into setting up module augmentation, if you run into any issues, please don't hesitate to [create an issue](https://github.com/web3/web3.js/issues/new/choose) or drop a message in the `web3js-general` channel in the ChainSafe [Discord](https://discord.gg/yjyvFRP), and someone from the team/community will assist you. + +### Creating an Export Helper File + +There exists a [limitation](https://github.com/web3/web3.js/pull/5393/#discussion_r1000727269) with TypeScript's module augmentation: it can only handle _named modules_. So that Web3.js stays backwards compatible, our most commonly used modules (e.g. `Web3`, `Web3Eth`, `Contract`) are exported as `default` exports and are not explicitly named as required by TypeScript for module augmentation. The workaround for this issue is to create a separate file within your project where you import the default module you wish to augment and re-export it as a named module: + +Re-exporting `Web3`, `Web3Context`, and `Web3Eth` as named modules: + +```typescript +import Web3 from 'web3'; +export { Web3 }; +``` + +```typescript +import Web3Context from 'web3-core'; +export { Web3Context }; +``` + +```typescript +import Web3Eth from 'web3-eth'; +export { Web3Eth }; +``` + +The file that performs this re-exporting can be named whatever, but for the sake of this guide, we'll be assuming the file is named `web3_export_helper.ts`. + +### Re-declaring the Module + +The first step is telling TypeScript that we're interested in re-defining a module's (i.e. a Web3.js package such as `web3-core`, `web3`, or `web3-eth`) interface. In simpler terms, TypeScript is already aware of what methods and classes exist for each Web3.js module, but when registering a plugin, we'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. + +We start with the following: + +```typescript +import { Web3 } from './web3_export_helper'; + +declare module 'web3' {...} +``` + +In the above example, we're interested in registering a plugin to an instance of `Web3Context` from the `web3-core` module. So we tell TypeScript that we're going to manually declare the module interface for `web3-core`. + +### Adding our Plugin's Interface + +Now that TypeScript's aware that the interface of the `web3-core` module is going to be augmented, we add our changes. In this case, we're adding the interface of `SimplePlugin` to the interface of `Web3Context` which is what we're going to be calling `.registerPlugin` on: + +```typescript +import SimplePlugin from 'web3-plugin'; + +import { Web3 } from './web3_export_helper'; + +declare module 'web3' { + interface Web3 { + simplePlugin: SimplePlugin; + } +} +``` + +:::info +The property name (i.e. `pluginNamespace`), `simplePlugin` in + +```typescript +{ + simplePlugin: SimplePlugin; +} +``` + +**MUST** be the same as the `pluginNamespace` set by the plugin. + +```typescript +import { Web3PluginBase } from 'web3-core'; + +export class SimplePlugin extends Web3PluginBase { + public pluginNamespace = 'simplePlugin'; + + ... +} +``` + +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 you call `.registerPlugin` on: + +```typescript +const web3 = new Web3('http://127.0.0.1:8545'); +web3.registerPlugin(new SimplePlugin()); +// Now simplePlugin (i.e. the pluginNamespace) isavailable +// on our instance of Web3 +web3.simplePlugin; +``` + +::: + +And that's all that's required to augment a named module to add type support for a plugin. Now you should be able to remove the `// @ts-expect-error` from the above code example: + +```typescript +import Web3 from 'web3'; +import SimplePlugin from 'web3-plugin'; + +declare module 'web3' { + interface Web3 { + simplePlugin: SimplePlugin; + } +} + +const web3 = new Web3('http://127.0.0.1:8545'); +web3.registerPlugin(new SimplePlugin()); + +web3.simplePlugin.simpleMethod(); +``` diff --git a/docs/docs/guides/web3_tree_shaking_support_guide/index.md b/docs/docs/guides/web3_tree_shaking_support_guide/index.md index a170e551f77..9dfe83d62cd 100644 --- a/docs/docs/guides/web3_tree_shaking_support_guide/index.md +++ b/docs/docs/guides/web3_tree_shaking_support_guide/index.md @@ -1,5 +1,5 @@ --- -sidebar_position: 1 +sidebar_position: 2 sidebar_label: Web3.tree.shaking ---