Skip to content

Commit

Permalink
Merge pull request #282 from Consensys/feat/add-mm-event-subscription…
Browse files Browse the repository at this point in the history
…-support

feat: add MetaMask event subscription support
  • Loading branch information
fracek authored Jan 22, 2025
2 parents b8611f2 + bcd4294 commit 2543802
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 35 deletions.
6 changes: 6 additions & 0 deletions .changeset/long-kangaroos-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@starknet-io/get-starknet-core": patch
---

refactor MetaMask Virtual Wallet to improve event handling, RPC requests, and
wallet initialization logic
147 changes: 112 additions & 35 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 @@ -124,8 +146,7 @@ class MetaMaskVirtualWallet
{
name: "MetaMaskStarknetSnapWallet",
alias: "MetaMaskStarknetSnapWallet",
entry:
"https://snaps.consensys.io/starknet/get-starknet/v1/remoteEntry.js", //"http://localhost:8082/remoteEntry.js",
entry: `https://snaps.consensys.io/starknet/get-starknet/v1/remoteEntry.js?ts=${Date.now()}`,
},
],
})
Expand All @@ -149,44 +170,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

0 comments on commit 2543802

Please sign in to comment.