web3.js icon indicating copy to clipboard operation
web3.js copied to clipboard

Research Plug-in base architecture

Open mconnelly8 opened this issue 2 years ago • 7 comments

mconnelly8 avatar May 31 '22 15:05 mconnelly8

For additional features in Web3 4.x, there can be several ways of instantiating new features on demand as additional plugin in web3.js.

Consider a scenario, we want to have chainlink plugin with chainlink getPrice() functionality support.

  • Offering plugin instantiation via additional call web3.eth.registerPlugin(ChainlinkPlugin) Internally, it should do required instantiation. In this way necessary plugin will be registered in Eth package and that plugin will have access to web3 context , request manager, config params ,..etc for performing its functionality.

  • and then making available plugin functionality to lib users like in above mentioned case following call web3.eth.chainlink.getPrice(ChainlinkFeeds.ETH-USD) will be available.

jdevcs avatar Jul 27 '22 11:07 jdevcs

Following can be rough idea as starting point of discussion:

Part of our mono repo:


export class Web3Eth extends Web3Context<Web3EthExecutionAPI, RegisteredSubscription> {


    public registerPlugIn( pluginTitle : string, plugin : Web3PluginBase){
        
        // add plugin in Eth class as property
        Object.assign(this, {pluginTitle: self.use(plugin) } );  // via web3Context use()

        ..... other instantiating etc
    }

.... Eth functionality

}


export abstract Web3PluginBase extends Web3Context {
    // additional specifications for plugins to implement
}

In independent solution:

class ChainlinkPlugin extends Web3PlugInBase {
    // will have access to request manager, context, and config params
 ......
}


jdevcs avatar Jul 27 '22 12:07 jdevcs

Related discussion

spacesailor24 avatar Aug 07 '22 20:08 spacesailor24

Some ideas which I think you should consider:

  • compatibility with web3 versions - I think plugin should declare version range of Web3.js so we can error on unsupported versions
  • plugins using other plugins - ability for plugin to declare dependency on other plugins

Example:

interface Web3Plugin {
   plugin: Plugin, //for example Layer2Service
   web3Version: string, // ">=4 <5" or "4.x"
   needs?: string[] // ["[email protected]"]
}

mpetrunic avatar Aug 08 '22 08:08 mpetrunic

After talking with @nazarhussain there are decisions to be made:

  • What exactly are the requirements for the plugin system?
    • The .use and .link methods that exist on any class extending Web3Context already allow them to make use of an already instantiated provider
  • How do we make appending a plugin to the Web3 object typesafe?
    • An interesting thing to keep in mind is we are building a "plug-in" solution, so the burden of typesafety should be on the dev and we shouldn't expect the user to have to do anything outside of some basic config
    • Nazar mentioned that each plugin can re-declare the Web3 typing so that TypeScript is now aware the web3.chainlink.getPrice exists, but I don't know how to do that yet

The below code has a couple of TSC errors I haven't figured out yet

/*
This file is part of web3.js.

web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

web3.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with web3.js.  If not, see <http://www.gnu.org/licenses/>.
*/

import { Web3Context } from 'web3-core';
import { ContractAbi } from 'web3-eth-abi';
import Contract from 'web3-eth-contract';
import { Address, EthExecutionAPI, SupportedProviders, Web3APISpec } from 'web3-types';
import Web3 from '../../src/index';

