@effectstream/wallets 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,429 @@
1
+ import type { MetaMaskInpageProvider } from "@metamask/providers";
2
+ import { AddressType } from "@effectstream/utils";
3
+ import {
4
+ type ActiveConnection,
5
+ type AddressAndType,
6
+ type ConnectionOption,
7
+ type IInjectedConnector,
8
+ type IProvider,
9
+ optionToActive,
10
+ type UserSignature,
11
+ } from "../IProvider.ts";
12
+ import type { EvmAddress } from "./types.ts";
13
+ import { getWindow } from "../windows.ts";
14
+ import { utf8ToHex } from "web3-utils";
15
+ import * as CHAINS from "viem/chains";
16
+ import type { Chain } from "viem/chains";
17
+
18
+ type EIP1193Provider = MetaMaskInpageProvider;
19
+
20
+ interface EIP5749ProviderInfo {
21
+ uuid: string;
22
+ name: string;
23
+ icon: `data:image/svg+xml;base64,${string}`;
24
+ description: string;
25
+ }
26
+ interface EIP5749ProviderWithInfo extends EIP1193Provider {
27
+ info: EIP5749ProviderInfo;
28
+ }
29
+ interface EIP5749EVMProviders {
30
+ /**
31
+ * The key is RECOMMENDED to be the name of the extension in snake_case. It MUST contain only lowercase letters, numbers, and underscores.
32
+ */
33
+ [index: string]: EIP5749ProviderWithInfo;
34
+ }
35
+
36
+ interface EIP6963ProviderInfo {
37
+ uuid: string;
38
+ name: string;
39
+ icon: string;
40
+ rdns: string;
41
+ }
42
+ interface EIP6963ProviderDetail {
43
+ info: EIP6963ProviderInfo;
44
+ provider: EIP1193Provider;
45
+ }
46
+ interface EIP6963AnnounceProviderEvent extends CustomEvent {
47
+ type: "eip6963:announceProvider";
48
+ detail: EIP6963ProviderDetail;
49
+ }
50
+
51
+ const eip5953Providers: EIP6963ProviderDetail[] = [];
52
+
53
+ const windowObj = getWindow();
54
+ if (windowObj && (windowObj as any).addEventListener) {
55
+ (windowObj as any).addEventListener(
56
+ "eip6963:announceProvider",
57
+ (event: EIP6963AnnounceProviderEvent) => {
58
+ eip5953Providers.push(event.detail);
59
+ });
60
+ }
61
+ if (windowObj && windowObj.dispatchEvent) {
62
+ windowObj?.dispatchEvent(new Event("eip6963:requestProvider"));
63
+ }
64
+ const CHAIN_NOT_ADDED_ERROR_CODE = 4902;
65
+
66
+ /**
67
+ * Type definition from EIP3085 (https://eips.ethereum.org/EIPS/eip-3085)
68
+ * I couldn't find this part of any NPM library for some reason
69
+ */
70
+ interface AddEthereumChainParameter {
71
+ chainId: string;
72
+ blockExplorerUrls?: string[];
73
+ chainName?: string;
74
+ iconUrls?: string[];
75
+ nativeCurrency?: {
76
+ name: string;
77
+ symbol: string;
78
+ decimals: number;
79
+ };
80
+ rpcUrls?: string[];
81
+ }
82
+
83
+ export type EvmApi = EIP1193Provider;
84
+
85
+ export class EvmInjectedConnector implements IInjectedConnector<EvmApi> {
86
+ private provider: EvmInjectedProvider | undefined;
87
+ private static INSTANCE: undefined | EvmInjectedConnector = undefined;
88
+
89
+ /**
90
+ * Wallets may inject themselves using multiple competing standards
91
+ * We have to track all of them (with duplicates) internally
92
+ * To make sure `connectNamed` calls properly find wallets no matter the name used
93
+ */
94
+ static getPossiblyDuplicateWalletOptions(): ConnectionOption<EvmApi>[] {
95
+ const allWallets: ConnectionOption<EvmApi>[] = [];
96
+
97
+ const windowObj = getWindow();
98
+ if (windowObj == null) return [];
99
+
100
+ // 1) Add EIP6963 and prioritize it for deduplicating
101
+ {
102
+ const eip6963Options = eip5953Providers.map(({ info, provider }) => ({
103
+ metadata: {
104
+ name: info.rdns,
105
+ displayName: info.name,
106
+ icon: info.icon,
107
+ },
108
+ api: (): Promise<MetaMaskInpageProvider> => Promise.resolve(provider),
109
+ }));
110
+ allWallets.push(...eip6963Options);
111
+ }
112
+
113
+ // 2) Add EIP5749
114
+ {
115
+ const eip5749Providers: EIP5749EVMProviders =
116
+ (windowObj as any).evmproviders ?? {};
117
+ const eip5749Options = Object.entries(eip5749Providers).map(
118
+ ([key, provider]) => ({
119
+ metadata: {
120
+ name: key,
121
+ displayName: provider.info.name,
122
+ icon: provider.info.icon,
123
+ },
124
+ api: (): Promise<EIP5749ProviderWithInfo> =>
125
+ Promise.resolve(provider),
126
+ }),
127
+ );
128
+ allWallets.push(...eip5749Options);
129
+ }
130
+
131
+ // some wallets aggressively put themselves in the list of wallets the user has installed
132
+ // even if the user has never actually used these
133
+ // we put these wallets at the bottom of the priority
134
+ const aggressiveWallets = ["com.brave.wallet"];
135
+ allWallets.sort((a, b) => {
136
+ const aAggressive = aggressiveWallets.includes(a.metadata.name);
137
+ const bAggressive = aggressiveWallets.includes(b.metadata.name);
138
+ if (aAggressive && !bAggressive) return 1;
139
+ if (bAggressive && !aAggressive) return -1;
140
+ return 0;
141
+ });
142
+ return allWallets;
143
+ }
144
+ static getWalletOptions(): ConnectionOption<EvmApi>[] {
145
+ const withDuplicates = EvmInjectedConnector
146
+ .getPossiblyDuplicateWalletOptions();
147
+ const seenNames: Set<string> = new Set();
148
+ const seenDisplayNames: Set<string> = new Set();
149
+
150
+ const result: ConnectionOption<EvmApi>[] = [];
151
+ for (const option of withDuplicates) {
152
+ const lowerCaseName = option.metadata.displayName.toLowerCase();
153
+ if (seenNames.has(option.metadata.name)) continue;
154
+ if (seenDisplayNames.has(lowerCaseName)) continue;
155
+ seenNames.add(option.metadata.name);
156
+ seenDisplayNames.add(lowerCaseName);
157
+ result.push(option);
158
+ }
159
+
160
+ return result;
161
+ }
162
+
163
+ static instance(): EvmInjectedConnector {
164
+ if (EvmInjectedConnector.INSTANCE == null) {
165
+ const newInstance = new EvmInjectedConnector();
166
+ EvmInjectedConnector.INSTANCE = newInstance;
167
+ }
168
+ return EvmInjectedConnector.INSTANCE;
169
+ }
170
+ connectSimple = async (): Promise<EvmInjectedProvider> => {
171
+ if (this.provider != null) {
172
+ return this.provider;
173
+ }
174
+ const options = EvmInjectedConnector.getWalletOptions();
175
+ if (options.length === 0) {
176
+ throw new Error(`No EVM wallet found`);
177
+ }
178
+ return await this.connectExternal(await optionToActive(options[0]));
179
+ };
180
+ connectExternal = async (
181
+ conn: ActiveConnection<EvmApi>,
182
+ ): Promise<EvmInjectedProvider> => {
183
+ if (this.provider?.getConnection().metadata?.name === conn.metadata.name) {
184
+ return this.provider;
185
+ }
186
+ this.provider = await EvmInjectedProvider.init(conn);
187
+
188
+ // Update the selected Eth address if the user changes after logging in.
189
+ // warning: not supported by all wallets (ex: Flint)
190
+ const ethereum: EIP1193Provider = (getWindow() as any).ethereum;
191
+ ethereum?.on("accountsChanged", (newAccounts) => {
192
+ const accounts = newAccounts as string[];
193
+ if (!accounts || !accounts[0] || accounts[0] !== this.provider?.address) {
194
+ this.provider = undefined;
195
+ }
196
+ });
197
+ return this.provider;
198
+ };
199
+ connectNamed = async (name: string): Promise<EvmInjectedProvider> => {
200
+ if (this.provider?.getConnection().metadata?.name === name) {
201
+ return this.provider;
202
+ }
203
+
204
+ // the eip6963 name for Metamask is io.metamask
205
+ // but we want to keep simply "metamask" working for backwards compatibility with older versions of Paima
206
+ const adjustedName = name.toLocaleLowerCase() === "metamask"
207
+ ? "io.metamask"
208
+ : name;
209
+ const provider = EvmInjectedConnector.getPossiblyDuplicateWalletOptions()
210
+ .find(
211
+ (entry) => entry.metadata.name === adjustedName,
212
+ );
213
+ if (provider == null) {
214
+ throw new Error(`EVM wallet ${name} not found`);
215
+ }
216
+ return await this.connectExternal(await optionToActive(provider));
217
+ };
218
+ getProvider = (): undefined | EvmInjectedProvider => {
219
+ return this.provider;
220
+ };
221
+ getOrThrowProvider = (): EvmInjectedProvider => {
222
+ if (this.provider == null) {
223
+ throw new Error(`EvmInjectedConnector not initialized yet`);
224
+ }
225
+ return this.provider;
226
+ };
227
+ isConnected = (): boolean => {
228
+ return this.provider != null;
229
+ };
230
+
231
+ switchChain = async (hexChainId: string, chain?: Chain): Promise<void> => {
232
+ try {
233
+ // careful: not all wallets support switching chains
234
+ await this.provider?.getConnection().api.request({
235
+ method: "wallet_switchEthereumChain",
236
+ params: [{ chainId: hexChainId }],
237
+ });
238
+
239
+ await this.verifyWalletChain(hexChainId);
240
+ } catch (se: any) {
241
+ // deno-lint-ignore no-prototype-builtins
242
+ if (se.hasOwnProperty("code") && se.code === CHAIN_NOT_ADDED_ERROR_CODE) {
243
+ let targetChain: Chain | undefined = undefined;
244
+ Object.entries(CHAINS).forEach(([key, value]) => {
245
+ const possibleChain = value as Chain;
246
+ if (
247
+ typeof possibleChain === "object" &&
248
+ // deno-lint-ignore no-prototype-builtins
249
+ possibleChain.hasOwnProperty("id") &&
250
+ possibleChain.id === parseInt(hexChainId, 16)
251
+ ) {
252
+ targetChain = possibleChain;
253
+ }
254
+ });
255
+
256
+ if (chain) {
257
+ targetChain = chain;
258
+ }
259
+ if (!targetChain) {
260
+ throw new Error(`[switchChain] error: Chain not found`);
261
+ }
262
+
263
+ await this.addChain({
264
+ chainId: hexChainId,
265
+ chainName: targetChain.name,
266
+ nativeCurrency: {
267
+ name: targetChain.nativeCurrency.name,
268
+ symbol: targetChain.nativeCurrency.symbol,
269
+ decimals: targetChain.nativeCurrency.decimals,
270
+ },
271
+ rpcUrls: [targetChain.rpcUrls.default.http[0]],
272
+ blockExplorerUrls: targetChain.blockExplorers?.default.url
273
+ ? [targetChain.blockExplorers?.default.url]
274
+ : undefined,
275
+ });
276
+ } else {
277
+ throw new Error(`[switchChain] error: ${se?.message} ` + se?.code);
278
+ }
279
+ }
280
+ };
281
+ verifyWalletChain = async (chainId: string): Promise<boolean> => {
282
+ try {
283
+ const walletChain = await this.provider?.getConnection().api.request({
284
+ method: "eth_chainId",
285
+ });
286
+ return parseInt(walletChain as string, 16) === parseInt(chainId, 16);
287
+ } catch (e: any) {
288
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
289
+ throw new Error(`[verifyWalletChain] error: ${e?.message} ` + e?.code);
290
+ }
291
+ };
292
+ addChain = async (newChain: AddEthereumChainParameter): Promise<void> => {
293
+ try {
294
+ // careful: not all wallets support adding arbitrary chains
295
+ await this.provider?.getConnection().api.request({
296
+ method: "wallet_addEthereumChain",
297
+ params: [newChain],
298
+ });
299
+ } catch (e: any) {
300
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
301
+ throw new Error(`[addChain] error: ${e?.message} ` + e?.code);
302
+ }
303
+ };
304
+ }
305
+
306
+ /**
307
+ * For some reason I can't find any official type definitions for this
308
+ * https://docs.metamask.io/wallet/reference/eth_sendtransaction/
309
+ */
310
+ type Web3TransactionRequest = {
311
+ to?: string;
312
+ from: string;
313
+
314
+ gas?: string;
315
+ gasPrice?: string;
316
+
317
+ data: string;
318
+ value?: string;
319
+
320
+ maxPriorityFeePerGas?: string;
321
+ maxFeePerGas?: string;
322
+ };
323
+
324
+ export class EvmInjectedProvider implements IProvider<EvmApi> {
325
+ constructor(
326
+ private readonly conn: ActiveConnection<EvmApi>,
327
+ readonly address: EvmAddress,
328
+ ) {}
329
+ static init = async (
330
+ conn: ActiveConnection<EvmApi>,
331
+ ): Promise<EvmInjectedProvider> => {
332
+ const accounts = (await conn.api.request({
333
+ method: "eth_requestAccounts",
334
+ })) as string[];
335
+ if (!accounts || accounts.length === 0) {
336
+ throw new Error("Unknown error while receiving EVM accounts");
337
+ }
338
+
339
+ return new EvmInjectedProvider(conn, accounts[0]);
340
+ };
341
+ getConnection = (): ActiveConnection<EvmApi> => {
342
+ return this.conn;
343
+ };
344
+ getAddress = (): AddressAndType => {
345
+ return {
346
+ type: AddressType.EVM,
347
+ address: this.address.toLowerCase(),
348
+ };
349
+ };
350
+ signMessage = async (message: string): Promise<UserSignature> => {
351
+ const hexMessage = utf8ToHex(message);
352
+ const signature = await this.conn.api.request({
353
+ method: "personal_sign",
354
+ params: [hexMessage, this.getAddress().address, ""],
355
+ });
356
+ if (typeof signature !== "string") {
357
+ console.log("[signMessageEth] invalid signature:", signature);
358
+ throw new Error(
359
+ `[signMessageEth] Received signature of type ${typeof signature}`,
360
+ );
361
+ }
362
+ return signature;
363
+ };
364
+ verifyWalletChain = async (): Promise<boolean> => {
365
+ try {
366
+ const walletChain = await this.conn.api.request({
367
+ method: "eth_chainId",
368
+ });
369
+ return true;
370
+ // TODO We have to move this to the top level
371
+ // return (
372
+ // parseInt(walletChain as string, 16) ===
373
+ // parseInt(this.gameInfo.gameChainId, 16)
374
+ // );
375
+ } catch (e: any) {
376
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
377
+ throw new Error(`[verifyWalletChain] error: ${e?.message} ` + e?.code);
378
+ }
379
+ };
380
+ addChain = async (newChain: AddEthereumChainParameter): Promise<void> => {
381
+ try {
382
+ // careful: not all wallets support adding arbitrary chains
383
+ await this.conn.api.request({
384
+ method: "wallet_addEthereumChain",
385
+ params: [newChain],
386
+ });
387
+ } catch (e: any) {
388
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
389
+ throw new Error(`[addChain] error: ${e?.message} ` + e?.code);
390
+ }
391
+ };
392
+ switchChain = async (hexChainId: string): Promise<void> => {
393
+ try {
394
+ // careful: not all wallets support switching chains
395
+ await this.conn.api.request({
396
+ method: "wallet_switchEthereumChain",
397
+ params: [{ chainId: hexChainId }],
398
+ });
399
+ } catch (e: any) {
400
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
401
+ throw new Error(`[switchChain] error: ${e?.message} ` + e?.code);
402
+ }
403
+ };
404
+ sendTransaction = async (
405
+ tx: Web3TransactionRequest,
406
+ ): Promise<{
407
+ txHash: string;
408
+ }> => {
409
+ await this.verifyWalletChain();
410
+ try {
411
+ const hash = await this.conn.api.request({
412
+ method: "eth_sendTransaction",
413
+ params: [tx],
414
+ });
415
+ if (typeof hash !== "string") {
416
+ console.log("[sendTransaction] invalid signature:", hash);
417
+ throw new Error(
418
+ `[sendTransaction] Received "hash" of type ${typeof hash}`,
419
+ );
420
+ }
421
+ return {
422
+ txHash: hash,
423
+ };
424
+ } catch (e: any) {
425
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
426
+ throw new Error(`[sendTransaction] error: ${e?.message} ` + e?.code);
427
+ }
428
+ };
429
+ }
@@ -0,0 +1,2 @@
1
+ export type EvmAddress = string;
2
+ export const DEFAULT_GAS_LIMIT = 100000;
@@ -0,0 +1,49 @@
1
+ import type { Result } from "@effectstream/utils";
2
+ import type { IProvider } from "../IProvider.ts";
3
+ import type { WalletMode, ApiForMode } from "../utils.ts";
4
+ import type { LoginInfoMap } from "../wallet-modes.ts";
5
+ import { type EthersApi, EthersConnector } from "./ethers.ts";
6
+
7
+ async function connectWallet(
8
+ loginInfo: LoginInfoMap[WalletMode.EvmEthers]
9
+ ): Promise<Result<IProvider<EthersApi>>> {
10
+ const name = loginInfo.connection.metadata.name;
11
+ try {
12
+ console.log(`ethersLoginWrapper: Attempting to log into ${name}`);
13
+ const provider = await EthersConnector.instance().connectExternal(
14
+ loginInfo.connection.api
15
+ );
16
+ return {
17
+ success: true,
18
+ result: provider,
19
+ };
20
+ } catch (err) {
21
+ console.log(`ethersLoginWrapper: Error while logging into wallet name}`);
22
+
23
+ return {
24
+ success: false,
25
+ errorMessage: err instanceof Error ? err.message : String(err),
26
+ };
27
+ }
28
+ }
29
+ export async function ethersLoginWrapper(
30
+ loginInfo: LoginInfoMap[WalletMode.EvmEthers]
31
+ ): Promise<Result<IProvider<ApiForMode<WalletMode.EvmEthers>>>> {
32
+ const loginResult = await connectWallet(loginInfo);
33
+ if (loginResult.success === false) {
34
+ return loginResult;
35
+ }
36
+ try {
37
+ // TODO Move this to the top level
38
+ // await updateFee();
39
+ } catch (err) {
40
+ // errorFxn(PaimaMiddlewareErrorCode.ERROR_UPDATING_FEE, err);
41
+ // The fee has a default value, so this is not fatal and we can continue.
42
+ // If the fee has increased beyond the default value, posting won't work.
43
+ }
44
+
45
+ return {
46
+ success: true,
47
+ result: loginResult.result,
48
+ };
49
+ }
@@ -0,0 +1,103 @@
1
+ import type { Result } from "@effectstream/utils";
2
+ import type { IProvider } from "../IProvider.ts";
3
+ import type { ApiForMode, WalletMode } from "../utils.ts";
4
+ import { connectInjected, type LoginInfoMap } from "../wallet-modes.ts";
5
+ import { EvmInjectedConnector } from "./injected.ts";
6
+ import type { Chain } from "viem/chains";
7
+ interface SwitchError {
8
+ code: number;
9
+ }
10
+
11
+ async function switchChain(chain: Chain | undefined): Promise<boolean> {
12
+ if (!chain) {
13
+ // We can't switch to a chain if we don't know what chain to switch to.
14
+ return false;
15
+ }
16
+ const hexChainId = "0x" + chain.id.toString(16);
17
+ await EvmInjectedConnector.instance().switchChain(
18
+ hexChainId,
19
+ chain,
20
+ );
21
+ return await verifyWalletChain(chain);
22
+ }
23
+
24
+ async function verifyWalletChain(chain: Chain | undefined): Promise<boolean> {
25
+ if (!chain) {
26
+ return false;
27
+ }
28
+ const hexChainId = "0x" + chain.id.toString(16);
29
+ return await EvmInjectedConnector.instance().verifyWalletChain(hexChainId);
30
+ }
31
+
32
+ export async function checkEthWalletStatus(): Promise<Result<string>> {
33
+ // const errorFxn = buildEndpointErrorFxn('checkEthWalletStatus');
34
+
35
+ // if (!hasLogin(WalletMode.EvmInjected)) {
36
+ // return { success: true, message: '' };
37
+ // }
38
+ if (EvmInjectedConnector.instance().getProvider() === null) {
39
+ return { success: false, errorMessage: "No address selected" };
40
+ }
41
+
42
+ try {
43
+ if (!(await verifyWalletChain(undefined))) {
44
+ return { success: false, errorMessage: "EVM_WRONG_CHAIN" };
45
+ }
46
+ } catch (err) {
47
+ return { success: false, errorMessage: "EVM_CHAIN_VERIFICATION" };
48
+ }
49
+
50
+ return { success: true, result: "OK" };
51
+ }
52
+
53
+ export async function evmLoginWrapper(
54
+ loginInfo: LoginInfoMap[WalletMode.EvmInjected],
55
+ ): Promise<Result<IProvider<ApiForMode<WalletMode.EvmInjected>>>> {
56
+ // const errorFxn = buildEndpointErrorFxn('evmLoginWrapper');
57
+
58
+ // const evmConfig = await GlobalConfig.mainEvmConfig();
59
+ // if (evmConfig == null) {
60
+ // return errorFxn(
61
+ // PaimaMiddlewareErrorCode.EVM_LOGIN,
62
+ // new Error(`No EVM network found in configuration`)
63
+ // );
64
+ // }
65
+ // const [_, config] = evmConfig;
66
+ // const gameInfo = {
67
+ // gameName: getGameName(),
68
+ // gameChainId: '0x' + config.chainId.toString(16),
69
+ // };
70
+ const loginResult = await connectInjected(
71
+ "evmLoginWrapper",
72
+ loginInfo,
73
+ EvmInjectedConnector.instance(),
74
+ );
75
+ if (loginResult.success === false) {
76
+ return loginResult;
77
+ }
78
+
79
+ try {
80
+ // TODO Move this to the top level
81
+ // await updateFee();
82
+ } catch (err) {
83
+ // errorFxn(PaimaMiddlewareErrorCode.ERROR_UPDATING_FEE, err);
84
+ // The fee has a default value, so this is not fatal and we can continue.
85
+ // If the fee has increased beyond the default value, posting won't work.
86
+ }
87
+ try {
88
+ if (loginInfo.checkChainId !== false) {
89
+ if (!(await verifyWalletChain(loginInfo.chain))) {
90
+ if (!(await switchChain(loginInfo.chain))) {
91
+ return { success: false, errorMessage: "EVM_CHAIN_SWITCH" };
92
+ }
93
+ }
94
+ }
95
+ } catch (err) {
96
+ return { success: false, errorMessage: "EVM_CHAIN_SWITCH" };
97
+ }
98
+
99
+ return {
100
+ success: true,
101
+ result: loginResult.result,
102
+ };
103
+ }