diff --git a/example/CHANGELOG.md b/example/CHANGELOG.md index 31d512d..138cab7 100644 --- a/example/CHANGELOG.md +++ b/example/CHANGELOG.md @@ -1,5 +1,17 @@ # @starknet-io/get-starknet-example +## 4.0.5 + +### Patch Changes + +- @starknet-io/get-starknet@4.0.5 + +## 4.0.4 + +### Patch Changes + +- @starknet-io/get-starknet@4.0.4 + ## 4.0.3 ### Patch Changes diff --git a/example/package.json b/example/package.json index 91a61e5..966b922 100644 --- a/example/package.json +++ b/example/package.json @@ -1,6 +1,6 @@ { "name": "@starknet-io/get-starknet-example", - "version": "4.0.3", + "version": "4.0.5", "private": true, "type": "module", "scripts": { diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index e8fddd4..87f7ab6 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,19 @@ # @starknet-io/get-starknet-core +## 4.0.5 + +### Patch Changes + +- 0263b88: Decouple Virtual Wallet Discovery for async workflow + +## 4.0.4 + +### Patch Changes + +- d2fee96: Fix loading MetaMask Virtual Wallet dynamically in WalletAccount and + add support for RPC APIs (wallet_supportedWalletApi and wallet_supportedSpecs) + in get-starknet v4 integration. + ## 4.0.3 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 7f9595b..2fc242e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@starknet-io/get-starknet-core", - "version": "4.0.3", + "version": "4.0.5", "keywords": [ "starknet", "starkware", diff --git a/packages/core/src/main.ts b/packages/core/src/main.ts index b6c7069..242f11a 100644 --- a/packages/core/src/main.ts +++ b/packages/core/src/main.ts @@ -13,6 +13,7 @@ import { sortBy } from "./wallet/sort" import { initiateVirtualWallets, resolveVirtualWallet, + virtualWallets, } from "./wallet/virtualWallets" import { Permission, type StarknetWindowObject } from "@starknet-io/types-js" @@ -116,6 +117,29 @@ export function getStarknet( return firstAuthorizedWallet }, + discoverVirtualWallets: async ( + walletNamesOrIds: string[] = [], + ): Promise => { + const walletNamesOrIdsSet = new Set(walletNamesOrIds) + + const virtualWalletToDiscover = + walletNamesOrIdsSet.size > 0 + ? virtualWallets.filter( + (virtualWallet) => + walletNamesOrIdsSet.has(virtualWallet.name) || + walletNamesOrIdsSet.has(virtualWallet.id), + ) + : virtualWallets + + await Promise.all( + virtualWalletToDiscover.map(async (virtualWallet) => { + const hasSupport = await virtualWallet.hasSupport(windowObject) + if (hasSupport) { + windowObject[virtualWallet.windowKey] = virtualWallet + } + }), + ) + }, enable: async (inputWallet, options) => { let wallet: StarknetWindowObject if (isVirtualWallet(inputWallet)) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3fb0c9e..49e81bf 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -69,6 +69,7 @@ export interface GetStarknetResult { ) => Promise // Returns only preauthorized wallets available in the window object getDiscoveryWallets: (options?: GetWalletOptions) => Promise // Returns all wallets in existence (from discovery file) getLastConnectedWallet: () => Promise // Returns the last wallet connected when it's still connected + discoverVirtualWallets: () => Promise // Discovers the virtual wallets by calling their hasSupport methods enable: ( wallet: StarknetWindowObject | VirtualWallet, options?: RequestAccountsParameters, diff --git a/packages/core/src/wallet/virtualWallets/index.ts b/packages/core/src/wallet/virtualWallets/index.ts index a7d5288..5924500 100644 --- a/packages/core/src/wallet/virtualWallets/index.ts +++ b/packages/core/src/wallet/virtualWallets/index.ts @@ -6,9 +6,11 @@ const virtualWallets: VirtualWallet[] = [metaMaskVirtualWallet] function initiateVirtualWallets(windowObject: Record) { virtualWallets.forEach(async (virtualWallet) => { - const hasSupport = await virtualWallet.hasSupport(windowObject) - if (hasSupport) { - windowObject[virtualWallet.windowKey] = virtualWallet + if (!(virtualWallet.windowKey in windowObject)) { + const hasSupport = await virtualWallet.hasSupport(windowObject) + if (hasSupport) { + windowObject[virtualWallet.windowKey] = virtualWallet + } } }) } @@ -28,4 +30,4 @@ async function resolveVirtualWallet( return wallet } -export { initiateVirtualWallets, resolveVirtualWallet } +export { initiateVirtualWallets, resolveVirtualWallet, virtualWallets } diff --git a/packages/core/src/wallet/virtualWallets/metaMaskVirtualWallet.ts b/packages/core/src/wallet/virtualWallets/metaMaskVirtualWallet.ts index 8205ae7..f8f8a6f 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,7 +146,8 @@ class MetaMaskVirtualWallet { name: "MetaMaskStarknetSnapWallet", alias: "MetaMaskStarknetSnapWallet", - entry: "http://localhost:8082/remoteEntry.js", + entry: + "https://dev.snaps.consensys.io/starknet/get-starknet/v1/remoteEntry.js", }, ], }) @@ -148,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) { 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() diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 55536fb..705b018 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,19 @@ # @starknet-io/get-starknet +## 4.0.5 + +### Patch Changes + +- Updated dependencies [0263b88] + - @starknet-io/get-starknet-core@4.0.5 + +## 4.0.4 + +### Patch Changes + +- Updated dependencies [d2fee96] + - @starknet-io/get-starknet-core@4.0.4 + ## 4.0.3 ### Patch Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index 0e93a95..9cf24b9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@starknet-io/get-starknet", - "version": "4.0.3", + "version": "4.0.5", "keywords": [ "starknet", "starkware",