Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/scripts/bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export QUICKNODE_MAINNET_URL=""
export QUICKNODE_OPTIMISM_URL=""
export QUICKNODE_POLYGON_URL=""
export QUICKNODE_SEI_URL=""
export QUICKNODE_MONAD_URL=""
export SEGMENT_BETA_WRITE_KEY=""
export SEGMENT_EXPERIMENTAL_WRITE_KEY=""
export SEGMENT_FLASK_WRITE_KEY=""
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
QUICKNODE_OPTIMISM_URL: ${{ secrets.QUICKNODE_OPTIMISM_URL }}
QUICKNODE_POLYGON_URL: ${{ secrets.QUICKNODE_POLYGON_URL }}
QUICKNODE_SEI_URL: ${{ secrets.QUICKNODE_SEI_URL }}
QUICKNODE_MONAD_URL: ${{ secrets.QUICKNODE_MONAD_URL }}
SEGMENT_BETA_WRITE_KEY: ${{ secrets.SEGMENT_BETA_WRITE_KEY }}
SEGMENT_EXPERIMENTAL_WRITE_KEY: ${{ secrets.SEGMENT_EXPERIMENTAL_WRITE_KEY }}
SEGMENT_FLASK_WRITE_KEY: ${{ secrets.SEGMENT_FLASK_WRITE_KEY }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/run-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ jobs:
QUICKNODE_OPTIMISM_URL: ${{ secrets.QUICKNODE_OPTIMISM_URL }}
QUICKNODE_POLYGON_URL: ${{ secrets.QUICKNODE_POLYGON_URL }}
QUICKNODE_SEI_URL: ${{ secrets.QUICKNODE_SEI_URL }}
QUICKNODE_MONAD_URL: ${{ secrets.QUICKNODE_MONAD_URL }}
SEGMENT_BETA_WRITE_KEY: ${{ secrets.SEGMENT_BETA_WRITE_KEY }}
SEGMENT_EXPERIMENTAL_WRITE_KEY: ${{ secrets.SEGMENT_EXPERIMENTAL_WRITE_KEY }}
SEGMENT_FLASK_WRITE_KEY: ${{ secrets.SEGMENT_FLASK_WRITE_KEY }}
Expand Down
201 changes: 201 additions & 0 deletions app/scripts/migrations/186.test.ts
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);
});
});
136 changes: 136 additions & 0 deletions app/scripts/migrations/186.ts
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;

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;
}
1 change: 1 addition & 0 deletions builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ env:
- QUICKNODE_BASE_URL: null
- QUICKNODE_BSC_URL: null
- QUICKNODE_SEI_URL: null
- QUICKNODE_MONAD_URL: null

###
# API keys to 3rd party services
Expand Down
1 change: 1 addition & 0 deletions development/build/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const VARIABLES_REQUIRED_IN_PRODUCTION = {
'QUICKNODE_BASE_URL',
'QUICKNODE_BSC_URL',
'QUICKNODE_SEI_URL',
'QUICKNODE_MONAD_URL',
'APPLE_PROD_CLIENT_ID',
'GOOGLE_PROD_CLIENT_ID',
],
Expand Down
2 changes: 2 additions & 0 deletions shared/constants/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,7 @@ export const QUICKNODE_ENDPOINT_URLS_BY_INFURA_NETWORK_NAME = {
'base-mainnet': () => process.env.QUICKNODE_BASE_URL,
'bsc-mainnet': () => process.env.QUICKNODE_BSC_URL,
'sei-mainnet': () => process.env.QUICKNODE_SEI_URL,
'monad-mainnet': () => process.env.QUICKNODE_MONAD_URL,
};

export function getFailoverUrlsForInfuraNetwork(
Expand Down Expand Up @@ -1579,6 +1580,7 @@ export const FEATURED_RPCS: AddNetworkFields[] = [
rpcEndpoints: [
{
url: `https://monad-mainnet.infura.io/v3/${infuraProjectId}`,
failoverUrls: getFailoverUrlsForInfuraNetwork('monad-mainnet'),
type: RpcEndpointType.Custom,
},
],
Expand Down
Loading