diff --git a/app/scripts/controllers/perps/infrastructure.ts b/app/scripts/controllers/perps/infrastructure.ts index 9770f826afcd..35f6520cf042 100644 --- a/app/scripts/controllers/perps/infrastructure.ts +++ b/app/scripts/controllers/perps/infrastructure.ts @@ -179,6 +179,14 @@ function createTracer(): PerpsTracer { setMeasurement: (name: string, value: number, unit: string) => { globalThis.sentry?.setMeasurement?.(name, value, unit); }, + addBreadcrumb: (_breadcrumb: { + category: string; + message: string; + level: 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug'; + data?: Record; + }) => { + // TODO: Integrate with Sentry breadcrumbs when ready + }, }; } diff --git a/app/scripts/messenger-client-init/perps-controller-init.test.ts b/app/scripts/messenger-client-init/perps-controller-init.test.ts index 33816bd3f82f..b51f4bc23f6c 100644 --- a/app/scripts/messenger-client-init/perps-controller-init.test.ts +++ b/app/scripts/messenger-client-init/perps-controller-init.test.ts @@ -47,13 +47,8 @@ jest.mock('@metamask/perps-controller', () => ({ marketFilterPreferences: { optionId: 'volume', direction: 'desc' }, hip3ConfigVersion: 0, selectedPaymentToken: null, - cachedMarketData: null, - cachedMarketDataTimestamp: 0, - cachedPositions: null, - cachedOrders: null, - cachedAccountState: null, - cachedUserDataTimestamp: 0, - cachedUserDataAddress: null, + cachedMarketDataByProvider: {}, + cachedUserDataByProvider: {}, }), PerpsController: jest.fn().mockImplementation(() => ({ state: { initializationState: 'uninitialized' }, @@ -154,6 +149,8 @@ describe('PerpsControllerInit', () => { beforeEach(() => { jest.clearAllMocks(); delete process.env.MM_PERPS_BLOCKED_REGIONS; + delete process.env.MM_PERPS_HL_BUILDER_ADDRESS_MAINNET; + delete process.env.MM_PERPS_HL_BUILDER_ADDRESS_TESTNET; }); describe('controller instantiation', () => { @@ -177,6 +174,9 @@ describe('PerpsControllerInit', () => { fallbackHip3Enabled: true, fallbackHip3AllowlistMarkets: [], fallbackBlockedRegions: [], + providerCredentials: { + hyperliquid: undefined, + }, }, deferEligibilityCheck: true, }); @@ -344,6 +344,55 @@ describe('PerpsControllerInit', () => { }); }); + describe('getHyperLiquidBuilderAddresses', () => { + it('returns undefined when no env vars are set', () => { + PerpsControllerInit(getInitRequestMock()); + + const constructorCall = PerpsControllerMock.mock.calls[0][0]; + expect( + constructorCall.clientConfig?.providerCredentials?.hyperliquid, + ).toBeUndefined(); + }); + + it('passes mainnet builder address from env var', () => { + process.env.MM_PERPS_HL_BUILDER_ADDRESS_MAINNET = '0xabc123'; + PerpsControllerInit(getInitRequestMock()); + + const constructorCall = PerpsControllerMock.mock.calls[0][0]; + expect( + constructorCall.clientConfig?.providerCredentials?.hyperliquid, + ).toEqual({ + builderAddressMainnet: '0xabc123', + }); + }); + + it('passes testnet builder address from env var', () => { + process.env.MM_PERPS_HL_BUILDER_ADDRESS_TESTNET = '0xdef456'; + PerpsControllerInit(getInitRequestMock()); + + const constructorCall = PerpsControllerMock.mock.calls[0][0]; + expect( + constructorCall.clientConfig?.providerCredentials?.hyperliquid, + ).toEqual({ + builderAddressTestnet: '0xdef456', + }); + }); + + it('passes both builder addresses when both env vars are set', () => { + process.env.MM_PERPS_HL_BUILDER_ADDRESS_MAINNET = '0xabc123'; + process.env.MM_PERPS_HL_BUILDER_ADDRESS_TESTNET = '0xdef456'; + PerpsControllerInit(getInitRequestMock()); + + const constructorCall = PerpsControllerMock.mock.calls[0][0]; + expect( + constructorCall.clientConfig?.providerCredentials?.hyperliquid, + ).toEqual({ + builderAddressMainnet: '0xabc123', + builderAddressTestnet: '0xdef456', + }); + }); + }); + describe('api method delegation', () => { const apiToController: [string, string][] = [ ['perpsInit', 'init'], diff --git a/app/scripts/messenger-client-init/perps-controller-init.ts b/app/scripts/messenger-client-init/perps-controller-init.ts index 66a690db198c..dc62293408ce 100644 --- a/app/scripts/messenger-client-init/perps-controller-init.ts +++ b/app/scripts/messenger-client-init/perps-controller-init.ts @@ -24,6 +24,26 @@ function getFallbackBlockedRegions(): string[] { .filter(Boolean); } +/** + * Read HyperLiquid builder fee wallet addresses from env vars. + * Returns undefined when neither env var is set (package defaults apply). + */ +function getHyperLiquidBuilderAddresses(): + | { builderAddressMainnet?: string; builderAddressTestnet?: string } + | undefined { + const mainnet = + process.env.MM_PERPS_HL_BUILDER_ADDRESS_MAINNET?.trim() || undefined; + const testnet = + process.env.MM_PERPS_HL_BUILDER_ADDRESS_TESTNET?.trim() || undefined; + if (!mainnet && !testnet) { + return undefined; + } + return { + ...(mainnet ? { builderAddressMainnet: mainnet } : {}), + ...(testnet ? { builderAddressTestnet: testnet } : {}), + }; +} + export const PerpsControllerInit: ControllerInitFunction< PerpsController, PerpsControllerMessenger @@ -50,6 +70,9 @@ export const PerpsControllerInit: ControllerInitFunction< fallbackHip3Enabled: true, fallbackHip3AllowlistMarkets: [], fallbackBlockedRegions, + providerCredentials: { + hyperliquid: getHyperLiquidBuilderAddresses(), + }, }, deferEligibilityCheck: !completedOnboarding || !useExternalServices, }); diff --git a/builds.yml b/builds.yml index e8984d6a3991..9e39db99d0b1 100644 --- a/builds.yml +++ b/builds.yml @@ -378,6 +378,9 @@ env: # Perps geoblock fallback (comma-separated region codes when remote flag unavailable) - MM_PERPS_BLOCKED_REGIONS: null + # Hyperliquid builder fee wallet address overrides (defaults to package-defined addresses when unset) + - MM_PERPS_HL_BUILDER_ADDRESS_MAINNET: null + - MM_PERPS_HL_BUILDER_ADDRESS_TESTNET: null # Build-time gate: omit PerpsController and perps RPC when false (mitigation for critical failures) - PERPS_ENABLED: false # Build-time gate: when false AssetsController is instantiated but state remains empty. diff --git a/development/webpack/test/webpack.config.test.ts b/development/webpack/test/webpack.config.test.ts index f6418cd9afb9..d5ac46a4edb7 100644 --- a/development/webpack/test/webpack.config.test.ts +++ b/development/webpack/test/webpack.config.test.ts @@ -403,119 +403,6 @@ ${Object.entries(env) ); }); - it('keeps the MYX provider ignore only while the warning still exists', async () => { - const config: Configuration = getWebpackConfig(); - const myxWarningPattern = /MYXProvider\.mjs/u; - const ignoreWarnings = config.ignoreWarnings ?? []; - const getPerpsControllerWarnings = async (): Promise => { - const tempDirectory = fs.mkdtempSync( - join(tmpdir(), 'webpack-perps-controller-warning-'), - ); - const repositoryRoot = resolve(__dirname, '../../..'); - const entryFilePath = join(tempDirectory, 'entry.js'); - - fs.writeFileSync(entryFilePath, "import '@metamask/perps-controller';\n"); - - try { - const compiler = webpack({ - mode: 'development', - context: repositoryRoot, - entry: entryFilePath, - output: { - path: join(tempDirectory, 'dist'), - filename: 'bundle.js', - clean: true, - }, - resolve: { - modules: [join(repositoryRoot, 'node_modules'), 'node_modules'], - }, - }); - - return await new Promise((resolveWarnings, reject) => { - compiler.run((error, stats) => { - const closeCompiler = (callback: () => void) => { - compiler.close((closeError) => { - if (closeError) { - reject(closeError); - return; - } - - callback(); - }); - }; - - if (error) { - closeCompiler(() => reject(error)); - return; - } - - if (!stats) { - closeCompiler(() => - reject(new Error('Webpack finished without returning stats.')), - ); - return; - } - - const errors = - stats.toJson({ all: false, errors: true }).errors ?? []; - if (errors.length > 0) { - closeCompiler(() => - reject( - new Error( - `Webpack build failed:\n${errors - .map((item) => - typeof item === 'string' ? item : item.message, - ) - .join('\n\n')}`, - ), - ), - ); - return; - } - - const warnings = - stats.toJson({ all: false, warnings: true }).warnings ?? []; - closeCompiler(() => - resolveWarnings( - warnings.map((item) => - typeof item === 'string' ? item : item.message, - ), - ), - ); - }); - }); - } finally { - fs.rmSync(tempDirectory, { recursive: true, force: true }); - } - }; - - assert.ok( - ignoreWarnings.some( - (warning) => - warning instanceof RegExp && warning.test('MYXProvider.mjs'), - ), - 'Expected webpack config to ignore MYXProvider.mjs while that warning still exists.', - ); - - // The published perps-controller package currently omits MYXProvider.mjs. - // This assertion should fail once that warning goes away, which is the cue - // to remove the ignoreWarnings entry from webpack.config.ts. - const warnings = await getPerpsControllerWarnings(); - assert.ok( - warnings.some((warning) => myxWarningPattern.test(warning)), - [ - 'Good news: webpack no longer warns about missing MYXProvider.mjs.', - 'That means the temporary workaround is probably no longer needed.', - 'Cleanup steps:', - '1. Remove this test: "keeps the MYX provider ignore only while the warning still exists".', - '2. Remove /MYXProvider\\.mjs/u from ignoreWarnings in development/webpack/webpack.config.ts.', - '3. Re-run the webpack unit tests.', - 'Warnings seen in this run:', - ...warnings, - ].join('\n'), - ); - }); - it('should write the `dry-run` message then call exit(0)', () => { const exit = mock.method(process, 'exit', noop, { times: 1 }); const error = mock.method(console, 'error', noop, { times: 1 }); diff --git a/development/webpack/webpack.config.ts b/development/webpack/webpack.config.ts index 51f72e22e230..eceff69fcaa1 100644 --- a/development/webpack/webpack.config.ts +++ b/development/webpack/webpack.config.ts @@ -557,9 +557,6 @@ const config = { }, ignoreWarnings: [ /the following module ids can't be controlled by policy and must be ignored at runtime/u, - // Optional perps-controller: dynamic optional provider not shipped in this build - // TODO: Remove this once the MYX provider is included in the published - /MYXProvider\.mjs/u, ], } as const satisfies Configuration; diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 2a2ef622c3b5..b1259bced426 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1876,7 +1876,7 @@ "clearTimeout": true, "fetch": true, "performance.now": true, - "process": true, + "process.env.NODE_ENV": true, "setInterval": true, "setTimeout": true }, diff --git a/lavamoat/browserify/experimental/policy.json b/lavamoat/browserify/experimental/policy.json index 2a2ef622c3b5..b1259bced426 100644 --- a/lavamoat/browserify/experimental/policy.json +++ b/lavamoat/browserify/experimental/policy.json @@ -1876,7 +1876,7 @@ "clearTimeout": true, "fetch": true, "performance.now": true, - "process": true, + "process.env.NODE_ENV": true, "setInterval": true, "setTimeout": true }, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 2a2ef622c3b5..b1259bced426 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1876,7 +1876,7 @@ "clearTimeout": true, "fetch": true, "performance.now": true, - "process": true, + "process.env.NODE_ENV": true, "setInterval": true, "setTimeout": true }, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 2a2ef622c3b5..b1259bced426 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1876,7 +1876,7 @@ "clearTimeout": true, "fetch": true, "performance.now": true, - "process": true, + "process.env.NODE_ENV": true, "setInterval": true, "setTimeout": true }, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index f94caf9656c9..5e733414895e 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -7760,6 +7760,7 @@ "process.versions.pnp": true }, "packages": { + "eslint-import-resolver-typescript>unrs-resolver>@unrs/resolver-binding-darwin-arm64": true, "eslint-import-resolver-typescript>unrs-resolver>@unrs/resolver-binding-linux-x64-gnu": true } }, diff --git a/lavamoat/webpack/mv2/beta/policy.json b/lavamoat/webpack/mv2/beta/policy.json index 8344b27b2d5f..132ce49dff84 100644 --- a/lavamoat/webpack/mv2/beta/policy.json +++ b/lavamoat/webpack/mv2/beta/policy.json @@ -1883,7 +1883,6 @@ "clearTimeout": true, "fetch": true, "performance.now": true, - "process": true, "setInterval": true, "setTimeout": true }, diff --git a/lavamoat/webpack/mv2/experimental/policy.json b/lavamoat/webpack/mv2/experimental/policy.json index 8344b27b2d5f..132ce49dff84 100644 --- a/lavamoat/webpack/mv2/experimental/policy.json +++ b/lavamoat/webpack/mv2/experimental/policy.json @@ -1883,7 +1883,6 @@ "clearTimeout": true, "fetch": true, "performance.now": true, - "process": true, "setInterval": true, "setTimeout": true }, diff --git a/lavamoat/webpack/mv2/flask/policy.json b/lavamoat/webpack/mv2/flask/policy.json index 8344b27b2d5f..132ce49dff84 100644 --- a/lavamoat/webpack/mv2/flask/policy.json +++ b/lavamoat/webpack/mv2/flask/policy.json @@ -1883,7 +1883,6 @@ "clearTimeout": true, "fetch": true, "performance.now": true, - "process": true, "setInterval": true, "setTimeout": true }, diff --git a/lavamoat/webpack/mv2/main/policy.json b/lavamoat/webpack/mv2/main/policy.json index 8344b27b2d5f..132ce49dff84 100644 --- a/lavamoat/webpack/mv2/main/policy.json +++ b/lavamoat/webpack/mv2/main/policy.json @@ -1883,7 +1883,6 @@ "clearTimeout": true, "fetch": true, "performance.now": true, - "process": true, "setInterval": true, "setTimeout": true }, diff --git a/package.json b/package.json index 19eb6e502519..30af66a05de9 100644 --- a/package.json +++ b/package.json @@ -267,7 +267,8 @@ "fast-xml-parser": "^5.5.7", "minimatch@npm:^10.1.1": "10.2.4", "@metamask/snaps-controllers": "^19.0.0", - "@metamask/messenger@npm:^0.3.0": "^1.1.1" + "@metamask/messenger@npm:^0.3.0": "^1.1.1", + "@metamask/perps-controller": "^3.0.0" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.26.10#~/.yarn/patches/@babel-runtime-npm-7.26.10-fe8c62510a.patch", @@ -369,7 +370,7 @@ "@metamask/permission-controller": "^12.2.1", "@metamask/permission-log-controller": "^5.0.0", "@metamask/permissions-kernel-snap": "^1.1.0", - "@metamask/perps-controller": "^1.3.0", + "@metamask/perps-controller": "^3.0.0", "@metamask/phishing-controller": "^17.1.1", "@metamask/polling-controller": "^16.0.2", "@metamask/post-message-stream": "^10.0.0", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index f5ea915b70bc..9c890751cb8b 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -116,13 +116,8 @@ "balances": "object", "browserEnvironment": { "browser": "string", "os": "string" }, "cacheTimestamp": "number", - "cachedAccountState": null, - "cachedMarketData": null, - "cachedMarketDataTimestamp": "number", - "cachedOrders": null, - "cachedPositions": null, - "cachedUserDataAddress": null, - "cachedUserDataTimestamp": "number", + "cachedMarketDataByProvider": "object", + "cachedUserDataByProvider": "object", "canTrackWalletFundsObtained": false, "claims": "object", "claimsConfigurations": "object", @@ -238,6 +233,8 @@ "isWalletResetInProgress": "boolean", "keyrings": "object", "knownMethodData": "object", + "lastCompletedWithdrawalTimestamp": null, + "lastCompletedWithdrawalTxHashes": "object", "lastDepositResult": null, "lastDepositTransactionId": null, "lastError": null, diff --git a/test/e2e/tests/settings/state-logs.json b/test/e2e/tests/settings/state-logs.json index 0ed3329ea85c..31371766bad7 100644 --- a/test/e2e/tests/settings/state-logs.json +++ b/test/e2e/tests/settings/state-logs.json @@ -609,13 +609,8 @@ "browser": "string", "os": "string" }, - "cachedAccountState": "null", - "cachedMarketData": "null", - "cachedMarketDataTimestamp": "number", - "cachedOrders": "null", - "cachedPositions": "null", - "cachedUserDataAddress": "null", - "cachedUserDataTimestamp": "number", + "cachedMarketDataByProvider": "object", + "cachedUserDataByProvider": "object", "cacheTimestamp": "number", "canTrackWalletFundsObtained": "boolean", "claims": [], diff --git a/ui/components/app/perps/utils/transactionTransforms.ts b/ui/components/app/perps/utils/transactionTransforms.ts index 246ab49d9ac3..a0170f00016c 100644 --- a/ui/components/app/perps/utils/transactionTransforms.ts +++ b/ui/components/app/perps/utils/transactionTransforms.ts @@ -537,7 +537,7 @@ export function transformFundingToTransactions( isPositive, fee: amountUSDC, feeNumber: parseFloat(amountUsd), - rate: `${new BigNumber(rate).times(100).toString()}%`, + rate: `${new BigNumber(rate ?? '0').times(100).toString()}%`, }, }; }); diff --git a/ui/selectors/perps-controller.test.ts b/ui/selectors/perps-controller.test.ts index 5dbe58d82cdc..21059b1b2ca5 100644 --- a/ui/selectors/perps-controller.test.ts +++ b/ui/selectors/perps-controller.test.ts @@ -336,7 +336,14 @@ describe('perps-controller selectors', () => { it('returns value from state', () => { const data = [{ market: 'ETH' }]; expect( - selectPerpsCachedMarketData(buildState({ cachedMarketData: data })), + selectPerpsCachedMarketData( + buildState({ + activeProvider: 'hyperliquid', + cachedMarketDataByProvider: { + hyperliquid: { data, timestamp: 0 }, + }, + }), + ), ).toBe(data); }); @@ -349,7 +356,20 @@ describe('perps-controller selectors', () => { it('returns value from state', () => { const positions = [{ market: 'ETH', size: 1 }]; expect( - selectPerpsCachedPositions(buildState({ cachedPositions: positions })), + selectPerpsCachedPositions( + buildState({ + activeProvider: 'hyperliquid', + cachedUserDataByProvider: { + hyperliquid: { + positions, + orders: [], + accountState: null, + timestamp: 0, + address: '', + }, + }, + }), + ), ).toBe(positions); }); @@ -362,7 +382,20 @@ describe('perps-controller selectors', () => { it('returns value from state', () => { const orders = [{ id: 'o1' }]; expect( - selectPerpsCachedOrders(buildState({ cachedOrders: orders })), + selectPerpsCachedOrders( + buildState({ + activeProvider: 'hyperliquid', + cachedUserDataByProvider: { + hyperliquid: { + positions: [], + orders, + accountState: null, + timestamp: 0, + address: '', + }, + }, + }), + ), ).toBe(orders); }); @@ -376,7 +409,18 @@ describe('perps-controller selectors', () => { const account = { balance: '100' }; expect( selectPerpsCachedAccountState( - buildState({ cachedAccountState: account }), + buildState({ + activeProvider: 'hyperliquid', + cachedUserDataByProvider: { + hyperliquid: { + positions: [], + orders: [], + accountState: account, + timestamp: 0, + address: '', + }, + }, + }), ), ).toBe(account); }); diff --git a/ui/selectors/perps-controller.ts b/ui/selectors/perps-controller.ts index cd63fe617d51..7a60468fd2a4 100644 --- a/ui/selectors/perps-controller.ts +++ b/ui/selectors/perps-controller.ts @@ -92,17 +92,27 @@ export const selectPerpsLastError = (state: PerpsState) => export const selectPerpsSelectedPaymentToken = (state: PerpsState) => state.metamask.selectedPaymentToken ?? null; -export const selectPerpsCachedMarketData = (state: PerpsState) => - state.metamask.cachedMarketData ?? null; +export const selectPerpsCachedMarketData = (state: PerpsState) => { + const provider = selectPerpsActiveProvider(state); + return state.metamask.cachedMarketDataByProvider?.[provider]?.data ?? null; +}; -export const selectPerpsCachedPositions = (state: PerpsState) => - state.metamask.cachedPositions ?? null; +export const selectPerpsCachedPositions = (state: PerpsState) => { + const provider = selectPerpsActiveProvider(state); + return state.metamask.cachedUserDataByProvider?.[provider]?.positions ?? null; +}; -export const selectPerpsCachedOrders = (state: PerpsState) => - state.metamask.cachedOrders ?? null; +export const selectPerpsCachedOrders = (state: PerpsState) => { + const provider = selectPerpsActiveProvider(state); + return state.metamask.cachedUserDataByProvider?.[provider]?.orders ?? null; +}; -export const selectPerpsCachedAccountState = (state: PerpsState) => - state.metamask.cachedAccountState ?? null; +export const selectPerpsCachedAccountState = (state: PerpsState) => { + const provider = selectPerpsActiveProvider(state); + return ( + state.metamask.cachedUserDataByProvider?.[provider]?.accountState ?? null + ); +}; export const selectPerpsPerpsBalances = (state: PerpsState) => state.metamask.perpsBalances ?? {}; diff --git a/yarn.lock b/yarn.lock index e1df79d9ccd8..73c3974bb86b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7471,21 +7471,24 @@ __metadata: languageName: node linkType: hard -"@metamask/perps-controller@npm:^1.3.0": - version: 1.3.0 - resolution: "@metamask/perps-controller@npm:1.3.0" +"@metamask/perps-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/perps-controller@npm:3.0.0" dependencies: "@metamask/abi-utils": "npm:^2.0.3" - "@metamask/base-controller": "npm:^9.0.0" - "@metamask/controller-utils": "npm:^11.19.0" - "@metamask/messenger": "npm:^0.3.0" + "@metamask/base-controller": "npm:^9.0.1" + "@metamask/controller-utils": "npm:^11.20.0" + "@metamask/messenger": "npm:^1.1.1" "@metamask/utils": "npm:^11.9.0" "@myx-trade/sdk": "npm:^0.1.265" "@nktkas/hyperliquid": "npm:^0.30.2" bignumber.js: "npm:^9.1.2" reselect: "npm:^5.1.1" uuid: "npm:^8.3.2" - checksum: 10/48e7721d53c1fbd781f8eac23402e1c62ea63ee859092521da3721596c54fe41ddcaaf38e1efacbc09d5e6939514920c95257937b1819b4eec2d1a42ec3a74f2 + dependenciesMeta: + "@myx-trade/sdk": + optional: true + checksum: 10/77510382c82d15b36735d10ef8483938b056a89b44a9d5968d697e68ebe5eccda316faa80518138a23697b12effb2c00f33a0136b2a0c5c255438bff462d67a8 languageName: node linkType: hard @@ -34171,7 +34174,7 @@ __metadata: "@metamask/permission-controller": "npm:^12.2.1" "@metamask/permission-log-controller": "npm:^5.0.0" "@metamask/permissions-kernel-snap": "npm:^1.1.0" - "@metamask/perps-controller": "npm:^1.3.0" + "@metamask/perps-controller": "npm:^3.0.0" "@metamask/phishing-controller": "npm:^17.1.1" "@metamask/phishing-warning": "npm:^5.1.0" "@metamask/polling-controller": "npm:^16.0.2"