Add metamask extension web3 (#566)

* added web3source

* tried fixing dependencies

* removed load event listener and added name

* add signer to interface

* added signPayload draft

* lint

* modified signpayload

* switch back to only signRaw

* add types to injecetd accounts

* hash tx and finalize code

* lint and type

* iterate on feedback

* lint index.ts

* prettier lint index.ts

* type optional again

* removed sigenr from injected account type

* lint

* wip remove web3

* remove laod event

* Update packages/extension-dapp/src/compat/metaMaskSource.ts

Co-authored-by: Jaco <jacogr@gmail.com>

* Update packages/extension-dapp/src/compat/metaMaskSource.ts

Co-authored-by: Jaco <jacogr@gmail.com>

* filter accounts by type

* lint

* fix type typing

* Update packages/extension-dapp/src/index.ts

Co-authored-by: Jaco <jacogr@gmail.com>

* filter by array of type, instead of single type

Co-authored-by: Jaco <jacogr@gmail.com>
This commit is contained in:
Antoine Estienne
2021-09-03 11:45:34 +02:00
committed by GitHub
co-authored by Jaco
parent 63a332a770
commit 5c53ee7d02
6 changed files with 1340 additions and 879 deletions
+6 -2
View File
@@ -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;
});
}
@@ -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> => {
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);
}
});
}
@@ -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;
if (win.SingleSource) {
injectSingleSource(win);
resolve(true);
} else {
resolve(false);
}
});
if (win.SingleSource) {
injectSingleSource(win);
resolve(true);
} else {
resolve(false);
}
});
}
+80 -59
View File
@@ -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(
([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);
web3EnablePromise = documentReadyPromise(
(): Promise<InjectedExtension[]> =>
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 (): void => {
// no ubsubscribe needed, this is a single-shot
};
};
}
return { ...info, ...ext };
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();
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 [];
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> {
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 => {
+3 -1
View File
@@ -25,6 +25,7 @@ export interface InjectedAccountWithMeta {
name?: string;
source: string;
};
type?: KeypairType;
}
export interface InjectedAccounts {
@@ -111,5 +112,6 @@ export type InjectedExtension = InjectedExtensionInfo & Injected;
export type InjectOptions = InjectedExtensionInfo;
export interface Web3AccountsOptions {
ss58Format?: number
ss58Format?: number,
accountType?: KeypairType[]
}
+1145 -807
View File
File diff suppressed because it is too large Load Diff