Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -47,6 +47,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/184.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 './184';

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/184.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 = 184;

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 app/scripts/migrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ const migrations = [
require('./181'),
require('./182'),
require('./183'),
require('./184'),
];

export default migrations;
1 change: 1 addition & 0 deletions builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,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
Loading
Loading