-
Notifications
You must be signed in to change notification settings - Fork 3
new retry implementation web3 #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| } | ||
|
alex-sandrk marked this conversation as resolved.
Comment on lines
9
to
14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Comment on lines
9
to
14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
|
|
@@ -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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| } | ||
|
|
||
| let subscriptionProvider | ||
|
|
||
| module.exports = { | ||
| createWeb3, | ||
| destroyWeb3, | ||
|
alex-sandrk marked this conversation as resolved.
Comment on lines
55
to
56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The exported functions |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
alex-sandrk marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| }), | ||
| }) | ||
| ) | ||
| 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) | ||
| } | ||
| }) | ||
| } | ||
|
alex-sandrk marked this conversation as resolved.
Comment on lines
+35
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The retry mechanism implemented here could lead to a potential infinite loop if the error persists across all providers. Although there is a check to avoid an infinite loop if the retry count exceeds the number of providers, the retry count is reset to 0 before the callback is called with the error. If the callback function in turn calls the |
||
| return true | ||
| } | ||
|
|
||
| setProviderOptions(options) { | ||
| this.currentProvider.host = options.host || this.currentProvider.host | ||
| this.currentProvider.timeout = | ||
| options.timeout || this.currentProvider.timeout | ||
| } | ||
|
alex-sandrk marked this conversation as resolved.
Comment on lines
+61
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Comment on lines
+61
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| } | ||
|
|
||
| module.exports = { Web3Http } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
createWeb3function creates a new instance ofWeb3Httpusingconfig.httpApiUrls. There is no validation ofconfig.httpApiUrlsbefore it's used. If it's not provided or is not in the expected format, this could lead to errors. Consider adding validation forconfig.httpApiUrlsbefore using it.