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

Commit

Permalink
feat: Simplify installSnap and initSnapEnv apis (#206)
Browse files Browse the repository at this point in the history
* simplify api

* remove stray comments

* fix undefined opts

* double question mark back :)

* Update src/snap/utils.ts

* 2s for my use case

* test perf

* elegant race with bad name
  • Loading branch information
Tbaut authored and mpetrunic committed Dec 15, 2022
1 parent a0c682f commit d7c51f9
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 50 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ async function main() {
// setup dappateer and install your snap
const { snapId, metaMask, dappPage } = await dappeteer.initSnapEnv({
snapIdOrLocation: builtSnapDir
hasPermissions: true,
hasKeyPermissions: false,
});

// invoke a method from your snap that promps users with approve/reject dialog
Expand Down
4 changes: 1 addition & 3 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,6 @@ type MetaMaskOptions = {
};

type InstallSnapOptions = {
hasPermissions: boolean;
hasKeyPermissions: boolean;
customSteps?: InstallStep[];
version?: string;
installationSnapUrl?: string;
Expand Down Expand Up @@ -230,7 +228,7 @@ deletes custom network from metaMask
# Snaps methods

<a name="installSnap"></a>
## `metaMask.snaps.installSnap: (snapIdOrLocation: string, opts: { hasPermissions: boolean; hasKeyPermissions: boolean; customSteps?: InstallStep[]; version?: string;},installationSnapUrl?: string`) => Promise<string>;
## `metaMask.snaps.installSnap: (snapIdOrLocation: string, opts?: { customSteps?: InstallStep[]; version?: string;},installationSnapUrl?: string`) => Promise<string>;
installs the snap. The `snapIdOrLocation` param is either the snapId or the full path to your snap directory.

<a name="invokeSnap"></a>
Expand Down
36 changes: 24 additions & 12 deletions src/snap/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@ import {
import { DappeteerPage } from "../page";
import { EXAMPLE_WEBSITE } from "../../test/constant";
import { startSnapServer, toUrl } from "./install-utils";
import { flaskOnly } from "./utils";
import { flaskOnly, isFirstElementAppearsFirst } from "./utils";
import { InstallSnapResult } from "./types";

declare let window: { ethereum: MetaMaskInpageProvider };

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

export type InstallSnapOptions = {
hasPermissions: boolean;
hasKeyPermissions: boolean;
customSteps?: InstallStep[];
version?: string;
installationSnapUrl?: string;
Expand All @@ -29,39 +27,53 @@ export const installSnap =
(flaskPage: DappeteerPage) =>
async (
snapIdOrLocation: string,
opts: InstallSnapOptions
opts?: InstallSnapOptions
): Promise<string> => {
flaskOnly(flaskPage);
//need to open page to access window.ethereum
const installPage = await flaskPage.browser().newPage();
await installPage.goto(opts.installationSnapUrl ?? EXAMPLE_WEBSITE);
await installPage.goto(opts?.installationSnapUrl ?? EXAMPLE_WEBSITE);
let snapServer: http.Server | undefined;
if (fs.existsSync(snapIdOrLocation)) {
//snap dist location
snapServer = await startSnapServer(snapIdOrLocation);
snapIdOrLocation = `local:${toUrl(snapServer.address())}`;
}
const installAction = installPage.evaluate(
(opts: { snapId: string; version?: string }) =>
({ snapId, version }: { snapId: string; version?: string }) =>
window.ethereum.request<InstallSnapResult>({
method: "wallet_enable",
params: [
{
[`wallet_snap_${opts.snapId}`]: {
version: opts.version ?? "latest",
[`wallet_snap_${snapId}`]: {
version: version ?? "latest",
},
},
],
}),
{ snapId: snapIdOrLocation, version: opts.version }
{ snapId: snapIdOrLocation, version: opts?.version }
);

await flaskPage.bringToFront();
await flaskPage.reload();
await clickOnButton(flaskPage, "Connect");
if (opts.hasPermissions) {

const isAskingForPermissions = await isFirstElementAppearsFirst({
selectorOrXpath1: `//*[contains(text(), 'Approve & install')]`,
selectorOrXpath2: `//*[contains(text(), 'Install')]`,
page: flaskPage,
});

if (isAskingForPermissions) {
await clickOnButton(flaskPage, "Approve & install");
if (opts.hasKeyPermissions) {

const isShowingWarning = await isFirstElementAppearsFirst({
selectorOrXpath1: ".popover-wrap.snap-install-warning",
selectorOrXpath2: ".app-header__metafox-logo--icon",
page: flaskPage,
});

if (isShowingWarning) {
await flaskPage.waitForSelector(".checkbox-label", {
visible: true,
});
Expand All @@ -74,7 +86,7 @@ export const installSnap =
await clickOnButton(flaskPage, "Install");
}

for (const step of opts.customSteps ?? []) {
for (const step of opts?.customSteps ?? []) {
await step(flaskPage);
}

Expand Down
53 changes: 53 additions & 0 deletions src/snap/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DappeteerElementHandle } from "../element";
import { DappeteerPage } from "../page";

export function flaskOnly(page: DappeteerPage): void {
Expand All @@ -17,3 +18,55 @@ export function isMetaMaskErrorObject(e: unknown): boolean {
if (!("originalError" in e["data"])) return false;
return true;
}

export function isElementVisible(
page: DappeteerPage,
selector: string,
timeout = 1000
): Promise<boolean> {
return new Promise((resolve) => {
page
.waitForSelector(selector, { visible: true, timeout })
.then(() => {
resolve(true);
})
.catch(() => {
resolve(false);
});
});
}

function getWaitingPromise(
page: DappeteerPage,
selectorOrXpath: string,
timeout: number
): Promise<DappeteerElementHandle<unknown, HTMLElement>> {
if (selectorOrXpath.startsWith("//")) {
return page.waitForXPath(selectorOrXpath, { timeout });
} else {
return page.waitForSelector(selectorOrXpath, { timeout });
}
}

interface IsFirstElementAppearsFirstParams {
selectorOrXpath1: string;
selectorOrXpath2: string;
timeout?: number;
page: DappeteerPage;
}

export async function isFirstElementAppearsFirst({
selectorOrXpath1,
selectorOrXpath2,
page,
timeout = 2000,
}: IsFirstElementAppearsFirstParams): Promise<boolean> {
const promise1 = getWaitingPromise(page, selectorOrXpath1, timeout).then(
() => true
);
const promise2 = getWaitingPromise(page, selectorOrXpath2, timeout).then(
() => false
);

return await Promise.race([promise1, promise2]);
}
16 changes: 6 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,15 @@ export type Dappeteer = {
params?: Params
) => Promise<Partial<Result>>;
/**
* Installs snap. Function will throw if there is an error while installing the snap.
* @param snapIdOrLocation either the snapId or the full path to your snap directory
* where we can find the bundled snap (you need to ensure the snap is built)
* @param opts {Object} the snap method you want to invoke
* @param opts.hasPermissions Set to true if the snap uses some permissions
* @param opts.hasKeyPermissions Set to true if the snap uses the key permissions
* @param installationSnapUrl the url of your dapp. Defaults to example.org
* Installs snap. Function will throw if there is an error while installing snap.
* @param snapIdOrLocation either pass in snapId or full path to your snap directory
* where we can find bundled snap (you need to ensure snap is built)
* @param opts {Object} snap method you want to invoke
* @param installationSnapUrl url of your dapp. Defaults to example.org
*/
installSnap: (
snapIdOrLocation: string,
opts: {
hasPermissions: boolean;
hasKeyPermissions: boolean;
opts?: {
customSteps?: InstallStep[];
version?: string;
},
Expand Down
2 changes: 1 addition & 1 deletion test/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type InjectableContext = Readonly<{
flask: boolean;
}>;

export const EXAMPLE_WEBSITE = "http://example.org/";
export const EXAMPLE_WEBSITE = "http://example.org";

// TestContext will be used by all the test
export type TestContext = Mocha.Context & InjectableContext;
Expand Down
27 changes: 5 additions & 22 deletions test/flask/snaps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,15 @@ describe("snaps", function () {
});

it("should install base snap from local server", async function (this: TestContext) {
await metaMask.snaps.installSnap(this.snapServers[Snaps.BASE_SNAP], {
hasPermissions: false,
hasKeyPermissions: false,
});
await metaMask.snaps.installSnap(this.snapServers[Snaps.BASE_SNAP]);
});

it("should install permissions snap local server", async function (this: TestContext) {
await metaMask.snaps.installSnap(this.snapServers[Snaps.PERMISSIONS_SNAP], {
hasPermissions: true,
hasKeyPermissions: false,
});
await metaMask.snaps.installSnap(this.snapServers[Snaps.PERMISSIONS_SNAP]);
});

it("should install keys snap from local server", async function (this: TestContext) {
await metaMask.snaps.installSnap(this.snapServers[Snaps.KEYS_SNAP], {
hasPermissions: true,
hasKeyPermissions: true,
});
await metaMask.snaps.installSnap(this.snapServers[Snaps.KEYS_SNAP]);
});

describe("should test snap methods", function () {
Expand All @@ -56,11 +47,7 @@ describe("snaps", function () {
this.skip();
}
snapId = await metaMask.snaps.installSnap(
this.snapServers[Snaps.METHODS_SNAP],
{
hasPermissions: true,
hasKeyPermissions: false,
}
this.snapServers[Snaps.METHODS_SNAP]
);
testPage = await metaMaskPage.browser().newPage();
await testPage.goto(EXAMPLE_WEBSITE);
Expand Down Expand Up @@ -92,11 +79,7 @@ describe("snaps", function () {

it("should return all notifications", async function (this: TestContext) {
const permissionSnapId = await metaMask.snaps.installSnap(
this.snapServers[Snaps.PERMISSIONS_SNAP],
{
hasPermissions: true,
hasKeyPermissions: false,
}
this.snapServers[Snaps.PERMISSIONS_SNAP]
);

const emitter = await metaMask.snaps.getNotificationEmitter();
Expand Down

0 comments on commit d7c51f9

Please sign in to comment.