Skip to content

Commit

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

feat: add event subscription support on `MetaMaskVirtualWallet`
  • Loading branch information
khanti42 authored Dec 16, 2024
2 parents 1360dfe + 8e1dd66 commit 9894852
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 41 deletions.
12 changes: 12 additions & 0 deletions example/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@starknet-io/get-starknet-example",
"version": "4.0.3",
"version": "4.0.5",
"private": true,
"type": "module",
"scripts": {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@starknet-io/get-starknet-core",
"version": "4.0.3",
"version": "4.0.5",
"keywords": [
"starknet",
"starkware",
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -116,6 +117,29 @@ export function getStarknet(

return firstAuthorizedWallet
},
discoverVirtualWallets: async (
walletNamesOrIds: string[] = [],
): Promise<void> => {
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)) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface GetStarknetResult {
) => Promise<StarknetWindowObject[]> // Returns only preauthorized wallets available in the window object
getDiscoveryWallets: (options?: GetWalletOptions) => Promise<WalletProvider[]> // Returns all wallets in existence (from discovery file)
getLastConnectedWallet: () => Promise<StarknetWindowObject | null | undefined> // Returns the last wallet connected when it's still connected
discoverVirtualWallets: () => Promise<void> // Discovers the virtual wallets by calling their hasSupport methods
enable: (
wallet: StarknetWindowObject | VirtualWallet,
options?: RequestAccountsParameters,
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/wallet/virtualWallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ const virtualWallets: VirtualWallet[] = [metaMaskVirtualWallet]

function initiateVirtualWallets(windowObject: Record<string, unknown>) {
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
}
}
})
}
Expand All @@ -28,4 +30,4 @@ async function resolveVirtualWallet(
return wallet
}

export { initiateVirtualWallets, resolveVirtualWallet }
export { initiateVirtualWallets, resolveVirtualWallet, virtualWallets }
147 changes: 113 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 @@ -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",
},
],
})
Expand All @@ -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<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
14 changes: 14 additions & 0 deletions packages/ui/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@starknet-io/get-starknet",
"version": "4.0.3",
"version": "4.0.5",
"keywords": [
"starknet",
"starkware",
Expand Down

0 comments on commit 9894852

Please sign in to comment.