Skip to content

Commit

Permalink
Merge pull request #824 from vsakos/solflare-metamask-standard-adapter
Browse files Browse the repository at this point in the history
Solflare: Initialize MetaMask standard adapter
  • Loading branch information
jordaaash authored Sep 8, 2023
2 parents 89e864e + 8390ba1 commit 02f20c2
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/clever-jeans-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/wallet-adapter-solflare': patch
---

Add Solflare MetaMask Snap support
5 changes: 4 additions & 1 deletion packages/wallets/solflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
},
"dependencies": {
"@solana/wallet-adapter-base": "workspace:^",
"@solflare-wallet/sdk": "^1.3.0"
"@solana/wallet-standard-chains": "^1.1.0",
"@solflare-wallet/metamask-sdk": "^1.0.2",
"@solflare-wallet/sdk": "^1.3.0",
"@wallet-standard/wallet": "^1.0.1"
},
"devDependencies": {
"@solana/web3.js": "^1.77.3",
Expand Down
2 changes: 2 additions & 0 deletions packages/wallets/solflare/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import type { Transaction, TransactionVersion, VersionedTransaction } from '@solana/web3.js';
import { type Connection, PublicKey, type TransactionSignature } from '@solana/web3.js';
import type { default as Solflare } from '@solflare-wallet/sdk';
import { detectAndRegisterSolflareMetaMaskWallet } from './metamask/detect.js';

interface SolflareWindow extends Window {
solflare?: {
Expand Down Expand Up @@ -70,6 +71,7 @@ export class SolflareWalletAdapter extends BaseMessageSignerWalletAdapter {
}
return false;
});
scopePollingDetectionStrategy(detectAndRegisterSolflareMetaMaskWallet);
}
}

Expand Down
61 changes: 61 additions & 0 deletions packages/wallets/solflare/src/metamask/detect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { EthereumProvider, WindowWithEthereum } from '@solflare-wallet/metamask-sdk';
import { registerWallet } from '@wallet-standard/wallet';
import { SolflareMetaMaskWallet } from './wallet.js';

let stopPolling = false;

/** @internal */
export function detectAndRegisterSolflareMetaMaskWallet(): boolean {
// If detected, stop polling.
if (stopPolling) return true;
(async function () {
try {
// Try to detect, stop polling if detected, and register the wallet.
if (await isSnapProviderDetected()) {
if (!stopPolling) {
stopPolling = true;
registerWallet(new SolflareMetaMaskWallet());
}
}
} catch (error) {
// Stop polling on unhandled errors (this should never happen).
stopPolling = true;
}
})();
// Keep polling.
return false;
}

async function isSnapProviderDetected(): Promise<boolean> {
try {
const provider = (window as WindowWithEthereum).ethereum;
if (!provider) return false;

const providerProviders = provider.providers;
if (providerProviders && Array.isArray(providerProviders)) {
for (const provider of providerProviders) {
if (await isSnapSupported(provider)) return true;
}
}

const providerDetected = provider.detected;
if (providerDetected && Array.isArray(providerDetected)) {
for (const provider of providerDetected) {
if (await isSnapSupported(provider)) return true;
}
}

return await isSnapSupported(provider);
} catch (error) {
return false;
}
}

async function isSnapSupported(provider: EthereumProvider): Promise<boolean> {
try {
await provider.request({ method: 'wallet_getSnaps' });
return true;
} catch (error) {
return false;
}
}
5 changes: 5 additions & 0 deletions packages/wallets/solflare/src/metamask/icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { WalletIcon } from '@wallet-standard/base';

