diff --git a/.changeset/long-kangaroos-talk.md b/.changeset/long-kangaroos-talk.md new file mode 100644 index 00000000..63bab2a0 --- /dev/null +++ b/.changeset/long-kangaroos-talk.md @@ -0,0 +1,6 @@ +--- +"@starknet-io/get-starknet-core": patch +--- + +refactor MetaMask Virtual Wallet to improve event handling, RPC requests, and +wallet initialization logic diff --git a/packages/core/src/wallet/virtualWallets/metaMaskVirtualWallet.ts b/packages/core/src/wallet/virtualWallets/metaMaskVirtualWallet.ts index 5ef1d637..f1067651 100644 --- a/packages/core/src/wallet/virtualWallets/metaMaskVirtualWallet.ts +++ b/packages/core/src/wallet/virtualWallets/metaMaskVirtualWallet.ts @@ -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 { @@ -87,17 +92,8 @@ export type Eip6963SupportedWallet = { provider: MetaMaskProvider | null } -export type EmptyVirtualWallet = { - swo: StarknetWindowObject | null - on(): void - off(): void - request( - call: Omit, - ): Promise -} - class MetaMaskVirtualWallet - implements VirtualWallet, Eip6963SupportedWallet, EmptyVirtualWallet + implements VirtualWallet, Eip6963SupportedWallet, StarknetWindowObject { id: string = "metamask" name: string = "MetaMask" @@ -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, + ): Promise { + // 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, ): Promise { if (!this.provider) { this.provider = await detectMetamaskSupport(windowObject) @@ -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()}`, }, ], }) @@ -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) { 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( - arg: Omit, + call: Omit, ): Promise { - 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, + ) as unknown as Data["result"] + }) } - async #handleRequest( - arg: Omit, - ): Promise { - // 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: 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: 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 = window, + ): Promise { 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()