Skip to content

Commit 5c53ee7

Browse files
joelamouchejacogr
andauthored
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 <[email protected]> * Update packages/extension-dapp/src/compat/metaMaskSource.ts Co-authored-by: Jaco <[email protected]> * filter accounts by type * lint * fix type typing * Update packages/extension-dapp/src/index.ts Co-authored-by: Jaco <[email protected]> * filter by array of type, instead of single type Co-authored-by: Jaco <[email protected]>
1 parent 63a332a commit 5c53ee7

File tree

6 files changed

+1343
-882
lines changed

6 files changed

+1343
-882
lines changed
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
// Copyright 2019-2021 @polkadot/extension-dapp authors & contributors
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import initMetaMaskSource from './metaMaskSource';
45
import singleSource from './singleSource';
56

67
// initialize all the compatibility engines
78
export default function initCompat (): Promise<boolean> {
89
return Promise.all([
9-
singleSource()
10-
]).then((): boolean => true);
10+
singleSource(),
11+
initMetaMaskSource()
12+
]).then((): boolean => {
13+
return true;
14+
});
1115
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2019-2020 @polkadot/extension-dapp authors & contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { Injected, InjectedAccount, InjectedWindow } from '@polkadot/extension-inject/types';
5+
6+
import detectEthereumProvider from '@metamask/detect-provider';
7+
import Web3 from 'web3';
8+
9+
import { SignerPayloadRaw, SignerResult } from '@polkadot/types/types';
10+
11+
interface RequestArguments {
12+
method: string;
13+
params?: unknown[];
14+
}
15+
16+
interface EthRpcSubscription {
17+
unsubscribe: () => void
18+
}
19+
20+
interface EthereumProvider {
21+
request: (args: RequestArguments) => Promise<any>;
22+
isMetaMask: boolean;
23+
on: (name: string, cb: any) => EthRpcSubscription;
24+
}
25+
26+
interface Web3Window extends InjectedWindow {
27+
// this is injected by metaMask
28+
ethereum: any;
29+
}
30+
31+
function isMetaMaskProvider (prov: unknown): EthereumProvider {
32+
if (prov !== null) {
33+
return (prov as EthereumProvider);
34+
} else {
35+
throw new Error('Injected provider is not MetaMask');
36+
}
37+
}
38+
39+
// transfor the Web3 accounts into a simple address/name array
40+
function transformAccounts (accounts: string[]): InjectedAccount[] {
41+
return accounts.map((acc, i) => {
42+
return { address: acc, name: 'MetaMask Address #' + i.toString(), type: 'ethereum' };
43+
});
44+
}
45+
46+
// add a compat interface of metaMaskSource to window.injectedWeb3
47+
function injectMetaMaskWeb3 (win: Web3Window): void {
48+
// decorate the compat interface
49+
win.injectedWeb3.Web3Source = {
50+
enable: async (): Promise<Injected> => {
51+
const providerRaw: unknown = await detectEthereumProvider({ mustBeMetaMask: true });
52+
const provider: EthereumProvider = isMetaMaskProvider(providerRaw);
53+
54+
await provider.request({ method: 'eth_requestAccounts' });
55+
56+
return {
57+
accounts: {
58+
get: async (): Promise<InjectedAccount[]> => {
59+
return transformAccounts(await provider.request({ method: 'eth_requestAccounts' }));
60+
},
61+
subscribe: (cb: (accounts: InjectedAccount[]) => void): (() => void) => {
62+
const sub = provider.on('accountsChanged', function (accounts: string[]) {
63+
cb(transformAccounts(accounts));
64+
});
65+
// TODO: add onchainchanged
66+
67+
return (): void => {
68+
sub.unsubscribe();
69+
};
70+
}
71+
},
72+
signer: {
73+
signRaw: async (raw: SignerPayloadRaw): Promise<SignerResult> => {
74+
const signature = (await provider.request({ method: 'eth_sign', params: [raw.address, Web3.utils.sha3(raw.data)] }) as string);
75+
76+
return { id: 0, signature };
77+
}
78+
}
79+
};
80+
},
81+
version: '0' // TODO: win.ethereum.version
82+
};
83+
}
84+
85+
// returns the MetaMask source instance, as per
86+
// https://github.com/cennznet/singlesource-extension/blob/f7cb35b54e820bf46339f6b88ffede1b8e140de0/react-example/src/App.js#L19
87+
export default function initMetaMaskSource (): Promise<boolean> {
88+
return new Promise((resolve): void => {
89+
const win = window as Window & Web3Window;
90+
91+
if (win.ethereum) {
92+
injectMetaMaskWeb3(win);
93+
resolve(true);
94+
} else {
95+
resolve(false);
96+
}
97+
});
98+
}

