Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 68 additions & 23 deletions integrations/wagmi/metamask-connector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type AddEthereumChainParameter,
createMetamaskConnectEVM,
type EIP1193Provider,
type MetamaskConnectEVM,
} from '@metamask/connect-evm';

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

import {
type EIP1193Provider,
type Address,
getAddress,
type ProviderConnectInfo,
ResourceUnavailableRpcError,
Expand Down Expand Up @@ -80,30 +81,83 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
});
},

async connect<withCapabilities extends boolean = false>(parameters?: {
async connect<withCapabilities extends boolean = false>(connectParams?: {
chainId?: number | undefined;
isReconnecting?: boolean | undefined;
withCapabilities?: withCapabilities | boolean | undefined;
}) {
const chainId = parameters?.chainId ?? DEFAULT_CHAIN_ID;
const withCapabilities = parameters?.withCapabilities;

// TODO: Add connectAndSign and connectWith support, including events
const chainId = connectParams?.chainId ?? DEFAULT_CHAIN_ID;
const withCapabilities = connectParams?.withCapabilities;

let accounts: readonly string[] = [];
if (connectParams?.isReconnecting) {
accounts = (await this.getAccounts().catch(() => [])).map((account) =>
getAddress(account),
);
}

try {
const result = await metamask.connect({
chainId,
account: undefined,
});
let signResponse: string | undefined;
let connectWithResponse: unknown | undefined;

if (!accounts?.length) {
if (parameters.connectAndSign || parameters.connectWith) {
if (parameters.connectAndSign) {
signResponse = await (metamask as any).connectAndSign({
msg: parameters.connectAndSign,
});
} else if (parameters.connectWith) {
connectWithResponse = await (metamask as any).connectWith({
method: parameters.connectWith.method,
params: parameters.connectWith.params,
});
}

accounts = (await this.getAccounts()).map((account) =>
getAddress(account),
);
} else {
const result = await metamask.connect({
chainId,
account: undefined,
});
accounts = result.accounts.map((account) => getAddress(account));
}
}

// Switch to chain if provided
let currentChainId = (await this.getChainId()) as number;
if (chainId && currentChainId !== chainId) {
const chain = await this.switchChain!({ chainId }).catch((error) => {
if (error.code === UserRejectedRequestError.code) throw error;
return { id: currentChainId };
});
currentChainId = chain?.id ?? currentChainId;
}

// Emit events for connectAndSign and connectWith
const provider = await this.getProvider();
if (signResponse)
provider.emit('connectAndSign', {
accounts: accounts as Address[],
chainId: currentChainId,
signResponse,
});
else if (connectWithResponse)
provider.emit('connectWith', {
accounts: accounts as Address[],
chainId: currentChainId,
connectWithResponse,
});

return {
accounts: (withCapabilities
? result.accounts.map((account) => ({
? accounts.map((account) => ({
address: account,
capabilities: {},
}))
: result.accounts) as never,
chainId: result.chainId ?? chainId,
: accounts) as never,
chainId: currentChainId,
};
} catch (err) {
const error = err as RpcError;
Expand Down Expand Up @@ -199,15 +253,6 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
},

async onAccountsChanged(accounts) {
// TODO: verify if this is needed or if we can just rely on the
// existing disconnect event instead
// Disconnect if there are no accounts
if (accounts.length === 0) {
this.onDisconnect();
return;
}
// Regular change event

config.emitter.emit('change', {
accounts: accounts.map((account) => getAddress(account)),
});
Expand All @@ -231,7 +276,7 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
// https://github.com/MetaMask/providers/pull/120
if (error && (error as unknown as RpcError<1013>).code === 1013) {
const provider = await this.getProvider();
if (provider && (await this.getAccounts()).length > 0) return;
if (provider && !!(await this.getAccounts()).length) return;
}

config.emitter.emit('disconnect');
Expand Down
86 changes: 84 additions & 2 deletions packages/connect-evm/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class MetamaskConnectEVM {
#sessionScopes: SessionData['sessionScopes'] = {};

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

/** The handler for the wallet_sessionChanged event */
readonly #sessionChangedHandler: (session?: SessionData) => void;
Expand Down Expand Up @@ -248,6 +248,88 @@ export class MetamaskConnectEVM {
})) as string;
}

/**
* Connects to the wallet and invokes a method with specified parameters.
*
* @param options - The options for connecting and invoking the method
* @param options.method - The method name to invoke
* @param options.params - The parameters to pass to the method, or a function that receives the account and returns params
* @param options.chainId - Optional chain ID to connect to (defaults to mainnet)
* @param options.account - Optional specific account to connect to
* @param options.forceRequest - Whether to force a request regardless of an existing session
* @returns A promise that resolves with the result of the method invocation
* @throws Error if the selected account is not available after timeout (for methods that require an account)
*/
async connectWith({
method,
params,
chainId,
account,
forceRequest,
}: {
method: string;
params: unknown[] | ((account: Address) => unknown[]);
chainId?: number;
account?: string | undefined;
forceRequest?: boolean;
}): Promise<unknown> {
await this.connect({
chainId: chainId ?? DEFAULT_CHAIN_ID,
account,
forceRequest,
});

// If account is already available, proceed immediately
if (this.#provider.selectedAccount) {
const resolvedParams =
typeof params === 'function'
? params(this.#provider.selectedAccount)
: params;
return await this.#provider.request({
method,
params: resolvedParams,
});
}

// Otherwise, wait for the accountsChanged event to be triggered
// This is only needed for methods that require an account
const timeout = 5000;
const accountPromise = new Promise<Address>((resolve, reject) => {
// eslint-disable-next-line prefer-const
let timeoutId: ReturnType<typeof setTimeout>;

const handler = (accounts: Address[]): void => {
if (accounts.length > 0) {
clearTimeout(timeoutId);
this.#provider.off('accountsChanged', handler);
resolve(accounts[0]);
}
};

this.#provider.on('accountsChanged', handler);

timeoutId = setTimeout(() => {
this.#provider.off('accountsChanged', handler);
reject(new Error('Selected account not available after timeout'));
}, timeout);

if (this.#provider.selectedAccount) {
clearTimeout(timeoutId);
this.#provider.off('accountsChanged', handler);
resolve(this.#provider.selectedAccount);
}
});

const selectedAccount = await accountPromise;
const resolvedParams =
typeof params === 'function' ? params(selectedAccount) : params;

return await this.#provider.request({
method,
params: resolvedParams,
});
}

/**
* Disconnects from the wallet by revoking the session and cleaning up event listeners.
*
Expand Down Expand Up @@ -615,7 +697,7 @@ export class MetamaskConnectEVM {
*/
export async function createMetamaskConnectEVM(
options: Pick<MultichainOptions, 'dapp' | 'api'> & {
eventHandlers?: EventHandlers;
eventHandlers?: Partial<EventHandlers>;
debug?: boolean;
},
): Promise<MetamaskConnectEVM> {
Expand Down
26 changes: 23 additions & 3 deletions packages/connect-evm/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,38 @@ export type EIP1193ProviderEvents = {
accountsChanged: [Address[]];
chainChanged: [Hex];
message: [{ type: string; data: unknown }];
connectAndSign: [
{ accounts: readonly Address[]; chainId: number; signResponse: string },
];
connectWith: [
{
accounts: readonly Address[];
chainId: number;
connectWithResponse: unknown;
},
];
};

export type EventHandlers = {
connect: (result: { chainId?: Hex }) => void;
connect: (result: { chainId: string }) => void;
disconnect: () => void;
accountsChanged: (accounts: Address[]) => void;
chainChanged: (chainId: Hex) => void;
connectAndSign: (result: {
accounts: readonly Address[];
chainId: number;
signResponse: string;
}) => void;
connectWith: (result: {
accounts: readonly Address[];
chainId: number;
connectWithResponse: unknown;
}) => void;
};

export type MetamaskConnectEVMOptions = {
core: MultichainCore;
eventHandlers?: EventHandlers;
eventHandlers?: Partial<EventHandlers>;
notificationQueue?: unknown[];
supportedNetworks?: Record<CaipChainId, string>;
};
Expand Down Expand Up @@ -73,7 +93,7 @@ type GenericProviderRequest = {
| 'wallet_switchEthereumChain'
| 'wallet_addEthereumChain'
>;
params: unknown;
params?: unknown;
};

// Discriminated union for provider requests
Expand Down
Loading
Loading