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 metamask extension web3 #566

Merged
merged 40 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4c8b1c8
added web3source
joelamouche Dec 8, 2020
a98b814
tried fixing dependencies
joelamouche Dec 8, 2020
a956347
removed load event listener and added name
joelamouche Dec 8, 2020
2d44686
add signer to interface
joelamouche Dec 10, 2020
f8c01a2
Merge branch 'master' into jlm-add-metamask-extension-web3
joelamouche Dec 10, 2020
f056a53
added signPayload draft
joelamouche Dec 10, 2020
42bf1fd
lint
joelamouche Dec 10, 2020
966c6db
modified signpayload
joelamouche Dec 10, 2020
b3ff173
switch back to only signRaw
joelamouche Dec 10, 2020
6303215
sync with master and update to new metamask norm
joelamouche May 5, 2021
a1f12b9
add types to injecetd accounts
joelamouche May 6, 2021
e275ad3
update deps
joelamouche May 7, 2021
6ff55de
hash tx and finalize code
joelamouche May 10, 2021
e8dc31c
lint and type
joelamouche May 10, 2021
83b02de
sync with master
joelamouche May 11, 2021
e40bcce
sync with master
joelamouche May 31, 2021
02c1543
iterate on feedback
joelamouche Jun 1, 2021
034ce08
lint index.ts
joelamouche Jun 1, 2021
0ca3c18
prettier lint index.ts
joelamouche Jun 1, 2021
40cc504
Merge branch 'jlm-lint-extension' into jlm-add-metamask-extension-web3
joelamouche Jun 1, 2021
2f590e6
type optional again
joelamouche Jun 1, 2021
786cdb9
Merge branch 'jlm-add-metamask-extension-web3' of github.com:PureStak…
joelamouche Jun 1, 2021
ba312be
removed sigenr from injected account type
joelamouche Jun 21, 2021
6081cef
sync with master
joelamouche Jun 21, 2021
1314d64
lint
joelamouche Jun 21, 2021
26f14eb
sync with master
joelamouche Jul 5, 2021
7bbb18b
wip remove web3
joelamouche Jul 7, 2021
74da34f
remove laod event
joelamouche Jul 8, 2021
3ba0757
sync
joelamouche Jul 12, 2021
e91483f
update packages
joelamouche Jul 23, 2021
d9822ed
Update packages/extension-dapp/src/compat/metaMaskSource.ts
joelamouche Aug 23, 2021
58188be
Update packages/extension-dapp/src/compat/metaMaskSource.ts
joelamouche Aug 23, 2021
a68bc59
filter accounts by type
joelamouche Aug 23, 2021
a312bf4
sync with master
joelamouche Aug 23, 2021
b8ee32c
lint
joelamouche Aug 23, 2021
5d315b3
fix type typing
joelamouche Aug 24, 2021
88b707c
Update packages/extension-dapp/src/index.ts
joelamouche Aug 25, 2021
bbf0627
filter by array of type, instead of single type
joelamouche Aug 25, 2021
3e046a9
sync with master
joelamouche Aug 26, 2021
846d274
sync with master
joelamouche Sep 2, 2021
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
8 changes: 6 additions & 2 deletions packages/extension-dapp/src/compat/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Copyright 2019-2021 @polkadot/extension-dapp authors & contributors
// SPDX-License-Identifier: Apache-2.0

import initMetaMaskSource from './metaMaskSource';
import singleSource from './singleSource';

// initialize all the compatibility engines
export default function initCompat (): Promise<boolean> {
return Promise.all([
singleSource()
]).then((): boolean => true);
singleSource(),
Copy link
Member

Choose a reason for hiding this comment

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

Note-to-self: After this can drop the singleSource. It was always great as a reference, but now that we have a proper reference available here, not needed anymore.

initMetaMaskSource()
]).then((): boolean => {
return true;
});
}
98 changes: 98 additions & 0 deletions packages/extension-dapp/src/compat/metaMaskSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2019-2020 @polkadot/extension-dapp authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Injected, InjectedAccount, InjectedWindow } from '@polkadot/extension-inject/types';

import detectEthereumProvider from '@metamask/detect-provider';
import Web3 from 'web3';

import { SignerPayloadRaw, SignerResult } from '@polkadot/types/types';

interface RequestArguments {
method: string;
params?: unknown[];
}

interface EthRpcSubscription {
unsubscribe: () => void
}

interface EthereumProvider {
request: (args: RequestArguments) => Promise<any>;
isMetaMask: boolean;
on: (name: string, cb: any) => EthRpcSubscription;
}