packages/extension-dapp/src/compat/singleSource.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface SingleWindow extends InjectedWindow {
2727
SingleSource: SingleSource;
2828
}
2929

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

79-
if (win.SingleSource) {
80-
injectSingleSource(win);
81-
resolve(true);
82-
} else {
83-
resolve(false);
84-
}
85-
});
78+
if (win.SingleSource) {
79+
injectSingleSource(win);
80+
resolve(true);
81+
} else {
82+
resolve(false);
83+
}
8684
});
8785
}

packages/extension-dapp/src/index.ts

Lines changed: 83 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Injected, InjectedAccount, InjectedAccountWithMeta, InjectedExtens
66
import { u8aEq } from '@polkadot/util';
77
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
88

9+
import initCompat from './compat';
910
import { documentReadyPromise } from './util';
1011

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

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

3536
return ({
3637
address: encodedAddress,
37-
meta: { genesisHash, name, source }
38+
meta: { genesisHash, name, source },
39+
type
3840
});
3941
});
4042
}
@@ -49,13 +51,14 @@ export { isWeb3Injected, web3EnablePromise };
4951

5052
function getWindowExtensions (originName: string): Promise<[InjectedExtensionInfo, Injected | void][]> {
5153
return Promise.all(
52-
Object.entries(win.injectedWeb3).map(([name, { enable, version }]): Promise<[InjectedExtensionInfo, Injected | void]> =>
53-
Promise.all([
54-
Promise.resolve({ name, version }),
55-
enable(originName).catch((error: Error): void => {
56-
console.error(`Error initializing ${name}: ${error.message}`);
57-
})
58-
])
54+
Object.entries(win.injectedWeb3).map(
55+
([name, { enable, version }]): Promise<[InjectedExtensionInfo, Injected | void]> =>
56+
Promise.all([
57+
Promise.resolve({ name, version }),
58+
enable(originName).catch((error: Error): void => {
59+
console.error(`Error initializing ${name}: ${error.message}`);
60+
})
61+
])
5962
)
6063
);
6164
}
@@ -66,42 +69,50 @@ export function web3Enable (originName: string): Promise<InjectedExtension[]> {
6669
throw new Error('You must pass a name for your app to the web3Enable function');
6770
}
6871

69-
web3EnablePromise = documentReadyPromise((): Promise<InjectedExtension[]> =>
70-
getWindowExtensions(originName)
71-
.then((values): InjectedExtension[] =>
72-
values
73-
.filter((value): value is [InjectedExtensionInfo, Injected] => !!value[1])
74-
.map(([info, ext]): InjectedExtension => {
75-
// if we don't have an accounts subscriber, add a single-shot version
76-
if (!ext.accounts.subscribe) {
77-
ext.accounts.subscribe = (cb: (accounts: InjectedAccount[]) => void | Promise<void>): Unsubcall => {
78-
ext.accounts.get().then(cb).catch(console.error);
79-
80-
return (): void => {
81-
// no ubsubscribe needed, this is a single-shot
82-
};
83-
};
84-
}
85-
86-
return { ...info, ...ext };
72+
web3EnablePromise = documentReadyPromise(
73+
(): Promise<InjectedExtension[]> =>
74+
initCompat().then(() =>
75+
getWindowExtensions(originName)
76+
.then((values): InjectedExtension[] => {
77+
return values
78+
.filter((value): value is [InjectedExtensionInfo, Injected] => !!value[1])
79+
.map(
80+
([info, ext]): InjectedExtension => {
81+
// if we don't have an accounts subscriber, add a single-shot version
82+
if (!ext.accounts.subscribe) {
83+
ext.accounts.subscribe = (cb: (accounts: InjectedAccount[]) => void | Promise<void>): Unsubcall => {
84+
ext.accounts.get().then(cb).catch(console.error);
85+
86+
return (): void => {
87+
// no ubsubscribe needed, this is a single-shot
88+
};
89+
};
90+
}
91+
92+
return { ...info, ...ext };
93+
}
94+
);
95+
}
96+
)
97+
.catch((): InjectedExtension[] => [])
98+
.then((values): InjectedExtension[] => {
99+
const names = values.map(({ name, version }): string => `${name}/${version}`);
100+
101+
isWeb3Injected = web3IsInjected();
102+
console.log(
103+
`web3Enable: Enabled ${values.length} extension${values.length !== 1 ? 's' : ''}: ${names.join(', ')}`
104+
);
105+
106+
return values;
87107
})
88108
)
89-
.catch((): InjectedExtension[] => [])
90-
.then((values): InjectedExtension[] => {
91-
const names = values.map(({ name, version }): string => `${name}/${version}`);
92-
93-
isWeb3Injected = web3IsInjected();
94-
console.log(`web3Enable: Enabled ${values.length} extension${values.length !== 1 ? 's' : ''}: ${names.join(', ')}`);
95-
96-
return values;
97-
})
98109
);
99110

100111
return web3EnablePromise;
101112
}
102113

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

112123
const retrieved = await Promise.all(
113-
injected.map(async ({ accounts, name: source }): Promise<InjectedAccountWithMeta[]> => {
114-
try {
115-
const list = await accounts.get();
116-
117-
return mapAccounts(source, list, ss58Format);
118-
} catch (error) {
119-
// cannot handle this one
120-
return [];
124+
injected.map(
125+
async ({ accounts, name: source }): Promise<InjectedAccountWithMeta[]> => {
126+
try {
127+
const list = await accounts.get();
128+
129+
return mapAccounts(source, list.filter((acc) => acc.type && accountType ? accountType.includes(acc.type) : true), ss58Format);
130+
} catch (error) {
131+
// cannot handle this one
132+
return [];
133+
}
121134
}
122-
})
135+
)
123136
);
124137

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

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

131-
console.log(`web3Accounts: Found ${accounts.length} address${accounts.length !== 1 ? 'es' : ''}: ${addresses.join(', ')}`);
144+
console.log(
145+
`web3Accounts: Found ${accounts.length} address${accounts.length !== 1 ? 'es' : ''}: ${addresses.join(', ')}`
146+
);
132147

133148
return accounts;
134149
}
135150

136-
export async function web3AccountsSubscribe (cb: (accounts: InjectedAccountWithMeta[]) => void | Promise<void>, { ss58Format }: Web3AccountsOptions = {}): Promise<Unsubcall> {
151+
export async function web3AccountsSubscribe (
152+
cb: (accounts: InjectedAccountWithMeta[]) => void | Promise<void>,
153+
{ ss58Format }: Web3AccountsOptions = {}
154+
): Promise<Unsubcall> {
137155
if (!web3EnablePromise) {
138156
return throwError('web3AccountsSubscribe');
139157
}
140158

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

143-
const triggerUpdate = (): void | Promise<void> => cb(
144-
Object
145-
.entries(accounts)
146-
.reduce((result: InjectedAccountWithMeta[], [source, list]): InjectedAccountWithMeta[] => {
147-
result.push(...mapAccounts(source, list, ss58Format));
161+
const triggerUpdate = (): void | Promise<void> =>
162+
cb(
163+
Object.entries(accounts).reduce(
164+
(result: InjectedAccountWithMeta[], [source, list]): InjectedAccountWithMeta[] => {
165+
result.push(...mapAccounts(source, list, ss58Format));
148166

149-
return result;
150-
}, [])
151-
);
167+
return result;
168+
},
169+
[]
170+
)
171+
);
152172

153-
const unsubs = (await web3EnablePromise).map(({ accounts: { subscribe }, name: source }): Unsubcall =>
154-
subscribe((result): void => {
155-
accounts[source] = result;
173+
const unsubs = (await web3EnablePromise).map(
174+
({ accounts: { subscribe }, name: source }): Unsubcall =>
175+
subscribe((result): void => {
176+
accounts[source] = result;
156177

157-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
158-
triggerUpdate();
159-
})
178+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
179+
triggerUpdate();
180+
})
160181
);
161182

162183
return (): void => {

packages/extension-inject/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface InjectedAccountWithMeta {
2525
name?: string;
2626
source: string;
2727
};
28+
type?: KeypairType;
2829
}
2930

3031
export interface InjectedAccounts {
@@ -111,5 +112,6 @@ export type InjectedExtension = InjectedExtensionInfo & Injected;
111112
export type InjectOptions = InjectedExtensionInfo;
112113

113114
export interface Web3AccountsOptions {
114-
ss58Format?: number
115+
ss58Format?: number,
116+
accountType?: KeypairType[]
115117
}

0 commit comments

Comments
 (0)