From cc99a2522892ad38c7fb4cc01037a58813c6a861 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 20 Feb 2023 17:13:12 +0000 Subject: [PATCH] Add desktop support (#17683) Use DesktopManager in background script to redirect internal and external connections to the desktop app. Include DesktopController in the MetaMask controller. Support desktop keyrings in MetaMask controller via the overrides object. Create middleware handler to connect to the desktop app while UI code is pending. Add build system support for desktop specific configuration variables. --- app/scripts/background.js | 60 ++++++++++++++++ .../handlers/desktop/enable-desktop.js | 43 ++++++++++++ .../rpc-method-middleware/handlers/index.js | 7 ++ app/scripts/lib/setupSentry.js | 56 ++++++++++----- app/scripts/metamask-controller.js | 70 ++++++++++++++++--- app/scripts/platforms/extension.js | 5 +- development/build/config.js | 5 ++ development/build/scripts.js | 6 +- package.json | 1 + shared/constants/app.ts | 4 ++ test/e2e/run-e2e-test.js | 2 + yarn.lock | 59 +++++++++++++++- 12 files changed, 285 insertions(+), 33 deletions(-) create mode 100644 app/scripts/lib/rpc-method-middleware/handlers/desktop/enable-desktop.js diff --git a/app/scripts/background.js b/app/scripts/background.js index 6f47041ea..d420929a2 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -54,6 +54,16 @@ import setupEnsIpfsResolver from './lib/ens-ipfs/setup'; import { deferredPromise, getPlatform } from './lib/util'; /* eslint-enable import/first */ +/* eslint-disable import/order */ +///: BEGIN:ONLY_INCLUDE_IN(desktop) +import { + CONNECTION_TYPE_EXTERNAL, + CONNECTION_TYPE_INTERNAL, +} from '@metamask/desktop/dist/constants'; +import DesktopManager from '@metamask/desktop/dist/desktop-manager'; +///: END:ONLY_INCLUDE_IN +/* eslint-enable import/order */ + const { sentry } = global; const firstTimeState = { ...rawFirstTimeState }; @@ -97,6 +107,13 @@ const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS; const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE'; const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE'; +///: BEGIN:ONLY_INCLUDE_IN(desktop) +const OVERRIDE_ORIGIN = { + EXTENSION: 'EXTENSION', + DESKTOP: 'DESKTOP_APP', +}; +///: END:ONLY_INCLUDE_IN + // Event emitter for state persistence export const statePersistenceEvents = new EventEmitter(); @@ -245,6 +262,11 @@ async function initialize() { try { const initState = await loadStateFromPersistence(); const initLangCode = await getFirstPreferredLangCode(); + + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + await DesktopManager.init(platform.getVersion()); + ///: END:ONLY_INCLUDE_IN + setupController(initState, initLangCode); if (!isManifestV3) { await loadPhishingWarningPage(); @@ -482,6 +504,26 @@ export function setupController(initState, initLangCode, overrides) { * @param {Port} remotePort - The port provided by a new context. */ connectRemote = async (remotePort) => { + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + if ( + DesktopManager.isDesktopEnabled() && + OVERRIDE_ORIGIN.DESKTOP !== overrides?.getOrigin?.() + ) { + DesktopManager.createStream(remotePort, CONNECTION_TYPE_INTERNAL).then( + () => { + // When in Desktop Mode the responsibility to send CONNECTION_READY is on the desktop app side + if (isManifestV3) { + // Message below if captured by UI code in app/scripts/ui.js which will trigger UI initialisation + // This ensures that UI is initialised only after background is ready + // It fixes the issue of blank screen coming when extension is loaded, the issue is very frequent in MV3 + remotePort.postMessage({ name: 'CONNECTION_READY' }); + } + }, + ); + return; + } + ///: END:ONLY_INCLUDE_IN + const processName = remotePort.name; if (metamaskBlockedPorts.includes(remotePort.name)) { @@ -584,6 +626,16 @@ export function setupController(initState, initLangCode, overrides) { // communication with page or other extension connectExternal = (remotePort) => { + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + if ( + DesktopManager.isDesktopEnabled() && + OVERRIDE_ORIGIN.DESKTOP !== overrides?.getOrigin?.() + ) { + DesktopManager.createStream(remotePort, CONNECTION_TYPE_EXTERNAL); + return; + } + ///: END:ONLY_INCLUDE_IN + const portStream = overrides?.getPortStream?.(remotePort) || new PortStream(remotePort); controller.setupUntrustedCommunication({ @@ -762,6 +814,14 @@ export function setupController(initState, initLangCode, overrides) { updateBadge(); } + + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + if (OVERRIDE_ORIGIN.DESKTOP !== overrides?.getOrigin?.()) { + controller.store.subscribe((state) => { + DesktopManager.setState(state); + }); + } + ///: END:ONLY_INCLUDE_IN } // diff --git a/app/scripts/lib/rpc-method-middleware/handlers/desktop/enable-desktop.js b/app/scripts/lib/rpc-method-middleware/handlers/desktop/enable-desktop.js new file mode 100644 index 000000000..37bd152e9 --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/handlers/desktop/enable-desktop.js @@ -0,0 +1,43 @@ +import { MESSAGE_TYPE } from '../../../../../../shared/constants/app'; + +/** + * A wrapper for `eth_accounts` that returns an empty array when permission is denied. + */ + +const requestEthereumAccounts = { + methodNames: [MESSAGE_TYPE.ENABLE_DESKTOP], + implementation: enableDesktop, + hookNames: { + testDesktopConnection: true, + generateOtp: true, + }, +}; +export default requestEthereumAccounts; + +/** + * @typedef {Record} EthAccountsOptions + * @property {Function} getAccounts - Gets the accounts for the requesting + * origin. + */ + +/** + * + * @param {import('json-rpc-engine').JsonRpcRequest} _req - The JSON-RPC request object. + * @param {import('json-rpc-engine').JsonRpcResponse} res - The JSON-RPC response object. + * @param {Function} _next - The json-rpc-engine 'next' callback. + * @param {Function} end - The json-rpc-engine 'end' callback. + * @param {EthAccountsOptions} options - The RPC method hooks. + */ +async function enableDesktop( + _req, + res, + _next, + end, + { testDesktopConnection, generateOtp }, +) { + const testResult = await testDesktopConnection(); + const otp = testResult.isConnected ? await generateOtp() : undefined; + + res.result = { ...testResult, otp }; + return end(); +} diff --git a/app/scripts/lib/rpc-method-middleware/handlers/index.js b/app/scripts/lib/rpc-method-middleware/handlers/index.js index 9b0fc02fe..5f502fde5 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/index.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/index.js @@ -7,6 +7,10 @@ import sendMetadata from './send-metadata'; import switchEthereumChain from './switch-ethereum-chain'; import watchAsset from './watch-asset'; +///: BEGIN:ONLY_INCLUDE_IN(desktop) +import enableDesktop from './desktop/enable-desktop'; +///: END:ONLY_INCLUDE_IN + const handlers = [ addEthereumChain, ethAccounts, @@ -16,5 +20,8 @@ const handlers = [ sendMetadata, switchEthereumChain, watchAsset, + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + enableDesktop, + ///: END:ONLY_INCLUDE_IN ]; export default handlers; diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 7ed335bd0..633ac770d 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -41,6 +41,7 @@ export const SENTRY_STATE = { currentLocale: true, customNonceValue: true, defaultHomeActiveTabName: true, + desktopEnabled: true, featureFlags: true, firstTimeFlowType: true, forgottenPassword: true, @@ -140,23 +141,7 @@ export default function setupSentry({ release, getState }) { ], release, beforeSend: (report) => rewriteReport(report, getState), - beforeBreadcrumb(breadcrumb) { - if (getState) { - const appState = getState(); - if ( - Object.values(appState).length && - (!appState?.store?.metamask?.participateInMetaMetrics || - !appState?.store?.metamask?.completedOnboarding || - breadcrumb?.category === 'ui.input') - ) { - return null; - } - } else { - return null; - } - const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb); - return newBreadcrumb; - }, + beforeBreadcrumb: beforeBreadcrumb(getState), }); return Sentry; @@ -178,6 +163,32 @@ function hideUrlIfNotInternal(url) { return url; } +/** + * Returns a method that handles the Sentry breadcrumb using a specific method to get the extension state + * + * @param {Function} getState - A method that returns the state of the extension + * @returns {(breadcrumb: object) => object} A method that modifies a Sentry breadcrumb object + */ +export function beforeBreadcrumb(getState) { + return (breadcrumb) => { + if (getState) { + const appState = getState(); + if ( + Object.values(appState).length && + (!appState?.store?.metamask?.participateInMetaMetrics || + !appState?.store?.metamask?.completedOnboarding || + breadcrumb?.category === 'ui.input') + ) { + return null; + } + } else { + return null; + } + const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb); + return newBreadcrumb; + }; +} + /** * Receives a Sentry breadcrumb object and potentially removes urls * from its `data` property, it particular those possibly found at @@ -315,8 +326,11 @@ function rewriteErrorMessages(report, rewriteFn) { } function rewriteReportUrls(report) { - // update request url - report.request.url = toMetamaskUrl(report.request.url); + if (report.request?.url) { + // update request url + report.request.url = toMetamaskUrl(report.request.url); + } + // update exception stack trace if (report.exception && report.exception.values) { report.exception.values.forEach((item) => { @@ -330,6 +344,10 @@ function rewriteReportUrls(report) { } function toMetamaskUrl(origUrl) { + if (!globalThis.location?.origin) { + return origUrl; + } + const filePath = origUrl?.split(globalThis.location.origin)[1]; if (!filePath) { return origUrl; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f5ec93dff..092e33c95 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -176,6 +176,16 @@ import { import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware'; import { securityProviderCheck } from './lib/security-provider-helpers'; +/* eslint-disable import/first */ +/* eslint-disable import/order */ + +///: BEGIN:ONLY_INCLUDE_IN(desktop) +import { DesktopController } from '@metamask/desktop/dist/controllers/desktop'; +///: END:ONLY_INCLUDE_IN + +/* eslint-enable import/first */ +/* eslint-enable import/order */ + export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) // The process of updating the badge happens in app/scripts/background.js. @@ -650,10 +660,12 @@ export default class MetamaskController extends EventEmitter { let additionalKeyrings = [keyringBuilderFactory(QRHardwareKeyring)]; if (this.canUseHardwareWallets()) { + const keyringOverrides = this.opts.overrides?.keyrings; + const additionalKeyringTypes = [ - TrezorKeyring, - LedgerBridgeKeyring, - LatticeKeyring, + keyringOverrides?.trezor || TrezorKeyring, + keyringOverrides?.ledger || LedgerBridgeKeyring, + keyringOverrides?.lattice || LatticeKeyring, QRHardwareKeyring, ]; additionalKeyrings = additionalKeyringTypes.map((keyringType) => @@ -743,7 +755,7 @@ export default class MetamaskController extends EventEmitter { }); ///: BEGIN:ONLY_INCLUDE_IN(flask) - this.snapExecutionService = new IframeExecutionService({ + const snapExecutionServiceArgs = { iframeUrl: new URL( 'https://metamask.github.io/iframe-execution-environment/0.12.0', ), @@ -751,7 +763,11 @@ export default class MetamaskController extends EventEmitter { name: 'ExecutionService', }), setupSnapProvider: this.setupSnapProvider.bind(this), - }); + }; + this.snapExecutionService = + this.opts.overrides?.createSnapExecutionService?.( + snapExecutionServiceArgs, + ) || new IframeExecutionService(snapExecutionServiceArgs); const snapControllerMessenger = this.controllerMessenger.getRestricted({ name: 'SnapController', @@ -852,6 +868,7 @@ export default class MetamaskController extends EventEmitter { messenger: cronjobControllerMessenger, }); ///: END:ONLY_INCLUDE_IN + this.detectTokensController = new DetectTokensController({ preferences: this.preferencesController, tokensController: this.tokensController, @@ -1112,6 +1129,12 @@ export default class MetamaskController extends EventEmitter { initState.SmartTransactionsController, ); + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + this.desktopController = new DesktopController({ + initState: initState.DesktopController, + }); + ///: END:ONLY_INCLUDE_IN + // ensure accountTracker updates balances after network change this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { this.accountTracker._updateAccounts(); @@ -1219,6 +1242,9 @@ export default class MetamaskController extends EventEmitter { CronjobController: this.cronjobController, NotificationController: this.notificationController, ///: END:ONLY_INCLUDE_IN + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + DesktopController: this.desktopController.store, + ///: END:ONLY_INCLUDE_IN ...resetOnRestartStore, }); @@ -1251,6 +1277,9 @@ export default class MetamaskController extends EventEmitter { CronjobController: this.cronjobController, NotificationController: this.notificationController, ///: END:ONLY_INCLUDE_IN + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + DesktopController: this.desktopController.store, + ///: END:ONLY_INCLUDE_IN ...resetOnRestartStore, }, controllerMessenger: this.controllerMessenger, @@ -2154,6 +2183,24 @@ export default class MetamaskController extends EventEmitter { assetsContractController.getBalancesInSingleCall.bind( assetsContractController, ), + + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + getDesktopEnabled: this.desktopController.getDesktopEnabled.bind( + this.desktopController, + ), + setDesktopEnabled: this.desktopController.setDesktopEnabled.bind( + this.desktopController, + ), + generateOtp: this.desktopController.generateOtp.bind( + this.desktopController, + ), + testDesktopConnection: this.desktopController.testDesktopConnection.bind( + this.desktopController, + ), + disableDesktop: this.desktopController.disableDesktop.bind( + this.desktopController, + ), + ///: END:ONLY_INCLUDE_IN }; } @@ -2623,6 +2670,7 @@ export default class MetamaskController extends EventEmitter { // async getKeyringForDevice(deviceName, hdPath = null) { + const keyringOverrides = this.opts.overrides?.keyrings; let keyringName = null; if ( deviceName !== HardwareDeviceNames.QR && @@ -2632,16 +2680,17 @@ export default class MetamaskController extends EventEmitter { } switch (deviceName) { case HardwareDeviceNames.trezor: - keyringName = TrezorKeyring.type; + keyringName = keyringOverrides?.trezor?.type || TrezorKeyring.type; break; case HardwareDeviceNames.ledger: - keyringName = LedgerBridgeKeyring.type; + keyringName = + keyringOverrides?.ledger?.type || LedgerBridgeKeyring.type; break; case HardwareDeviceNames.qr: keyringName = QRHardwareKeyring.type; break; case HardwareDeviceNames.lattice: - keyringName = LatticeKeyring.type; + keyringName = keyringOverrides?.lattice?.type || LatticeKeyring.type; break; default: throw new Error( @@ -3999,6 +4048,11 @@ export default class MetamaskController extends EventEmitter { this.alertController.setWeb3ShimUsageRecorded.bind( this.alertController, ), + + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + testDesktopConnection: this.desktopController.testDesktopConnection, + generateOtp: this.desktopController.generateOtp, + ///: END:ONLY_INCLUDE_IN }), ); diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index a3f7d1b91..faf9af160 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -182,11 +182,12 @@ export default class ExtensionPlatform { this._showNotification(title, message); } - _showNotification(title, message, url) { + async _showNotification(title, message, url) { + const iconUrl = await browser.runtime.getURL('../../images/icon-64.png'); browser.notifications.create(url, { type: 'basic', title, - iconUrl: browser.runtime.getURL('../../images/icon-64.png'), + iconUrl, message, }); } diff --git a/development/build/config.js b/development/build/config.js index fb485c5a1..a400c8f7e 100644 --- a/development/build/config.js +++ b/development/build/config.js @@ -15,6 +15,11 @@ const configurationPropertyNames = [ 'SEGMENT_WRITE_KEY', 'SENTRY_DSN_DEV', 'SWAPS_USE_DEV_APIS', + // Desktop + 'COMPATIBILITY_VERSION_EXTENSION', + 'DISABLE_WEB_SOCKET_ENCRYPTION', + 'METAMASK_DEBUG', + 'SKIP_OTP_PAIRING_FLOW', ]; const productionConfigurationPropertyNames = [ diff --git a/development/build/scripts.js b/development/build/scripts.js index 589344a11..ba1f49c30 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -1110,7 +1110,7 @@ async function getEnvironmentVariables({ buildTarget, buildType, version }) { environment, testing, }), - METAMASK_DEBUG: devMode, + METAMASK_DEBUG: devMode || config.METAMASK_DEBUG === '1', METAMASK_ENVIRONMENT: environment, METAMASK_VERSION: version, METAMASK_BUILD_TYPE: buildType, @@ -1126,6 +1126,10 @@ async function getEnvironmentVariables({ buildTarget, buildType, version }) { SWAPS_USE_DEV_APIS: config.SWAPS_USE_DEV_APIS === '1', TOKEN_ALLOWANCE_IMPROVEMENTS: config.TOKEN_ALLOWANCE_IMPROVEMENTS === '1', TRANSACTION_SECURITY_PROVIDER: config.TRANSACTION_SECURITY_PROVIDER === '1', + // Desktop + COMPATIBILITY_VERSION_EXTENSION: config.COMPATIBILITY_VERSION_EXTENSION, + DISABLE_WEB_SOCKET_ENCRYPTION: config.DISABLE_WEB_SOCKET_ENCRYPTION === '1', + SKIP_OTP_PAIRING_FLOW: config.SKIP_OTP_PAIRING_FLOW === '1', }; } diff --git a/package.json b/package.json index 393b99c03..ee83bbb57 100644 --- a/package.json +++ b/package.json @@ -231,6 +231,7 @@ "@metamask/contract-metadata": "^2.2.0", "@metamask/controller-utils": "^1.0.0", "@metamask/design-tokens": "^1.9.0", + "@metamask/desktop": "^0.2.0", "@metamask/eth-json-rpc-infura": "^7.0.0", "@metamask/eth-keyring-controller": "^10.0.0", "@metamask/eth-ledger-bridge-keyring": "^0.13.0", diff --git a/shared/constants/app.ts b/shared/constants/app.ts index 6069ca6b0..da1e933cd 100644 --- a/shared/constants/app.ts +++ b/shared/constants/app.ts @@ -27,6 +27,7 @@ export const ENVIRONMENT_TYPE_BACKGROUND = 'background'; */ export const BuildType = { beta: 'beta', + desktop: 'desktop', flask: 'flask', main: 'main', } as const; @@ -60,6 +61,9 @@ export const MESSAGE_TYPE = { SNAP_DIALOG_CONFIRMATION: `${RestrictedMethods.snap_dialog}:confirmation`, SNAP_DIALOG_PROMPT: `${RestrictedMethods.snap_dialog}:prompt`, ///: END:ONLY_INCLUDE_IN + ///: BEGIN:ONLY_INCLUDE_IN(desktop) + ENABLE_DESKTOP: `metamask_enableDesktop`, + ///: END:ONLY_INCLUDE_IN } as const; ///: BEGIN:ONLY_INCLUDE_IN(flask) diff --git a/test/e2e/run-e2e-test.js b/test/e2e/run-e2e-test.js index 93dd9f662..92eef2533 100644 --- a/test/e2e/run-e2e-test.js +++ b/test/e2e/run-e2e-test.js @@ -92,6 +92,7 @@ async function main() { } const configFile = path.join(__dirname, '.mocharc.js'); + const extraArgs = process.env.E2E_ARGS?.split(' ') || []; const dir = 'test/test-results/e2e'; fs.mkdir(dir, { recursive: true }); @@ -104,6 +105,7 @@ async function main() { `--config=${configFile}`, `--timeout=${testTimeoutInMilliseconds}`, '--reporter=xunit', + ...extraArgs, e2eTestPath, exit, ], diff --git a/yarn.lock b/yarn.lock index 118264d0d..745820b5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3666,6 +3666,24 @@ __metadata: languageName: node linkType: hard +"@metamask/desktop@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask/desktop@npm:0.2.0" + dependencies: + "@metamask/obs-store": ^5.0.0 + eciesjs: ^0.3.15 + end-of-stream: ^1.4.4 + extension-port-stream: ^2.0.0 + loglevel: ^1.8.0 + obj-multiplex: ^1.0.0 + otpauth: ^8.0.3 + uuid: ^8.3.2 + webextension-polyfill: ^0.8.0 + ws: ^7.4.6 + checksum: 052d5dd58951c77733b538d9c392a5aa5b7d87bb600cf04a97e3213048f84936a4446adbf901258417c8eed01dbc68eb2c7117a53b6af7a416ecea975460ffb7 + languageName: node + linkType: hard + "@metamask/eslint-config-jest@npm:^9.0.0": version: 9.0.0 resolution: "@metamask/eslint-config-jest@npm:9.0.0" @@ -7537,7 +7555,7 @@ __metadata: languageName: node linkType: hard -"@types/secp256k1@npm:^4.0.1": +"@types/secp256k1@npm:^4.0.1, @types/secp256k1@npm:^4.0.3": version: 4.0.3 resolution: "@types/secp256k1@npm:4.0.3" dependencies: @@ -14292,6 +14310,17 @@ __metadata: languageName: node linkType: hard +"eciesjs@npm:^0.3.15": + version: 0.3.16 + resolution: "eciesjs@npm:0.3.16" + dependencies: + "@types/secp256k1": ^4.0.3 + futoin-hkdf: ^1.5.1 + secp256k1: ^4.0.3 + checksum: e5e6b8d8d27d8ca4aed89f79c581f7b9bd329551a2332b0a70d61c9b4e378ad98058cd7a16c9cee10cce8bef26a203865dc514ef10d732af3137ba5c13e4254d + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -17475,6 +17504,13 @@ __metadata: languageName: node linkType: hard +"futoin-hkdf@npm:^1.5.1": + version: 1.5.1 + resolution: "futoin-hkdf@npm:1.5.1" + checksum: 1912bbf6013e56ff2866590242c9493ab1fe83dc132a175378890b75008ca844524a61dbc20b3fe2c7276ea214589f92f3f0cd48e26d5f5d00c404a6201c5e23 + languageName: node + linkType: hard + "ganache@npm:^v7.0.4": version: 7.0.4 resolution: "ganache@npm:7.0.4" @@ -22386,6 +22422,13 @@ __metadata: languageName: node linkType: hard +"jssha@npm:~3.2.0": + version: 3.2.0 + resolution: "jssha@npm:3.2.0" + checksum: 2adb8a9a57a79360379e843c0548e240d072c2ef12aef39ef6a784315686bd6f65501e9353fdd2f3a604f64af07e7eab04a0ed92b221cdfea97d671d7b8e14f4 + languageName: node + linkType: hard + "jsx-ast-utils@npm:^2.4.1 || ^3.0.0": version: 3.3.2 resolution: "jsx-ast-utils@npm:3.3.2" @@ -23270,7 +23313,7 @@ __metadata: languageName: node linkType: hard -"loglevel@npm:^1.8.1": +"loglevel@npm:^1.8.0, loglevel@npm:^1.8.1": version: 1.8.1 resolution: "loglevel@npm:1.8.1" checksum: a1a62db40291aaeaef2f612334c49e531bff71cc1d01a2acab689ab80d59e092f852ab164a5aedc1a752fdc46b7b162cb097d8a9eb2cf0b299511106c29af61d @@ -24019,6 +24062,7 @@ __metadata: "@metamask/contract-metadata": ^2.2.0 "@metamask/controller-utils": ^1.0.0 "@metamask/design-tokens": ^1.9.0 + "@metamask/desktop": ^0.2.0 "@metamask/eslint-config": ^9.0.0 "@metamask/eslint-config-jest": ^9.0.0 "@metamask/eslint-config-mocha": ^9.0.0 @@ -26179,6 +26223,15 @@ __metadata: languageName: node linkType: hard +"otpauth@npm:^8.0.3": + version: 8.0.3 + resolution: "otpauth@npm:8.0.3" + dependencies: + jssha: ~3.2.0 + checksum: bc5f95194c7c942eb1d17fa0d515934803ef7db951a2e89bc31b75dfff03c47403346147b54664861720bdff82e0849ad48914e47fd84776b014d5f7ed73763c + languageName: node + linkType: hard + "outpipe@npm:^1.1.0": version: 1.1.1 resolution: "outpipe@npm:1.1.1" @@ -30405,7 +30458,7 @@ __metadata: languageName: node linkType: hard -"secp256k1@npm:^4.0.0, secp256k1@npm:^4.0.1": +"secp256k1@npm:^4.0.0, secp256k1@npm:^4.0.1, secp256k1@npm:^4.0.3": version: 4.0.3 resolution: "secp256k1@npm:4.0.3" dependencies: