-
Notifications
You must be signed in to change notification settings - Fork 5.5k
feat: add monad failover rpc #38428
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
Merged
+538
−1
Merged
feat: add monad failover rpc #38428
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
4c4f5ca
feat: add monad failover rpc
khanti42 8fa30b6
Merge branch 'main' into feat/add-monad-failover-rpc
khanti42 af62975
chore: lint
khanti42 10e38cf
chore: e2e update snapshot
khanti42 de1029a
Merge branch 'main' into feat/add-monad-failover-rpc
khanti42 784c258
Merge branch 'main' into feat/add-monad-failover-rpc
khanti42 3d36559
chore: update snapshot state version
khanti42 c931b4e
chore: change migration version
khanti42 9449ae6
Merge branch 'main' into feat/add-monad-failover-rpc
khanti42 050aba1
fix: migration version
khanti42 bedbc49
chore: update migration to use the new pattern
khanti42 4f2a339
fix: migration 186 tests
khanti42 9c61c20
fix: migration script
khanti42 1551eb8
fix: unit tests and snapshot
khanti42 e1d1c7c
fix: only add failover to infura monad rpc
khanti42 53b4711
fix: eslint
khanti42 ee2d32e
chore: lint
khanti42 be63189
Merge branch 'main' into feat/add-monad-failover-rpc
sgextcsi 0ab3c1e
chore: change migration number
khanti42 6139ee6
Merge branch 'main' into feat/add-monad-failover-rpc
khanti42 7e35f48
chore: change migration number
khanti42 0cc2f6d
Merge branch 'main' into feat/add-monad-failover-rpc
khanti42 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| import { RpcEndpointType } from '@metamask/network-controller'; | ||
| import { CHAIN_IDS } from '../../../shared/constants/network'; | ||
| import { migrate, version } from './185'; | ||
|
|
||
| const VERSION = version; | ||
| const oldVersion = VERSION - 1; | ||
| const QUICKNODE_MONAD_URL = 'https://example.quicknode.com/monad'; | ||
| const MONAD_CHAIN_ID = CHAIN_IDS.MONAD; | ||
|
|
||
| describe(`migration #${VERSION}`, () => { | ||
| let originalEnv: NodeJS.ProcessEnv; | ||
|
|
||
| beforeEach(() => { | ||
| originalEnv = { ...process.env }; | ||
| global.sentry = { captureException: jest.fn() }; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| process.env = originalEnv; | ||
| global.sentry = undefined; | ||
| }); | ||
|
|
||
| it('updates the version metadata', async () => { | ||
| const oldStorage = { | ||
| meta: { version: oldVersion }, | ||
| data: { | ||
| NetworkController: { | ||
| networkConfigurationsByChainId: {}, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const newStorage = await migrate(oldStorage); | ||
|
|
||
| expect(newStorage.meta).toStrictEqual({ version: VERSION }); | ||
| }); | ||
|
|
||
| it('logs a warning and returns the original state if NetworkController is missing', async () => { | ||
| const oldStorage = { | ||
| meta: { version: oldVersion }, | ||
| data: {}, | ||
| }; | ||
|
|
||
| const mockWarn = jest.spyOn(console, 'warn').mockImplementation(jest.fn()); | ||
|
|
||
| const newStorage = await migrate(oldStorage); | ||
|
|
||
| expect(mockWarn).toHaveBeenCalledWith( | ||
| `Migration ${VERSION}: NetworkController not found.`, | ||
| ); | ||
| expect(newStorage.data).toStrictEqual(oldStorage.data); | ||
| }); | ||
|
|
||
| it('does nothing if Monad network does not exist in the state', async () => { | ||
| const oldStorage = { | ||
| meta: { version: oldVersion }, | ||
| data: { | ||
| NetworkController: { | ||
| networkConfigurationsByChainId: { | ||
| '0x1': { | ||
| rpcEndpoints: [ | ||
| { | ||
| type: RpcEndpointType.Infura, | ||
| url: `https://mainnet.infura.io/v3/`, | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const newStorage = await migrate(oldStorage); | ||
|
|
||
| expect(newStorage.data).toStrictEqual(oldStorage.data); | ||
| }); | ||
|
|
||
| it('does not add failover URL if QUICKNODE_MONAD_URL env variable is not set', async () => { | ||
| const oldStorage = { | ||
| meta: { version: oldVersion }, | ||
| data: { | ||
| NetworkController: { | ||
| networkConfigurationsByChainId: { | ||
| [MONAD_CHAIN_ID]: { | ||
| rpcEndpoints: [ | ||
| { | ||
| type: RpcEndpointType.Custom, | ||
| url: `https://monad-mainnet.infura.io/v3/`, | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const newStorage = await migrate(oldStorage); | ||
|
|
||
| // When QUICKNODE_MONAD_URL is not set, no failover URL should be added | ||
| expect(newStorage.data).toStrictEqual(oldStorage.data); | ||
| }); | ||
|
|
||
| it('does not add failover URL if there is already a failover URL', async () => { | ||
| process.env.QUICKNODE_MONAD_URL = QUICKNODE_MONAD_URL; | ||
|
|
||
| const existingFailoverUrl = 'https://existing-failover.com'; | ||
|
|
||
| const oldStorage = { | ||
| meta: { version: oldVersion }, | ||
| data: { | ||
| NetworkController: { | ||
| networkConfigurationsByChainId: { | ||
| [MONAD_CHAIN_ID]: { | ||
| rpcEndpoints: [ | ||
| { | ||
| type: RpcEndpointType.Custom, | ||
| url: `https://monad-mainnet.infura.io/v3/`, | ||
| failoverUrls: [existingFailoverUrl], | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const newStorage = await migrate(oldStorage); | ||
|
|
||
| expect(newStorage.data).toStrictEqual(oldStorage.data); | ||
| }); | ||
|
|
||
| it('adds QuickNode failover URL to all Monad RPC endpoints when no failover URLs exist', async () => { | ||
| process.env.QUICKNODE_MONAD_URL = QUICKNODE_MONAD_URL; | ||
|
|
||
| const oldStorage = { | ||
| meta: { version: oldVersion }, | ||
| data: { | ||
| NetworkController: { | ||
| networkConfigurationsByChainId: { | ||
| [MONAD_CHAIN_ID]: { | ||
| rpcEndpoints: [ | ||
| { | ||
| type: RpcEndpointType.Infura, | ||
| url: `https://monad-mainnet.infura.io/v3/`, | ||
| }, | ||
| { | ||
| type: RpcEndpointType.Custom, | ||
| url: `https://some-monad-rpc.com`, | ||
| }, | ||
| ], | ||
| }, | ||
| '0x1': { | ||
| rpcEndpoints: [ | ||
| { | ||
| type: RpcEndpointType.Custom, | ||
| url: `https://ethereum-mainnet.infura.io/v3/`, | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const expectedStorage = { | ||
| meta: { version: VERSION }, | ||
| data: { | ||
| NetworkController: { | ||
| networkConfigurationsByChainId: { | ||
| [MONAD_CHAIN_ID]: { | ||
| rpcEndpoints: [ | ||
| { | ||
| type: RpcEndpointType.Infura, | ||
| url: `https://monad-mainnet.infura.io/v3/`, | ||
| failoverUrls: [QUICKNODE_MONAD_URL], | ||
| }, | ||
| { | ||
| type: RpcEndpointType.Custom, | ||
| url: `https://some-monad-rpc.com`, | ||
| failoverUrls: [QUICKNODE_MONAD_URL], | ||
| }, | ||
| ], | ||
| }, | ||
| '0x1': { | ||
| rpcEndpoints: [ | ||
| { | ||
| type: RpcEndpointType.Custom, | ||
| url: `https://ethereum-mainnet.infura.io/v3/`, | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const newStorage = await migrate(oldStorage); | ||
|
|
||
| expect(newStorage).toStrictEqual(expectedStorage); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { getErrorMessage, hasProperty, Hex, isObject } from '@metamask/utils'; | ||
| import { cloneDeep } from 'lodash'; | ||
| import { captureException } from '../../../shared/lib/sentry'; | ||
| import { CHAIN_IDS } from '../../../shared/constants/network'; | ||
|
|
||
| type VersionedData = { | ||
| meta: { version: number }; | ||
| data: Record<string, unknown>; | ||
| }; | ||
|
|
||
| export const version = 185; | ||
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const MONAD_CHAIN_ID: Hex = CHAIN_IDS.MONAD; | ||
|
|
||
| /** | ||
| * This migration adds QuickNode failover URL to Monad network RPC endpoints | ||
| * that use Infura and don't already have a failover URL configured. | ||
| * | ||
| * @param originalVersionedData - The original MetaMask extension state. | ||
| * @returns Updated versioned MetaMask extension state. | ||
| */ | ||
| export async function migrate( | ||
| originalVersionedData: VersionedData, | ||
| ): Promise<VersionedData> { | ||
| const versionedData = cloneDeep(originalVersionedData); | ||
| versionedData.meta.version = version; | ||
|
|
||
| try { | ||
| transformState(versionedData.data); | ||
| } catch (error) { | ||
| console.error(error); | ||
| const newError = new Error( | ||
| `Migration #${version}: ${getErrorMessage(error)}`, | ||
| ); | ||
| captureException(newError); | ||
| // Even though we encountered an error, we need the migration to pass for | ||
| // the migrator tests to work | ||
| versionedData.data = originalVersionedData.data; | ||
| } | ||
|
|
||
| return versionedData; | ||
| } | ||
|
|
||
| function transformState(state: Record<string, unknown>) { | ||
| if (!hasProperty(state, 'NetworkController')) { | ||
| console.warn(`Migration ${version}: NetworkController not found.`); | ||
| return state; | ||
| } | ||
|
|
||
| const networkState = state.NetworkController; | ||
| if (!isObject(networkState)) { | ||
| global.sentry?.captureException?.( | ||
| new Error( | ||
| `Migration ${version}: NetworkController is not an object: ${typeof networkState}`, | ||
| ), | ||
| ); | ||
| return state; | ||
| } | ||
|
|
||
| if (!hasProperty(networkState, 'networkConfigurationsByChainId')) { | ||
| global.sentry?.captureException?.( | ||
| new Error( | ||
| `Migration ${version}: NetworkController missing property networkConfigurationsByChainId.`, | ||
| ), | ||
| ); | ||
| return state; | ||
| } | ||
|
|
||
| if (!isObject(networkState.networkConfigurationsByChainId)) { | ||
| global.sentry?.captureException?.( | ||
| new Error( | ||
| `Migration ${version}: NetworkController.networkConfigurationsByChainId is not an object: ${typeof networkState.networkConfigurationsByChainId}.`, | ||
| ), | ||
| ); | ||
| return state; | ||
| } | ||
|
|
||
| const { networkConfigurationsByChainId } = networkState; | ||
|
|
||
| // Get Monad network configuration | ||
| const monadNetworkConfiguration = | ||
| networkConfigurationsByChainId[MONAD_CHAIN_ID]; | ||
|
|
||
| if (!monadNetworkConfiguration) { | ||
| // Monad network doesn't exist, nothing to migrate | ||
| return state; | ||
| } | ||
|
|
||
| if ( | ||
| !isObject(monadNetworkConfiguration) || | ||
| !hasProperty(monadNetworkConfiguration, 'rpcEndpoints') || | ||
| !Array.isArray(monadNetworkConfiguration.rpcEndpoints) | ||
| ) { | ||
| global.sentry?.captureException?.( | ||
| new Error( | ||
| `Migration ${version}: Monad network configuration has invalid rpcEndpoints.`, | ||
| ), | ||
| ); | ||
| return state; | ||
| } | ||
|
|
||
| // Update RPC endpoints to add failover URL if needed | ||
| monadNetworkConfiguration.rpcEndpoints = | ||
| monadNetworkConfiguration.rpcEndpoints.map((rpcEndpoint) => { | ||
| // Skip if endpoint is not an object or doesn't have a url property | ||
| if ( | ||
| !isObject(rpcEndpoint) || | ||
| !hasProperty(rpcEndpoint, 'url') || | ||
| typeof rpcEndpoint.url !== 'string' | ||
| ) { | ||
| return rpcEndpoint; | ||
| } | ||
|
|
||
| // Skip if endpoint already has failover URLs | ||
| if ( | ||
| hasProperty(rpcEndpoint, 'failoverUrls') && | ||
| Array.isArray(rpcEndpoint.failoverUrls) && | ||
| rpcEndpoint.failoverUrls.length > 0 | ||
| ) { | ||
| return rpcEndpoint; | ||
| } | ||
|
|
||
| // Add QuickNode failover URL | ||
| const quickNodeUrl = process.env.QUICKNODE_MONAD_URL; | ||
| if (quickNodeUrl) { | ||
| return { | ||
| ...rpcEndpoint, | ||
| failoverUrls: [quickNodeUrl], | ||
| }; | ||
| } | ||
|
|
||
| return rpcEndpoint; | ||
| }); | ||
|
|
||
| return state; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.