-
Notifications
You must be signed in to change notification settings - Fork 419
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
Changes from all commits
4c8b1c8
a98b814
a956347
2d44686
f8c01a2
f056a53
42bf1fd
966c6db
b3ff173
6303215
a1f12b9
e275ad3
6ff55de
e8dc31c
83b02de
e40bcce
02c1543
034ce08
0ca3c18
40cc504
2f590e6
786cdb9
ba312be
6081cef
1314d64
26f14eb
7bbb18b
74da34f
3ba0757
e91483f
d9822ed
58188be
a68bc59
a312bf4
b8ee32c
5d315b3
88b707c
bbf0627
3e046a9
846d274
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(), | ||
initMetaMaskSource() | ||
]).then((): boolean => { | ||
return true; | ||
}); | ||
} |
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); | ||
} | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
}); | ||
}); | ||
} | ||
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`); | ||
}) | ||
]) | ||
) | ||
); | ||
} | ||
|
@@ -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[]> => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an easy way to revert the formating? Shouldn't |
||
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'); | ||
} | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So where do you think the filter should be? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So on 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 Should work in the case of the apps UI as well, e.g. in the case of Moonbeam, we just pass through the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 => { | ||
|
@@ -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 => { | ||
|
There was a problem hiding this comment.
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.