Skip to content
This repository has been 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 5 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
37 changes: 25 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, isElementVisible } 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,54 @@ 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) {

// if the snap is requesting for permissions
const isAskingForPermissions = await isElementVisible(
flaskPage,
".permissions-connect-permission-list"
);

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

// if the snap requires key permissions
// a dedicated warning will apprear
const isShowingWarning = await isElementVisible(
flaskPage,
".popover-wrap.snap-install-warning"
);

if (isShowingWarning) {
await flaskPage.waitForSelector(".checkbox-label", {
visible: true,
});
Expand All @@ -74,7 +87,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
17 changes: 17 additions & 0 deletions src/snap/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,20 @@ export function isMetaMaskErrorObject(e: unknown): boolean {
if (!("originalError" in e["data"])) return false;
return true;
}

export function isElementVisible(
page: DappeteerPage,
selector: string,
timeout = 2000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 sec seems a lot for checking if an element is visible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, only 1 second was mostly not enough the 2 times I used this function. I can have a 1s be the default and still use 2 sec in my particular test.

The very good part about this, is that after all it'll be a maximum 2s so if the element is there in less time, this function will actually take less.

Suggested change
timeout = 2000
timeout = 1000

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah now you see why I've added hasPermissions and hasKeyPermissions options is to avoid waiting for that timeout if element will never exist 😛

Copy link
Contributor Author

@Tbaut Tbaut Dec 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right haha I knew before! But I think it makes the api so much cleaner that way!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks to Bero's push, we came up with something even more elegant. I'm now racing between 2 possible elements that could appear, the issue of the race telling us in which state we are in. It's relatively elegant beside the bad name I came up with.

): Promise<boolean> {
return new Promise((resolve) => {
page
.waitForSelector(selector, { visible: true, timeout })
.then(() => {
resolve(true);
})
.catch(() => {
resolve(false);
});
});
}
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