From 4ffc810ec0b8443f188420aacd04fc5b18a9ec21 Mon Sep 17 00:00:00 2001 From: Owen Craston Date: Mon, 27 Mar 2023 12:09:28 -0400 Subject: [PATCH] Install snap from a Dapp (#6002) - extract icon from tar file - show request permissions for install snap and account access - move snap webview to the root of the app and make it invisible - create RPC method handlers to register snaps rpc methods --- app/components/Nav/Main/RootRPCMethodsUI.js | 38 +++++++-------- app/components/Nav/Main/index.js | 4 ++ .../UI/SnapsExecutionWebView/styles.ts | 2 +- app/components/Views/Snaps/SnapsDev.tsx | 4 -- app/core/BackgroundBridge/BackgroundBridge.js | 41 +++++++++++++++- app/core/Engine.js | 16 ++++--- app/core/Snaps/SnapsState.ts | 4 +- app/core/Snaps/createSnapMethodMiddleware.ts | 47 +++++++++++++++++++ app/core/Snaps/location/npm.ts | 36 ++++++++++++-- .../Light-Swift-Untar.swift | 1 - ios/RNTar.swift | 1 - 11 files changed, 155 insertions(+), 39 deletions(-) create mode 100644 app/core/Snaps/createSnapMethodMiddleware.ts diff --git a/app/components/Nav/Main/RootRPCMethodsUI.js b/app/components/Nav/Main/RootRPCMethodsUI.js index e29aa583664..bcc6b95d45e 100644 --- a/app/components/Nav/Main/RootRPCMethodsUI.js +++ b/app/components/Nav/Main/RootRPCMethodsUI.js @@ -771,25 +771,25 @@ const RootRPCMethodsUI = (props) => { console.log('Update Snap'); break; case ApprovalTypes.REQUEST_PERMISSIONS: - if (requestData?.permissions?.eth_accounts) { - const { - metadata: { id }, - } = requestData; - - const totalAccounts = props.accountsLength; - - trackEvent(MetaMetricsEvents.CONNECT_REQUEST_STARTED, { - number_of_accounts: totalAccounts, - source: 'PERMISSION SYSTEM', - }); - - props.navigation.navigate( - ...createAccountConnectNavDetails({ - hostInfo: requestData, - permissionRequestId: id, - }), - ); - } + // eslint-disable-next-line no-case-declarations + const { + metadata: { id }, + } = requestData; + + // eslint-disable-next-line no-case-declarations + const totalAccounts = props.accountsLength; + + trackEvent(MetaMetricsEvents.CONNECT_REQUEST_STARTED, { + number_of_accounts: totalAccounts, + source: 'PERMISSION SYSTEM', + }); + + props.navigation.navigate( + ...createAccountConnectNavDetails({ + hostInfo: requestData, + permissionRequestId: id, + }), + ); break; case ApprovalTypes.CONNECT_ACCOUNTS: setHostToApprove({ data: requestData, id: request.id }); diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index c917e744195..916e41ac6e5 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -71,6 +71,7 @@ import { selectProviderConfig, selectProviderType, } from '../../../selectors/networkController'; +import { SnapsExecutionWebView } from '../../UI/SnapsExecutionWebView'; const Stack = createStackNavigator(); @@ -352,6 +353,9 @@ const Main = (props) => { ) : ( renderLoader() )} + + + diff --git a/app/components/UI/SnapsExecutionWebView/styles.ts b/app/components/UI/SnapsExecutionWebView/styles.ts index fb9e704a369..659b2428b60 100644 --- a/app/components/UI/SnapsExecutionWebView/styles.ts +++ b/app/components/UI/SnapsExecutionWebView/styles.ts @@ -5,7 +5,7 @@ import { StyleSheet } from 'react-native'; export const createStyles = () => StyleSheet.create({ webview: { - height: 60, + height: 0, // marginBottom: 50, // borderWidth: 1, // borderStyle: 'dashed', diff --git a/app/components/Views/Snaps/SnapsDev.tsx b/app/components/Views/Snaps/SnapsDev.tsx index 531a9b9bba6..8e6e5d9169b 100644 --- a/app/components/Views/Snaps/SnapsDev.tsx +++ b/app/components/Views/Snaps/SnapsDev.tsx @@ -10,7 +10,6 @@ import Button, { } from '../../../component-library/components/Buttons/Button'; import { useTheme } from '../../../util/theme'; import { getClosableNavigationOptions } from '../../UI/Navbar'; -import { SnapsExecutionWebView } from '../../UI/SnapsExecutionWebView'; import Engine from '../../../core/Engine'; import { createStyles } from './styles'; @@ -74,9 +73,6 @@ const SnapsDev = () => { return ( - - - + new Promise((resolve, reject) => { + resolve('mockAppKey'); + }), + getUnlockPromise: () => Promise.resolve(), + getSnaps: Engine.controllerMessenger.call.bind( + Engine.controllerMessenger, + 'SnapController:getPermitted', + origin, + ), + requestPermissions: async (requestedPermissions) => { + const [approvedPermissions] = + await Engine.context.PermissionController.requestPermissions( + { origin }, + requestedPermissions, + ); + + return Object.values(approvedPermissions); + }, + getPermissions: Engine.context.PermissionController.getPermissions.bind( + Engine.context.PermissionController, + origin, + ), + getAccounts: (origin) => getPermittedAccounts(origin), + installSnaps: Engine.controllerMessenger.call.bind( + Engine.controllerMessenger, + 'SnapController:install', + origin, + ), + }), + ); + // user-facing RPC methods engine.push( this.createMiddleware({ @@ -350,7 +390,6 @@ export class BackgroundBridge extends EventEmitter { }), ); } - // forward to metamask primary provider engine.push(providerAsMiddleware(provider)); return engine; diff --git a/app/core/Engine.js b/app/core/Engine.js index 37785abda8b..73911f33dcd 100644 --- a/app/core/Engine.js +++ b/app/core/Engine.js @@ -264,7 +264,9 @@ class Engine { messenger: this.controllerMessenger.getRestricted({ name: 'ApprovalController', }), - showApprovalRequest: () => null, + showApprovalRequest: () => { + console.log('Snaps/ approvalController showApprovalRequest'); + }, }); const phishingController = new PhishingController(); @@ -329,16 +331,16 @@ class Engine { this.controllerMessenger, 'SnapController:getSnapState', ), - // showConfirmation: (origin, confirmationData) => - // this.approvalController.addAndShowApprovalRequest({ - // origin, - // type: MESSAGE_TYPE.SNAP_CONFIRM, - // requestData: confirmationData, - // }), updateSnapState: this.controllerMessenger.call.bind( this.controllerMessenger, 'SnapController:updateSnapState', ), + showConfirmation: (origin, confirmationData) => + this.approvalController.addAndShowApprovalRequest({ + origin, + type: 'snapConfirmation', + requestData: confirmationData, + }), }), }); diff --git a/app/core/Snaps/SnapsState.ts b/app/core/Snaps/SnapsState.ts index 7d0153dddb4..d53fc6f5da8 100644 --- a/app/core/Snaps/SnapsState.ts +++ b/app/core/Snaps/SnapsState.ts @@ -1,6 +1,6 @@ const snapsState = { - stream: null, - webview: null, + stream: undefined, + webview: undefined, }; export default snapsState; diff --git a/app/core/Snaps/createSnapMethodMiddleware.ts b/app/core/Snaps/createSnapMethodMiddleware.ts new file mode 100644 index 00000000000..c9fb8ad6233 --- /dev/null +++ b/app/core/Snaps/createSnapMethodMiddleware.ts @@ -0,0 +1,47 @@ +import { handlers as permittedSnapMethods } from '@metamask/rpc-methods/dist/permitted'; +import { selectHooks } from '@metamask/rpc-methods/dist/utils'; +import { ethErrors } from 'eth-rpc-errors'; + +/* + copied form extension + https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js#L83 +*/ +const snapHandlerMap = permittedSnapMethods.reduce((map, handler) => { + for (const methodName of handler.methodNames) { + map.set(methodName, handler); + } + return map; +}, new Map()); + +// eslint-disable-next-line import/prefer-default-export +export function createSnapMethodMiddleware(isSnap: boolean, hooks: any) { + return async function methodMiddleware( + req: unknown, + res: unknown, + next: unknown, + end: unknown, + ) { + const handler = snapHandlerMap.get(req.method); + if (handler) { + if (/^snap_/iu.test(req.method) && !isSnap) { + return end(ethErrors.rpc.methodNotFound()); + } + + const { implementation, hookNames } = handler; + try { + // Implementations may or may not be async, so we must await them. + return await implementation( + req, + res, + next, + end, + selectHooks(hooks, hookNames), + ); + } catch (error) { + console.error(error); + return end(error); + } + } + return next(); + }; +} diff --git a/app/core/Snaps/location/npm.ts b/app/core/Snaps/location/npm.ts index c117671418b..efd662f8a9e 100644 --- a/app/core/Snaps/location/npm.ts +++ b/app/core/Snaps/location/npm.ts @@ -99,6 +99,16 @@ const readAndParseManifest = async (path: string) => { } }; +const readAndParseIcon = async (path: string) => { + try { + const iconPath = `${path}/package/images/icon.svg`; + const data = await ReactNativeBlobUtil.fs.readFile(iconPath, 'utf8'); + return data; + } catch (error) { + Logger.log(SNAPS_NPM_LOG_TAG, 'readAndParseManifest error', error); + } +}; + const fetchAndStoreNPMPackage = async ( inputRequest: RequestInfo, ): Promise => { @@ -251,7 +261,7 @@ export class NpmLocation implements SnapLocation { async #lazyInit() { assert(this.files === undefined); - const [manifest, sourceCode, actualVersion] = await fetchNpmTarball( + const [manifest, sourceCode, icon, actualVersion] = await fetchNpmTarball( this.meta.packageName, this.meta.requestedRange, this.meta.registry, @@ -286,6 +296,16 @@ export class NpmLocation implements SnapLocation { }); this.files = new Map(); + + if (icon) { + const iconVFile = new VirtualFile({ + value: icon, + path: 'images/icon.svg', + data: { canonicalPath: canonicalBase }, + }); + this.files.set('images/icon.svg', iconVFile); + } + this.files.set('snap.manifest.json', manifestVFile); this.files.set('dist/bundle.js', sourceCodeVFile); } @@ -309,7 +329,7 @@ async function fetchNpmTarball( versionRange: SemVerRange, registryUrl: string, fetchFunction: typeof fetch, -): Promise<[string, string, SemVerVersion]> { +): Promise<[string, string, string, SemVerVersion]> { const urlToFetch = new URL(packageName, registryUrl).toString(); const packageMetadata = await (await fetchFunction(urlToFetch)).json(); @@ -360,8 +380,18 @@ async function fetchNpmTarball( const manifest = await readAndParseManifest(npmPackageDataLocation); const sourceCode = await readAndParseSourceCode(npmPackageDataLocation); + let icon; + try { + icon = await readAndParseIcon(npmPackageDataLocation); + } catch (error) { + Logger.log( + `Failed to fetch icon for package "${packageName}". Using default icon instead.`, + error, + ); + } + if (!manifest || !sourceCode) { throw new Error(`Failed to fetch tarball for package "${packageName}".`); } - return [manifest, sourceCode, targetVersion]; + return [manifest, sourceCode, icon, targetVersion]; } diff --git a/ios/Light-Swift-Untar-V2/Light-Swift-Untar.swift b/ios/Light-Swift-Untar-V2/Light-Swift-Untar.swift index c0b92671301..06468a7e99f 100644 --- a/ios/Light-Swift-Untar-V2/Light-Swift-Untar.swift +++ b/ios/Light-Swift-Untar-V2/Light-Swift-Untar.swift @@ -102,7 +102,6 @@ public extension FileManager { private func writeFileData(object: Any, location _loc: UInt64, length _len: UInt64, path: String) { - let pathURL = URL(fileURLWithPath: path) let directoryPathURL = pathURL.deletingLastPathComponent() if let data = object as? Data { diff --git a/ios/RNTar.swift b/ios/RNTar.swift index 5ac0eb0bbae..9d06980ae5c 100644 --- a/ios/RNTar.swift +++ b/ios/RNTar.swift @@ -71,5 +71,4 @@ class RNTar: NSObject { rejecter("Error uncompressing file:", error.localizedDescription, error) } } - }