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 support for Metamask Snap #1327

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bedc510
add snap support
Nick-1979 Dec 24, 2023
b40edf8
Update utils.ts
Nick-1979 Dec 30, 2023
801dd71
Update defaults.ts
Nick-1979 Dec 30, 2023
deb3cbf
fix firefox issue
Nick-1979 Jan 5, 2024
930f03d
Update defaults.ts
Nick-1979 Jan 5, 2024
8dabb51
Update utils.ts
Nick-1979 Jan 5, 2024
0df7b7e
update copyright
Nick-1979 Jan 14, 2024
792c474
Update defaults.ts
Nick-1979 Jan 20, 2024
2d32950
Update utils.ts
Nick-1979 Jan 20, 2024
da59ba3
Update defaults.ts
Nick-1979 Jan 25, 2024
7761c78
Create index.spec.ts
AMIRKHANEF Jan 20, 2024
d5fa1d6
Update tests
AMIRKHANEF Jan 21, 2024
0cd6409
Update index.spec.ts
Nick-1979 Jan 25, 2024
4d6550c
rename to polkagate snap
Nick-1979 Feb 13, 2024
0a56ef4
Update utils.ts
Nick-1979 Mar 14, 2024
e40bc58
update copyright date
Nick-1979 Mar 14, 2024
7bcf7e5
fix ci issues
Nick-1979 Mar 14, 2024
b059e27
add snap only enable
Nick-1979 Apr 6, 2024
ce54241
fix overwrite issue if using w3ux
Nick-1979 Apr 6, 2024
819babf
fix linting
Nick-1979 Apr 6, 2024
3d66f97
Update bundle.ts
Nick-1979 Apr 6, 2024
fa9cec1
prevent enabling extensions if a dapp has requested snap_only to just…
Nick-1979 Apr 10, 2024
f1dc74c
remove DEFAULTS
Nick-1979 Jun 27, 2024
5506b26
refactor
Nick-1979 Jul 2, 2024
2085336
build
Nick-1979 Jul 2, 2024
bd662d7
resolve conflicts
Nick-1979 Jul 4, 2024
526a273
update polkadot types
Nick-1979 Jul 4, 2024
fa4aedf
Update yarn.lock
Nick-1979 Jul 4, 2024
0ec598a
Merge branch 'resolve-minor-confilicts' of https://github.com/PolkaGa…
Nick-1979 Jul 31, 2024
e23e2dc
sync with origin
Nick-1979 Jul 31, 2024
7a0f23a
fix related linting
Nick-1979 Jul 31, 2024
86cecfe
fix new type issue
Nick-1979 Jul 31, 2024
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: 2 additions & 0 deletions packages/extension-dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"version": "0.50.2-1-x",
"main": "index.js",
"dependencies": {
"@metamask/utils": "^9.0.0",
"@polkadot/extension-inject": "0.50.2-1-x",
"@polkadot/types": "^12.2.1",
"@polkadot/util": "^13.0.2",
"@polkadot/util-crypto": "^13.0.2",
"tslib": "^2.6.2"
Expand Down
48 changes: 46 additions & 2 deletions packages/extension-dapp/src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import { isPromise, objectSpread, u8aEq } from '@polkadot/util';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';

import { injectedMetamaskSnap } from './snap/index.js';
import { SNAPS } from './snap/snapList.js';
import { hasMetamask } from './snap/utils.js';
import { documentReadyPromise } from './util.js';

// expose utility functions
Expand Down Expand Up @@ -60,10 +63,51 @@

/** @internal retrieves all the extensions available on the window */
function getWindowExtensions (originName: string): Promise<InjectedExtension[]> {
/** Since web3Enable enables all available extensions, which is the default behavior
* for Polkadot JS apps, some dapps, like the Staking dashboard, provide an extension
* list where users can choose specific extensions to enable. Therefore, we utilize
* "Snap only" to inject snaps as an additional feature, suitable for such dapps.
* */
const isSnapOnlyRequested = ['onlysnap', 'only_snap', 'snaponly', 'snap_only'].includes(originName.toLowerCase());
const extensions = isSnapOnlyRequested
? Object.fromEntries(
Object.entries(SNAPS).map(([origin, { name }]) => [
name,
injectedMetamaskSnap(origin),

Check failure on line 76 in packages/extension-dapp/src/bundle.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Unexpected trailing comma
])
)
: win.injectedWeb3;

/** inject snaps into window */
if (hasMetamask) {
Object.entries(SNAPS).map(([origin, { name }]) => {

Check failure on line 83 in packages/extension-dapp/src/bundle.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Array.prototype.map() expects a return value from arrow function
win.injectedWeb3[name] = injectedMetamaskSnap(origin)

Check failure on line 84 in packages/extension-dapp/src/bundle.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Missing semicolon
})

Check failure on line 85 in packages/extension-dapp/src/bundle.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Missing semicolon
}

if (isSnapOnlyRequested) {
return Promise
.all(
Object
.entries(extensions)
.map(([nameOrHash, { version }]): Promise<(InjectedExtension | void)> =>
Promise
.resolve()
.then(() =>
objectSpread<InjectedExtension>({ name: nameOrHash, version: version || 'unknown' })
)
.catch(({ message }: Error): void => {
console.error(`Error injecting ${nameOrHash}: ${message}`);
})
)
)
.then((exts) => exts.filter((e): e is InjectedExtension => !!e));
}

return Promise
.all(
Object
.entries(win.injectedWeb3)
.entries(extensions)
.map(([nameOrHash, { connect, enable, version }]): Promise<(InjectedExtension | void)> =>
Promise
.resolve()
Expand Down Expand Up @@ -130,7 +174,7 @@
.catch(console.error);

return (): void => {
// no ubsubscribe needed, this is a single-shot
// no unsubscribe needed, this is a single-shot
};
};
}
Expand Down
224 changes: 224 additions & 0 deletions packages/extension-dapp/src/snap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Copyright 2019-2024 @polkadot/extension-dapp authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { InjectedAccount, InjectedExtension, InjectedMetadata, InjectedMetadataKnown, InjectedWindowProvider, MetadataDef } from '@polkadot/extension-inject/types';
import type { SignerPayloadJSON, SignerPayloadRaw, SignerResult } from '@polkadot/types/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { InvokeSnapResult, RequestSnapsResult, Snap, SnapRpcRequestParams } from './types';

import { SNAPS } from './snapList.js';

export default class Metadata implements InjectedMetadata {
private snapId: string;

constructor (snapId: string) {
this.snapId = snapId;
}

public get (): Promise<InjectedMetadataKnown[]> {
return getMetaDataList(this.snapId);
}

public provide (definition: MetadataDef): Promise<boolean> {
return setMetadata(definition, this.snapId);
}
}

/** @internal Requests permission for a dapp to communicate with the specified Snaps and attempts to install them if they're not already installed. */
const connectSnap = async (origin: string): Promise<RequestSnapsResult> => {
const { version } = SNAPS[origin];

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await window.ethereum.request({
method: 'wallet_requestSnaps',
params: {
[origin]: {
version
}
}
}) as RequestSnapsResult;
};

/** @internal Invokes a method on a Snap and returns the result. */
const invokeSnap = async (args: SnapRpcRequestParams): Promise<InvokeSnapResult> => {
console.info('args in invoke Snap:', args);

const snapId = args.snapId;
const request = {
method: args.method,
params: args?.params || []
};

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {
request,
snapId
}
}) as InvokeSnapResult;
};

