diff --git a/packages/wallets/solflare/package.json b/packages/wallets/solflare/package.json index 5765342e8..1b1fb183f 100644 --- a/packages/wallets/solflare/package.json +++ b/packages/wallets/solflare/package.json @@ -35,11 +35,13 @@ "@solana/web3.js": "^1.77.3" }, "dependencies": { + "@metamask/providers": "^12.0.0", "@solana/wallet-adapter-base": "workspace:^", "@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" + "@wallet-standard/wallet": "^1.0.1", + "readable-stream": "^3.6.2" }, "devDependencies": { "@solana/web3.js": "^1.77.3", diff --git a/packages/wallets/solflare/src/metamask/WindowPostMessageStream.ts b/packages/wallets/solflare/src/metamask/WindowPostMessageStream.ts new file mode 100644 index 000000000..3863d5faa --- /dev/null +++ b/packages/wallets/solflare/src/metamask/WindowPostMessageStream.ts @@ -0,0 +1,128 @@ +import { Duplex } from 'readable-stream'; + +type StreamData = number | string | Record | unknown[]; + +interface StreamMessage { + data: StreamData; + [key: string]: unknown; +} + +export interface PostMessageEvent { + data?: StreamData; + origin: string; + source: typeof window; +} + +interface WindowPostMessageStreamArgs { + name: string; + target: string; + targetOrigin?: string; + targetWindow?: Window; +} + +function isValidStreamMessage(message: unknown): message is StreamMessage { + return ( + isObject(message) && + Boolean(message.data) && + (typeof message.data === 'number' || typeof message.data === 'object' || typeof message.data === 'string') + ); +} + +function isObject(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +const getSource = Object.getOwnPropertyDescriptor(MessageEvent.prototype, 'source')?.get; + +const getOrigin = Object.getOwnPropertyDescriptor(MessageEvent.prototype, 'origin')?.get; + +export class WindowPostMessageStream extends Duplex { + private _name: string; + + private _target: string; + + private _targetOrigin: string; + + private _targetWindow: Window; + + /** + * Creates a stream for communicating with other streams across the same or + * different `window` objects. + * + * @param args - Options bag. + * @param args.name - The name of the stream. Used to differentiate between + * multiple streams sharing the same window object. + * @param args.target - The name of the stream to exchange messages with. + * @param args.targetOrigin - The origin of the target. Defaults to + * `location.origin`, '*' is permitted. + * @param args.targetWindow - The window object of the target stream. Defaults + * to `window`. + */ + constructor({ name, target, targetOrigin = location.origin, targetWindow = window }: WindowPostMessageStreamArgs) { + super({ + objectMode: true, + }); + + if (typeof window === 'undefined' || typeof window.postMessage !== 'function') { + throw new Error( + 'window.postMessage is not a function. This class should only be instantiated in a Window.' + ); + } + + this._name = name; + this._target = target; + this._targetOrigin = targetOrigin; + this._targetWindow = targetWindow; + this._onMessage = this._onMessage.bind(this); + + window.addEventListener('message', this._onMessage as any, false); + } + + protected _onData(data: StreamData): void { + try { + this.push(data); + } catch (err) { + this.emit('error', err); + } + } + + _read(): void { + return undefined; + } + + _write(data: StreamData, _encoding: string | null, cb: () => void): void { + this._postMessage(data); + cb(); + } + + protected _postMessage(data: unknown): void { + this._targetWindow.postMessage( + { + target: this._target, + data, + }, + this._targetOrigin + ); + } + + private _onMessage(event: PostMessageEvent): void { + const message = event.data; + + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + if ( + (this._targetOrigin !== '*' && getOrigin!.call(event) !== this._targetOrigin) || + getSource!.call(event) !== this._targetWindow || + !isValidStreamMessage(message) || + message.target !== this._name + ) { + return; + } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + + this._onData(message.data); + } + + _destroy(): void { + window.removeEventListener('message', this._onMessage as any, false); + } +} diff --git a/packages/wallets/solflare/src/metamask/detect.ts b/packages/wallets/solflare/src/metamask/detect.ts index 88fb60bf5..21cde4bb2 100644 --- a/packages/wallets/solflare/src/metamask/detect.ts +++ b/packages/wallets/solflare/src/metamask/detect.ts @@ -1,7 +1,9 @@ -import type { EthereumProvider, WindowWithEthereum } from '@solflare-wallet/metamask-sdk'; +import type { MetaMaskInpageProvider } from '@metamask/providers'; import { registerWallet } from '@wallet-standard/wallet'; import { SolflareMetaMaskWallet } from './wallet.js'; +let providerInstance: MetaMaskInpageProvider | null = null; + let stopPolling = false; /** @internal */ @@ -11,7 +13,7 @@ export function detectAndRegisterSolflareMetaMaskWallet(): boolean { (async function () { try { // Try to detect, stop polling if detected, and register the wallet. - if (await isSnapProviderDetected()) { + if (await isSnapSupported()) { if (!stopPolling) { stopPolling = true; registerWallet(new SolflareMetaMaskWallet()); @@ -26,35 +28,36 @@ export function detectAndRegisterSolflareMetaMaskWallet(): boolean { return false; } -async function isSnapProviderDetected(): Promise { - try { - const provider = (window as WindowWithEthereum).ethereum; - if (!provider) return false; +async function getMetamaskProvider(): Promise { + if (providerInstance) { + return providerInstance; + } - const providerProviders = provider.providers; - if (providerProviders && Array.isArray(providerProviders)) { - for (const provider of providerProviders) { - if (await isSnapSupported(provider)) return true; - } - } + const { WindowPostMessageStream } = await import('./WindowPostMessageStream.js'); + const { MetaMaskInpageProvider } = await import('@metamask/providers'); - const providerDetected = provider.detected; - if (providerDetected && Array.isArray(providerDetected)) { - for (const provider of providerDetected) { - if (await isSnapSupported(provider)) return true; - } - } + const metamaskStream = new WindowPostMessageStream({ + name: 'metamask-inpage', + target: 'metamask-contentscript', + }) as any; - return await isSnapSupported(provider); - } catch (error) { - return false; - } + providerInstance = new MetaMaskInpageProvider(metamaskStream, { + shouldSendMetadata: false, + }); + + return providerInstance; } -async function isSnapSupported(provider: EthereumProvider): Promise { +async function isSnapSupported(): Promise { try { - await provider.request({ method: 'wallet_getSnaps' }); - return true; + const provider = await getMetamaskProvider(); + + const snaps = await Promise.race([ + provider.request({ method: 'wallet_getSnaps' }), + new Promise((resolve, reject) => setTimeout(() => reject('MetaMask provider not found'), 1000)), + ]); + + return typeof snaps === 'object'; } catch (error) { return false; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df6494fbc..4267bf389 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -988,6 +988,9 @@ importers: packages/wallets/solflare: dependencies: + '@metamask/providers': + specifier: ^12.0.0 + version: 12.0.0 '@solana/wallet-adapter-base': specifier: workspace:^ version: link:../../core/base @@ -1003,6 +1006,9 @@ importers: '@wallet-standard/wallet': specifier: ^1.0.1 version: 1.0.1 + readable-stream: + specifier: ^3.6.2 + version: 3.6.2 devDependencies: '@solana/web3.js': specifier: ^1.77.3 @@ -4132,6 +4138,46 @@ packages: read-yaml-file: 1.1.0 dev: true + /@metamask/json-rpc-engine@7.1.1: + resolution: {integrity: sha512-wPB8Or74OqMwcxa87JPOEjXwtgpyHPEXiLKblKRAtCjTJNQFp1Co//1CgFm5xj4Z5JbBGfGFiQNnj09Et40sig==} + engines: {node: '>=16.0.0'} + dependencies: + '@metamask/rpc-errors': 6.0.0 + '@metamask/safe-event-emitter': 3.0.0 + '@metamask/utils': 8.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@metamask/object-multiplex@1.2.0: + resolution: {integrity: sha512-hksV602d3NWE2Q30Mf2Np1WfVKaGqfJRy9vpHAmelbaD0OkDt06/0KQkRR6UVYdMbTbkuEu8xN5JDUU80inGwQ==} + engines: {node: '>=12.0.0'} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + readable-stream: 2.3.8 + dev: false + + /@metamask/providers@12.0.0: + resolution: {integrity: sha512-NkrSvOF8v8kDz9f2TY1AYK19hJdpYbYhbXWhjmmmXrSMYotn+o7ZV1b1Yd0fqD/HKVL0Vd2BWBUT9U0ggIDTEA==} + engines: {node: '>=16.0.0'} + dependencies: + '@metamask/json-rpc-engine': 7.1.1 + '@metamask/object-multiplex': 1.2.0 + '@metamask/rpc-errors': 6.0.0 + '@metamask/safe-event-emitter': 3.0.0 + '@metamask/utils': 8.1.0 + detect-browser: 5.3.0 + extension-port-stream: 2.1.1 + fast-deep-equal: 3.1.3 + is-stream: 2.0.1 + json-rpc-middleware-stream: 4.2.3 + pump: 3.0.0 + webextension-polyfill: 0.10.0 + transitivePeerDependencies: + - supports-color + dev: false + /@metamask/rpc-errors@5.1.1: resolution: {integrity: sha512-JjZnDi2y2CfvbohhBl+FOQRzmFlJpybcQlIk37zEX8B96eVSPbH/T8S0p7cSF8IE33IWx6JkD8Ycsd+2TXFxCw==} engines: {node: '>=16.0.0'} @@ -4142,6 +4188,25 @@ packages: - supports-color dev: false + /@metamask/rpc-errors@6.0.0: + resolution: {integrity: sha512-sAZwcdmidJDPbZV3XSKcWZC7CSTdjqDNRsDDdb2SstCOLEJtNqHpx32FWgwWB0arqWxUcUxYxgR39edUbsWz7A==} + engines: {node: '>=16.0.0'} + dependencies: + '@metamask/utils': 8.1.0 + fast-safe-stringify: 2.1.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@metamask/safe-event-emitter@2.0.0: + resolution: {integrity: sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==} + dev: false + + /@metamask/safe-event-emitter@3.0.0: + resolution: {integrity: sha512-j6Z47VOmVyGMlnKXZmL0fyvWfEYtKWCA9yGZkU3FCsGZUT5lHGmvaV9JA5F2Y+010y7+ROtR3WMXIkvl/nVzqQ==} + engines: {node: '>=12.0.0'} + dev: false + /@metamask/utils@5.0.2: resolution: {integrity: sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==} engines: {node: '>=14.0.0'} @@ -4155,6 +4220,20 @@ packages: - supports-color dev: false + /@metamask/utils@8.1.0: + resolution: {integrity: sha512-sFNpzBKRicDgM2ZuU6vrPROlqNGm8/jDsjc5WrU1RzCkAMc4Xr3vUUf8p59uQ6B09etUWNb8d2GTCbISdmH/Ug==} + engines: {node: '>=16.0.0'} + dependencies: + '@ethereumjs/tx': 4.2.0 + '@noble/hashes': 1.3.1 + '@types/debug': 4.1.8 + debug: 4.3.4 + semver: 7.5.4 + superstruct: 1.0.3 + transitivePeerDependencies: + - supports-color + dev: false + /@mischnic/json-sourcemap@0.1.0: resolution: {integrity: sha512-dQb3QnfNqmQNYA4nFSN/uLaByIic58gOXq4Y4XqLOWmOrw73KmJPt/HLyG0wvn1bnR6mBKs/Uwvkh+Hns1T0XA==} engines: {node: '>=12.0.0'} @@ -10469,6 +10548,13 @@ packages: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} dev: true + /extension-port-stream@2.1.1: + resolution: {integrity: sha512-qknp5o5rj2J9CRKfVB8KJr+uXQlrojNZzdESUPhKYLXf97TPcGf6qWWKmpsNNtUyOdzFhab1ON0jzouNxHHvow==} + engines: {node: '>=12.0.0'} + dependencies: + webextension-polyfill: 0.10.0 + dev: false + /external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -13036,6 +13122,23 @@ packages: /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + /json-rpc-engine@6.1.0: + resolution: {integrity: sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==} + engines: {node: '>=10.0.0'} + dependencies: + '@metamask/safe-event-emitter': 2.0.0 + eth-rpc-errors: 4.0.3 + dev: false + + /json-rpc-middleware-stream@4.2.3: + resolution: {integrity: sha512-4iFb0yffm5vo3eFKDbQgke9o17XBcLQ2c3sONrXSbcOLzP8LTojqo8hRGVgtJShhm5q4ZDSNq039fAx9o65E1w==} + engines: {node: '>=14.0.0'} + dependencies: + '@metamask/safe-event-emitter': 3.0.0 + json-rpc-engine: 6.1.0 + readable-stream: 2.3.8 + dev: false + /json-rpc-random-id@1.0.1: resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} dev: false @@ -18718,6 +18821,10 @@ packages: resolution: {integrity: sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==} dev: false + /webextension-polyfill@0.10.0: + resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==} + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}