1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

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.
This commit is contained in:
Matthew Walsh 2023-02-20 17:13:12 +00:00 committed by GitHub
parent 0af56b1b1e
commit cc99a25228
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 285 additions and 33 deletions

View File

@ -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
}
//

View File

@ -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<string, Function>} EthAccountsOptions
* @property {Function} getAccounts - Gets the accounts for the requesting
* origin.
*/
/**
*
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} _req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<true>} 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();
}

View File

@ -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;

View File

@ -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) {
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;

View File

@ -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
}),
);

View File

@ -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,
});
}

View File

@ -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 = [

View File

@ -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',
};
}

View File

@ -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",

View File

@ -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)

View File

@ -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,
],

View File

@ -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: