Skip to content

Commit

Permalink
fix metamask snap connect
Browse files Browse the repository at this point in the history
  • Loading branch information
peterjah committed Dec 19, 2024
1 parent 4b5cd37 commit cc6950d
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 227 deletions.
3 changes: 3 additions & 0 deletions jest.e2e.config.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ module.exports = {
testEnvironmentOptions: {
url: 'https://station.massa',
},
globals: {
window: {},
},
};
2 changes: 1 addition & 1 deletion src/bearbyWallet/BearbyWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class BearbyWallet implements Wallet {
*
* @returns a boolean indicating whether the wallet is connected.
*/
public connected(): boolean {
public async connected(): Promise<boolean> {
return web3.wallet.connected;
}

Expand Down
2 changes: 1 addition & 1 deletion src/massaStation/MassaStationWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export class MassaStationWallet implements Wallet {
* Indicates if the station is connected.
* Always returns `true` because the station is always connected when running.
*/
public connected(): boolean {
public async connected(): Promise<boolean> {
return true;
}

Expand Down
22 changes: 6 additions & 16 deletions src/metamaskSnap/MetamaskWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@ import {
Provider,
} from '@massalabs/massa-web3';
import { WalletName } from '../wallet';
import {
getMetamaskProvider,
isMetaMaskUnlocked,
promptAndWaitForWalletUnlock,
} from './metamask';
import { connectSnap, getMassaSnapInfo } from './snap';
import { getMetamaskProvider } from './metamask';
import { connectSnap, isConnected } from './snap';
import { MetamaskAccount } from './MetamaskAccount';
import { MetaMaskInpageProvider } from '@metamask/providers';
import { getActiveAccount, getNetwork, setRpcUrl } from './services';
Expand Down Expand Up @@ -140,15 +136,9 @@ export class MetamaskWallet implements Wallet {
*/
public async connect() {
try {
const isUnlocked = await isMetaMaskUnlocked();

if (!isUnlocked) {
await promptAndWaitForWalletUnlock();
}

const snap = await getMassaSnapInfo(this.metamaskProvider);
const connected = await isConnected(this.metamaskProvider);

if (!snap) {
if (!connected) {
await connectSnap(this.metamaskProvider);
}
return true;
Expand All @@ -161,8 +151,8 @@ export class MetamaskWallet implements Wallet {
throw new Error('Method not implemented.');
}

public connected(): boolean {
return this.metamaskProvider.isConnected();
public connected(): Promise<boolean> {
return isConnected(this.metamaskProvider);
}

public enabled(): boolean {
Expand Down
12 changes: 2 additions & 10 deletions src/metamaskSnap/config/snap.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,2 @@
/**
* The snap origin to use.
* Will default to the local hosted snap if no value is provided in environment.
*
* You may be tempted to change this to the URL where your production snap is hosted, but please
* don't. Instead, rename `.env.production.dist` to `.env.production` and set the production URL
* there. Running `yarn build` will automatically use the production environment variables.
*/
// TODO - Change this to the massa snap origin when published
export const MASSA_SNAP_ID = `local:http://localhost:8080`;
export const MASSA_SNAP_ID = 'npm:@massalabs/metamask-snap';
// export const MASSA_SNAP_ID = `local:http://localhost:8080`;
168 changes: 31 additions & 137 deletions src/metamaskSnap/metamask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@ import type {
EIP6963AnnounceProviderEvent,
MetaMaskInpageProvider,
} from '@metamask/providers';

declare global {
interface Window {
ethereum?: MetaMaskInpageProvider & {
detected?: MetaMaskInpageProvider[];
providers?: MetaMaskInpageProvider[];
};
}
}
import { MetaMaskProvider } from './types/snap';
import { getInstalledSnaps } from './snap';

const PROVIDER_DETECTION_TIMEOUT = 500;

Expand All @@ -21,22 +14,27 @@ const PROVIDER_DETECTION_TIMEOUT = 500;
* @returns A promise that resolves to true if snaps are supported
*/
export async function hasSnapsSupport(
provider?: MetaMaskInpageProvider,
provider: MetaMaskInpageProvider,
): Promise<boolean> {
if (!provider) {
return false;
}

try {
await provider.request({
method: 'wallet_getSnaps',
});
await getInstalledSnaps(provider);
return true;
} catch {
return false;
}
}

function isMetaMaskProvider(obj: unknown): obj is MetaMaskProvider {
return (
obj !== null &&
typeof obj === 'object' &&
// eslint-disable-next-line no-prototype-builtins
obj.hasOwnProperty('isMetaMask') &&
// eslint-disable-next-line no-prototype-builtins
obj.hasOwnProperty('request')
);
}

/**
* Get a MetaMask provider using EIP6963 specification.
*
Expand All @@ -50,148 +48,44 @@ export async function getMetaMaskEIP6963Provider(): Promise<MetaMaskInpageProvid
}, PROVIDER_DETECTION_TIMEOUT);

function cleanup() {
window.removeEventListener(
'eip6963:announceProvider',
handleAnnouncement,
);
if (typeof window.removeEventListener === 'function') {
window.removeEventListener(
'eip6963:announceProvider',
handleAnnouncement,
);
}
clearTimeout(timeout);
}

function handleAnnouncement({ detail }: EIP6963AnnounceProviderEvent) {
if (detail.info.rdns.includes('io.metamask')) {
if (
['io.metamask', 'io.metamask.flask'].includes(detail.info.rdns) &&
isMetaMaskProvider(detail.provider)
) {
cleanup();
resolve(detail.provider);
}
}

window.addEventListener('eip6963:announceProvider', handleAnnouncement);
window.dispatchEvent(new Event('eip6963:requestProvider'));
});
}

/**
* Check providers from an array for snaps support
*
* @param providers - Array of providers to check
* @returns First provider with snaps support or null
*/
async function findProviderWithSnaps(
providers?: MetaMaskInpageProvider[],
): Promise<MetaMaskInpageProvider | null> {
if (!providers?.length) {
return null;
}

for (const provider of providers) {
if (await hasSnapsSupport(provider)) {
return provider;
if (typeof window.addEventListener === 'function') {
window.addEventListener('eip6963:announceProvider', handleAnnouncement);
}
}
return null;
if (typeof window.dispatchEvent === 'function') {
window.dispatchEvent(new Event('eip6963:requestProvider'));
}
});
}

/**
* Get a MetaMask provider that supports snaps.
* Checks multiple provider sources in order of priority.
*
* @returns Promise resolving to a compatible provider or null
*/
export async function getMetamaskProvider(): Promise<MetaMaskInpageProvider | null> {
// Check window.ethereum first
if (window.ethereum && (await hasSnapsSupport(window.ethereum))) {
return window.ethereum;
}

// Check detected providers
const detectedProvider = await findProviderWithSnaps(
window.ethereum?.detected,
);
if (detectedProvider) {
return detectedProvider;
}

// Check providers array
const arrayProvider = await findProviderWithSnaps(window.ethereum?.providers);
if (arrayProvider) {
return arrayProvider;
}

// Finally, try EIP6963
const eip6963Provider = await getMetaMaskEIP6963Provider();
if (eip6963Provider && (await hasSnapsSupport(eip6963Provider))) {
return eip6963Provider;
}

return null;
}

export async function isMetaMaskUnlocked() {
if (!isMetamaskInstalled()) {
return false;
}

const accounts: string[] = await window.ethereum.request({
method: 'eth_accounts',
});
return accounts.length > 0;
}

export function isMetamaskInstalled() {
return Boolean(window.ethereum);
}

export async function promptAndWaitForWalletUnlock(): Promise<string[]> {
if (typeof window.ethereum === 'undefined') {
throw new Error(
'MetaMask is not installed. Please install it and try again.',
);
}

const ethereum = window.ethereum;

try {
// Prompt the user to unlock the wallet
await ethereum.request({ method: 'eth_requestAccounts' });

// Wait for accounts to become available
return new Promise((resolve, reject) => {
const checkAccounts = async () => {
try {
const accounts: string[] = await ethereum.request({
method: 'eth_accounts',
});
if (accounts && accounts.length > 0) {
resolve(accounts); // Wallet is unlocked
}
} catch (error) {
reject(
new Error('Error checking accounts: ' + (error as Error).message),
);
}
};

// Initial check for accounts
checkAccounts();

ethereum.on('accountsChanged', (accounts: string[]) => {
if (accounts.length > 0) {
console.log('Wallet unlocked:', accounts);
resolve(accounts);
} else {
console.warn('Accounts changed, but no accounts are available.');
}
});
ethereum.on('disconnect', () => {
reject(new Error('MetaMask disconnected.'));
});
});
} catch (error: any) {
if (error.code === 4001) {
throw new Error('User rejected the request.');
} else if (error.message.includes('User closed popup')) {
throw new Error('MetaMask popup was closed without connecting.');
} else {
throw new Error('Wallet unlocking failed.');
}
}
}
Loading

0 comments on commit cc6950d

Please sign in to comment.