Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebHID endowment #1219

Closed
wants to merge 11 commits into from
Closed
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
6 changes: 3 additions & 3 deletions packages/snaps-controllers/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 89.19,
"functions": 95.01,
"lines": 96.45,
"statements": 96.35
"functions": 95.04,
"lines": 96.47,
"statements": 96.37
}
1 change: 1 addition & 0 deletions packages/snaps-controllers/src/snaps/endowments/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export enum SnapEndowments {
EthereumProvider = 'endowment:ethereum-provider',
Rpc = 'endowment:rpc',
WebAssemblyAccess = 'endowment:webassembly',
WebHID = 'endowment:webhid',
}
2 changes: 2 additions & 0 deletions packages/snaps-controllers/src/snaps/endowments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
transactionInsightEndowmentBuilder,
} from './transaction-insight';
import { webAssemblyEndowmentBuilder } from './web-assembly';
import { webhidEndowmentBuilder } from './webhid';

export const endowmentPermissionBuilders = {
[networkAccessEndowmentBuilder.targetKey]: networkAccessEndowmentBuilder,
Expand All @@ -38,6 +39,7 @@ export const endowmentPermissionBuilders = {
ethereumProviderEndowmentBuilder,
[rpcEndowmentBuilder.targetKey]: rpcEndowmentBuilder,
[webAssemblyEndowmentBuilder.targetKey]: webAssemblyEndowmentBuilder,
[webhidEndowmentBuilder.targetKey]: webhidEndowmentBuilder,
} as const;

export const endowmentCaveatSpecifications = {
Expand Down
18 changes: 18 additions & 0 deletions packages/snaps-controllers/src/snaps/endowments/webhid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PermissionType } from '@metamask/permission-controller';

import { SnapEndowments } from './enum';
import { webhidEndowmentBuilder } from './webhid';

describe('endowment:webhid', () => {
it('builds the expected permission specification', () => {
const specification = webhidEndowmentBuilder.specificationBuilder({});
expect(specification).toStrictEqual({
permissionType: PermissionType.Endowment,
targetKey: SnapEndowments.WebHID,
endowmentGetter: expect.any(Function),
allowedCaveats: null,
});

expect(specification.endowmentGetter()).toStrictEqual(['navigator']);
});
});
44 changes: 44 additions & 0 deletions packages/snaps-controllers/src/snaps/endowments/webhid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
EndowmentGetterParams,
PermissionSpecificationBuilder,
PermissionType,
ValidPermissionSpecification,
} from '@metamask/permission-controller';

import { SnapEndowments } from './enum';

const permissionName = SnapEndowments.WebHID;

type WebHIDEndowmentSpecification = ValidPermissionSpecification<{
permissionType: PermissionType.Endowment;
targetKey: typeof permissionName;
endowmentGetter: (_options?: any) => ['navigator'];
allowedCaveats: null;
}>;

/**
* `endowment:webhid` returns the name of global browser API(s) that
* enable the usage of navigator.webhid. This enables the connection of devices that uses webhid such as Ledger.
*
* @param _builderOptions - Optional specification builder options.
* @returns The specification for the webhid endowment.
*/
const specificationBuilder: PermissionSpecificationBuilder<
PermissionType.Endowment,
any,
WebHIDEndowmentSpecification
> = (_builderOptions?: any) => {
return {
permissionType: PermissionType.Endowment,
targetKey: permissionName,
allowedCaveats: null,
endowmentGetter: (_getterOptions?: EndowmentGetterParams) => {
return ['navigator'];
},
};
};

export const webhidEndowmentBuilder = Object.freeze({
targetKey: permissionName,
specificationBuilder,
} as const);
6 changes: 3 additions & 3 deletions packages/snaps-execution-environments/coverage.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"branches": 77.41,
"functions": 91.26,
"lines": 85.23,
"statements": 85.32
"functions": 91.33,
"lines": 85.29,
"statements": 85.37
}
1 change: 1 addition & 0 deletions packages/snaps-execution-environments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@metamask/eslint-config-typescript": "^11.0.0",
"@types/jest": "^27.5.1",
"@types/node": "^17.0.36",
"@types/w3c-web-hid": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"@wdio/browser-runner": "^8.3.10",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import network from './network';
import textDecoder from './textDecoder';
import textEncoder from './textEncoder';
import timeout from './timeout';
import webhid from './webhid';

