diff --git a/package.json b/package.json index bf949db..ecd53cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lumerin/wallet-core", - "version": "1.0.73", + "version": "1.0.74", "author": { "name": "Lumerin", "email": "developer@lumerin.io", diff --git a/src/plugins/eth/web3.js b/src/plugins/eth/web3.js index a34cddc..8ce768c 100644 --- a/src/plugins/eth/web3.js +++ b/src/plugins/eth/web3.js @@ -3,31 +3,13 @@ const logger = require('../../logger'); const Web3 = require('web3') -const https = require('https') +const { Web3Http } = require('./web3Http'); -let providers = []; function createWeb3(config) { // debug.enabled = config.debug - providers = config.httpApiUrls.map((url) => { - return new Web3.providers.HttpProvider(url, { - agent: new https.Agent({ - rejectUnauthorized: false, // Set to false if your HTTPS node endpoint uses a self-signed certificate - }), - }) - }) - - const web3 = new Web3(providers[0], { - agent: new https.Agent({ - rejectUnauthorized: false, // Set to false if your HTTPS node endpoint uses a self-signed certificate - }), - }) - - overrideFunctions(web3, providers) - overrideFunctions(web3.eth, providers) - web3.subscriptionProvider = subscriptionProvider - + const web3 = new Web3Http(config.httpApiUrls) return web3 } @@ -66,85 +48,9 @@ function createWeb3Subscribable(config, eventBus) { } function destroyWeb3(web3) { - web3.currentProvider.disconnect() -} - -const urls = [ - process.env.HTTP_ETH_NODE_ADDRESS, - process.env.HTTP_ETH_NODE_ADDRESS2, - process.env.HTTP_ETH_NODE_ADDRESS3, -] - -let lastUsedProviderIndex = -1 - -const overrideFunctions = function (object, providers) { - const originalSetProvider = object.setProvider - - const originalFunctions = Object.assign({}, object) - Object.keys(originalFunctions).forEach((key) => { - if ( - typeof originalFunctions[key] === 'function' && - !key.startsWith('set') - ) { - object[key] = function () { - const originalFunction = originalFunctions[key] - const isAsync = originalFunction[Symbol.toStringTag] === 'AsyncFunction' - const args = arguments - let providerIndex = lastUsedProviderIndex - let result - do { - providerIndex = (providerIndex + 1) % providers.length - const provider = providers[providerIndex] - originalSetProvider(provider) - if (isAsync) { - result = originalFunction - .apply(this, args) - .then((res) => { - if (res !== undefined) { - return res - } - throw new Error('Result is undefined') - }) - .catch((error) => { - console.error(`Error with provider ${provider.host}:`, error) - throw error - }) - } else { - try { - if (new.target) { - function F(args) { - return originalFunction.apply(this, args) - } - - F.prototype = originalFunction.prototype - - return new F(args) - } else { - result = originalFunctions[key].apply(this, args) - } - - if (typeof result !== "undefined") { - break - } - } catch (error) { - console.error(`Error with provider ${provider.host}:`, error) - } - } - } while (providerIndex !== lastUsedProviderIndex) - lastUsedProviderIndex = providerIndex - if (typeof result === "undefined") { - throw new Error('All providers failed to execute the function') - } - return result - } - } - }) - - object.setProvider = originalSetProvider + web3.currentProvider?.disconnect() } -let subscriptionProvider - module.exports = { createWeb3, destroyWeb3, diff --git a/src/plugins/eth/web3Http.js b/src/plugins/eth/web3Http.js new file mode 100644 index 0000000..a4117c7 --- /dev/null +++ b/src/plugins/eth/web3Http.js @@ -0,0 +1,68 @@ +const Web3 = require('web3') +const https = require('https') +const logger = require('../../logger') + +class Web3Http extends Web3 { + constructor(providers, options) { + super() + + this.providers = providers.map( + (provider) => + new Web3.providers.HttpProvider(provider, { + agent: new https.Agent({ + rejectUnauthorized: false, // Set to false if your HTTPS node endpoint uses a self-signed certificate + }), + }) + ) + this.currentIndex = 0 + this.retryCount = 0 + + // Initialize Web3 with the first provider from the list + this.setCustomProvider(this.providers[this.currentIndex]) + + // Set options if provided + if (options) { + this.setProviderOptions(options) + } + } + + setCustomProvider(provider) { + // Override the setProvider method to handle switching providers on failure + this.setProvider(provider) + + // Hook into provider's request and response handling + const originalSend = this.currentProvider.send.bind(this.currentProvider) + this.currentProvider.send = (payload, callback) => { + originalSend(payload, (error, response) => { + if (error) { + // Avoid infinite loop + if (this.retryCount >= this.providers.length) { + callback(error, null) + this.retryCount = 0 + return; + } + // If the request fails, switch to the next provider and try again + this.currentIndex = (this.currentIndex + 1) % this.providers.length + this.setCustomProvider(this.providers[this.currentIndex]) + logger.error( + `Switched to provider: ${this.providers[this.currentIndex].host}` + ) + this.retryCount += 1 + this.currentProvider.send(payload, callback) // Retry the request + } else { + this.retryCount = 0 + callback(null, response) + } + }) + } + return true + } + + setProviderOptions(options) { + this.currentProvider.host = options.host || this.currentProvider.host + this.currentProvider.timeout = + options.timeout || this.currentProvider.timeout + } +} + +module.exports = { Web3Http } diff --git a/src/plugins/eth/web3Http.spec.js b/src/plugins/eth/web3Http.spec.js new file mode 100644 index 0000000..c5f4ee8 --- /dev/null +++ b/src/plugins/eth/web3Http.spec.js @@ -0,0 +1,139 @@ +//@ts-check + +const { expect } = require('chai') +const { Web3Http } = require('./web3Http') +const { Lumerin, CloneFactory } = require('contracts-js') + +const invalidNode = 'https://arbitrum.llamarpc.com_INVALID' +const validNode = 'https://arbitrum.blockpi.network/v1/rpc/public' +const providerList = [ + invalidNode, + validNode, + 'https://rpc.ankr.com/arbitrum', + 'https://arbitrum.api.onfinality.io/public', + 'https://arb1-mainnet-public.unifra.io', + 'https://arbitrum-one.public.blastapi.io', + 'https://endpoints.omniatech.io/v1/arbitrum/one/public', + 'https://1rpc.io/arb', +] + +describe('Web3 multiple nodes integration tests', () => { + const lumerinAddress = '0x0FC0c323Cf76E188654D63D62e668caBeC7a525b' + const cloneFactoryAddress = '0x05C9F9E9041EeBCD060df2aee107C66516E2b9bA' + + it('should work with simple blockchain query', async () => { + const web3 = new Web3Http(providerList) + + const result = await web3.eth.getBlockNumber() + expect(typeof result).eq('number') + expect(web3.currentIndex).eq(1) + }) + + it('should iterate all nodes', async () => { + const web3 = new Web3Http([ + invalidNode, + invalidNode, + invalidNode, + invalidNode, + validNode, + ]) + + const result = await web3.eth.getBlockNumber() + expect(typeof result).eq('number') + expect(web3.currentIndex).eq(4) + }) + + it('should work with Contract.call()', async () => { + const web3 = new Web3Http(providerList) + const lumerin = Lumerin(web3, lumerinAddress) + + const result = await lumerin.methods + .balanceOf('0x0000000000000000000000000000000000000000') + .call() + expect(typeof result).eq('string') + expect(web3.currentIndex).eq(1) + }) + + it('should work with Contract.send()', async () => { + const web3 = new Web3Http(providerList) + const cf = CloneFactory(web3, cloneFactoryAddress) + + try { + await cf.methods + .setCreateNewRentalContract( + '0', + '0', + '0', + '0', + '0x0000000000000000000000000000000000000000', + '0' + ) + .send({ + from: '0x0000000000000000000000000000000000000000', + }) + expect(1).eq(0) + } catch (err) { + expect(err.message.includes('unknown account')).eq(true) + } + }) + + it('should work with Contract.estimateGas()', async () => { + const web3 = new Web3Http(providerList) + const cf = CloneFactory(web3, cloneFactoryAddress) + + try { + await cf.methods + .setCreateNewRentalContract( + '0', + '0', + '0', + '0', + '0x0000000000000000000000000000000000000000', + '0' + ) + .estimateGas({ + from: '0x0000000000000000000000000000000000000000', + }) + expect(1).eq(0) + } catch (err) { + expect(err.message.includes('execution reverted')).eq(true) + expect(web3.currentIndex).eq(1) + } + }) + + it('should not iterate if request if invalid/reverted', async () => { + const web3 = new Web3Http([validNode, validNode, validNode]) + const cf = CloneFactory(web3, cloneFactoryAddress) + + try { + await cf.methods + .setCreateNewRentalContract( + '0', + '0', + '0', + '0', + '0x0000000000000000000000000000000000000000', + '0' + ) + .send({ + from: '0x0000000000000000000000000000000000000000', + }) + expect(1).eq(0) + } catch (err) { + expect(err.message.includes('unknown account')).eq(true) + expect(web3.currentIndex).eq(0) + } + }) + + it('should not loop forever', async () => { + const web3 = new Web3Http([invalidNode, invalidNode, invalidNode]) + + try { + await web3.eth.getBlockNumber() + expect(1).eq(0) + } catch (err) { + expect(web3.retryCount).eq(0) + expect(web3.currentIndex).eq(0) + } + }) +})