/** @internal Gets the list of Snap accounts available in the connected wallet. */
const getSnapAccounts = (
// anyType?: boolean,
snapId: string
): () => Promise<InjectedAccount[]> => {
return async (): Promise<InjectedAccount[]> => {
const _addressAnyChain = await invokeSnap({
method: 'getAddress',
// params: { chainName: anyType ? 'any' : undefined }, // if we can have chainName here, we can show formatted address to users
snapId
});

const account = {
address: _addressAnyChain,
name: 'Metamask account 🍻',
type: 'sr25519' as KeypairType
};

return [account] as InjectedAccount[];
};
};

/** @internal Requests the Snap to sign a JSON payload with the connected wallet. */
const requestSignJSON = (snapId: string) => {
return async (
payload: SignerPayloadJSON
): Promise<SignerResult> => {
return await invokeSnap({
method: 'signJSON',
params: { payload },
snapId
}) as unknown as SignerResult;
};
};

/** @internal Requests the Snap to sign a raw payload with the connected wallet. */
const requestSignRaw = (snapId: string) => {
return async (
raw: SignerPayloadRaw
): Promise<SignerResult> => {
return await invokeSnap({
method: 'signRaw',
params: { raw },
snapId
}) as unknown as SignerResult;
};
};

/**
* @summary Retrieves a list of known metadata related to Polkadot eco chains.
* @description
* This function sends a request to the connected Snap to retrieve a list of known metadata.
* The metadata includes information about Polkadot eco chains and other relevant details.
* This information is stored and retrieved from the local state of Metamask Snaps.
*/
export const getMetaDataList = async (snapId: string): Promise<InjectedMetadataKnown[]> => {
return await invokeSnap({
method: 'getMetadataList',
params: {},
snapId
}) as unknown as InjectedMetadataKnown[];
};