interface Web3Window extends InjectedWindow {
// this is injected by metaMask
ethereum: any;
}

function isMetaMaskProvider (prov: unknown): EthereumProvider {
if (prov !== null) {
return (prov as EthereumProvider);
} else {
throw new Error('Injected provider is not MetaMask');
}
}

// transfor the Web3 accounts into a simple address/name array
function transformAccounts (accounts: string[]): InjectedAccount[] {
return accounts.map((acc, i) => {
return { address: acc, name: 'MetaMask Address #' + i.toString(), type: 'ethereum' };
});
}

// add a compat interface of metaMaskSource to window.injectedWeb3
function injectMetaMaskWeb3 (win: Web3Window): void {
// decorate the compat interface
win.injectedWeb3.Web3Source = {
enable: async (): Promise<Injected> => {
const providerRaw: unknown = await detectEthereumProvider({ mustBeMetaMask: true });
const provider: EthereumProvider = isMetaMaskProvider(providerRaw);

await provider.request({ method: 'eth_requestAccounts' });

return {
accounts: {
get: async (): Promise<InjectedAccount[]> => {
return transformAccounts(await provider.request({ method: 'eth_requestAccounts' }));
},
subscribe: (cb: (accounts: InjectedAccount[]) => void): (() => void) => {
const sub = provider.on('accountsChanged', function (accounts: string[]) {
cb(transformAccounts(accounts));
});
// TODO: add onchainchanged

return (): void => {
sub.unsubscribe();
};
}
},
signer: {
signRaw: async (raw: SignerPayloadRaw): Promise<SignerResult> => {
joelamouche marked this conversation as resolved.
Show resolved Hide resolved
const signature = (await provider.request({ method: 'eth_sign', params: [raw.address, Web3.utils.sha3(raw.data)] }) as string);

return { id: 0, signature };
}
}
};
},
version: '0' // TODO: win.ethereum.version
};
}

// returns the MetaMask source instance, as per
// https://github.com/cennznet/singlesource-extension/blob/f7cb35b54e820bf46339f6b88ffede1b8e140de0/react-example/src/App.js#L19
export default function initMetaMaskSource (): Promise<boolean> {
return new Promise((resolve): void => {
const win = window as Window & Web3Window;

if (win.ethereum) {
injectMetaMaskWeb3(win);
resolve(true);
} else {
resolve(false);
}
});
}
18 changes: 8 additions & 10 deletions packages/extension-dapp/src/compat/singleSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface SingleWindow extends InjectedWindow {
SingleSource: SingleSource;
}

