Skip to content

Commit 02f20c2

Browse files
authored
Merge pull request #824 from vsakos/solflare-metamask-standard-adapter
Solflare: Initialize MetaMask standard adapter
2 parents 89e864e + 8390ba1 commit 02f20c2

File tree

7 files changed

+251
-1
lines changed

7 files changed

+251
-1
lines changed

.changeset/clever-jeans-shout.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solana/wallet-adapter-solflare': patch
3+
---
4+
5+
Add Solflare MetaMask Snap support

packages/wallets/solflare/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
},
3737
"dependencies": {
3838
"@solana/wallet-adapter-base": "workspace:^",
39-
"@solflare-wallet/sdk": "^1.3.0"
39+
"@solana/wallet-standard-chains": "^1.1.0",
40+
"@solflare-wallet/metamask-sdk": "^1.0.2",
41+
"@solflare-wallet/sdk": "^1.3.0",
42+
"@wallet-standard/wallet": "^1.0.1"
4043
},
4144
"devDependencies": {
4245
"@solana/web3.js": "^1.77.3",

packages/wallets/solflare/src/adapter.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import type { Transaction, TransactionVersion, VersionedTransaction } from '@solana/web3.js';
2323
import { type Connection, PublicKey, type TransactionSignature } from '@solana/web3.js';
2424
import type { default as Solflare } from '@solflare-wallet/sdk';
25+
import { detectAndRegisterSolflareMetaMaskWallet } from './metamask/detect.js';
2526

2627
interface SolflareWindow extends Window {
2728
solflare?: {
@@ -70,6 +71,7 @@ export class SolflareWalletAdapter extends BaseMessageSignerWalletAdapter {
7071
}
7172
return false;
7273
});
74+
scopePollingDetectionStrategy(detectAndRegisterSolflareMetaMaskWallet);
7375
}
7476
}
7577

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { EthereumProvider, WindowWithEthereum } from '@solflare-wallet/metamask-sdk';
2+
import { registerWallet } from '@wallet-standard/wallet';
3+
import { SolflareMetaMaskWallet } from './wallet.js';
4+
5+
let stopPolling = false;
6+
7+
/** @internal */
8+
export function detectAndRegisterSolflareMetaMaskWallet(): boolean {
9+
// If detected, stop polling.
10+
if (stopPolling) return true;
11+
(async function () {
12+
try {
13+
// Try to detect, stop polling if detected, and register the wallet.
14+
if (await isSnapProviderDetected()) {
15+
if (!stopPolling) {
16+
stopPolling = true;
17+
registerWallet(new SolflareMetaMaskWallet());
18+
}
19+
}
20+
} catch (error) {
21+
// Stop polling on unhandled errors (this should never happen).
22+
stopPolling = true;
23+
}
24+
})();
25+
// Keep polling.
26+
return false;
27+
}
28+
29+
async function isSnapProviderDetected(): Promise<boolean> {
30+
try {
31+
const provider = (window as WindowWithEthereum).ethereum;
32+
if (!provider) return false;
33+
34+
const providerProviders = provider.providers;
35+
if (providerProviders && Array.isArray(providerProviders)) {
36+
for (const provider of providerProviders) {
37+
if (await isSnapSupported(provider)) return true;
38+
}
39+
}
40+
41+
const providerDetected = provider.detected;
42+
if (providerDetected && Array.isArray(providerDetected)) {
43+
for (const provider of providerDetected) {
44+
if (await isSnapSupported(provider)) return true;
45+
}
46+
}
47+
48+
return await isSnapSupported(provider);
49+
} catch (error) {
50+
return false;
51+
}
52+
}
53+
54+
async function isSnapSupported(provider: EthereumProvider): Promise<boolean> {
55+
try {
56+
await provider.request({ method: 'wallet_getSnaps' });
57+
return true;
58+
} catch (error) {
59+
return false;
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { WalletIcon } from '@wallet-standard/base';
2+
3+
/** @internal */
4+
export const icon: WalletIcon =
5+
'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjMxIiB2aWV3Qm94PSIwIDAgMzEgMzEiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGxpbmVhckdyYWRpZW50IGlkPSJhIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjIwLjI1IiB4Mj0iMjYuNTcxIiB5MT0iMjcuMTczIiB5Mj0iMTkuODU4Ij48c3RvcCBvZmZzZXQ9Ii4wOCIgc3RvcC1jb2xvcj0iIzk5NDVmZiIvPjxzdG9wIG9mZnNldD0iLjMiIHN0b3AtY29sb3I9IiM4NzUyZjMiLz48c3RvcCBvZmZzZXQ9Ii41IiBzdG9wLWNvbG9yPSIjNTQ5N2Q1Ii8+PHN0b3Agb2Zmc2V0PSIuNiIgc3RvcC1jb2xvcj0iIzQzYjRjYSIvPjxzdG9wIG9mZnNldD0iLjcyIiBzdG9wLWNvbG9yPSIjMjhlMGI5Ii8+PHN0b3Agb2Zmc2V0PSIuOTciIHN0b3AtY29sb3I9IiMxOWZiOWIiLz48L2xpbmVhckdyYWRpZW50PjxnIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iLjA5NCI+PHBhdGggZD0ibTI2LjEwOSAzLjY0My05LjM2OSA2Ljk1OSAxLjczMy00LjEwNSA3LjYzNy0yLjg1M3oiIGZpbGw9IiNlMjc2MWIiIHN0cm9rZT0iI2UyNzYxYiIvPjxnIGZpbGw9IiNlNDc2MWIiIHN0cm9rZT0iI2U0NzYxYiI+PHBhdGggZD0ibTQuNDgxIDMuNjQzIDkuMjk0IDcuMDI0LTEuNjQ4LTQuMTcxem0xOC4yNTggMTYuMTMtMi40OTUgMy44MjMgNS4zMzkgMS40NjkgMS41MzUtNS4yMDctNC4zNzgtLjA4NXptLTE5LjI0Ny4wODUgMS41MjUgNS4yMDcgNS4zMzktMS40NjktMi40OTUtMy44MjN6Ii8+PHBhdGggZD0ibTEwLjA1NSAxMy4zMTMtMS40ODggMi4yNTEgNS4zMDEuMjM1LS4xODgtNS42OTd6bTEwLjQ4IDAtMy42NzItMy4yNzctLjEyMiA1Ljc2MyA1LjI5Mi0uMjM1LTEuNDk3LTIuMjUxem0tMTAuMTc4IDEwLjI4MyAzLjE4My0xLjU1NC0yLjc0OS0yLjE0Ny0uNDMzIDMuNzAxem02LjY5NS0xLjU1NCAzLjE5MiAxLjU1NC0uNDQzLTMuNzAxeiIvPjwvZz48cGF0aCBkPSJtMjAuMjQ0IDIzLjU5Ni0zLjE5Mi0xLjU1NC4yNTQgMi4wODEtLjAyOC44NzZ6bS05Ljg4NyAwIDIuOTY2IDEuNDAzLS4wMTktLjg3Ni4yMzUtMi4wODEtMy4xODMgMS41NTR6IiBmaWxsPSIjZDdjMWIzIiBzdHJva2U9IiNkN2MxYjMiLz48cGF0aCBkPSJtMTMuMzY5IDE4LjUyMS0yLjY1NS0uNzgxIDEuODc0LS44NTd6bTMuODUxIDAgLjc4MS0xLjYzOCAxLjg4My44NTctMi42NjUuNzgxeiIgZmlsbD0iIzIzMzQ0NyIgc3Ryb2tlPSIjMjMzNDQ3Ii8+PHBhdGggZD0ibTEwLjM1NyAyMy41OTYuNDUyLTMuODIzLTIuOTQ3LjA4NXptOS40MzUtMy44MjMuNDUyIDMuODIzIDIuNDk1LTMuNzM4em0yLjI0MS00LjIwOS01LjI5Mi4yMzUuNDkgMi43MjEuNzgyLTEuNjM4IDEuODgzLjg1N3ptLTExLjMxOCAyLjE3NSAxLjg4My0uODU3Ljc3MiAxLjYzOC40OTktMi43MjEtNS4zMDEtLjIzNXoiIGZpbGw9IiNjZDYxMTYiIHN0cm9rZT0iI2NkNjExNiIvPjxwYXRoIGQ9Im04LjU2NyAxNS41NjQgMi4yMjIgNC4zMzEtLjA3NS0yLjE1NnptMTEuMzI4IDIuMTc1LS4wOTQgMi4xNTYgMi4yMzItNC4zMzEtMi4xMzcgMi4xNzV6bS02LjAyNi0xLjk0LS40OTkgMi43MjEuNjIxIDMuMjExLjE0MS00LjIyOC0uMjY0LTEuNzA0em0yLjg3MiAwLS4yNTQgMS42OTUuMTEzIDQuMjM3LjYzMS0zLjIxMXoiIGZpbGw9IiNlNDc1MWYiIHN0cm9rZT0iI2U0NzUxZiIvPjxwYXRoIGQ9Im0xNy4yMyAxOC41Mi0uNjMxIDMuMjExLjQ1Mi4zMTEgMi43NS0yLjE0Ny4wOTQtMi4xNTZ6bS02LjUxNi0uNzgxLjA3NSAyLjE1NiAyLjc1IDIuMTQ3LjQ1Mi0uMzExLS42MjItMy4yMTF6IiBmaWxsPSIjZjY4NTFiIiBzdHJva2U9IiNmNjg1MWIiLz48cGF0aCBkPSJtMTcuMjc3IDI0Ljk5OS4wMjgtLjg3Ni0uMjM1LS4yMDdoLTMuNTVsLS4yMTcuMjA3LjAxOS44NzYtMi45NjYtMS40MDMgMS4wMzYuODQ4IDIuMSAxLjQ1OWgzLjYwNmwyLjEwOS0xLjQ1OSAxLjAzNi0uODQ4eiIgZmlsbD0iI2MwYWQ5ZSIgc3Ryb2tlPSIjYzBhZDllIi8+PHBhdGggZD0ibTE3LjA1MSAyMi4wNDItLjQ1Mi0uMzExaC0yLjYwOGwtLjQ1Mi4zMTEtLjIzNSAyLjA4MS4yMTctLjIwN2gzLjU1bC4yMzUuMjA3LS4yNTQtMi4wODF6IiBmaWxsPSIjMTYxNjE2IiBzdHJva2U9IiMxNjE2MTYiLz48cGF0aCBkPSJtMjYuNTA1IDExLjA1My44LTMuODQyLTEuMTk2LTMuNTY5LTkuMDU4IDYuNzIzIDMuNDg0IDIuOTQ3IDQuOTI1IDEuNDQxIDEuMDkyLTEuMjcxLS40NzEtLjMzOS43NTMtLjY4Ny0uNTg0LS40NTIuNzUzLS41NzQtLjQ5OS0uMzc3em0tMjMuMjExLTMuODQxLjggMy44NDItLjUwOC4zNzcuNzUzLjU3NC0uNTc0LjQ1Mi43NTMuNjg3LS40NzEuMzM5IDEuMDgzIDEuMjcxIDQuOTI1LTEuNDQxIDMuNDg0LTIuOTQ3LTkuMDU5LTYuNzIzeiIgZmlsbD0iIzc2M2QxNiIgc3Ryb2tlPSIjNzYzZDE2Ii8+PHBhdGggZD0ibTI1LjQ2IDE0Ljc1NC00LjkyNS0xLjQ0MSAxLjQ5NyAyLjI1MS0yLjIzMiA0LjMzMSAyLjkzOC0uMDM4aDQuMzc4bC0xLjY1Ny01LjEwNHptLTE1LjQwNS0xLjQ0MS00LjkyNSAxLjQ0MS0xLjYzOCA1LjEwNGg0LjM2OWwyLjkyOC4wMzgtMi4yMjItNC4zMzEgMS40ODgtMi4yNTF6bTYuNjg1IDIuNDg2LjMxMS01LjQzMyAxLjQzMS0zLjg3aC02LjM1NmwxLjQxMyAzLjg3LjMyOSA1LjQzMy4xMTMgMS43MTQuMDA5IDQuMjE5aDIuNjFsLjAxOS00LjIxOS4xMjItMS43MTR6IiBmaWxsPSIjZjY4NTFiIiBzdHJva2U9IiNmNjg1MWIiLz48L2c+PGNpcmNsZSBjeD0iMjMuNSIgY3k9IjIzLjUiIGZpbGw9IiMwMDAiIHI9IjYuNSIvPjxwYXRoIGQ9Im0yNy40NzMgMjUuNTQ1LTEuMzEgMS4zNjhjLS4wMjkuMDMtLjA2My4wNTMtLjEwMS4wN2EuMzEuMzEgMCAwIDEgLS4xMjEuMDI0aC02LjIwOWMtLjAzIDAtLjA1OS0uMDA4LS4wODMtLjAyNGEuMTUuMTUgMCAwIDEgLS4wNTYtLjA2NWMtLjAxMi0uMDI2LS4wMTUtLjA1Ni0uMDEtLjA4NHMuMDE4LS4wNTUuMDM5LS4wNzZsMS4zMTEtMS4zNjhjLjAyOC0uMDMuMDYzLS4wNTMuMTAxLS4wNjlhLjMxLjMxIDAgMCAxIC4xMjEtLjAyNWg2LjIwOGMuMDMgMCAuMDU5LjAwOC4wODMuMDI0YS4xNS4xNSAwIDAgMSAuMDU2LjA2NWMuMDEyLjAyNi4wMTUuMDU2LjAxLjA4NHMtLjAxOC4wNTUtLjAzOS4wNzZ6bS0xLjMxLTIuNzU2Yy0uMDI5LS4wMy0uMDYzLS4wNTMtLjEwMS0uMDdhLjMxLjMxIDAgMCAwIC0uMTIxLS4wMjRoLTYuMjA5Yy0uMDMgMC0uMDU5LjAwOC0uMDgzLjAyNHMtLjA0NC4wMzgtLjA1Ni4wNjUtLjAxNS4wNTYtLjAxLjA4NC4wMTguMDU1LjAzOS4wNzZsMS4zMTEgMS4zNjhjLjAyOC4wMy4wNjMuMDUzLjEwMS4wNjlhLjMxLjMxIDAgMCAwIC4xMjEuMDI1aDYuMjA4Yy4wMyAwIC4wNTktLjAwOC4wODMtLjAyNGEuMTUuMTUgMCAwIDAgLjA1Ni0uMDY1Yy4wMTItLjAyNi4wMTUtLjA1Ni4wMS0uMDg0cy0uMDE4LS4wNTUtLjAzOS0uMDc2em0tNi40MzEtLjk4M2g2LjIwOWEuMzEuMzEgMCAwIDAgLjEyMS0uMDI0Yy4wMzgtLjAxNi4wNzMtLjA0LjEwMS0uMDdsMS4zMS0xLjM2OGMuMDItLjAyMS4wMzQtLjA0Ny4wMzktLjA3NnMuMDAxLS4wNTgtLjAxLS4wODRhLjE1LjE1IDAgMCAwIC0uMDU2LS4wNjVjLS4wMjUtLjAxNi0uMDU0LS4wMjQtLjA4My0uMDI0aC02LjIwOGEuMzEuMzEgMCAwIDAgLS4xMjEuMDI1Yy0uMDM4LjAxNi0uMDcyLjA0LS4xMDEuMDY5bC0xLjMxIDEuMzY4Yy0uMDIuMDIxLS4wMzQuMDQ3LS4wMzkuMDc2cy0uMDAxLjA1OC4wMS4wODQuMDMxLjA0OS4wNTYuMDY1LjA1NC4wMjQuMDgzLjAyNHoiIGZpbGw9InVybCgjYSkiLz48L3N2Zz4=' as const;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { WalletNotConnectedError } from '@solana/wallet-adapter-base';
2+
import { SOLANA_DEVNET_CHAIN, SOLANA_MAINNET_CHAIN, SOLANA_TESTNET_CHAIN } from '@solana/wallet-standard-chains';
3+
import {
4+
SolanaSignAndSendTransaction,
5+
type SolanaSignAndSendTransactionFeature,
6+
type SolanaSignAndSendTransactionMethod,
7+
SolanaSignMessage,
8+
type SolanaSignMessageFeature,
9+
type SolanaSignMessageMethod,
10+
SolanaSignTransaction,
11+
type SolanaSignTransactionFeature,
12+
type SolanaSignTransactionMethod,
13+
} from '@solana/wallet-standard-features';
14+
import type { default as SolflareMetaMask } from '@solflare-wallet/metamask-sdk';
15+
import type { Wallet } from '@wallet-standard/base';
16+
import {
17+
StandardConnect,
18+
type StandardConnectFeature,
19+
type StandardConnectMethod,
20+
StandardDisconnect,
21+
type StandardDisconnectFeature,
22+
type StandardDisconnectMethod,
23+
StandardEvents,
24+
type StandardEventsChangeProperties,
25+
type StandardEventsFeature,
26+
type StandardEventsListeners,
27+
type StandardEventsNames,
28+
type StandardEventsOnMethod,
29+
} from '@wallet-standard/features';
30+
import { icon } from './icon.js';
31+
32+
export class SolflareMetaMaskWallet implements Wallet {
33+
readonly #listeners: { [E in StandardEventsNames]?: StandardEventsListeners[E][] } = {};
34+
readonly #version = '1.0.0' as const;
35+
readonly #name = 'MetaMask' as const;
36+
readonly #icon = icon;
37+
#solflareMetaMask: SolflareMetaMask | null = null;
38+
39+
get version() {
40+
return this.#version;
41+
}
42+
43+
get name() {
44+
return this.#name;
45+
}
46+
47+
get icon() {
48+
return this.#icon;
49+
}
50+
51+
get chains() {
52+
return [SOLANA_MAINNET_CHAIN, SOLANA_DEVNET_CHAIN, SOLANA_TESTNET_CHAIN] as const;
53+
}
54+
55+
get features(): StandardConnectFeature &
56+
StandardDisconnectFeature &
57+
StandardEventsFeature &
58+
SolanaSignAndSendTransactionFeature &
59+
SolanaSignTransactionFeature &
60+
SolanaSignMessageFeature {
61+
return {
62+
[StandardConnect]: {
63+
version: '1.0.0',
64+
connect: this.#connect,
65+
},
66+
[StandardDisconnect]: {
67+
version: '1.0.0',
68+
disconnect: this.#disconnect,
69+
},
70+
[StandardEvents]: {
71+
version: '1.0.0',
72+
on: this.#on,
73+
},
74+
[SolanaSignAndSendTransaction]: {
75+
version: '1.0.0',
76+
supportedTransactionVersions: ['legacy', 0],
77+
signAndSendTransaction: this.#signAndSendTransaction,
78+
},
79+
[SolanaSignTransaction]: {
80+
version: '1.0.0',
81+
supportedTransactionVersions: ['legacy', 0],
82+
signTransaction: this.#signTransaction,
83+
},
84+
[SolanaSignMessage]: {
85+
version: '1.0.0',
86+
signMessage: this.#signMessage,
87+
},
88+
};
89+
}
90+
91+
get accounts() {
92+
return this.#solflareMetaMask ? this.#solflareMetaMask.standardAccounts : [];
93+
}
94+
95+
#on: StandardEventsOnMethod = (event, listener) => {
96+
this.#listeners[event]?.push(listener) || (this.#listeners[event] = [listener]);
97+
return (): void => this.#off(event, listener);
98+
};
99+
100+
#emit<E extends StandardEventsNames>(event: E, ...args: Parameters<StandardEventsListeners[E]>): void {
101+
// eslint-disable-next-line prefer-spread
102+
this.#listeners[event]?.forEach((listener) => listener.apply(null, args));
103+
}
104+
105+
#off<E extends StandardEventsNames>(event: E, listener: StandardEventsListeners[E]): void {
106+
this.#listeners[event] = this.#listeners[event]?.filter((existingListener) => listener !== existingListener);
107+
}
108+
109+
#connect: StandardConnectMethod = async () => {
110+
if (!this.#solflareMetaMask) {
111+
let SolflareMetaMaskClass: typeof SolflareMetaMask;
112+
try {
113+
SolflareMetaMaskClass = (await import('@solflare-wallet/metamask-sdk')).default;
114+
} catch (error: any) {
115+
throw new Error('Unable to load Solflare MetaMask SDK');
116+
}
117+
this.#solflareMetaMask = new SolflareMetaMaskClass();
118+
this.#solflareMetaMask.on('standard_change', (properties: StandardEventsChangeProperties) =>
119+
this.#emit('change', properties)
120+
);
121+
}
122+
123+
if (!this.accounts.length) {
124+
await this.#solflareMetaMask.connect();
125+
}
126+
127+
return { accounts: this.accounts };
128+
};
129+
130+
#disconnect: StandardDisconnectMethod = async () => {
131+
if (!this.#solflareMetaMask) return;
132+
await this.#solflareMetaMask.disconnect();
133+
};
134+
135+
#signAndSendTransaction: SolanaSignAndSendTransactionMethod = async (...inputs) => {
136+
if (!this.#solflareMetaMask) throw new WalletNotConnectedError();
137+
return await this.#solflareMetaMask.standardSignAndSendTransaction(...inputs);
138+
};
139+
140+
#signTransaction: SolanaSignTransactionMethod = async (...inputs) => {
141+
if (!this.#solflareMetaMask) throw new WalletNotConnectedError();
142+
return await this.#solflareMetaMask.standardSignTransaction(...inputs);
143+
};
144+
145+
#signMessage: SolanaSignMessageMethod = async (...inputs) => {
146+
if (!this.#solflareMetaMask) throw new WalletNotConnectedError();
147+
return await this.#solflareMetaMask.standardSignMessage(...inputs);
148+
};
149+
}

pnpm-lock.yaml

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)