@multiplatform.one/web3 7.31.1 → 7.33.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.
- package/README.md +84 -0
- package/dist/cjs/clients/evm.cjs +52 -1
- package/dist/cjs/clients/evm.native.js +56 -1
- package/dist/cjs/clients/evm.native.js.map +1 -1
- package/dist/cjs/clients/hooks.cjs +5 -2
- package/dist/cjs/clients/hooks.native.js +7 -2
- package/dist/cjs/clients/hooks.native.js.map +1 -1
- package/dist/cjs/clients/index.cjs +4 -0
- package/dist/cjs/clients/index.native.js +4 -0
- package/dist/cjs/clients/index.native.js.map +1 -1
- package/dist/cjs/clients/preparedSend.cjs +96 -0
- package/dist/cjs/clients/preparedSend.native.js +225 -0
- package/dist/cjs/clients/preparedSend.native.js.map +1 -0
- package/dist/cjs/connectkit/connectors/metaMaskConnectConnector.cjs +108 -27
- package/dist/cjs/connectkit/connectors/metaMaskConnectConnector.native.js +105 -28
- package/dist/cjs/connectkit/connectors/metaMaskConnectConnector.native.js.map +1 -1
- package/dist/cjs/connectkit/index.cjs +2 -0
- package/dist/cjs/connectkit/index.native.js +2 -0
- package/dist/cjs/connectkit/index.native.js.map +1 -1
- package/dist/cjs/connectkit/openConnector/context.cjs +16 -3
- package/dist/cjs/connectkit/openConnector/context.native.js +19 -4
- package/dist/cjs/connectkit/openConnector/context.native.js.map +1 -1
- package/dist/cjs/connectkit/openConnector/launcher.cjs +2 -2
- package/dist/cjs/connectkit/openConnector/launcher.native.js +2 -2
- package/dist/cjs/connectkit/openConnector/launcher.native.js.map +1 -1
- package/dist/cjs/phantom/evm.cjs +133 -0
- package/dist/cjs/phantom/evm.native.js +269 -0
- package/dist/cjs/phantom/evm.native.js.map +1 -0
- package/dist/cjs/phantom/index.cjs +36 -0
- package/dist/cjs/phantom/index.native.js +39 -0
- package/dist/cjs/phantom/index.native.js.map +1 -0
- package/dist/cjs/phantom/urls.cjs +33 -0
- package/dist/cjs/phantom/urls.native.js +38 -0
- package/dist/cjs/phantom/urls.native.js.map +1 -0
- package/dist/cjs/wagmi/driver.cjs +13 -9
- package/dist/cjs/wagmi/driver.native.js +17 -9
- package/dist/cjs/wagmi/driver.native.js.map +1 -1
- package/dist/esm/clients/evm.mjs +53 -2
- package/dist/esm/clients/evm.mjs.map +1 -1
- package/dist/esm/clients/evm.native.js +57 -2
- package/dist/esm/clients/evm.native.js.map +1 -1
- package/dist/esm/clients/hooks.mjs +5 -2
- package/dist/esm/clients/hooks.mjs.map +1 -1
- package/dist/esm/clients/hooks.native.js +7 -2
- package/dist/esm/clients/hooks.native.js.map +1 -1
- package/dist/esm/clients/index.mjs +2 -1
- package/dist/esm/clients/index.mjs.map +1 -1
- package/dist/esm/clients/index.native.js +2 -1
- package/dist/esm/clients/index.native.js.map +1 -1
- package/dist/esm/clients/preparedSend.mjs +69 -0
- package/dist/esm/clients/preparedSend.mjs.map +1 -0
- package/dist/esm/clients/preparedSend.native.js +195 -0
- package/dist/esm/clients/preparedSend.native.js.map +1 -0
- package/dist/esm/connectkit/connectors/metaMaskConnectConnector.mjs +107 -28
- package/dist/esm/connectkit/connectors/metaMaskConnectConnector.mjs.map +1 -1
- package/dist/esm/connectkit/connectors/metaMaskConnectConnector.native.js +104 -29
- package/dist/esm/connectkit/connectors/metaMaskConnectConnector.native.js.map +1 -1
- package/dist/esm/connectkit/index.mjs +2 -2
- package/dist/esm/connectkit/index.mjs.map +1 -1
- package/dist/esm/connectkit/index.native.js +2 -2
- package/dist/esm/connectkit/index.native.js.map +1 -1
- package/dist/esm/connectkit/openConnector/context.mjs +17 -4
- package/dist/esm/connectkit/openConnector/context.mjs.map +1 -1
- package/dist/esm/connectkit/openConnector/context.native.js +20 -5
- package/dist/esm/connectkit/openConnector/context.native.js.map +1 -1
- package/dist/esm/connectkit/openConnector/launcher.mjs +2 -2
- package/dist/esm/connectkit/openConnector/launcher.mjs.map +1 -1
- package/dist/esm/connectkit/openConnector/launcher.native.js +2 -2
- package/dist/esm/connectkit/openConnector/launcher.native.js.map +1 -1
- package/dist/esm/phantom/evm.mjs +103 -0
- package/dist/esm/phantom/evm.mjs.map +1 -0
- package/dist/esm/phantom/evm.native.js +236 -0
- package/dist/esm/phantom/evm.native.js.map +1 -0
- package/dist/esm/phantom/index.mjs +4 -0
- package/dist/esm/phantom/index.mjs.map +1 -0
- package/dist/esm/phantom/index.native.js +4 -0
- package/dist/esm/phantom/index.native.js.map +1 -0
- package/dist/esm/phantom/urls.mjs +7 -0
- package/dist/esm/phantom/urls.mjs.map +1 -0
- package/dist/esm/phantom/urls.native.js +9 -0
- package/dist/esm/phantom/urls.native.js.map +1 -0
- package/dist/esm/wagmi/driver.mjs +14 -10
- package/dist/esm/wagmi/driver.mjs.map +1 -1
- package/dist/esm/wagmi/driver.native.js +18 -10
- package/dist/esm/wagmi/driver.native.js.map +1 -1
- package/dist/jsx/clients/evm.mjs +53 -2
- package/dist/jsx/clients/evm.mjs.map +1 -1
- package/dist/jsx/clients/evm.native.js +56 -1
- package/dist/jsx/clients/evm.native.js.map +1 -1
- package/dist/jsx/clients/hooks.mjs +5 -2
- package/dist/jsx/clients/hooks.mjs.map +1 -1
- package/dist/jsx/clients/hooks.native.js +7 -2
- package/dist/jsx/clients/hooks.native.js.map +1 -1
- package/dist/jsx/clients/index.mjs +2 -1
- package/dist/jsx/clients/index.mjs.map +1 -1
- package/dist/jsx/clients/index.native.js +4 -0
- package/dist/jsx/clients/index.native.js.map +1 -1
- package/dist/jsx/clients/preparedSend.mjs +69 -0
- package/dist/jsx/clients/preparedSend.mjs.map +1 -0
- package/dist/jsx/clients/preparedSend.native.js +225 -0
- package/dist/jsx/clients/preparedSend.native.js.map +1 -0
- package/dist/jsx/connectkit/connectors/metaMaskConnectConnector.mjs +107 -28
- package/dist/jsx/connectkit/connectors/metaMaskConnectConnector.mjs.map +1 -1
- package/dist/jsx/connectkit/connectors/metaMaskConnectConnector.native.js +105 -28
- package/dist/jsx/connectkit/connectors/metaMaskConnectConnector.native.js.map +1 -1
- package/dist/jsx/connectkit/index.mjs +2 -2
- package/dist/jsx/connectkit/index.mjs.map +1 -1
- package/dist/jsx/connectkit/index.native.js +2 -0
- package/dist/jsx/connectkit/index.native.js.map +1 -1
- package/dist/jsx/connectkit/openConnector/context.mjs +17 -4
- package/dist/jsx/connectkit/openConnector/context.mjs.map +1 -1
- package/dist/jsx/connectkit/openConnector/context.native.js +19 -4
- package/dist/jsx/connectkit/openConnector/context.native.js.map +1 -1
- package/dist/jsx/connectkit/openConnector/launcher.mjs +2 -2
- package/dist/jsx/connectkit/openConnector/launcher.mjs.map +1 -1
- package/dist/jsx/connectkit/openConnector/launcher.native.js +2 -2
- package/dist/jsx/connectkit/openConnector/launcher.native.js.map +1 -1
- package/dist/jsx/phantom/evm.mjs +103 -0
- package/dist/jsx/phantom/evm.mjs.map +1 -0
- package/dist/jsx/phantom/evm.native.js +269 -0
- package/dist/jsx/phantom/evm.native.js.map +1 -0
- package/dist/jsx/phantom/index.mjs +4 -0
- package/dist/jsx/phantom/index.mjs.map +1 -0
- package/dist/jsx/phantom/index.native.js +39 -0
- package/dist/jsx/phantom/index.native.js.map +1 -0
- package/dist/jsx/phantom/urls.mjs +7 -0
- package/dist/jsx/phantom/urls.mjs.map +1 -0
- package/dist/jsx/phantom/urls.native.js +38 -0
- package/dist/jsx/phantom/urls.native.js.map +1 -0
- package/dist/jsx/wagmi/driver.mjs +14 -10
- package/dist/jsx/wagmi/driver.mjs.map +1 -1
- package/dist/jsx/wagmi/driver.native.js +17 -9
- package/dist/jsx/wagmi/driver.native.js.map +1 -1
- package/dist/keycloak/login.iife.js +382 -382
- package/package.json +12 -6
- package/src/clients/evm.ts +86 -0
- package/src/clients/hooks.spec.tsx +1 -0
- package/src/clients/hooks.ts +4 -1
- package/src/clients/index.ts +12 -0
- package/src/clients/preparedSend.spec.ts +214 -0
- package/src/clients/preparedSend.ts +136 -0
- package/src/connectkit/connectors/metaMaskConnectConnector.spec.ts +256 -4
- package/src/connectkit/connectors/metaMaskConnectConnector.ts +145 -33
- package/src/connectkit/index.ts +2 -0
- package/src/connectkit/openConnector/context.tsx +10 -5
- package/src/connectkit/openConnector/launcher.spec.ts +24 -2
- package/src/connectkit/openConnector/launcher.ts +10 -3
- package/src/core/types.ts +7 -0
- package/src/phantom/evm.spec.ts +195 -0
- package/src/phantom/evm.ts +163 -0
- package/src/phantom/index.ts +16 -0
- package/src/phantom/urls.spec.ts +29 -0
- package/src/phantom/urls.ts +16 -0
- package/src/wagmi/driver.spec.ts +31 -0
- package/src/wagmi/driver.ts +11 -4
- package/types/clients/evm.d.ts +32 -0
- package/types/clients/evm.d.ts.map +1 -1
- package/types/clients/hooks.d.ts.map +1 -1
- package/types/clients/index.d.ts +2 -1
- package/types/clients/index.d.ts.map +1 -1
- package/types/clients/preparedSend.d.ts +71 -0
- package/types/clients/preparedSend.d.ts.map +1 -0
- package/types/connectkit/connectors/metaMaskConnectConnector.d.ts +27 -0
- package/types/connectkit/connectors/metaMaskConnectConnector.d.ts.map +1 -1
- package/types/connectkit/index.d.ts +1 -1
- package/types/connectkit/index.d.ts.map +1 -1
- package/types/connectkit/openConnector/context.d.ts.map +1 -1
- package/types/connectkit/openConnector/launcher.d.ts +8 -1
- package/types/connectkit/openConnector/launcher.d.ts.map +1 -1
- package/types/core/types.d.ts +7 -0
- package/types/core/types.d.ts.map +1 -1
- package/types/phantom/evm.d.ts +76 -0
- package/types/phantom/evm.d.ts.map +1 -0
- package/types/phantom/index.d.ts +3 -0
- package/types/phantom/index.d.ts.map +1 -0
- package/types/phantom/urls.d.ts +11 -0
- package/types/phantom/urls.d.ts.map +1 -0
- package/types/wagmi/driver.d.ts +1 -1
- package/types/wagmi/driver.d.ts.map +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EventHandlers } from "@metamask/connect-evm";
|
|
2
2
|
import type { Chain } from "viem";
|
|
3
|
-
import { getAddress, SwitchChainError } from "viem";
|
|
4
|
-
import { base, baseSepolia, mainnet, polygon, polygonAmoy } from "viem/chains";
|
|
3
|
+
import { getAddress, SwitchChainError, UserRejectedRequestError } from "viem";
|
|
4
|
+
import { base, baseSepolia, bscTestnet, mainnet, polygon, polygonAmoy, sepolia } from "viem/chains";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { createConfig, http } from "wagmi";
|
|
7
7
|
import { connect, reconnect, sendTransaction, signMessage, signTypedData } from "wagmi/actions";
|
|
@@ -9,9 +9,12 @@ import { connect, reconnect, sendTransaction, signMessage, signTypedData } from
|
|
|
9
9
|
import {
|
|
10
10
|
metaMaskConnect,
|
|
11
11
|
metaMaskConnectChainIds,
|
|
12
|
+
metaMaskMayLackChain,
|
|
12
13
|
metaMaskNetworks,
|
|
14
|
+
reopenMetaMask,
|
|
13
15
|
resetMetaMaskConnectClient,
|
|
14
16
|
} from "./metaMaskConnectConnector";
|
|
17
|
+
import { wagmiDriver } from "../../wagmi/driver";
|
|
15
18
|
import {
|
|
16
19
|
BITCOIN_CHAIN_ID,
|
|
17
20
|
SOLANA_CHAIN_ID,
|
|
@@ -39,9 +42,16 @@ vi.mock("@metamask/connect-evm", () => ({ createEVMClient }));
|
|
|
39
42
|
const synthetic = [SOLANA_CHAIN_ID, SUI_CHAIN_ID, BITCOIN_CHAIN_ID];
|
|
40
43
|
const hex = (id: number) => `0x${id.toString(16)}`;
|
|
41
44
|
|
|
42
|
-
function build(
|
|
45
|
+
function build(
|
|
46
|
+
chains: readonly Chain[],
|
|
47
|
+
options: {
|
|
48
|
+
mobile?: boolean;
|
|
49
|
+
addChainFirst?: (chain: Chain) => boolean;
|
|
50
|
+
useDeeplink?: boolean;
|
|
51
|
+
} = {},
|
|
52
|
+
) {
|
|
43
53
|
const transports = Object.fromEntries(chains.map((chain) => [chain.id, http()]));
|
|
44
|
-
return metaMaskConnect({ dapp: { name: "test", url: "https://example.test" } })({
|
|
54
|
+
return metaMaskConnect({ dapp: { name: "test", url: "https://example.test" }, ...options })({
|
|
45
55
|
chains: chains as [Chain, ...Chain[]],
|
|
46
56
|
emitter: { uid: "metaMaskSDK", emit: vi.fn(), on: vi.fn(), once: vi.fn(), off: vi.fn() },
|
|
47
57
|
transports,
|
|
@@ -397,6 +407,22 @@ describe("metaMaskConnect under wagmi", () => {
|
|
|
397
407
|
}));
|
|
398
408
|
});
|
|
399
409
|
|
|
410
|
+
it("connects through the connection driver on the chain the page names, asking MetaMask for it first", async () => {
|
|
411
|
+
const config = createConfig({
|
|
412
|
+
chains: [mainnet, base, baseSepolia],
|
|
413
|
+
connectors: [metaMaskConnect({ dapp: { name: "test", url: "https://example.test" } })],
|
|
414
|
+
transports: { [mainnet.id]: http(), [base.id]: http(), [baseSepolia.id]: http() },
|
|
415
|
+
multiInjectedProviderDiscovery: false,
|
|
416
|
+
storage: null,
|
|
417
|
+
});
|
|
418
|
+
client.getChainId.mockReturnValue(hex(baseSepolia.id));
|
|
419
|
+
|
|
420
|
+
const account = await wagmiDriver(config).connect("metaMaskSDK", { chainId: baseSepolia.id });
|
|
421
|
+
|
|
422
|
+
expect(client.connect.mock.calls[0]![0].chainIds[0]).toBe(hex(baseSepolia.id));
|
|
423
|
+
expect(account.chainId).toBe(baseSepolia.id);
|
|
424
|
+
});
|
|
425
|
+
|
|
400
426
|
it("ignores a connect that carries no account", async () => {
|
|
401
427
|
const config = wagmiConfig();
|
|
402
428
|
const events = await clientEvents();
|
|
@@ -404,3 +430,229 @@ describe("metaMaskConnect under wagmi", () => {
|
|
|
404
430
|
expect(config.state.status).toBe("disconnected");
|
|
405
431
|
});
|
|
406
432
|
});
|
|
433
|
+
|
|
434
|
+
// What MetaMask Connect does on a phone, read from @metamask/connect-evm 2.1.1 and
|
|
435
|
+
// @metamask/connect-multichain 1.2.0: a request the wallet answers goes over the
|
|
436
|
+
// relay, and 10 ms later MetaMask Connect opens metamask://connect/mwp?id=<session>.
|
|
437
|
+
// switchChain does that for wallet_switchEthereumChain, but for a chain MetaMask
|
|
438
|
+
// lacks the wallet only answers "Unrecognized chain ID", and MetaMask Connect then
|
|
439
|
+
// sends wallet_addEthereumChain with a second deeplink that no tap started.
|
|
440
|
+
// Chrome on Android drops it, so the add waits in the relay and the switch never
|
|
441
|
+
// settles (PROMA-121, reproduced on an Android 16 emulator with MetaMask 8.1.0).
|
|
442
|
+
describe("metaMaskConnect switchChain on a phone", () => {
|
|
443
|
+
let request: ReturnType<typeof vi.fn>;
|
|
444
|
+
|
|
445
|
+
beforeEach(() => {
|
|
446
|
+
resetMetaMaskConnectClient();
|
|
447
|
+
vi.clearAllMocks();
|
|
448
|
+
client.accounts = [];
|
|
449
|
+
request = vi.fn(async () => null);
|
|
450
|
+
client.getProvider.mockReturnValue({ request } as never);
|
|
451
|
+
client.getChainId.mockReturnValue("0x1");
|
|
452
|
+
client.connect.mockImplementation(async () => ({
|
|
453
|
+
accounts: ["0x0000000000000000000000000000000000000001"],
|
|
454
|
+
chainId: "0x1",
|
|
455
|
+
}));
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
afterEach(() => {
|
|
459
|
+
resetMetaMaskConnectClient();
|
|
460
|
+
client.getChainId.mockImplementation(() => "0x2105");
|
|
461
|
+
client.getProvider.mockImplementation(() => ({ request: vi.fn() }));
|
|
462
|
+
client.switchChain.mockImplementation(async () => undefined);
|
|
463
|
+
client.connect.mockImplementation(async () => ({
|
|
464
|
+
accounts: ["0x0000000000000000000000000000000000000001"],
|
|
465
|
+
chainId: "0x2105",
|
|
466
|
+
}));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const addCalls = () =>
|
|
470
|
+
request.mock.calls.filter(
|
|
471
|
+
([call]) => (call as { method: string }).method === "wallet_addEthereumChain",
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
it("adds a testnet MetaMask does not ship in one wallet request, not a switch then an add", async () => {
|
|
475
|
+
const connector = build([baseSepolia, bscTestnet], { mobile: true });
|
|
476
|
+
await connector.connect();
|
|
477
|
+
|
|
478
|
+
await expect(connector.switchChain!({ chainId: baseSepolia.id })).resolves.toBe(baseSepolia);
|
|
479
|
+
|
|
480
|
+
expect(client.switchChain).not.toHaveBeenCalled();
|
|
481
|
+
expect(addCalls()).toHaveLength(1);
|
|
482
|
+
expect(addCalls()[0]![0]).toMatchObject({
|
|
483
|
+
params: [
|
|
484
|
+
{
|
|
485
|
+
chainId: hex(baseSepolia.id),
|
|
486
|
+
chainName: baseSepolia.name,
|
|
487
|
+
rpcUrls: [baseSepolia.rpcUrls.default.http[0]],
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("switches to a chain MetaMask ships without a chain configuration, so no second request can follow", async () => {
|
|
494
|
+
const connector = build([base, sepolia], { mobile: true });
|
|
495
|
+
await connector.connect();
|
|
496
|
+
|
|
497
|
+
await connector.switchChain!({ chainId: base.id });
|
|
498
|
+
await connector.switchChain!({ chainId: sepolia.id });
|
|
499
|
+
|
|
500
|
+
expect(client.switchChain.mock.calls).toEqual([
|
|
501
|
+
[{ chainId: hex(base.id) }],
|
|
502
|
+
[{ chainId: hex(sepolia.id) }],
|
|
503
|
+
]);
|
|
504
|
+
expect(addCalls()).toHaveLength(0);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("fails a switch MetaMask cannot place with 4902, then adds the chain on the next switch", async () => {
|
|
508
|
+
const connector = build([base, polygonAmoy], { mobile: true, addChainFirst: () => false });
|
|
509
|
+
await connector.connect();
|
|
510
|
+
client.switchChain.mockRejectedValueOnce(
|
|
511
|
+
Object.assign(new Error('Unrecognized chain ID "0x13882".'), { code: 4902 }),
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const failed = await connector.switchChain!({ chainId: polygonAmoy.id }).catch((e) => e);
|
|
515
|
+
expect(failed).toBeInstanceOf(SwitchChainError);
|
|
516
|
+
expect(failed.code).toBe(4902);
|
|
517
|
+
expect(String(failed.details)).toMatch(/Switch again from a tap/);
|
|
518
|
+
expect(client.switchChain).toHaveBeenCalledTimes(1);
|
|
519
|
+
expect((client.switchChain.mock.calls[0] as unknown[])[0]).not.toHaveProperty(
|
|
520
|
+
"chainConfiguration",
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
await connector.switchChain!({ chainId: polygonAmoy.id });
|
|
524
|
+
expect(client.switchChain).toHaveBeenCalledTimes(1);
|
|
525
|
+
expect(addCalls()).toHaveLength(1);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("keeps a switch to a chain the session already permits in the page", async () => {
|
|
529
|
+
const connector = build([baseSepolia, bscTestnet], { mobile: true });
|
|
530
|
+
await connector.connect();
|
|
531
|
+
await connector.switchChain!({ chainId: baseSepolia.id });
|
|
532
|
+
await connector.switchChain!({ chainId: bscTestnet.id });
|
|
533
|
+
request.mockClear();
|
|
534
|
+
|
|
535
|
+
await connector.switchChain!({ chainId: baseSepolia.id });
|
|
536
|
+
|
|
537
|
+
expect(addCalls()).toHaveLength(0);
|
|
538
|
+
expect(client.switchChain).toHaveBeenCalledWith({ chainId: hex(baseSepolia.id) });
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("sends nothing when MetaMask is already on the chain", async () => {
|
|
542
|
+
client.getChainId.mockReturnValue(hex(baseSepolia.id));
|
|
543
|
+
const connector = build([baseSepolia], { mobile: true });
|
|
544
|
+
|
|
545
|
+
await expect(connector.switchChain!({ chainId: baseSepolia.id })).resolves.toBe(baseSepolia);
|
|
546
|
+
|
|
547
|
+
expect(client.switchChain).not.toHaveBeenCalled();
|
|
548
|
+
expect(request).not.toHaveBeenCalled();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("turns a rejection in MetaMask into UserRejectedRequestError, on either path", async () => {
|
|
552
|
+
const rejected = Object.assign(new Error("User rejected the request."), { code: 4001 });
|
|
553
|
+
const connector = build([base, baseSepolia], { mobile: true });
|
|
554
|
+
await connector.connect();
|
|
555
|
+
|
|
556
|
+
request.mockRejectedValueOnce(rejected);
|
|
557
|
+
await expect(connector.switchChain!({ chainId: baseSepolia.id })).rejects.toBeInstanceOf(
|
|
558
|
+
UserRejectedRequestError,
|
|
559
|
+
);
|
|
560
|
+
client.switchChain.mockRejectedValueOnce(rejected);
|
|
561
|
+
await expect(connector.switchChain!({ chainId: base.id })).rejects.toBeInstanceOf(
|
|
562
|
+
UserRejectedRequestError,
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("forgets what the session permitted when it disconnects", async () => {
|
|
567
|
+
const connector = build([baseSepolia], { mobile: true });
|
|
568
|
+
await connector.connect();
|
|
569
|
+
await connector.switchChain!({ chainId: baseSepolia.id });
|
|
570
|
+
await connector.disconnect();
|
|
571
|
+
await connector.connect();
|
|
572
|
+
request.mockClear();
|
|
573
|
+
|
|
574
|
+
await connector.switchChain!({ chainId: baseSepolia.id });
|
|
575
|
+
|
|
576
|
+
expect(addCalls()).toHaveLength(1);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("keeps the desktop switch as MetaMask Connect does it, with the chain configuration", async () => {
|
|
580
|
+
const connector = build([baseSepolia], { mobile: false });
|
|
581
|
+
await connector.connect();
|
|
582
|
+
|
|
583
|
+
await connector.switchChain!({ chainId: baseSepolia.id });
|
|
584
|
+
|
|
585
|
+
expect(addCalls()).toHaveLength(0);
|
|
586
|
+
expect(client.switchChain).toHaveBeenCalledWith({
|
|
587
|
+
chainId: hex(baseSepolia.id),
|
|
588
|
+
chainConfiguration: expect.objectContaining({ chainId: hex(baseSepolia.id) }),
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe("metaMaskMayLackChain", () => {
|
|
594
|
+
it("is true for a testnet MetaMask does not ship and false for mainnets and its own testnets", () => {
|
|
595
|
+
expect(metaMaskMayLackChain(baseSepolia)).toBe(true);
|
|
596
|
+
expect(metaMaskMayLackChain(bscTestnet)).toBe(true);
|
|
597
|
+
expect(metaMaskMayLackChain(sepolia)).toBe(false);
|
|
598
|
+
expect(metaMaskMayLackChain(base)).toBe(false);
|
|
599
|
+
expect(metaMaskMayLackChain(mainnet)).toBe(false);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe("reopenMetaMask", () => {
|
|
604
|
+
beforeEach(() => {
|
|
605
|
+
resetMetaMaskConnectClient();
|
|
606
|
+
vi.clearAllMocks();
|
|
607
|
+
});
|
|
608
|
+
afterEach(() => resetMetaMaskConnectClient());
|
|
609
|
+
|
|
610
|
+
const mobileOptions = () =>
|
|
611
|
+
(
|
|
612
|
+
createEVMClient.mock.calls[0]![0] as {
|
|
613
|
+
mobile: { preferredOpenLink?: (url: string, target?: string) => void };
|
|
614
|
+
}
|
|
615
|
+
).mobile;
|
|
616
|
+
|
|
617
|
+
it("opens the last MetaMask link again, through the app's own opener", async () => {
|
|
618
|
+
const open = vi.fn();
|
|
619
|
+
const connector = metaMaskConnect({
|
|
620
|
+
dapp: { name: "test", url: "https://example.test" },
|
|
621
|
+
preferredOpenLink: open,
|
|
622
|
+
})({
|
|
623
|
+
chains: [baseSepolia],
|
|
624
|
+
emitter: { uid: "metaMaskSDK", emit: vi.fn(), on: vi.fn(), once: vi.fn(), off: vi.fn() },
|
|
625
|
+
transports: { [baseSepolia.id]: http() },
|
|
626
|
+
} as never);
|
|
627
|
+
await connector.setup?.();
|
|
628
|
+
expect(reopenMetaMask()).toBe(false);
|
|
629
|
+
|
|
630
|
+
mobileOptions().preferredOpenLink!("metamask://connect/mwp?id=abc", "_self");
|
|
631
|
+
open.mockClear();
|
|
632
|
+
|
|
633
|
+
expect(reopenMetaMask()).toBe(true);
|
|
634
|
+
expect(open).toHaveBeenCalledWith("metamask://connect/mwp?id=abc", "_self");
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("forgets the link when the session ends", async () => {
|
|
638
|
+
const connector = metaMaskConnect({
|
|
639
|
+
dapp: { name: "test", url: "https://example.test" },
|
|
640
|
+
preferredOpenLink: vi.fn(),
|
|
641
|
+
})({
|
|
642
|
+
chains: [baseSepolia],
|
|
643
|
+
emitter: { uid: "metaMaskSDK", emit: vi.fn(), on: vi.fn(), once: vi.fn(), off: vi.fn() },
|
|
644
|
+
transports: { [baseSepolia.id]: http() },
|
|
645
|
+
} as never);
|
|
646
|
+
await connector.setup?.();
|
|
647
|
+
mobileOptions().preferredOpenLink!("metamask://connect/mwp?id=abc");
|
|
648
|
+
|
|
649
|
+
await connector.disconnect();
|
|
650
|
+
|
|
651
|
+
expect(reopenMetaMask()).toBe(false);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it("leaves MetaMask Connect's universal-link opener alone when deeplinks are off", async () => {
|
|
655
|
+
await build([baseSepolia], { useDeeplink: false }).setup?.();
|
|
656
|
+
expect(mobileOptions()).toEqual({ useDeeplink: false });
|
|
657
|
+
});
|
|
658
|
+
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// and ConnectWithInjector's display_uri listener all key off it, and so does
|
|
13
13
|
// every consumer's persisted "recent connector".
|
|
14
14
|
import type { EIP1193Provider, MetamaskConnectEVM } from "@metamask/connect-evm";
|
|
15
|
+
import { isMobile } from "@multiplatform.one/connectkit";
|
|
15
16
|
import { ChainNotConfiguredError, type CreateConnectorFn, createConnector } from "wagmi";
|
|
16
17
|
import {
|
|
17
18
|
type Address,
|
|
@@ -36,6 +37,18 @@ export type MetaMaskConnectParameters = {
|
|
|
36
37
|
/** `metamask://` instead of the https://metamask.app.link universal link. */
|
|
37
38
|
useDeeplink?: boolean;
|
|
38
39
|
debug?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* MetaMask is the phone app, reached over the relay: every request it has to
|
|
42
|
+
* approve is a trip to the app that only a tap can start. Defaults to
|
|
43
|
+
* connectkit's `isMobile()`.
|
|
44
|
+
*/
|
|
45
|
+
mobile?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* On a phone, whether a switch to `chain` asks MetaMask to add it
|
|
48
|
+
* (`wallet_addEthereumChain`, which adds the network and switches in one trip)
|
|
49
|
+
* rather than to switch to it. Defaults to `metaMaskMayLackChain`.
|
|
50
|
+
*/
|
|
51
|
+
addChainFirst?: (chain: Chain) => boolean;
|
|
39
52
|
};
|
|
40
53
|
|
|
41
54
|
type ConnectorConfig = Parameters<CreateConnectorFn>[0];
|
|
@@ -149,6 +162,26 @@ function isUrl(value: string): boolean {
|
|
|
149
162
|
}
|
|
150
163
|
}
|
|
151
164
|
|
|
165
|
+
// The testnets MetaMask 8 lists in its own network picker. An add request for
|
|
166
|
+
// one of these shows "Update <network>" with a warning that the RPC URL is not a
|
|
167
|
+
// known provider, so a switch asks for them instead.
|
|
168
|
+
const metaMaskBuiltInTestnets = new Set([11155111, 59141, 10143, 6342, 6343]);
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether a phone's MetaMask may not have `chain` yet: a testnet it does not
|
|
172
|
+
* ship. A switch to a chain MetaMask lacks only answers "Unrecognized chain ID",
|
|
173
|
+
* and the add request that has to follow is a second trip to the app, which
|
|
174
|
+
* Chrome on Android drops when no tap started it.
|
|
175
|
+
*/
|
|
176
|
+
export function metaMaskMayLackChain(chain: Chain): boolean {
|
|
177
|
+
return chain.testnet === true && !metaMaskBuiltInTestnets.has(chain.id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isUnrecognizedChain(err: unknown): boolean {
|
|
181
|
+
const { code, message } = (err ?? {}) as { code?: number; message?: unknown };
|
|
182
|
+
return code === 4902 || (typeof message === "string" && message.includes("Unrecognized chain"));
|
|
183
|
+
}
|
|
184
|
+
|
|
152
185
|
/** The chain ids a connect asks MetaMask for, the requested one first (it becomes the active chain). */
|
|
153
186
|
export function metaMaskConnectChainIds(networks: Record<Hex, string>, chainId?: number): Hex[] {
|
|
154
187
|
const ids = Object.keys(networks) as Hex[];
|
|
@@ -164,6 +197,43 @@ export function metaMaskConnectChainIds(networks: Record<Hex, string>, chainId?:
|
|
|
164
197
|
// build connectors that wagmi never uses, and one of those may be the newest.
|
|
165
198
|
let client: Promise<MetamaskConnectEVM> | undefined;
|
|
166
199
|
let owner: Emitter | undefined;
|
|
200
|
+
// Chains the session is known to permit, where a switch stays in the page, and
|
|
201
|
+
// chains MetaMask answered "Unrecognized chain ID" for, which a switch adds.
|
|
202
|
+
const permittedChains = new Set<number>();
|
|
203
|
+
const unrecognizedChains = new Set<number>();
|
|
204
|
+
let lastDeeplink: { url: string; open: (url: string, target?: string) => void } | undefined;
|
|
205
|
+
|
|
206
|
+
function forgetChains(): void {
|
|
207
|
+
permittedChains.clear();
|
|
208
|
+
unrecognizedChains.clear();
|
|
209
|
+
lastDeeplink = undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Opens the MetaMask app again on the request it was last sent to. Call it
|
|
214
|
+
* synchronously from a tap, for an "Open MetaMask" button the page shows while
|
|
215
|
+
* a request waits: MetaMask can open before the request reaches it and show
|
|
216
|
+
* nothing, and a request sent after an await or a trip back opens nothing on
|
|
217
|
+
* Chrome for Android. Returns false when there is nothing to reopen.
|
|
218
|
+
*/
|
|
219
|
+
export function reopenMetaMask(): boolean {
|
|
220
|
+
if (!lastDeeplink) return false;
|
|
221
|
+
lastDeeplink.open(lastDeeplink.url, "_self");
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function recordingOpenLink(
|
|
226
|
+
open: (url: string, target?: string) => void,
|
|
227
|
+
): (url: string, target?: string) => void {
|
|
228
|
+
return (url, target) => {
|
|
229
|
+
lastDeeplink = { url, open };
|
|
230
|
+
open(url, target);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function navigate(url: string): void {
|
|
235
|
+
if (typeof window !== "undefined") window.location.href = url;
|
|
236
|
+
}
|
|
167
237
|
|
|
168
238
|
function getClient(
|
|
169
239
|
parameters: MetaMaskConnectParameters,
|
|
@@ -184,18 +254,17 @@ function getClient(
|
|
|
184
254
|
skipAutoAnnounce: true,
|
|
185
255
|
debug: parameters.debug,
|
|
186
256
|
...(parameters.headless ? { ui: { headless: true } } : {}),
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
: {}),
|
|
257
|
+
mobile: {
|
|
258
|
+
// MetaMask Connect's own opener does the same location change for a
|
|
259
|
+
// metamask:// link; going through one here keeps the link to reopen.
|
|
260
|
+
// With useDeeplink false it clicks a universal link instead, left as is.
|
|
261
|
+
...(parameters.preferredOpenLink
|
|
262
|
+
? { preferredOpenLink: recordingOpenLink(parameters.preferredOpenLink) }
|
|
263
|
+
: parameters.useDeeplink === false
|
|
264
|
+
? {}
|
|
265
|
+
: { preferredOpenLink: recordingOpenLink(navigate) }),
|
|
266
|
+
...(parameters.useDeeplink !== undefined ? { useDeeplink: parameters.useDeeplink } : {}),
|
|
267
|
+
},
|
|
199
268
|
eventHandlers: {
|
|
200
269
|
// A session approved outside connect(), in another tab say. wagmi drops
|
|
201
270
|
// `change` for a connector it has no connection for, so only `connect`
|
|
@@ -208,11 +277,19 @@ function getClient(
|
|
|
208
277
|
});
|
|
209
278
|
},
|
|
210
279
|
accountsChanged: (accounts) => {
|
|
211
|
-
if (accounts.length === 0)
|
|
212
|
-
|
|
280
|
+
if (accounts.length === 0) {
|
|
281
|
+
forgetChains();
|
|
282
|
+
owner?.emit("disconnect");
|
|
283
|
+
} else owner?.emit("change", { accounts: accounts.map((a) => getAddress(a)) });
|
|
284
|
+
},
|
|
285
|
+
chainChanged: (chainId) => {
|
|
286
|
+
permittedChains.add(Number(chainId));
|
|
287
|
+
owner?.emit("change", { chainId: Number(chainId) });
|
|
288
|
+
},
|
|
289
|
+
disconnect: () => {
|
|
290
|
+
forgetChains();
|
|
291
|
+
owner?.emit("disconnect");
|
|
213
292
|
},
|
|
214
|
-
chainChanged: (chainId) => owner?.emit("change", { chainId: Number(chainId) }),
|
|
215
|
-
disconnect: () => owner?.emit("disconnect"),
|
|
216
293
|
displayUri: (uri) => owner?.emit("message", { type: "display_uri", data: uri }),
|
|
217
294
|
},
|
|
218
295
|
}),
|
|
@@ -227,6 +304,7 @@ function getClient(
|
|
|
227
304
|
export function resetMetaMaskConnectClient(): void {
|
|
228
305
|
client = undefined;
|
|
229
306
|
owner = undefined;
|
|
307
|
+
forgetChains();
|
|
230
308
|
}
|
|
231
309
|
|
|
232
310
|
function rethrow(err: unknown): never {
|
|
@@ -244,6 +322,8 @@ export function metaMaskConnect(parameters: MetaMaskConnectParameters) {
|
|
|
244
322
|
const networks = metaMaskNetworks(config.chains, config.transports);
|
|
245
323
|
const supportedNetworks = metaMaskSupportedNetworks(networks, config.transports);
|
|
246
324
|
const fallbackChainId = Number(Object.keys(networks)[0] ?? 1);
|
|
325
|
+
const mobile = parameters.mobile ?? isMobile();
|
|
326
|
+
const addChainFirst = parameters.addChainFirst ?? metaMaskMayLackChain;
|
|
247
327
|
const instance = () => getClient(parameters, supportedNetworks);
|
|
248
328
|
let guarded: { provider: EIP1193Provider; wrapped: EIP1193Provider } | undefined;
|
|
249
329
|
// Every call wagmi makes on the connector it uses claims the client's events;
|
|
@@ -280,12 +360,15 @@ export function metaMaskConnect(parameters: MetaMaskConnectParameters) {
|
|
|
280
360
|
});
|
|
281
361
|
accounts = result.accounts.map((a) => getAddress(a));
|
|
282
362
|
}
|
|
283
|
-
|
|
363
|
+
const current = chainIdOf(mm);
|
|
364
|
+
permittedChains.add(current);
|
|
365
|
+
return { accounts: accounts as never, chainId: current };
|
|
284
366
|
} catch (err) {
|
|
285
367
|
rethrow(err);
|
|
286
368
|
}
|
|
287
369
|
},
|
|
288
370
|
async disconnect() {
|
|
371
|
+
forgetChains();
|
|
289
372
|
await (await claim()).disconnect();
|
|
290
373
|
},
|
|
291
374
|
async getAccounts() {
|
|
@@ -317,27 +400,56 @@ export function metaMaskConnect(parameters: MetaMaskConnectParameters) {
|
|
|
317
400
|
const chain = config.chains.find((c) => c.id === chainId);
|
|
318
401
|
if (!chain || !networks[numberToHex(chainId)])
|
|
319
402
|
throw new SwitchChainError(new ChainNotConfiguredError());
|
|
403
|
+
const chainConfiguration = {
|
|
404
|
+
chainId: numberToHex(chainId),
|
|
405
|
+
chainName: addEthereumChainParameter?.chainName ?? chain.name,
|
|
406
|
+
nativeCurrency: addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency,
|
|
407
|
+
rpcUrls: addEthereumChainParameter?.rpcUrls
|
|
408
|
+
? [...addEthereumChainParameter.rpcUrls]
|
|
409
|
+
: [networks[numberToHex(chainId)]!],
|
|
410
|
+
...(chain.blockExplorers?.default.url
|
|
411
|
+
? { blockExplorerUrls: [chain.blockExplorers.default.url] }
|
|
412
|
+
: {}),
|
|
413
|
+
};
|
|
414
|
+
const mm = await claim();
|
|
415
|
+
if (mm.getChainId() === numberToHex(chainId)) return chain;
|
|
320
416
|
try {
|
|
321
|
-
|
|
322
|
-
await
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
417
|
+
if (!mobile) {
|
|
418
|
+
await mm.switchChain({ chainId: numberToHex(chainId), chainConfiguration });
|
|
419
|
+
} else if (
|
|
420
|
+
!permittedChains.has(chainId) &&
|
|
421
|
+
(unrecognizedChains.has(chainId) || addChainFirst(chain))
|
|
422
|
+
) {
|
|
423
|
+
// One trip that adds the network and permits it. MetaMask Connect
|
|
424
|
+
// takes the chain on once the wallet answers.
|
|
425
|
+
await mm.getProvider().request({
|
|
426
|
+
method: "wallet_addEthereumChain",
|
|
427
|
+
params: [chainConfiguration],
|
|
428
|
+
} as never);
|
|
429
|
+
} else {
|
|
430
|
+
// Without a configuration, so a chain MetaMask lacks fails here
|
|
431
|
+
// instead of MetaMask Connect sending the add request as a second
|
|
432
|
+
// trip, which no tap started and Chrome on Android drops.
|
|
433
|
+
await mm.switchChain({ chainId: numberToHex(chainId) });
|
|
434
|
+
}
|
|
435
|
+
permittedChains.add(chainId);
|
|
436
|
+
unrecognizedChains.delete(chainId);
|
|
337
437
|
return chain;
|
|
338
438
|
} catch (err) {
|
|
339
439
|
if ((err as { code?: number })?.code === UserRejectedRequestError.code)
|
|
340
440
|
throw new UserRejectedRequestError(err as Error);
|
|
441
|
+
if (mobile && isUnrecognizedChain(err)) {
|
|
442
|
+
unrecognizedChains.add(chainId);
|
|
443
|
+
throw new SwitchChainError(
|
|
444
|
+
Object.assign(
|
|
445
|
+
new Error(
|
|
446
|
+
`MetaMask does not have ${chain.name} yet. Switch again from a tap to add it.`,
|
|
447
|
+
{ cause: err },
|
|
448
|
+
),
|
|
449
|
+
{ code: 4902 },
|
|
450
|
+
),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
341
453
|
throw new SwitchChainError(err as Error);
|
|
342
454
|
}
|
|
343
455
|
},
|
package/src/connectkit/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { default as getDefaultConnectors } from "./defaultConnectors";
|
|
|
6
6
|
export { wallets } from "./wallets";
|
|
7
7
|
export {
|
|
8
8
|
metaMaskConnect,
|
|
9
|
+
metaMaskMayLackChain,
|
|
10
|
+
reopenMetaMask,
|
|
9
11
|
type MetaMaskConnectParameters,
|
|
10
12
|
} from "./connectors/metaMaskConnectConnector";
|
|
11
13
|
|
|
@@ -7,7 +7,7 @@ import React, {
|
|
|
7
7
|
useRef,
|
|
8
8
|
useSyncExternalStore,
|
|
9
9
|
} from "react";
|
|
10
|
-
import { useAccountEffect, type Connector } from "wagmi";
|
|
10
|
+
import { ChainNotConfiguredError, useAccountEffect, useConfig, type Connector } from "wagmi";
|
|
11
11
|
import {
|
|
12
12
|
routes,
|
|
13
13
|
useContext,
|
|
@@ -96,10 +96,12 @@ export const ConnectorLauncherProvider: React.FC<{ children: React.ReactNode }>
|
|
|
96
96
|
const wallets = useWallets();
|
|
97
97
|
const { connectAsync } = useConnect();
|
|
98
98
|
const connectors = useConnectors();
|
|
99
|
+
const { chains } = useConfig();
|
|
99
100
|
|
|
100
101
|
const depsRef = useRef<LauncherDeps>(null as unknown as LauncherDeps);
|
|
101
102
|
depsRef.current = {
|
|
102
|
-
connect: (connector) =>
|
|
103
|
+
connect: (connector, chainId) =>
|
|
104
|
+
connectAsync(chainId === undefined ? { connector } : { connector, chainId }),
|
|
103
105
|
showConnecting: (walletId) => {
|
|
104
106
|
context.setConnector({ id: walletId });
|
|
105
107
|
context.setRoute(routes.CONNECT);
|
|
@@ -111,8 +113,8 @@ export const ConnectorLauncherProvider: React.FC<{ children: React.ReactNode }>
|
|
|
111
113
|
},
|
|
112
114
|
onFailed: () => clearConnectIntent(),
|
|
113
115
|
};
|
|
114
|
-
const latest = useRef({ wallets, connectors, options: context.options });
|
|
115
|
-
latest.current = { wallets, connectors, options: context.options };
|
|
116
|
+
const latest = useRef({ wallets, connectors, chains, options: context.options });
|
|
117
|
+
latest.current = { wallets, connectors, chains, options: context.options };
|
|
116
118
|
|
|
117
119
|
const launcher = useMemo(() => createConnectorLauncher(() => depsRef.current), []);
|
|
118
120
|
useEffect(() => () => launcher.cancel(), [launcher]);
|
|
@@ -133,7 +135,9 @@ export const ConnectorLauncherProvider: React.FC<{ children: React.ReactNode }>
|
|
|
133
135
|
|
|
134
136
|
const openConnector = useCallback(
|
|
135
137
|
(id: string, options: OpenConnectorOptions = {}) => {
|
|
136
|
-
const { wallets, connectors } = latest.current;
|
|
138
|
+
const { wallets, connectors, chains } = latest.current;
|
|
139
|
+
if (options.chainId !== undefined && !chains.some((chain) => chain.id === options.chainId))
|
|
140
|
+
throw new ChainNotConfiguredError();
|
|
137
141
|
const wallet = resolveWallet(wallets, connectors, id);
|
|
138
142
|
if (!wallet) {
|
|
139
143
|
context.log(`openConnector: no connector ${id}, showing the wallet list.`);
|
|
@@ -145,6 +149,7 @@ export const ConnectorLauncherProvider: React.FC<{ children: React.ReactNode }>
|
|
|
145
149
|
setConnectIntent();
|
|
146
150
|
launcher.open(wallet.id, wallet.connector, {
|
|
147
151
|
fallbackMs: options.fallbackMs ?? defaultOpenConnectorFallbackMs(),
|
|
152
|
+
...(options.chainId === undefined ? {} : { chainId: options.chainId }),
|
|
148
153
|
});
|
|
149
154
|
},
|
|
150
155
|
// context.log is stable enough for a debug line; the rest is read through refs.
|
|
@@ -68,7 +68,7 @@ describe("createConnectorLauncher", () => {
|
|
|
68
68
|
launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
|
|
69
69
|
|
|
70
70
|
expect(deps.connect).toHaveBeenCalledTimes(1);
|
|
71
|
-
expect(deps.connect).toHaveBeenCalledWith(mm);
|
|
71
|
+
expect(deps.connect).toHaveBeenCalledWith(mm, undefined);
|
|
72
72
|
expect(calls).toEqual(["connect:metaMaskSDK", "connecting:metaMaskSDK"]);
|
|
73
73
|
expect(launcher.pending()?.walletId).toBe("metaMaskSDK");
|
|
74
74
|
});
|
|
@@ -146,7 +146,7 @@ describe("createConnectorLauncher", () => {
|
|
|
146
146
|
launcher.open("metaMaskSDK", mm, { fallbackMs: 6000 });
|
|
147
147
|
|
|
148
148
|
expect(deps.connect).toHaveBeenCalledTimes(2);
|
|
149
|
-
expect(deps.connect).toHaveBeenLastCalledWith(mm);
|
|
149
|
+
expect(deps.connect).toHaveBeenLastCalledWith(mm, undefined);
|
|
150
150
|
expect(launcher.pending()).toBe(first);
|
|
151
151
|
expect(vi.getTimerCount()).toBe(1);
|
|
152
152
|
|
|
@@ -323,3 +323,25 @@ describe("createConnectorLauncher", () => {
|
|
|
323
323
|
expect(deps.showList).not.toHaveBeenCalled();
|
|
324
324
|
});
|
|
325
325
|
});
|
|
326
|
+
|
|
327
|
+
describe("createConnectorLauncher with a chainId", () => {
|
|
328
|
+
it("hands the chain to connect() inside the same synchronous call", () => {
|
|
329
|
+
const { launcher, deps } = setup();
|
|
330
|
+
const mm = fakeConnector("metaMaskSDK");
|
|
331
|
+
|
|
332
|
+
launcher.open("metaMaskSDK", mm, { fallbackMs: 0, chainId: 84532 });
|
|
333
|
+
|
|
334
|
+
expect(deps.connect).toHaveBeenCalledTimes(1);
|
|
335
|
+
expect(deps.connect).toHaveBeenCalledWith(mm, 84532);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("re-issues a pending connect on the chain the new tap asks for", () => {
|
|
339
|
+
const { launcher, deps } = setup();
|
|
340
|
+
const mm = fakeConnector("metaMaskSDK");
|
|
341
|
+
launcher.open("metaMaskSDK", mm, { fallbackMs: 0, chainId: 84532 });
|
|
342
|
+
|
|
343
|
+
launcher.open("metaMaskSDK", mm, { fallbackMs: 0, chainId: 84532 });
|
|
344
|
+
|
|
345
|
+
expect(deps.connect).toHaveBeenNthCalledWith(2, mm, 84532);
|
|
346
|
+
});
|
|
347
|
+
});
|