Skip to content

Commit

Permalink
Install snap from a Dapp (#6002)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
owencraston authored Mar 27, 2023
1 parent e6eb204 commit 4ffc810
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 39 deletions.
38 changes: 19 additions & 19 deletions app/components/Nav/Main/RootRPCMethodsUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
4 changes: 4 additions & 0 deletions app/components/Nav/Main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
selectProviderConfig,
selectProviderType,
} from '../../../selectors/networkController';
import { SnapsExecutionWebView } from '../../UI/SnapsExecutionWebView';

const Stack = createStackNavigator();

Expand Down Expand Up @@ -352,6 +353,9 @@ const Main = (props) => {
) : (
renderLoader()
)}
<View>
<SnapsExecutionWebView />
</View>
<GlobalAlert />
<FadeOutOverlay />
<Notification navigation={props.navigation} />
Expand Down
2 changes: 1 addition & 1 deletion app/components/UI/SnapsExecutionWebView/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 0 additions & 4 deletions app/components/Views/Snaps/SnapsDev.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -74,9 +73,6 @@ const SnapsDev = () => {

return (
<View style={styles.container}>
<View style={styles.webviewContainer}>
<SnapsExecutionWebView />
</View>
<TextInput
style={styles.input}
onChangeText={setSnapInput}
Expand Down
41 changes: 40 additions & 1 deletion app/core/BackgroundBridge/BackgroundBridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
import RemotePort from './RemotePort';
import WalletConnectPort from './WalletConnectPort';
import Port from './Port';
import { createSnapMethodMiddleware } from '../Snaps/createSnapMethodMiddleware';
import { getPermittedAccounts } from '../Permissions';

const createFilterMiddleware = require('eth-json-rpc-filters');
const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager');
Expand Down Expand Up @@ -333,6 +335,44 @@ export class BackgroundBridge extends EventEmitter {
engine.push(subscriptionManager.middleware);
// watch asset

// Snaps middleware
/*
from extension https://github.dev/MetaMask/metamask-extension/blob/1d5e8a78400d7aaaf2b3cbdb30cff9399061df34/app/scripts/metamask-controller.js#L3830-L3861
*/
engine.push(
createSnapMethodMiddleware(true, {
getAppKey: async () =>
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({
Expand All @@ -350,7 +390,6 @@ export class BackgroundBridge extends EventEmitter {
}),
);
}

// forward to metamask primary provider
engine.push(providerAsMiddleware(provider));
return engine;
Expand Down
16 changes: 9 additions & 7 deletions app/core/Engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ class Engine {
messenger: this.controllerMessenger.getRestricted({
name: 'ApprovalController',
}),
showApprovalRequest: () => null,
showApprovalRequest: () => {
console.log('Snaps/ approvalController showApprovalRequest');
},
});

const phishingController = new PhishingController();
Expand Down Expand Up @@ -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,
}),
}),
});

Expand Down
4 changes: 2 additions & 2 deletions app/core/Snaps/SnapsState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const snapsState = {
stream: null,
webview: null,
stream: undefined,
webview: undefined,
};

export default snapsState;
47 changes: 47 additions & 0 deletions app/core/Snaps/createSnapMethodMiddleware.ts
Original file line number Diff line number Diff line change
@@ -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();
};
}
36 changes: 33 additions & 3 deletions app/core/Snaps/location/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -286,6 +296,16 @@ export class NpmLocation implements SnapLocation {
});

this.files = new Map<string, VirtualFile>();

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);
}
Expand All @@ -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();

Expand Down Expand Up @@ -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];
}
1 change: 0 additions & 1 deletion ios/Light-Swift-Untar-V2/Light-Swift-Untar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion ios/RNTar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,4 @@ class RNTar: NSObject {
rejecter("Error uncompressing file:", error.localizedDescription, error)
}
}

}

0 comments on commit 4ffc810

Please sign in to comment.