Skip to content

Commit

Permalink
feat: virtual wallet support + Metamask integration (#227)
Browse files Browse the repository at this point in the history
  • Loading branch information
naorye authored Jun 26, 2024
1 parent f9d510d commit 078dcb4
Show file tree
Hide file tree
Showing 12 changed files with 293 additions and 44 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"test": "vitest"
},
"dependencies": {
"@module-federation/runtime": "^0.1.2",
"@starknet-io/types-js": "^0.7.7"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/__test__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe("getDiscoveryWallets()", () => {
it("should return all discovery wallets", async () => {
const sn = getWallet({})
const discoveryWallets = await sn.getDiscoveryWallets()
expect(discoveryWallets.length).toBe(2)
expect(discoveryWallets.length).toBe(3)
expect(discoveryWallets.map((w) => w.id)).contains(ArgentXMock.id)
expect(discoveryWallets.map((w) => w.id)).contains(BraavosMock.id)
})
Expand Down
30 changes: 20 additions & 10 deletions packages/core/src/__test__/wallet.mock.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import wallets from "../discovery"
import { Permission, type StarknetWindowObject } from "@starknet-io/types-js"

type WalletMock = Pick<StarknetWindowObject, "id" | "name" | "icon" | "request">

export const UnknownWalletAMock: WalletMock = {
export const UnknownWalletAMock: StarknetWindowObject = {
id: "wallet-a",
name: "Wallet A",
version: "0.0.0",
icon: "https://avatars.dicebear.com/api/initials/Wallet%20A.svg",
request: async () => false,
on: () => {},
off: () => {},
}
export const UnknownWalletBMock: WalletMock = {
export const UnknownWalletBMock: StarknetWindowObject = {
id: "wallet-b",
name: "Wallet B",
version: "0.0.0",
icon: "https://avatars.dicebear.com/api/initials/Wallet%20B.svg",
request: async () => false,
on: () => {},
off: () => {},
}

export const ArgentXMock: WalletMock = {
export const ArgentXMock: StarknetWindowObject = {
...wallets.find((w) => w.id === "argentX")!,
version: "0.0.0",
request: async (request) => {
switch (request.type) {
case "wallet_getPermissions":
Expand All @@ -26,10 +31,13 @@ export const ArgentXMock: WalletMock = {
return undefined as any
}
},
on: () => {},
off: () => {},
}

export const BraavosMock: WalletMock = {
export const BraavosMock: StarknetWindowObject = {
...wallets.find((w) => w.id === "braavos")!,
version: "0.0.0",
request: async (request) => {
switch (request.type) {
case "wallet_getPermissions":
Expand All @@ -38,10 +46,12 @@ export const BraavosMock: WalletMock = {
return undefined as any
}
},
on: () => {},
off: () => {},
}

export function makeAuthorized(authorized: boolean) {
return (wallet: WalletMock) =>
return (wallet: StarknetWindowObject) =>
({
...wallet,
request: async (request) => {
Expand All @@ -52,11 +62,11 @@ export function makeAuthorized(authorized: boolean) {
return wallet.request(request)
}
},
} as WalletMock)
} as StarknetWindowObject)
}

export function makeConnected(isConnected: boolean) {
return (wallet: WalletMock) => {
return (wallet: StarknetWindowObject) => {
return {
...makeAuthorized(true)(wallet),
request: async ({ type }) => {
Expand All @@ -67,6 +77,6 @@ export function makeConnected(isConnected: boolean) {
return []
}
},
} as WalletMock
} as StarknetWindowObject
}
}
13 changes: 13 additions & 0 deletions packages/core/src/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { metaMaskVirtualWallet } from "./wallet/virtualWallets/metaMaskVirtualWallet"

export type WalletProvider = {
id: string
name: string
Expand Down Expand Up @@ -31,6 +33,17 @@ const wallets: WalletProvider[] = [
edge: "https://microsoftedge.microsoft.com/addons/detail/braavos-wallet/hkkpjehhcnhgefhbdcgfkeegglpjchdc",
},
},
{
id: metaMaskVirtualWallet.id,
name: metaMaskVirtualWallet.name,
icon: metaMaskVirtualWallet.icon,
downloads: {
chrome:
"https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn",
firefox: "https://addons.mozilla.org/en-US/firefox/addon/ether-metamask/",
edge: "https://microsoftedge.microsoft.com/addons/detail/metamask/ejbalbakoplchlghecdalmeeeajnimhm?hl=en-US",
},
},
]

export default wallets
35 changes: 23 additions & 12 deletions packages/core/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import { LocalStorageWrapper } from "./localStorageStore"
import type { GetStarknetOptions, GetStarknetResult } from "./types"
import { pipe } from "./utils"
import { filterBy, filterByAuthorized } from "./wallet/filter"
import { isWalletObj } from "./wallet/isWalletObject"
import {
isFullWallet,
isVirtualWallet,
isWalletObject,
} from "./wallet/isWalletObject"
import { scanObjectForWallets } from "./wallet/scan"
import { sortBy } from "./wallet/sort"
import {
initiateVirtualWallets,
resolveVirtualWallet,
} from "./wallet/virtualWallets"
import { Permission, type StarknetWindowObject } from "@starknet-io/types-js"

export type {
Expand All @@ -31,10 +39,8 @@ export type {
WalletEvents,
} from "@starknet-io/types-js"

export { Permission } from "@starknet-io/types-js"

export { scanObjectForWallets } from "./wallet/scan"
export { isWalletObj } from "./wallet/isWalletObject"
export { isWalletObject } from "./wallet/isWalletObject"

export type {
DisconnectOptions,
Expand All @@ -48,16 +54,10 @@ const ssrSafeWindow = typeof window !== "undefined" ? window : {}

const defaultOptions: GetStarknetOptions = {
windowObject: ssrSafeWindow,
isWalletObject: isWalletObj,
isWalletObject,
storageFactoryImplementation: (name: string) => new LocalStorageWrapper(name),
}

declare global {
interface Window {
[key: `starknet_${string}`]: StarknetWindowObject | undefined
}
}

export function getStarknet(
options: Partial<GetStarknetOptions> = {},
): GetStarknetResult {
Expand All @@ -67,6 +67,8 @@ export function getStarknet(
}
const lastConnectedStore = storageFactoryImplementation("gsw-last")

initiateVirtualWallets(windowObject)

return {
getAvailableWallets: async (options = {}) => {
const availableWallets = scanObjectForWallets(
Expand Down Expand Up @@ -112,7 +114,16 @@ export function getStarknet(

return firstAuthorizedWallet
},
enable: async (wallet, options) => {
enable: async (inputWallet, options) => {
let wallet: StarknetWindowObject
if (isVirtualWallet(inputWallet)) {
wallet = await resolveVirtualWallet(windowObject, inputWallet)
} else if (isFullWallet(inputWallet)) {
wallet = inputWallet
} else {
throw new Error("Invalid wallet object")
}

await wallet.request({
type: "wallet_requestAccounts",
params: {
Expand Down
41 changes: 39 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { WalletProvider } from "./discovery"
import { IStorageWrapper } from "./localStorageStore"
import { ensureKeysArray } from "./utils"
import { FilterList } from "./wallet/filter"
import { Sort } from "./wallet/sort"
import type {
Expand All @@ -11,7 +12,7 @@ export type { WalletProvider } from "./discovery"

export interface GetStarknetOptions {
windowObject: Record<string, any>
isWalletObject: (wallet: any) => boolean
isWalletObject: (wallet: unknown) => boolean
storageFactoryImplementation: (name: string) => IStorageWrapper
}

Expand All @@ -25,6 +26,36 @@ export interface DisconnectOptions {
clearLastWallet?: boolean
}

export interface VirtualWallet {
id: string
name: string
icon: string
windowKey: string
loadWallet: (
windowObject: Record<string, unknown>,
) => Promise<StarknetWindowObject>
hasSupport: (windowObject: Record<string, unknown>) => Promise<boolean>
}

export const virtualWalletKeys = ensureKeysArray<VirtualWallet>({
id: true,
name: true,
icon: true,
windowKey: true,
loadWallet: true,
hasSupport: true,
})

export const fullWalletKeys = ensureKeysArray<StarknetWindowObject>({
id: true,
name: true,
version: true,
icon: true,
request: true,
on: true,
off: true,
})

export interface GetStarknetResult {
getAvailableWallets: (
options?: GetWalletOptions,
Expand All @@ -35,8 +66,14 @@ export interface GetStarknetResult {
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
enable: (
wallet: StarknetWindowObject,
wallet: StarknetWindowObject | VirtualWallet,
options?: RequestAccountsParameters,
) => Promise<StarknetWindowObject> // Connects to a wallet
disconnect: (options?: DisconnectOptions) => Promise<void> // Disconnects from a wallet
}

declare global {
interface Window {
[key: string]: unknown
}
}
6 changes: 6 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ export const pipe =
<T>(...fns: Array<(arg: T) => AllowPromise<T>>): ((arg: T) => Promise<T>) =>
(arg: T) =>
fns.reduce<Promise<T>>((acc, fn) => acc.then(fn), Promise.resolve(arg))

export function ensureKeysArray<T extends object>(keysGuard: {
[k in keyof T]: true
}) {
return Object.keys(keysGuard) as (keyof T)[]
}
16 changes: 10 additions & 6 deletions packages/core/src/wallet/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ export const filterByAuthorized = async (
wallets: StarknetWindowObject[],
): Promise<StarknetWindowObject[]> => {
const preAuthResponses = await Promise.all(
wallets.map((w) =>
w
.request({ type: "wallet_getPermissions" })
.then((result: Permission[]) => result.includes(Permission.ACCOUNTS))
.catch(() => false),
),
wallets.map(async (wallet) => {
try {
const result: Permission[] = await wallet.request({
type: "wallet_getPermissions",
})
return result.includes(Permission.ACCOUNTS)
} catch {
return false
}
}),
)
return wallets.filter((_, i) => preAuthResponses[i])
}
30 changes: 17 additions & 13 deletions packages/core/src/wallet/isWalletObject.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
export const isWalletObj = (wallet: any): boolean => {
try {
import { fullWalletKeys, virtualWalletKeys } from "../types"

function createWalletGuard<T>(keys: (keyof T)[]) {
return function hasKeys(obj: unknown): obj is T {
return (
wallet &&
[
// wallet's must have methods/members, see IStarknetWindowObject
"request",
"on",
"off",
"version",
"id",
"name",
"icon",
].every((key) => key in wallet)
obj !== null && typeof obj === "object" && keys.every((key) => key in obj)
)
}
}

const isFullWallet = createWalletGuard(fullWalletKeys)

const isVirtualWallet = createWalletGuard(virtualWalletKeys)

function isWalletObject(wallet: unknown): boolean {
try {
return isFullWallet(wallet) || isVirtualWallet(wallet)
} catch (err) {}
return false
}

export { isVirtualWallet, isFullWallet, isWalletObject }
31 changes: 31 additions & 0 deletions packages/core/src/wallet/virtualWallets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { VirtualWallet } from "../../types"
import { metaMaskVirtualWallet } from "./metaMaskVirtualWallet"
import type { StarknetWindowObject } from "@starknet-io/types-js"

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
}
})
}

const virtualWalletsMap: Record<string, StarknetWindowObject> = {}

async function resolveVirtualWallet(
windowObject: Record<string, unknown>,
virtualWallet: VirtualWallet,
) {
let wallet: StarknetWindowObject = virtualWalletsMap[virtualWallet.id]
if (!wallet) {
wallet = await virtualWallet.loadWallet(windowObject)
virtualWalletsMap[virtualWallet.id] = wallet
}

return wallet
}

export { initiateVirtualWallets, resolveVirtualWallet }
Loading

0 comments on commit 078dcb4

Please sign in to comment.