// transfor the SingleSource accounts into a simple address/name array
// transform the SingleSource accounts into a simple address/name array
function transformAccounts (accounts: SingleSourceAccount[]): InjectedAccount[] {
return accounts.map(({ address, name }): InjectedAccount => ({
address,
Expand Down Expand Up @@ -73,15 +73,13 @@ function injectSingleSource (win: SingleWindow): void {
// https://github.com/cennznet/singlesource-extension/blob/f7cb35b54e820bf46339f6b88ffede1b8e140de0/react-example/src/App.js#L19
export default function initSingleSource (): Promise<boolean> {
return new Promise((resolve): void => {
window.addEventListener('load', (): void => {
const win = window as Window & SingleWindow;
const win = window as Window & SingleWindow;
Copy link
Member

Choose a reason for hiding this comment

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

Believe I commented elsewhere - the original is correct. (All ok, since this will be dropped anyway)


if (win.SingleSource) {
injectSingleSource(win);
resolve(true);
} else {
resolve(false);
}
});
if (win.SingleSource) {
injectSingleSource(win);
resolve(true);
} else {
resolve(false);
}
});
}
145 changes: 83 additions & 62 deletions packages/extension-dapp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Injected, InjectedAccount, InjectedAccountWithMeta, InjectedExtens
import { u8aEq } from '@polkadot/util';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';

import initCompat from './compat';
import { documentReadyPromise } from './util';

// expose utility functions
Expand All @@ -29,12 +30,13 @@ function throwError (method: string): never {

// internal helper to map from Array<InjectedAccount> -> Array<InjectedAccountWithMeta>
function mapAccounts (source: string, list: InjectedAccount[], ss58Format?: number): InjectedAccountWithMeta[] {
return list.map(({ address, genesisHash, name }): InjectedAccountWithMeta => {
return list.map(({ address, genesisHash, name, type }): InjectedAccountWithMeta => {
const encodedAddress = address.length === 42 ? address : encodeAddress(decodeAddress(address), ss58Format);

return ({
address: encodedAddress,
meta: { genesisHash, name, source }
meta: { genesisHash, name, source },
type
});
});
}
Expand All @@ -49,13 +51,14 @@ export { isWeb3Injected, web3EnablePromise };

function getWindowExtensions (originName: string): Promise<[InjectedExtensionInfo, Injected | void][]> {
return Promise.all(
Object.entries(win.injectedWeb3).map(([name, { enable, version }]): Promise<[InjectedExtensionInfo, Injected | void]> =>
Promise.all([
Promise.resolve({ name, version }),
enable(originName).catch((error: Error): void => {
console.error(`Error initializing ${name}: ${error.message}`);
})
])
Object.entries(win.injectedWeb3).map(
Copy link
Member

Choose a reason for hiding this comment

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

No logic changes, only reformatting. In this case the polkadot-js style is actually very different. In these types of situations, the chain is actually split if needed. Would just suggest to revert.

Copy link
Member

Choose a reason for hiding this comment

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

Still applicable, but will do a manual revert of this and the one above once in.

([name, { enable, version }]): Promise<[InjectedExtensionInfo, Injected | void]> =>
Promise.all([
Promise.resolve({ name, version }),
enable(originName).catch((error: Error): void => {
console.error(`Error initializing ${name}: ${error.message}`);
})
])
)
);
}
Expand All @@ -66,42 +69,50 @@ export function web3Enable (originName: string): Promise<InjectedExtension[]> {
throw new Error('You must pass a name for your app to the web3Enable function');
}

web3EnablePromise = documentReadyPromise((): Promise<InjectedExtension[]> =>
getWindowExtensions(originName)
.then((values): InjectedExtension[] =>
values
.filter((value): value is [InjectedExtensionInfo, Injected] => !!value[1])
.map(([info, ext]): InjectedExtension => {
// if we don't have an accounts subscriber, add a single-shot version
if (!ext.accounts.subscribe) {
ext.accounts.subscribe = (cb: (accounts: InjectedAccount[]) => void | Promise<void>): Unsubcall => {
ext.accounts.get().then(cb).catch(console.error);

return (): void => {
// no ubsubscribe needed, this is a single-shot
};
};
}

return { ...info, ...ext };
web3EnablePromise = documentReadyPromise(
(): Promise<InjectedExtension[]> =>
Copy link
Member

Choose a reason for hiding this comment

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

Like the previous comment, quite difficult to extract readability improvements vs logic changes. It seems the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there an easy way to revert the formating? Shouldn't yarn run lint do the trick? Thats what I ran on this repo

initCompat().then(() =>
getWindowExtensions(originName)
.then((values): InjectedExtension[] => {
return values
.filter((value): value is [InjectedExtensionInfo, Injected] => !!value[1])
.map(
([info, ext]): InjectedExtension => {
// if we don't have an accounts subscriber, add a single-shot version
if (!ext.accounts.subscribe) {
ext.accounts.subscribe = (cb: (accounts: InjectedAccount[]) => void | Promise<void>): Unsubcall => {
ext.accounts.get().then(cb).catch(console.error);

return (): void => {
// no ubsubscribe needed, this is a single-shot
};
};
}

return { ...info, ...ext };
}
);
}
)
.catch((): InjectedExtension[] => [])
.then((values): InjectedExtension[] => {
const names = values.map(({ name, version }): string => `${name}/${version}`);

isWeb3Injected = web3IsInjected();
console.log(
`web3Enable: Enabled ${values.length} extension${values.length !== 1 ? 's' : ''}: ${names.join(', ')}`
);

return values;
})
)
.catch((): InjectedExtension[] => [])
.then((values): InjectedExtension[] => {
const names = values.map(({ name, version }): string => `${name}/${version}`);

isWeb3Injected = web3IsInjected();
console.log(`web3Enable: Enabled ${values.length} extension${values.length !== 1 ? 's' : ''}: ${names.join(', ')}`);

return values;
})
);

return web3EnablePromise;
}

// retrieve all the accounts accross all providers
export async function web3Accounts ({ ss58Format }: Web3AccountsOptions = {}): Promise<InjectedAccountWithMeta[]> {
export async function web3Accounts ({ accountType, ss58Format }: Web3AccountsOptions = {}): Promise<InjectedAccountWithMeta[]> {
if (!web3EnablePromise) {
return throwError('web3Accounts');
}
Expand All @@ -110,16 +121,18 @@ export async function web3Accounts ({ ss58Format }: Web3AccountsOptions = {}): P
const injected = await web3EnablePromise;

const retrieved = await Promise.all(
injected.map(async ({ accounts, name: source }): Promise<InjectedAccountWithMeta[]> => {
try {
const list = await accounts.get();

return mapAccounts(source, list, ss58Format);
} catch (error) {
// cannot handle this one
return [];
injected.map(
async ({ accounts, name: source }): Promise<InjectedAccountWithMeta[]> => {
try {
const list = await accounts.get();
Copy link
Member

Choose a reason for hiding this comment

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

The issue here is that in this case it will also inject the Ethereum accounts into apps that have no use for them, for instance on Kusama. This is opt-in. So web3Accounts should have a flag that will retrieve these kinds of accounts, which will only be set for specific chains.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So where do you think the filter should be?
op1: filter here the accounts by account.type
opt2: pass a flag to initCompat and only call the relevant sources in compat/index

Copy link
Member

Choose a reason for hiding this comment

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

So on web3Accounts() would just add an extra flag for the types, e.g. ['sr25519', 'ed25519', 'ethereum'] - there may be some dance required by users to correctly pass the flag, but it is all ok.

So I think the easiest is to indeed initialise all source, but then just filter the accounts if need be. By default would not use (if nothing passed) the filter as `['sr25519', 'ed25519'] - obviously if no type is available, just pass it through.

The tricky bit here is that if we don't have sr25519 we never pass through the accounts with no type.

Should work in the case of the apps UI as well, e.g. in the case of Moonbeam, we just pass through the ['ethereum'] type. (As mentioned above that interface may also require some additional juggling, but all ok)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jacogr let me know if the latest commit corresponds to what you were thinking


return mapAccounts(source, list.filter((acc) => acc.type && accountType ? accountType.includes(acc.type) : true), ss58Format);
} catch (error) {
// cannot handle this one
return [];
}
}
})
)
);

retrieved.forEach((result): void => {
Expand All @@ -128,35 +141,43 @@ export async function web3Accounts ({ ss58Format }: Web3AccountsOptions = {}): P

const addresses = accounts.map(({ address }): string => address);

console.log(`web3Accounts: Found ${accounts.length} address${accounts.length !== 1 ? 'es' : ''}: ${addresses.join(', ')}`);
console.log(
`web3Accounts: Found ${accounts.length} address${accounts.length !== 1 ? 'es' : ''}: ${addresses.join(', ')}`
);

return accounts;
}

export async function web3AccountsSubscribe (cb: (accounts: InjectedAccountWithMeta[]) => void | Promise<void>, { ss58Format }: Web3AccountsOptions = {}): Promise<Unsubcall> {
export async function web3AccountsSubscribe (
cb: (accounts: InjectedAccountWithMeta[]) => void | Promise<void>,
{ ss58Format }: Web3AccountsOptions = {}
): Promise<Unsubcall> {
jacogr marked this conversation as resolved.
Show resolved Hide resolved
if (!web3EnablePromise) {
return throwError('web3AccountsSubscribe');
}

const accounts: Record<string, InjectedAccount[]> = {};

const triggerUpdate = (): void | Promise<void> => cb(
Object
.entries(accounts)
.reduce((result: InjectedAccountWithMeta[], [source, list]): InjectedAccountWithMeta[] => {
result.push(...mapAccounts(source, list, ss58Format));
const triggerUpdate = (): void | Promise<void> =>
cb(
Object.entries(accounts).reduce(
(result: InjectedAccountWithMeta[], [source, list]): InjectedAccountWithMeta[] => {
result.push(...mapAccounts(source, list, ss58Format));

return result;
}, [])
);
return result;
},
[]
)
);

const unsubs = (await web3EnablePromise).map(({ accounts: { subscribe }, name: source }): Unsubcall =>
subscribe((result): void => {
accounts[source] = result;
const unsubs = (await web3EnablePromise).map(
({ accounts: { subscribe }, name: source }): Unsubcall =>
subscribe((result): void => {
accounts[source] = result;

// eslint-disable-next-line @typescript-eslint/no-floating-promises
triggerUpdate();
})
// eslint-disable-next-line @typescript-eslint/no-floating-promises
triggerUpdate();
})
);

return (): void => {
Expand Down
4 changes: 3 additions & 1 deletion packages/extension-inject/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface InjectedAccountWithMeta {
name?: string;
source: string;
};
type?: KeypairType;
}

export interface InjectedAccounts {
Expand Down Expand Up @@ -111,5 +112,6 @@ export type InjectedExtension = InjectedExtensionInfo & Injected;
export type InjectOptions = InjectedExtensionInfo;

export interface Web3AccountsOptions {
ss58Format?: number
ss58Format?: number,
accountType?: KeypairType[]
}
Loading