Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add MetaMask event subscription support #282

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 112 additions & 34 deletions packages/core/src/wallet/virtualWallets/metaMaskVirtualWallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { VirtualWallet } from "../../types"
import { init, loadRemote } from "@module-federation/runtime"
import { RpcMessage, StarknetWindowObject } from "@starknet-io/types-js"
import {
RequestFnCall,
RpcMessage,
StarknetWindowObject,
WalletEventHandlers,
} from "@starknet-io/types-js"
import { Mutex } from "async-mutex"

interface MetaMaskProvider {
Expand Down Expand Up @@ -87,17 +92,8 @@ export type Eip6963SupportedWallet = {
provider: MetaMaskProvider | null
}

export type EmptyVirtualWallet = {
swo: StarknetWindowObject | null
on(): void
off(): void
request<Data extends RpcMessage>(
call: Omit<Data, "result">,
): Promise<Data["result"]>
}

class MetaMaskVirtualWallet
implements VirtualWallet, Eip6963SupportedWallet, EmptyVirtualWallet
implements VirtualWallet, Eip6963SupportedWallet, StarknetWindowObject
{
id: string = "metamask"
name: string = "MetaMask"
Expand All @@ -106,13 +102,39 @@ class MetaMaskVirtualWallet
provider: MetaMaskProvider | null = null
swo: StarknetWindowObject | null = null
lock: Mutex
version: string = "v2.0.0"

constructor() {
this.lock = new Mutex()
}

/**
* Load and resolve the `StarknetWindowObject`.
*
* @param windowObject The window object.
* @returns A promise to resolve a `StarknetWindowObject`.
*/
async loadWallet(
windowObject: Record<string, unknown>,
): Promise<StarknetWindowObject> {
// Using `this.#loadSwoSafe` to prevent race condition when the wallet is loading.
await this.#loadSwoSafe(windowObject)
// The `MetaMaskVirtualWallet` object acts as a proxy for the `this.swo` object.
// When `request`, `on`, or `off` is called, the wallet is loaded into `this.swo`,
// and the function call is forwarded to it.
// To maintain consistent behaviour, the `MetaMaskVirtualWallet`
// object (`this`) is returned instead of `this.swo`.
return this
}

/**
* Load the remote `StarknetWindowObject` with module federation.
*
* @param windowObject The window object.
* @returns A promise to resolve a `StarknetWindowObject`.
*/
async #loadSwo(
windowObject: Record<string, unknown>,
): Promise<StarknetWindowObject> {
if (!this.provider) {
this.provider = await detectMetamaskSupport(windowObject)
Expand All @@ -125,7 +147,7 @@ class MetaMaskVirtualWallet
name: "MetaMaskStarknetSnapWallet",
alias: "MetaMaskStarknetSnapWallet",
entry:
"https://snaps.consensys.io/starknet/get-starknet/v1/remoteEntry.js", //"http://localhost:8082/remoteEntry.js",
"https://snaps.consensys.io/starknet/get-starknet/v1/remoteEntry.js",
},
],
})
Expand All @@ -149,44 +171,100 @@ class MetaMaskVirtualWallet
)
}

/**
* Verify if the hosting machine supports the Wallet or not without loading the wallet itself.
*
* @param windowObject The window object.
* @returns A promise that resolves to a boolean value to indicate the support status.
*/
async hasSupport(windowObject: Record<string, unknown>) {
this.provider = await detectMetamaskSupport(windowObject)
return this.provider !== null
}

/**
* Proxy the RPC request to the `this.swo` object.
* Load the `this.swo` if not loaded.
*
* @param call The RPC API arguments.
* @returns A promise to resolve a response of the proxy RPC API.
*/
async request<Data extends RpcMessage>(
arg: Omit<Data, "result">,
call: Omit<Data, "result">,
): Promise<Data["result"]> {
const { type } = arg
// `wallet_supportedWalletApi` and `wallet_supportedSpecs` should enabled even if the wallet is not loaded/connected
switch (type) {
case "wallet_supportedWalletApi":
return ["0.7"] as unknown as Data["result"]
case "wallet_supportedSpecs":
return ["0.7"] as unknown as Data["result"]
default:
return this.#handleRequest(arg)
}
return this.#loadSwoSafe().then((swo: StarknetWindowObject) => {
// Forward the request to the `this.swo` object.
// Except RPCs `wallet_supportedSpecs` and `wallet_getPermissions`, other RPCs will trigger the Snap to install if not installed.
return swo.request(
call as unknown as RequestFnCall<Data["type"]>,
) as unknown as Data["result"]
})
}

async #handleRequest<Data extends RpcMessage>(
arg: Omit<RpcMessage, "result">,
): Promise<Data["result"]> {
// Using lock to ensure the load wallet operation is not fall into a racing condirtion
/**
* Subscribe the `accountsChanged` or `networkChanged` event.
* Proxy the subscription to the `this.swo` object.
* Load the `this.swo` if not loaded.
*
* @param event - The event name.
* @param handleEvent - The event handler function.
*/
on<Event extends keyof WalletEventHandlers>(
event: Event,
handleEvent: WalletEventHandlers[Event],
): void {
this.#loadSwoSafe().then((swo: StarknetWindowObject) =>
swo.on(event, handleEvent),
)
}

/**
* Un-subscribe the `accountsChanged` or `networkChanged` event for a given handler.
* Proxy the un-subscribe request to the `this.swo` object.
* Load the `this.swo` if not loaded.
*
* @param event - The event name.
* @param handleEvent - The event handler function.
*/
off<Event extends keyof WalletEventHandlers>(
event: Event,
handleEvent: WalletEventHandlers[Event],
): void {
this.#loadSwoSafe().then((swo: StarknetWindowObject) =>
swo.off(event, handleEvent),
)
}

/**
* Load the `StarknetWindowObject` safely with lock.
* And prevent the loading operation fall into a racing condition.
*
* @returns A promise to resolve a `StarknetWindowObject`.
*/
async #loadSwoSafe(
windowObject: Record<string, unknown> = window,
): Promise<StarknetWindowObject> {
return this.lock.runExclusive(async () => {
// Using `this.swo` to prevent the wallet is loaded multiple times
if (!this.swo) {
this.swo = await this.loadWallet(window)
this.swo = await this.#loadSwo(windowObject)
this.#bindSwoProperties()
}
// forward the request to the actual connect wallet object
// it will also trigger the Snap to install if not installed
return this.swo.request(arg) as unknown as Data["result"]
return this.swo
})
}

// MetaMask Snap Wallet does not support `on` and `off` method
on() {}
off() {}
/**
* Bind properties to `MetaMaskVirtualWallet` from `this.swo`.
*/
#bindSwoProperties(): void {
if (this.swo) {
this.version = this.swo.version
this.name = this.swo.name
this.id = this.swo.id
this.icon = this.swo.icon as string
}
}
}
const metaMaskVirtualWallet = new MetaMaskVirtualWallet()

Expand Down
Loading