/**
* @summary Sets metadata related to Polkadot eco chains using Metamask Snaps.
* @description
* This function sends a request to the connected Snap to set metadata.
* The provided `metaData` object contains the information to be set. This data is stored using
* the local state of Metamask Snaps for future use.
*/
export const setMetadata = async (metaData: MetadataDef, snapId: string): Promise<boolean> => {
return await invokeSnap({
method: 'setMetadata',
params: { metaData },
snapId
}) as boolean;
};

/** @internal Creates a subscription manager for notifying subscribers about changes in injected accounts. */
export const snapSubscriptionManager = (snapId: string) => {
return () => {
let subscribers: ((accounts: InjectedAccount[]) => void | Promise<void>)[] = [];

/** Subscribe to changes in injected accounts. The callback function to be invoked when changes in injected accounts occur. */
const subscribe = (callback: (accounts: InjectedAccount[]) => void | Promise<void>) => {
subscribers.push(callback);

return () => {
subscribers = subscribers.filter((subscriber) => subscriber !== callback);

getSnapAccounts(snapId)()
.then(callback)
.catch(console.error);
};
};

/** Notify all subscribers about changes in injected accounts. */
const notifySubscribers = (accounts: InjectedAccount[]) => {
subscribers.forEach((callback) => callback(accounts) as void);
};

return { notifySubscribers, subscribe };
};
};

/** @internal This object encapsulates the functionality of Metamask Snap for seamless integration with dApps. */
const metamaskSnap = (snapId: string): InjectedExtension => {
const { name, version } = SNAPS[snapId];

return {
accounts: {
get: getSnapAccounts(snapId),
subscribe: snapSubscriptionManager(snapId)().subscribe
},
metadata: new Metadata(snapId),
name,
// provider?: InjectedProvider,
signer: {
signPayload: requestSignJSON(snapId),
signRaw: requestSignRaw(snapId)
// update?: (id: number, status: H256 | ISubmittableResult) => void
},
version
};
};

/** @internal Connects to a specified dApp using the Snap API. */
const connect = (
origin: string
) => {
return async (appName: string) => {
const { name } = SNAPS[origin];

console.info(`${name} is connecting to ${appName} ...`);
const response = await connectSnap(origin);

if (!response?.[origin]) {
throw new Error(`Something went wrong while connecting to the snap:${origin}`);
}

return {
...metamaskSnap(origin),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
version: (response[origin] as Snap)?.version
};
};
};

/**
* @summary Injected Metamask Snaps for dApp connection.
* @description
* Provides the necessary functionality to connect the injected Metamask Snaps to a dApp.
* The version property represents the version of the injected Metamask Snaps.
*/
export const injectedMetamaskSnap = (origin: string): InjectedWindowProvider => {
const { version } = SNAPS[origin];

return {
connect: connect(origin),
enable: connect(origin),
version
};
};
11 changes: 11 additions & 0 deletions packages/extension-dapp/src/snap/snapList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2019-2024 @polkadot/extension-dapp authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { SupportedSnaps } from './types';

export const SNAPS: SupportedSnaps = {
'npm:@polkagate/snap': {
name: 'polkagate-snap',
version: '>=0.3.0'
}
};
53 changes: 53 additions & 0 deletions packages/extension-dapp/src/snap/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2019-2024 @polkadot/extension-dapp authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Json, JsonRpcError, Opaque, SemVerVersion } from '@metamask/utils';

export type SnapId = Opaque<string, typeof snapIdSymbol>;
declare const snapIdSymbol: unique symbol;

/**
* The result returned by the `wallet_requestSnaps` method.
*
* It consists of a map of Snap IDs to either the Snap object or an error.
*/
export type RequestSnapsResult = Record<string, Snap | { error: JsonRpcError }>;

export interface Snap {
id: SnapId;
initialPermissions: Record<string, unknown>;
version: SemVerVersion;
enabled: boolean;
blocked: boolean;
}

interface EthereumProvider {
isMetaMask: boolean;
request(args: { method: string; params?: unknown }): Promise<unknown>;
// Add more methods and properties as needed
}

declare global {
interface Window {
ethereum: EthereumProvider;
}
}

export interface SnapRpcRequestParams {
snapId?: string;
method: string;
params?: Record<string, unknown>;
}

export interface SupportedSnap {
version: string;
name: string;
}

export type SupportedSnaps = Record<string, SupportedSnap>;

/**
* The result returned by the `wallet_invokeSnap` method, which is the result
* returned by the Snap.
*/
export type InvokeSnapResult = Json;
4 changes: 4 additions & 0 deletions packages/extension-dapp/src/snap/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2019-2024 @polkadot/extension-dapp authors & contributors
// SPDX-License-Identifier: Apache-2.0

export const hasMetamask = window.ethereum?.isMetaMask;
Loading
Loading