/** @internal */
export const icon: WalletIcon =
'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjMxIiB2aWV3Qm94PSIwIDAgMzEgMzEiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGxpbmVhckdyYWRpZW50IGlkPSJhIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjIwLjI1IiB4Mj0iMjYuNTcxIiB5MT0iMjcuMTczIiB5Mj0iMTkuODU4Ij48c3RvcCBvZmZzZXQ9Ii4wOCIgc3RvcC1jb2xvcj0iIzk5NDVmZiIvPjxzdG9wIG9mZnNldD0iLjMiIHN0b3AtY29sb3I9IiM4NzUyZjMiLz48c3RvcCBvZmZzZXQ9Ii41IiBzdG9wLWNvbG9yPSIjNTQ5N2Q1Ii8+PHN0b3Agb2Zmc2V0PSIuNiIgc3RvcC1jb2xvcj0iIzQzYjRjYSIvPjxzdG9wIG9mZnNldD0iLjcyIiBzdG9wLWNvbG9yPSIjMjhlMGI5Ii8+PHN0b3Agb2Zmc2V0PSIuOTciIHN0b3AtY29sb3I9IiMxOWZiOWIiLz48L2xpbmVhckdyYWRpZW50PjxnIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iLjA5NCI+PHBhdGggZD0ibTI2LjEwOSAzLjY0My05LjM2OSA2Ljk1OSAxLjczMy00LjEwNSA3LjYzNy0yLjg1M3oiIGZpbGw9IiNlMjc2MWIiIHN0cm9rZT0iI2UyNzYxYiIvPjxnIGZpbGw9IiNlNDc2MWIiIHN0cm9rZT0iI2U0NzYxYiI+PHBhdGggZD0ibTQuNDgxIDMuNjQzIDkuMjk0IDcuMDI0LTEuNjQ4LTQuMTcxem0xOC4yNTggMTYuMTMtMi40OTUgMy44MjMgNS4zMzkgMS40NjkgMS41MzUtNS4yMDctNC4zNzgtLjA4NXptLTE5LjI0Ny4wODUgMS41MjUgNS4yMDcgNS4zMzktMS40NjktMi40OTUtMy44MjN6Ii8+PHBhdGggZD0ibTEwLjA1NSAxMy4zMTMtMS40ODggMi4yNTEgNS4zMDEuMjM1LS4xODgtNS42OTd6bTEwLjQ4IDAtMy42NzItMy4yNzctLjEyMiA1Ljc2MyA1LjI5Mi0uMjM1LTEuNDk3LTIuMjUxem0tMTAuMTc4IDEwLjI4MyAzLjE4My0xLjU1NC0yLjc0OS0yLjE0Ny0uNDMzIDMuNzAxem02LjY5NS0xLjU1NCAzLjE5MiAxLjU1NC0uNDQzLTMuNzAxeiIvPjwvZz48cGF0aCBkPSJtMjAuMjQ0IDIzLjU5Ni0zLjE5Mi0xLjU1NC4yNTQgMi4wODEtLjAyOC44NzZ6bS05Ljg4NyAwIDIuOTY2IDEuNDAzLS4wMTktLjg3Ni4yMzUtMi4wODEtMy4xODMgMS41NTR6IiBmaWxsPSIjZDdjMWIzIiBzdHJva2U9IiNkN2MxYjMiLz48cGF0aCBkPSJtMTMuMzY5IDE4LjUyMS0yLjY1NS0uNzgxIDEuODc0LS44NTd6bTMuODUxIDAgLjc4MS0xLjYzOCAxLjg4My44NTctMi42NjUuNzgxeiIgZmlsbD0iIzIzMzQ0NyIgc3Ryb2tlPSIjMjMzNDQ3Ii8+PHBhdGggZD0ibTEwLjM1NyAyMy41OTYuNDUyLTMuODIzLTIuOTQ3LjA4NXptOS40MzUtMy44MjMuNDUyIDMuODIzIDIuNDk1LTMuNzM4em0yLjI0MS00LjIwOS01LjI5Mi4yMzUuNDkgMi43MjEuNzgyLTEuNjM4IDEuODgzLjg1N3ptLTExLjMxOCAyLjE3NSAxLjg4My0uODU3Ljc3MiAxLjYzOC40OTktMi43MjEtNS4zMDEtLjIzNXoiIGZpbGw9IiNjZDYxMTYiIHN0cm9rZT0iI2NkNjExNiIvPjxwYXRoIGQ9Im04LjU2NyAxNS41NjQgMi4yMjIgNC4zMzEtLjA3NS0yLjE1NnptMTEuMzI4IDIuMTc1LS4wOTQgMi4xNTYgMi4yMzItNC4zMzEtMi4xMzcgMi4xNzV6bS02LjAyNi0xLjk0LS40OTkgMi43MjEuNjIxIDMuMjExLjE0MS00LjIyOC0uMjY0LTEuNzA0em0yLjg3MiAwLS4yNTQgMS42OTUuMTEzIDQuMjM3LjYzMS0zLjIxMXoiIGZpbGw9IiNlNDc1MWYiIHN0cm9rZT0iI2U0NzUxZiIvPjxwYXRoIGQ9Im0xNy4yMyAxOC41Mi0uNjMxIDMuMjExLjQ1Mi4zMTEgMi43NS0yLjE0Ny4wOTQtMi4xNTZ6bS02LjUxNi0uNzgxLjA3NSAyLjE1NiAyLjc1IDIuMTQ3LjQ1Mi0uMzExLS42MjItMy4yMTF6IiBmaWxsPSIjZjY4NTFiIiBzdHJva2U9IiNmNjg1MWIiLz48cGF0aCBkPSJtMTcuMjc3IDI0Ljk5OS4wMjgtLjg3Ni0uMjM1LS4yMDdoLTMuNTVsLS4yMTcuMjA3LjAxOS44NzYtMi45NjYtMS40MDMgMS4wMzYuODQ4IDIuMSAxLjQ1OWgzLjYwNmwyLjEwOS0xLjQ1OSAxLjAzNi0uODQ4eiIgZmlsbD0iI2MwYWQ5ZSIgc3Ryb2tlPSIjYzBhZDllIi8+PHBhdGggZD0ibTE3LjA1MSAyMi4wNDItLjQ1Mi0uMzExaC0yLjYwOGwtLjQ1Mi4zMTEtLjIzNSAyLjA4MS4yMTctLjIwN2gzLjU1bC4yMzUuMjA3LS4yNTQtMi4wODF6IiBmaWxsPSIjMTYxNjE2IiBzdHJva2U9IiMxNjE2MTYiLz48cGF0aCBkPSJtMjYuNTA1IDExLjA1My44LTMuODQyLTEuMTk2LTMuNTY5LTkuMDU4IDYuNzIzIDMuNDg0IDIuOTQ3IDQuOTI1IDEuNDQxIDEuMDkyLTEuMjcxLS40NzEtLjMzOS43NTMtLjY4Ny0uNTg0LS40NTIuNzUzLS41NzQtLjQ5OS0uMzc3em0tMjMuMjExLTMuODQxLjggMy44NDItLjUwOC4zNzcuNzUzLjU3NC0uNTc0LjQ1Mi43NTMuNjg3LS40NzEuMzM5IDEuMDgzIDEuMjcxIDQuOTI1LTEuNDQxIDMuNDg0LTIuOTQ3LTkuMDU5LTYuNzIzeiIgZmlsbD0iIzc2M2QxNiIgc3Ryb2tlPSIjNzYzZDE2Ii8+PHBhdGggZD0ibTI1LjQ2IDE0Ljc1NC00LjkyNS0xLjQ0MSAxLjQ5NyAyLjI1MS0yLjIzMiA0LjMzMSAyLjkzOC0uMDM4aDQuMzc4bC0xLjY1Ny01LjEwNHptLTE1LjQwNS0xLjQ0MS00LjkyNSAxLjQ0MS0xLjYzOCA1LjEwNGg0LjM2OWwyLjkyOC4wMzgtMi4yMjItNC4zMzEgMS40ODgtMi4yNTF6bTYuNjg1IDIuNDg2LjMxMS01LjQzMyAxLjQzMS0zLjg3aC02LjM1NmwxLjQxMyAzLjg3LjMyOSA1LjQzMy4xMTMgMS43MTQuMDA5IDQuMjE5aDIuNjFsLjAxOS00LjIxOS4xMjItMS43MTR6IiBmaWxsPSIjZjY4NTFiIiBzdHJva2U9IiNmNjg1MWIiLz48L2c+PGNpcmNsZSBjeD0iMjMuNSIgY3k9IjIzLjUiIGZpbGw9IiMwMDAiIHI9IjYuNSIvPjxwYXRoIGQ9Im0yNy40NzMgMjUuNTQ1LTEuMzEgMS4zNjhjLS4wMjkuMDMtLjA2My4wNTMtLjEwMS4wN2EuMzEuMzEgMCAwIDEgLS4xMjEuMDI0aC02LjIwOWMtLjAzIDAtLjA1OS0uMDA4LS4wODMtLjAyNGEuMTUuMTUgMCAwIDEgLS4wNTYtLjA2NWMtLjAxMi0uMDI2LS4wMTUtLjA1Ni0uMDEtLjA4NHMuMDE4LS4wNTUuMDM5LS4wNzZsMS4zMTEtMS4zNjhjLjAyOC0uMDMuMDYzLS4wNTMuMTAxLS4wNjlhLjMxLjMxIDAgMCAxIC4xMjEtLjAyNWg2LjIwOGMuMDMgMCAuMDU5LjAwOC4wODMuMDI0YS4xNS4xNSAwIDAgMSAuMDU2LjA2NWMuMDEyLjAyNi4wMTUuMDU2LjAxLjA4NHMtLjAxOC4wNTUtLjAzOS4wNzZ6bS0xLjMxLTIuNzU2Yy0uMDI5LS4wMy0uMDYzLS4wNTMtLjEwMS0uMDdhLjMxLjMxIDAgMCAwIC0uMTIxLS4wMjRoLTYuMjA5Yy0uMDMgMC0uMDU5LjAwOC0uMDgzLjAyNHMtLjA0NC4wMzgtLjA1Ni4wNjUtLjAxNS4wNTYtLjAxLjA4NC4wMTguMDU1LjAzOS4wNzZsMS4zMTEgMS4zNjhjLjAyOC4wMy4wNjMuMDUzLjEwMS4wNjlhLjMxLjMxIDAgMCAwIC4xMjEuMDI1aDYuMjA4Yy4wMyAwIC4wNTktLjAwOC4wODMtLjAyNGEuMTUuMTUgMCAwIDAgLjA1Ni0uMDY1Yy4wMTItLjAyNi4wMTUtLjA1Ni4wMS0uMDg0cy0uMDE4LS4wNTUtLjAzOS0uMDc2em0tNi40MzEtLjk4M2g2LjIwOWEuMzEuMzEgMCAwIDAgLjEyMS0uMDI0Yy4wMzgtLjAxNi4wNzMtLjA0LjEwMS0uMDdsMS4zMS0xLjM2OGMuMDItLjAyMS4wMzQtLjA0Ny4wMzktLjA3NnMuMDAxLS4wNTgtLjAxLS4wODRhLjE1LjE1IDAgMCAwIC0uMDU2LS4wNjVjLS4wMjUtLjAxNi0uMDU0LS4wMjQtLjA4My0uMDI0aC02LjIwOGEuMzEuMzEgMCAwIDAgLS4xMjEuMDI1Yy0uMDM4LjAxNi0uMDcyLjA0LS4xMDEuMDY5bC0xLjMxIDEuMzY4Yy0uMDIuMDIxLS4wMzQuMDQ3LS4wMzkuMDc2cy0uMDAxLjA1OC4wMS4wODQuMDMxLjA0OS4wNTYuMDY1LjA1NC4wMjQuMDgzLjAyNHoiIGZpbGw9InVybCgjYSkiLz48L3N2Zz4=' as const;
149 changes: 149 additions & 0 deletions packages/wallets/solflare/src/metamask/wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { WalletNotConnectedError } from '@solana/wallet-adapter-base';
import { SOLANA_DEVNET_CHAIN, SOLANA_MAINNET_CHAIN, SOLANA_TESTNET_CHAIN } from '@solana/wallet-standard-chains';
import {
SolanaSignAndSendTransaction,
type SolanaSignAndSendTransactionFeature,
type SolanaSignAndSendTransactionMethod,
SolanaSignMessage,
type SolanaSignMessageFeature,
type SolanaSignMessageMethod,
SolanaSignTransaction,
type SolanaSignTransactionFeature,
type SolanaSignTransactionMethod,
} from '@solana/wallet-standard-features';
import type { default as SolflareMetaMask } from '@solflare-wallet/metamask-sdk';
import type { Wallet } from '@wallet-standard/base';
import {
StandardConnect,
type StandardConnectFeature,
type StandardConnectMethod,
StandardDisconnect,
type StandardDisconnectFeature,
type StandardDisconnectMethod,
StandardEvents,
type StandardEventsChangeProperties,
type StandardEventsFeature,
type StandardEventsListeners,
type StandardEventsNames,
type StandardEventsOnMethod,
} from '@wallet-standard/features';
import { icon } from './icon.js';

