Skip to content

Commit fe42517

Browse files
wenfixjiexiadonesky1
authored
feat: connectWith method and playground updates (#53)
* connect with * add connectWith and update legacy playground to support it * add connectWith and connectAndSign event types * make params optional in GenericProviderRequest * fix onConnect event handler params * sync wagmi connector * Apply suggestion from @jiexi * rename to checksummedAccounts in wagmi connector * Cleanup/fix connectWith and connectAndSign flow/events? * changelog * changelog format fix --------- Co-authored-by: jiexi <jiexiluan@gmail.com> Co-authored-by: Alex Donesky <adonesky@gmail.com>
1 parent 32f3054 commit fe42517

5 files changed

Lines changed: 215 additions & 100 deletions

File tree

integrations/wagmi/metamask-connector.ts

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
type AddEthereumChainParameter,
33
createMetamaskConnectEVM,
4+
type EIP1193Provider,
45
type MetamaskConnectEVM,
56
} from '@metamask/connect-evm';
67

@@ -13,7 +14,7 @@ import {
1314
import type { OneOf } from '@wagmi/core/internal';
1415

1516
import {
16-
type EIP1193Provider,
17+
type Address,
1718
getAddress,
1819
type ProviderConnectInfo,
1920
ResourceUnavailableRpcError,
@@ -80,30 +81,74 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
8081
});
8182
},
8283