const aggregatorV3InterfaceABI = [{ "inputs": [], "name": "decimals", "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "description", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint80", "name": "_roundId", "type": "uint80" }], "name": "getRoundData", "outputs": [{ "internalType": "uint80", "name": "roundId", "type": "uint80" }, { "internalType": "int256", "name": "answer", "type": "int256" }, { "internalType": "uint256", "name": "startedAt", "type": "uint256" }, { "internalType": "uint256", "name": "updatedAt", "type": "uint256" }, { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "latestRoundData", "outputs": [{ "internalType": "uint80", "name": "roundId", "type": "uint80" }, { "internalType": "int256", "name": "answer", "type": "int256" }, { "internalType": "uint256", "name": "startedAt", "type": "uint256" }, { "internalType": "uint256", "name": "updatedAt", "type": "uint256" }, { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "version", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }];
const aggregatorAddress = '0xECe365B379E1dD183B20fc5f022230C044d51404';

abstract class Web3PluginBase<API extends Web3APISpec> extends Web3Context<API> {
	static pluginNamespace: string;
}

type ChainlinkPluginAPI = {};
class ChainlinkPlugin extends Web3PluginBase<ChainlinkPluginAPI> {
	static pluginNamespace = 'chainlink';

	readonly _contract: Contract<ContractAbi>;

	constructor(abi: ContractAbi, address: Address) {
		super();
		this._contract = new Contract(abi, address);
	}

	public getPrice() {
		if (this._contract.currentProvider === undefined) this._contract.link(this);
		return this._contract.methods.latestRoundData().call();
	}
}

class Web3Mod extends Web3 {
	constructor(provider?: SupportedProviders<EthExecutionAPI> | string) {
		super(provider);
	}

	public registerPlugin<API extends Web3APISpec>(
		plugin: Web3PluginBase<API>,
		constructorArgs: unknown[],
	) {
		const _pluginObject: Record<string, Web3PluginBase<API>> = {};
		// @ts-expect-error
		// TODO Property 'pluginNamespace' does not exist on type 'Web3PluginBase<API>'
		// TODO Argument of type 'Web3PluginBase<API>' is not assignable to parameter of type 'Web3ContextConstructor<Web3PluginBase<API>, unknown[]>'
		_pluginObject[plugin.pluginNamespace] = this.use(plugin, ...constructorArgs);
		// @ts-expect-error
		// TODO Property 'pluginNamespace' does not exist on type 'Web3PluginBase<API>'
		// TODO Argument of type 'this' is not assignable to parameter of type 'Web3Context<API, any>'
		_pluginObject[plugin.pluginNamespace].link(this);
		Object.assign(this, _pluginObject);
	}
}

(async () => {
	const web3 = new Web3Mod('https://rpc.ankr.com/eth_rinkeby');
	// @ts-expect-error
	// TODO Argument of type 'typeof ChainlinkPlugin' is not assignable to parameter of type 'Web3PluginBase<Web3APISpec>'
	web3.registerPlugin(ChainlinkPlugin, [aggregatorV3InterfaceABI, aggregatorAddress]);
	// @ts-expect-error
	// TODO Property 'chainlink' does not exist on type 'Web3Mod'
	console.log(await web3.chainlink.getPrice());
})();

Another option mentioned was to keep the plugin object/instance separate from the Web3 instance e.g.:

/*
This file is part of web3.js.

web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

web3.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with web3.js.  If not, see <http://www.gnu.org/licenses/>.
*/

import { Web3Context } from 'web3-core';
import { ContractAbi } from 'web3-eth-abi';
import Contract from 'web3-eth-contract';
import { Address, Web3APISpec } from 'web3-types';
import Web3 from '../../src/index';

const aggregatorV3InterfaceABI = [{ "inputs": [], "name": "decimals", "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "description", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [{ "internalType": "uint80", "name": "_roundId", "type": "uint80" }], "name": "getRoundData", "outputs": [{ "internalType": "uint80", "name": "roundId", "type": "uint80" }, { "internalType": "int256", "name": "answer", "type": "int256" }, { "internalType": "uint256", "name": "startedAt", "type": "uint256" }, { "internalType": "uint256", "name": "updatedAt", "type": "uint256" }, { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "latestRoundData", "outputs": [{ "internalType": "uint80", "name": "roundId", "type": "uint80" }, { "internalType": "int256", "name": "answer", "type": "int256" }, { "internalType": "uint256", "name": "startedAt", "type": "uint256" }, { "internalType": "uint256", "name": "updatedAt", "type": "uint256" }, { "internalType": "uint80", "name": "answeredInRound", "type": "uint80" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "version", "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], "stateMutability": "view", "type": "function" }];
const aggregatorAddress = '0xECe365B379E1dD183B20fc5f022230C044d51404';

class ChainlinkPlugin extends Web3Context<Web3APISpec> {
	readonly _contract: Contract<ContractAbi>;

	constructor(abi: ContractAbi, address: Address) {
		super();
		// TODO We should instantiate with this._contract.currentProvider
		// but it's undefined since the .link is called after instantiation
		this._contract = new Contract(abi, address);
	}

	public getPrice() {
		// TODO Refactor having to check currentProvider
		if (this._contract.currentProvider === undefined) this._contract.link(this);
		return this._contract.methods.latestRoundData().call();
	}
}

(async () => {
	const web3 = new Web3('https://rpc.ankr.com/eth_rinkeby');
	const chainlinkPlugin = new ChainlinkPlugin(aggregatorV3InterfaceABI, aggregatorAddress);
	chainlinkPlugin.link(web3);

	console.log(await chainlinkPlugin.getPrice());
})();

spacesailor24 avatar Aug 09 '22 00:08 spacesailor24

Nazar mentioned that each plugin can re-declare the Web3 typing so that TypeScript is now aware the web3.chainlink.getPrice exists, but I don't know how to do that yet

This is how fastify handles it, as soon as plugin is imported it will enhance types: https://github.com/NodeFactoryIo/fastify-sse-v2/blob/master/src/index.ts#L12

mpetrunic avatar Aug 09 '22 08:08 mpetrunic

@mpetrunic and everyone else following this thread, #5393 is an example implementation of the first idea mentioned by me above

spacesailor24 avatar Aug 30 '22 02:08 spacesailor24

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions. If you believe this was a mistake, please comment.

github-actions[bot] avatar Oct 30 '22 00:10 github-actions[bot]