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 16 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
4 changes: 3 additions & 1 deletion packages/extension-dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
"sideEffects": false,
"dependencies": {
"@babel/runtime": "^7.14.0",
"@metamask/detect-provider": "^1.2.0",
"@polkadot/extension-inject": "^0.38.4-0",
"@polkadot/util": "^6.6.1",
"@polkadot/util-crypto": "^6.6.1"
"@polkadot/util-crypto": "^6.6.1",
"web3": "^1.3.0"
},
"peerDependencies": {
"@polkadot/api": "*",
Expand Down
4 changes: 3 additions & 1 deletion packages/extension-dapp/src/compat/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// 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()
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 => true);
}
103 changes: 103 additions & 0 deletions packages/extension-dapp/src/compat/metaMaskSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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 {
web3: Web3; // TODO: this could probably be removed
// 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 SingleSource to window.injectedWeb3
function injectMetaMaskWeb3 (win: Web3Window): void {
// decorate the compat interface
win.injectedWeb3.Web3Source = {
enable: async (_: string): Promise<Injected> => {
win.web3 = new Web3(win.ethereum);

const providerRaw: unknown = await detectEthereumProvider({ mustBeMetaMask: true });
const provider: EthereumProvider = isMetaMaskProvider(providerRaw);

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

return {
accounts: {
get: async (): Promise<InjectedAccount[]> => {
console.log('fetching accounts');

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);
joelamouche marked this conversation as resolved.
Show resolved Hide resolved

return { id: 0, signature };
}
}
};
},
version: win.web3.version
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 what is expected here? MetaMask doesnt inject web3 anymore so I'm trying to get rid of web3 all together

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 this is not needed, is it?

};
}

// 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);
}
});
}
22 changes: 12 additions & 10 deletions packages/extension-dapp/src/compat/singleSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ interface SingleWindow extends InjectedWindow {
function transformAccounts (accounts: SingleSourceAccount[]): InjectedAccount[] {
return accounts.map(({ address, name }): InjectedAccount => ({
address,
name
name,
type: 'ethereum'
jacogr marked this conversation as resolved.
Show resolved Hide resolved
}));
}

Expand Down Expand Up @@ -73,15 +74,16 @@ 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;
// window.addEventListener('load', (): void => {
console.log('loading singlesource');
joelamouche marked this conversation as resolved.
Show resolved Hide resolved
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);
}
// });
});
}
164 changes: 95 additions & 69 deletions packages/extension-dapp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

import type { Injected, InjectedAccount, InjectedAccountWithMeta, InjectedExtension, InjectedExtensionInfo, InjectedProviderWithMeta, InjectedWindow, ProviderList, Unsubcall, Web3AccountsOptions } from '@polkadot/extension-inject/types';

import { Signer } from '@polkadot/api/types';
import { u8aEq } from '@polkadot/util';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';

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

// just a helper (otherwise we cast all-over, so shorter and more readable)
Expand All @@ -25,15 +27,19 @@ 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 => {
const encodedAddress = encodeAddress(decodeAddress(address), ss58Format);

return ({
address: encodedAddress,
meta: { genesisHash, name, source }
});
});
function mapAccounts (source: string, list: InjectedAccount[], signer: Signer, ss58Format?: number): 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 },
signer,
type: type || 'ethereum'
Copy link
Member

Choose a reason for hiding this comment

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

No. We cannot add an ethereum type where there are none. This is for instance Ledger and Parity Signer (or Stylo), which are certainly not Ethereum accounts.

};
}
);
}

// have we found a properly constructed window.injectedWeb3
Expand All @@ -46,13 +52,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 @@ -63,35 +70,42 @@ 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[] =>
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;
Expand All @@ -104,18 +118,22 @@ export async function web3Accounts ({ ss58Format }: Web3AccountsOptions = {}): P
}

const accounts: InjectedAccountWithMeta[] = [];
const injected = await web3EnablePromise;
const injected: InjectedExtension[] = 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, signer }): 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


console.log('web3Accounts call res', list);

return mapAccounts(source, list, signer, ss58Format);
} catch (error) {
// cannot handle this one
return [];
}
}
})
)
);

retrieved.forEach((result): void => {
Expand All @@ -124,35 +142,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, result[0].signer, 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
Loading