export type EndowmentFactory = {
names: readonly string[];
Expand Down Expand Up @@ -58,6 +59,7 @@ const buildCommonEndowments = (): EndowmentFactory[] => {
textDecoder,
textEncoder,
date,
webhid,
];

commonEndowments.forEach((endowmentSpecification) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ describe('endowments', () => {
factory: expect.any(Function),
names: ['Date'],
},
{ factory: expect.any(Function), names: ['navigator'] },
{
factory: expect.any(Function),
names: ['AbortController'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { rootRealmGlobal } from '../globalObject';
import Webhid from './webhid';

describe('Webhid endowment', () => {
it('has expected properties', () => {
expect(Webhid).toMatchObject({
names: ['navigator'],
factory: expect.any(Function),
});
});

it('does not return the navigator from rootRealmGlobal', () => {
const { navigator } = Webhid.factory();
expect(navigator).not.toStrictEqual(rootRealmGlobal.navigator);
});

it('contains the hid key only', () => {
const { navigator } = Webhid.factory();
expect(Object.keys(navigator)).toContain('hid');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { rootRealmGlobal } from '../globalObject';

/**
* Create a {@link navigator} object, with the same properties as the global
* {@link navigator} object, but only with access to hid.
*
* @returns The {@link navigator} object with only access to hid.
*/
function createHID() {
Copy link
Member

Choose a reason for hiding this comment

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

We need to have a discussion about this endowment in general since it is not available in Firefox. Any snap that tries to leverage it will not work in FF. In the past we have not added endowments that aren't platform agnostic.

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps the snap manifest can have a target field?

{
  "targets": {
    "chrome": ">=89",
  },
}

return {
navigator: {
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/hid
// https://wicg.github.io/webhid/#dom-navigator-hid
hid: rootRealmGlobal.navigator?.hid,
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be hardened using harden. Can you verify that it still works after doing that?

Also, I wonder if there is more locking down of the object required since it is an EventTarget 🤔

Thoughts @weizman

},
};
}

const endowmentModule = {
names: ['navigator'] as const,
factory: createHID,
};

export default endowmentModule;
2 changes: 1 addition & 1 deletion packages/snaps-utils/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"branches": 92.92,
"functions": 100,
"lines": 98.94,
"statements": 98.95
"statements": 98.96
}
4 changes: 4 additions & 0 deletions packages/snaps-utils/src/iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export async function createWindow(
// MDN article for `load` event: https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
// Re: `load` firing twice: https://stackoverflow.com/questions/10781880/dynamically-created-iframe-triggers-onload-event-twice/15880489#15880489
iframe.setAttribute('src', uri);

// Enable WebHID to be executed in the iframe.
iframe.setAttribute('allow', 'hid');
Copy link
Member

Choose a reason for hiding this comment

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

Would be interesting if we could set this conditionally based on the permission 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The snaps iframe is created before the snaps controller, so i'm not sure if we can conditionally set that.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, it would be better if we didn't give HID to any iframes that don't need it, hmm 🤔

Copy link
Member

@Mrtenz Mrtenz Mar 14, 2023

Choose a reason for hiding this comment

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

The iframe is created by the execution service, after the snaps controller starts a snap, right? We could pass the endowments to the execution service when creating the iframe, and only set this attribute based on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

@FrederikBolding FrederikBolding Mar 15, 2023

Choose a reason for hiding this comment

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

Maarten is right, the iframe is created when executeSnap is called on an execution service. The IframeExecutionService does not create the iframe before it is needed to execute snap code. So we can pass along the endowments from snapData and use that.

For reference:
https://github.com/MetaMask/snaps-monorepo/blob/main/packages/snaps-controllers/src/services/AbstractExecutionService.ts#L340

https://github.com/MetaMask/snaps-monorepo/blob/main/packages/snaps-controllers/src/services/AbstractExecutionService.ts#L220

https://github.com/MetaMask/snaps-monorepo/blob/main/packages/snaps-controllers/src/services/iframe/IframeExecutionService.ts#L40


document.body.appendChild(iframe);

iframe.addEventListener('load', () => {
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3131,6 +3131,7 @@ __metadata:
"@metamask/utils": ^3.4.1
"@types/jest": ^27.5.1
"@types/node": ^17.0.36
"@types/w3c-web-hid": ^1.0.3
"@typescript-eslint/eslint-plugin": ^5.42.1
"@typescript-eslint/parser": ^5.42.1
"@wdio/browser-runner": ^8.3.10
Expand Down Expand Up @@ -4416,6 +4417,13 @@ __metadata:
languageName: node
linkType: hard

"@types/w3c-web-hid@npm:^1.0.3":
version: 1.0.3
resolution: "@types/w3c-web-hid@npm:1.0.3"
checksum: 90ee1eeb2acf5d5ddf0b7acefd4f8aaa7d0175d991c3606a9ad62bdfa7a8de93665f5f6218dc4ecb34ea1d2f3e357813b315f46c1ea6b8aa1693e217e436c9b2
languageName: node
linkType: hard

"@types/watchify@npm:^3.11.1":
version: 3.11.1
resolution: "@types/watchify@npm:3.11.1"
Expand Down