Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat: ability to install snap #154

Merged
merged 7 commits into from
Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@
],
"license": "MIT",
"dependencies": {
"@metamask/providers": "^9.1.0",
"@types/chai-as-promised": "^7.1.5",
"chai-as-promised": "^7.1.1",
"node-stream-zip": "^1.13.0"
},
"devDependencies": {
"@chainsafe/eslint-config": "^1.0.0",
"@jest/types": "^27.1.1",
"@metamask/snap-types": "^0.22.0",
"@metamask/snaps-cli": "^0.22.0",
"@rushstack/eslint-patch": "^1.2.0",
"@types/chai": "^4.2.22",
"@types/mocha": "^9.1.1",
Expand Down
2 changes: 2 additions & 0 deletions src/snap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { flaskOnly } from "./utils";
export { installSnap } from "./install";
88 changes: 88 additions & 0 deletions src/snap/install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { MetaMaskInpageProvider } from "@metamask/providers";
import { Page } from "puppeteer";
import {
clickOnButton,
clickOnElement,
clickOnLogo,
openProfileDropdown,
} from "../helpers";
import { flaskOnly } from "./utils";

declare let window: { ethereum: MetaMaskInpageProvider };

export type InstallStep = (page: Page) => Promise<void>;

export async function installSnap(
page: Page,
snapId: string,
opts: {
hasPermissions: boolean;
hasKeyPermissions: boolean;
customSteps?: InstallStep[];
version?: string;
}
): Promise<void> {
flaskOnly(page);
//need to open page to access window.ethereum
const installPage = await page.browser().newPage();
await installPage.goto("https://google.com");
await installPage.evaluate(
(opts: { snapId: string; version?: string }) => {
void window.ethereum.request({
BeroBurny marked this conversation as resolved.
Show resolved Hide resolved
method: "wallet_enable",
params: [
{
[`wallet_snap_${opts.snapId}`]: {
version: opts.version ?? "latest",
},
},
],
});
},
{ snapId, version: opts.version }
);
await installPage.close({ runBeforeUnload: true });
await page.bringToFront();
await page.reload();
await clickOnButton(page, "Connect");
if (opts.hasPermissions) {
await clickOnButton(page, "Approve & install");
if (opts.hasKeyPermissions) {
const checkbox = await page.waitForSelector(".checkbox-label", {
visible: true,
});
await checkbox.click();
await clickOnButton(page, "Confirm");
}
} else {
await clickOnButton(page, "Install");
}
for (const step of opts.customSteps ?? []) {
await step(page);
}
if (!(await isSnapInstalled(page, snapId))) {
throw new Error("Failed to install snap " + snapId);
}
}

export async function isSnapInstalled(
page: Page,
snapId: string
): Promise<boolean> {
await page.bringToFront();
await openProfileDropdown(page);

await clickOnElement(page, "Settings");
await clickOnElement(page, "Snaps");
let found = false;
try {
await page.waitForXPath(`//*[contains(text(), '${snapId}')]`, {
timeout: 5000,
});
found = true;
} catch (e) {
found = false;
}
await clickOnLogo(page);
return found;
}
10 changes: 10 additions & 0 deletions src/snap/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Page } from "puppeteer";
import { DappeteerBrowser } from "../setup";

export function flaskOnly(page: Page): void {
if ((page.browser() as DappeteerBrowser).flask == null) {
throw new Error(
"This method is only available when running Metamask Flask"
);
}
}
8 changes: 4 additions & 4 deletions test/constant.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import http from "http";

import { Provider, Server } from "ganache";
import { Browser } from "puppeteer";

import { Dappeteer } from "../src";
import { Dappeteer, DappeteerBrowser } from "../src";

import { Contract } from "./deploy";
import { Contract, Snaps } from "./deploy";

export type InjectableContext = Readonly<{
provider: Provider;
ethereum: Server<"ethereum">;
testPageServer: http.Server;
browser: Browser;
snapServers?: Record<Snaps, http.Server>;
browser: DappeteerBrowser;
metamask: Dappeteer;
contract: Contract;
flask: boolean;
Expand Down
47 changes: 47 additions & 0 deletions test/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import handler from "serve-handler";
import Web3 from "web3";

import { compileContracts } from "./contract";
import { toUrl } from "./utils/utils";

const counterContract: { address: string } | null = null;

Expand Down Expand Up @@ -77,3 +78,49 @@ export async function startTestServer(): Promise<http.Server> {
});
return server;
}

