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

feat: Simplify installSnap and initSnapEnv apis #206

Merged
merged 9 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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