export class SolflareMetaMaskWallet implements Wallet {
readonly #listeners: { [E in StandardEventsNames]?: StandardEventsListeners[E][] } = {};
readonly #version = '1.0.0' as const;
readonly #name = 'MetaMask' as const;
readonly #icon = icon;
#solflareMetaMask: SolflareMetaMask | null = null;

get version() {
return this.#version;
}

get name() {
return this.#name;
}

get icon() {
return this.#icon;
}

get chains() {
return [SOLANA_MAINNET_CHAIN, SOLANA_DEVNET_CHAIN, SOLANA_TESTNET_CHAIN] as const;
}

get features(): StandardConnectFeature &
StandardDisconnectFeature &
StandardEventsFeature &
SolanaSignAndSendTransactionFeature &
SolanaSignTransactionFeature &
SolanaSignMessageFeature {
return {
[StandardConnect]: {
version: '1.0.0',
connect: this.#connect,
},
[StandardDisconnect]: {
version: '1.0.0',
disconnect: this.#disconnect,
},
[StandardEvents]: {
version: '1.0.0',
on: this.#on,
},
[SolanaSignAndSendTransaction]: {
version: '1.0.0',
supportedTransactionVersions: ['legacy', 0],
signAndSendTransaction: this.#signAndSendTransaction,
},
[SolanaSignTransaction]: {
version: '1.0.0',
supportedTransactionVersions: ['legacy', 0],
signTransaction: this.#signTransaction,
},
[SolanaSignMessage]: {
version: '1.0.0',
signMessage: this.#signMessage,
},
};
}

get accounts() {
return this.#solflareMetaMask ? this.#solflareMetaMask.standardAccounts : [];
}

#on: StandardEventsOnMethod = (event, listener) => {
this.#listeners[event]?.push(listener) || (this.#listeners[event] = [listener]);
return (): void => this.#off(event, listener);
};