export enum Snaps {
BASE_SNAP = "base-snap",
KEYS_SNAP = "keys-snap",
PERMISSIONS_SNAP = "permissions-snap",
}

export async function startSnapServers(): Promise<Record<Snaps, http.Server>> {
return {
[Snaps.BASE_SNAP]: await startSnapServer(Snaps.BASE_SNAP),
[Snaps.KEYS_SNAP]: await startSnapServer(Snaps.KEYS_SNAP),
[Snaps.PERMISSIONS_SNAP]: await startSnapServer(Snaps.PERMISSIONS_SNAP),
};
}

async function startSnapServer(snap: Snaps): Promise<http.Server> {
console.log(`Starting ${snap} server...`);
const server = http.createServer((req, res) => {
void handler(req, res, {
public: path.resolve(__dirname, `./flask/${snap}`),
headers: [
{
source: "**/*",
headers: [
{
key: "Cache-Control",
value: "no-cache",
},
{
key: "Access-Control-Allow-Origin",
value: "*",
},
],
},
],
});
});

await new Promise<void>((resolve) => {
server.listen(0, () => {
console.log(`Server for ${snap} running at `, toUrl(server.address()));
resolve();
});
});
return server;
}
65 changes: 65 additions & 0 deletions test/flask/base-snap/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!doctype html>
<html>
</head>
<title>Hello, Snaps!</title>
<link rel="icon" type="image/svg" href="./images/icon.svg"/>
</head>

<body>
<h1>Hello, Snaps!</h1>
<details>
<summary>Instructions</summary>
<ul>
<li>First, click "Connect". Then, try out the other buttons!</li>
<li>Please note that:</li>
<ul>
<li>
The <code>snap.manifest.json</code> and <code>package.json</code> must be located in the server root directory...
</li>
<li>
The Snap bundle must be hosted at the location specified by the <code>location</code> field of <code>snap.manifest.json</code>.
</li>
</ul>
</ul>
</details>
<br/>

<button class="connect">Connect</button>
<button class="sendHello">Send Hello</button>
</body>

<script>
const snapId = `local:${window.location.href}`;

const connectButton = document.querySelector('button.connect')
const sendButton = document.querySelector('button.sendHello')

connectButton.addEventListener('click', connect)
sendButton.addEventListener('click', send)

// here we get permissions to interact with and install the snap
async function connect () {
await ethereum.request({
method: 'wallet_enable',
params: [{
wallet_snap: { [snapId]: {} },
}]
})
}

// here we call the snap's "hello" method
async function send () {
try {
await ethereum.request({
method: 'wallet_invokeSnap',
params: [snapId, {
method: 'hello'
}]
})
} catch (err) {
console.error(err)
alert('Problem happened: ' + err.message || err)
}
}
</script>
</html>
18 changes: 18 additions & 0 deletions test/flask/base-snap/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "base-snap",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"prefix": "mm-snap build",
"fix": "mm-snap manifest --fix"
},
"devDependencies": {
"@metamask/snaps-cli": "^0.22.0"
},
"author": "",
"license": "ISC",
"dependencies": {
"@metamask/snap-types": "^0.22.0"
}
}
6 changes: 6 additions & 0 deletions test/flask/base-snap/snap.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
cliOptions: {
src: './src/index.ts',
port: 8080,
},
};
17 changes: 17 additions & 0 deletions test/flask/base-snap/snap.manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "1.0.0",
"description": "An example Snap written in TypeScript.",
"proposedName": "Base Snap\n",
"source": {
"shasum": "sApNS/DJjjs8psniSQzWlOdkzJ75IsbtlpdipfKarRI=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
"packageName": "base-snap",
"registry": "https://registry.npmjs.org/"
}
}
},
"initialPermissions": {},
"manifestVersion": "0.1"
}
21 changes: 21 additions & 0 deletions test/flask/base-snap/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { OnRpcRequestHandler } from "@metamask/snap-types";

export const onRpcRequest: OnRpcRequestHandler = ({ origin, request }) => {
switch (request.method) {
case "hello":
return wallet.request({
method: "snap_confirm",
params: [
{
prompt: `Hello, ${origin}!`,
description:
"This custom confirmation is just for display purposes.",
textAreaContent:
"But you can edit the snap source code to make it do something, if you want to!",
},
],
});
default:
throw new Error("Method not found.");
}
};