83-
async connect<withCapabilities extends boolean = false>(parameters?: {
84+
async connect<withCapabilities extends boolean = false>(connectParams?: {
8485
chainId?: number | undefined;
8586
isReconnecting?: boolean | undefined;
8687
withCapabilities?: withCapabilities | boolean | undefined;
8788
}) {
88-
const chainId = parameters?.chainId ?? DEFAULT_CHAIN_ID;
89-
const withCapabilities = parameters?.withCapabilities;
90-
91-
// TODO: Add connectAndSign and connectWith support, including events
89+
const chainId = connectParams?.chainId ?? DEFAULT_CHAIN_ID;
90+
const withCapabilities = connectParams?.withCapabilities;
91+
92+
let accounts: readonly Address[] = [];
93+
if (connectParams?.isReconnecting) {
94+
try {
95+
accounts = await this.getAccounts()
96+
} catch {
97+
// noop
98+
}
99+
}
92100

93101
try {
94-
const result = await metamask.connect({
95-
chainId,
96-
account: undefined,
97-
});
102+
let signResponse: string | undefined;
103+
let connectWithResponse: unknown | undefined;
104+
105+
if (!accounts?.length) {
106+
if (parameters.connectAndSign || parameters.connectWith) {
107+
if (parameters.connectAndSign) {
108+
signResponse = await (metamask as any).connectAndSign({
109+
msg: parameters.connectAndSign,
110+
});
111+
} else if (parameters.connectWith) {
112+
connectWithResponse = await (metamask as any).connectWith({
113+
method: parameters.connectWith.method,
114+
params: parameters.connectWith.params,
115+
});
116+
}
117+
118+
try {
119+
accounts = await this.getAccounts()
120+
} catch {
121+
// noop
122+
}
123+
} else {
124+
const result = await metamask.connect({
125+
chainId,
126+
account: undefined,
127+
});
128+
accounts = result.accounts
129+
}
130+
}
131+
132+
const checksummedAccounts = accounts.map((account) => getAddress(account));
133+
134+
// Switch to chain if provided
135+
let currentChainId = (await this.getChainId()) as number;
136+
if (chainId && currentChainId !== chainId) {
137+
const chain = await this.switchChain!({ chainId }).catch((error) => {
138+
if (error.code === UserRejectedRequestError.code) throw error;
139+
return { id: currentChainId };
140+
});
141+
currentChainId = chain?.id ?? currentChainId;
142+
}
98143

99144
return {
100145
accounts: (withCapabilities
101-
? result.accounts.map((account) => ({
146+
? checksummedAccounts.map((account) => ({
102147
address: account,
103148
capabilities: {},
104149
}))
105-
: result.accounts) as never,
106-
chainId: result.chainId ?? chainId,
150+
: checksummedAccounts) as never,
151+
chainId: currentChainId,
107152
};
108153
} catch (err) {
109154
const error = err as RpcError;
@@ -199,15 +244,6 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
199244
},
200245

201246
async onAccountsChanged(accounts) {
202-
// TODO: verify if this is needed or if we can just rely on the
203-
// existing disconnect event instead
204-
// Disconnect if there are no accounts
205-
if (accounts.length === 0) {
206-
this.onDisconnect();
207-
return;
208-
}
209-
// Regular change event
210-
211247
config.emitter.emit('change', {
212248
accounts: accounts.map((account) => getAddress(account)),
213249
});
@@ -231,7 +267,7 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
231267
// https://github.com/MetaMask/providers/pull/120
232268
if (error && (error as unknown as RpcError<1013>).code === 1013) {
233269
const provider = await this.getProvider();
234-
if (provider && (await this.getAccounts()).length > 0) return;
270+
if (provider && !!(await this.getAccounts()).length) return;
235271
}
236272

237273
config.emitter.emit('disconnect');

packages/connect-evm/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Added `connectWith()` method which makes a request for the method to be paired together with a connection request ([#53](https://github.com/MetaMask/connect-monorepo/pull/53))
13+
- This method expects param options `method`, `params`, `chainId`, and `account`
14+
- `params` can be a function or a plain value. When it is a function, it will be called with the selected account as the first param and must return a value that will be used as the actual params for the request.
15+
- Added `eventHandlers.connectAndSign` handler which is called on successful `connectAndSign` request with an object including the `accounts`, `chainId`, and `signResponse` ([#53](https://github.com/MetaMask/connect-monorepo/pull/53))
16+
- Added `eventHandlers.connectWith` handler which is called on successful `connectWith` request with an object including the `accounts`, `chainId`, and `connectWithResponse` ([#53](https://github.com/MetaMask/connect-monorepo/pull/53))
1217
- Added analytics tracking of request handling ([#46](https://github.com/MetaMask/connect-monorepo/pull/46))
1318
- Initial release ([#21](https://github.com/MetaMask/connect-monorepo/pull/21))
1419

20+
### Changed
21+
22+
- `connect()` now always returns `chainId`, previously it could undefined ([#53](https://github.com/MetaMask/connect-monorepo/pull/53))
23+
1524
### Fixed
1625

1726
- `ethereum_switchChain` fallback when chain is not configured ([#31](https://github.com/MetaMask/connect-monorepo/pull/31))

packages/connect-evm/src/connect.ts

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class MetamaskConnectEVM {
8080
#sessionScopes: SessionData['sessionScopes'] = {};
8181

8282
/** Optional event handlers for the EIP-1193 provider events. */
83-
readonly #eventHandlers?: EventHandlers | undefined;
83+
readonly #eventHandlers?: Partial<EventHandlers> | undefined;
8484

8585
/** The handler for the wallet_sessionChanged event */
8686
readonly #sessionChangedHandler: (session?: SessionData) => void;
@@ -273,7 +273,7 @@ export class MetamaskConnectEVM {
273273
account?: string | undefined;
274274
forceRequest?: boolean;
275275
} = { chainId: DEFAULT_CHAIN_ID }, // Default to mainnet if no chain ID is provided
276-
): Promise<{ accounts: Address[]; chainId?: number }> {
276+
): Promise<{ accounts: Address[]; chainId: number }> {
277277
logger('request: connect', { chainId, account });
278278
const caipChainId: Scope[] = chainId ? [`eip155:${chainId}`] : [];
279279

@@ -333,50 +333,69 @@ export class MetamaskConnectEVM {
333333
* @throws Error if the selected account is not available after timeout
334334
*/
335335
async connectAndSign(message: string): Promise<string> {
336-
await this.connect();
337-
338-
// If account is already available, proceed immediately
339-
if (this.#provider.selectedAccount) {
340-
return (await this.#provider.request({
341-
method: 'personal_sign',
342-
params: [this.#provider.selectedAccount, message],
343-
})) as string;
344-
}
336+
const { accounts, chainId } = await this.connect();
345337

346-
// Otherwise, wait for the accountsChanged event to be triggered
347-
const timeout = 5000;
348-
const accountPromise = new Promise<Address>((resolve, reject) => {
349-
// eslint-disable-next-line prefer-const
350-
let timeoutId: ReturnType<typeof setTimeout>;
351-
352-
const handler = (accounts: Address[]): void => {
353-
if (accounts.length > 0) {
354-
clearTimeout(timeoutId);
355-
this.#provider.off('accountsChanged', handler);
356-
resolve(accounts[0]);
357-
}
358-
};
338+
const result = (await this.#provider.request({
339+
method: 'personal_sign',
340+
params: [accounts[0], message],
341+
})) as string;
359342

360-
this.#provider.on('accountsChanged', handler);
343+
this.#eventHandlers?.connectAndSign?.({
344+
accounts,
345+
chainId,
346+
signResponse: result,
347+
})
361348

362-
timeoutId = setTimeout(() => {
363-
this.#provider.off('accountsChanged', handler);
364-
reject(new Error('Selected account not available after timeout'));
365-
}, timeout);
349+
return result;
350+
}
366351

367-
if (this.#provider.selectedAccount) {
368-
clearTimeout(timeoutId);
369-
this.#provider.off('accountsChanged', handler);
370-
resolve(this.#provider.selectedAccount);
371-
}
352+
353+
/**
354+
* Connects to the wallet and invokes a method with specified parameters.
355+
*
356+
* @param options - The options for connecting and invoking the method
357+
* @param options.method - The method name to invoke
358+
* @param options.params - The parameters to pass to the method, or a function that receives the account and returns params
359+
* @param options.chainId - Optional chain ID to connect to (defaults to mainnet)
360+
* @param options.account - Optional specific account to connect to
361+
* @param options.forceRequest - Whether to force a request regardless of an existing session
362+
* @returns A promise that resolves with the result of the method invocation
363+
* @throws Error if the selected account is not available after timeout (for methods that require an account)
364+
*/
365+
async connectWith({
366+
method,
367+
params,
368+
chainId,
369+
account,
370+
forceRequest,
371+
}: {
372+
method: string;
373+
params: unknown[] | ((account: Address) => unknown[]);
374+
chainId?: number;
375+
account?: string | undefined;
376+
forceRequest?: boolean;
377+
}): Promise<unknown> {
378+
const { accounts: connectedAccounts, chainId: connectedChainId} = await this.connect({
379+
chainId: chainId ?? DEFAULT_CHAIN_ID,
380+
account,
381+
forceRequest,
372382
});
373383

374-
const selectedAccount = await accountPromise;
384+
const resolvedParams =
385+
typeof params === 'function' ? params(connectedAccounts[0]) : params;
375386

376-
return (await this.#provider.request({
377-
method: 'personal_sign',
378-
params: [selectedAccount, message],
379-
})) as string;
387+
const result = await this.#provider.request({
388+
method,
389+
params: resolvedParams,
390+
});
391+
392+
this.#eventHandlers?.connectWith?.({
393+
accounts: connectedAccounts,
394+
chainId: connectedChainId,
395+
connectWithResponse: result,
396+
})
397+
398+
return result;
380399
}
381400

382401
/**
@@ -783,7 +802,7 @@ export class MetamaskConnectEVM {
783802
*/
784803
export async function createMetamaskConnectEVM(
785804
options: Pick<MultichainOptions, 'dapp' | 'api'> & {
786-
eventHandlers?: EventHandlers;
805+
eventHandlers?: Partial<EventHandlers>;
787806
debug?: boolean;
788807
},
789808
): Promise<MetamaskConnectEVM> {

packages/connect-evm/src/types.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,38 @@ export type EIP1193ProviderEvents = {
1212
accountsChanged: [Address[]];
1313
chainChanged: [Hex];
1414
message: [{ type: string; data: unknown }];
15+
connectAndSign: [
16+
{ accounts: readonly Address[]; chainId: number; signResponse: string },
17+
];
18+
connectWith: [
19+
{
20+
accounts: readonly Address[];
21+
chainId: number;
22+
connectWithResponse: unknown;
23+
},
24+
];
1525
};
1626

1727
export type EventHandlers = {
18-
connect: (result: { chainId?: Hex }) => void;
28+
connect: (result: { chainId: string, accounts: Address[] }) => void;
1929
disconnect: () => void;
2030
accountsChanged: (accounts: Address[]) => void;
2131
chainChanged: (chainId: Hex) => void;
32+
connectAndSign: (result: {
33+
accounts: readonly Address[];
34+
chainId: number;
35+
signResponse: string;
36+
}) => void;
37+
connectWith: (result: {
38+
accounts: readonly Address[];
39+
chainId: number;
40+
connectWithResponse: unknown;
41+
}) => void;
2242
};
2343

2444
export type MetamaskConnectEVMOptions = {
2545
core: MultichainCore;
26-
eventHandlers?: EventHandlers;
46+
eventHandlers?: Partial<EventHandlers>;
2747
notificationQueue?: unknown[];
2848
supportedNetworks?: Record<CaipChainId, string>;
2949
};
@@ -78,7 +98,7 @@ type GenericProviderRequest = {
7898
| 'wallet_switchEthereumChain'
7999
| 'wallet_addEthereumChain'
80100
>;
81-
params: unknown;
101+
params?: unknown;
82102
};
83103

84104
// Discriminated union for provider requests

0 commit comments

Comments
 (0)