#emit<E extends StandardEventsNames>(event: E, ...args: Parameters<StandardEventsListeners[E]>): void {
// eslint-disable-next-line prefer-spread
this.#listeners[event]?.forEach((listener) => listener.apply(null, args));
}

#off<E extends StandardEventsNames>(event: E, listener: StandardEventsListeners[E]): void {
this.#listeners[event] = this.#listeners[event]?.filter((existingListener) => listener !== existingListener);
}

#connect: StandardConnectMethod = async () => {
if (!this.#solflareMetaMask) {
let SolflareMetaMaskClass: typeof SolflareMetaMask;
try {
SolflareMetaMaskClass = (await import('@solflare-wallet/metamask-sdk')).default;
} catch (error: any) {
throw new Error('Unable to load Solflare MetaMask SDK');
}
this.#solflareMetaMask = new SolflareMetaMaskClass();
this.#solflareMetaMask.on('standard_change', (properties: StandardEventsChangeProperties) =>
this.#emit('change', properties)
);
}

if (!this.accounts.length) {
await this.#solflareMetaMask.connect();
}

return { accounts: this.accounts };
};

#disconnect: StandardDisconnectMethod = async () => {
if (!this.#solflareMetaMask) return;
await this.#solflareMetaMask.disconnect();
};

#signAndSendTransaction: SolanaSignAndSendTransactionMethod = async (...inputs) => {
if (!this.#solflareMetaMask) throw new WalletNotConnectedError();
return await this.#solflareMetaMask.standardSignAndSendTransaction(...inputs);
};

#signTransaction: SolanaSignTransactionMethod = async (...inputs) => {
if (!this.#solflareMetaMask) throw new WalletNotConnectedError();
return await this.#solflareMetaMask.standardSignTransaction(...inputs);
};

#signMessage: SolanaSignMessageMethod = async (...inputs) => {
if (!this.#solflareMetaMask) throw new WalletNotConnectedError();
return await this.#solflareMetaMask.standardSignMessage(...inputs);
};
}
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 02f20c2

Please sign in to comment.