Skip to content

Commit

Permalink
Use an internal MetaMask extension provider
Browse files Browse the repository at this point in the history
  • Loading branch information
vsakos committed Oct 9, 2023
1 parent 0574e65 commit ceac85d
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 26 deletions.
4 changes: 3 additions & 1 deletion packages/wallets/solflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
128 changes: 128 additions & 0 deletions packages/wallets/solflare/src/metamask/WindowPostMessageStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Duplex } from 'readable-stream';

type StreamData = number | string | Record<string, unknown> | 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<PropertyKey, unknown> {
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);
}
}
53 changes: 28 additions & 25 deletions packages/wallets/solflare/src/metamask/detect.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -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());
Expand All @@ -26,35 +28,36 @@ export function detectAndRegisterSolflareMetaMaskWallet(): boolean {
return false;
}

async function isSnapProviderDetected(): Promise<boolean> {
try {
const provider = (window as WindowWithEthereum).ethereum;
if (!provider) return false;
async function getMetamaskProvider(): Promise<MetaMaskInpageProvider> {
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<boolean> {
async function isSnapSupported(): Promise<boolean> {
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;
}
Expand Down
107 changes: 107 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 ceac85d

Please sign in to comment.