Skip to content

Commit

Permalink
feat: add metamask-snap support (#208)
Browse files Browse the repository at this point in the history
  • Loading branch information
naorye authored Apr 24, 2024
1 parent cf99c85 commit 4d359d8
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 29 deletions.
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"dev": "vite build --watch",
"test": "vitest"
},
"dependencies": {
"@module-federation/runtime": "^0.1.2"
},
"devDependencies": {
"c8": "^7.12.0",
"happy-dom": "^6.0.4",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/StarknetWindowObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface IStarknetWindowObject {
provider?: ProviderInterface | ProviderInterfaceV4
selectedAddress?: string
chainId?: string
isConnected: boolean
}

export interface ConnectedStarknetWindowObject extends IStarknetWindowObject {
Expand All @@ -118,6 +119,7 @@ declare global {
starknet?: StarknetWindowObject
starknet_braavos?: StarknetWindowObject
starknet_argentX?: StarknetWindowObject
starknet_metamask?: StarknetWindowObject
[key: `starknet_${string}`]: StarknetWindowObject | undefined
}
}
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 @@ -137,7 +137,7 @@ describe("getDiscoveryWallets()", () => {
it("should return all discovery wallets", async () => {
const sn = getWallet({})
const discoveryWallets = await sn.getDiscoveryWallets()
expect(discoveryWallets.length).toBe(3)
expect(discoveryWallets.length).toBe(4)
expect(discoveryWallets.map((w) => w.id)).contains(ArgentXMock.id)
expect(discoveryWallets.map((w) => w.id)).contains(BraavosMock.id)
expect(discoveryWallets.map((w) => w.id)).contains(OKXMock.id)
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ const wallets: WalletProvider[] = [
safari: "https://apps.apple.com/us/app/okx-wallet/id6463797825",
},
},
{
id: "metamask",
name: "MetaMask",
icon: `data:image/svg+xml;utf8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMTIiIGhlaWdodD0iMTg5IiB2aWV3Qm94PSIwIDAgMjEyIDE4OSI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cG9seWdvbiBmaWxsPSIjQ0RCREIyIiBwb2ludHM9IjYwLjc1IDE3My4yNSA4OC4zMTMgMTgwLjU2MyA4OC4zMTMgMTcxIDkwLjU2MyAxNjguNzUgMTA2LjMxMyAxNjguNzUgMTA2LjMxMyAxODAgMTA2LjMxMyAxODcuODc1IDg5LjQzOCAxODcuODc1IDY4LjYyNSAxNzguODc1Ii8+PHBvbHlnb24gZmlsbD0iI0NEQkRCMiIgcG9pbnRzPSIxMDUuNzUgMTczLjI1IDEzMi43NSAxODAuNTYzIDEzMi43NSAxNzEgMTM1IDE2OC43NSAxNTAuNzUgMTY4Ljc1IDE1MC43NSAxODAgMTUwLjc1IDE4Ny44NzUgMTMzLjg3NSAxODcuODc1IDExMy4wNjMgMTc4Ljg3NSIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMjU2LjUgMCkiLz48cG9seWdvbiBmaWxsPSIjMzkzOTM5IiBwb2ludHM9IjkwLjU2MyAxNTIuNDM4IDg4LjMxMyAxNzEgOTEuMTI1IDE2OC43NSAxMjAuMzc1IDE2OC43NSAxMjMuNzUgMTcxIDEyMS41IDE1Mi40MzggMTE3IDE0OS42MjUgOTQuNSAxNTAuMTg4Ii8+PHBvbHlnb24gZmlsbD0iI0Y4OUMzNSIgcG9pbnRzPSI3NS4zNzUgMjcgODguODc1IDU4LjUgOTUuMDYzIDE1MC4xODggMTE3IDE1MC4xODggMTIzLjc1IDU4LjUgMTM2LjEyNSAyNyIvPjxwb2x5Z29uIGZpbGw9IiNGODlEMzUiIHBvaW50cz0iMTYuMzEzIDk2LjE4OCAuNTYzIDE0MS43NSAzOS45MzggMTM5LjUgNjUuMjUgMTM5LjUgNjUuMjUgMTE5LjgxMyA2NC4xMjUgNzkuMzEzIDU4LjUgODMuODEzIi8+PHBvbHlnb24gZmlsbD0iI0Q4N0MzMCIgcG9pbnRzPSI0Ni4xMjUgMTAxLjI1IDkyLjI1IDEwMi4zNzUgODcuMTg4IDEyNiA2NS4yNSAxMjAuMzc1Ii8+PHBvbHlnb24gZmlsbD0iI0VBOEQzQSIgcG9pbnRzPSI0Ni4xMjUgMTAxLjgxMyA2NS4yNSAxMTkuODEzIDY1LjI1IDEzNy44MTMiLz48cG9seWdvbiBmaWxsPSIjRjg5RDM1IiBwb2ludHM9IjY1LjI1IDEyMC4zNzUgODcuNzUgMTI2IDk1LjA2MyAxNTAuMTg4IDkwIDE1MyA2NS4yNSAxMzguMzc1Ii8+PHBvbHlnb24gZmlsbD0iI0VCOEYzNSIgcG9pbnRzPSI2NS4yNSAxMzguMzc1IDYwLjc1IDE3My4yNSA5MC41NjMgMTUyLjQzOCIvPjxwb2x5Z29uIGZpbGw9IiNFQThFM0EiIHBvaW50cz0iOTIuMjUgMTAyLjM3NSA5NS4wNjMgMTUwLjE4OCA4Ni42MjUgMTI1LjcxOSIvPjxwb2x5Z29uIGZpbGw9IiNEODdDMzAiIHBvaW50cz0iMzkuMzc1IDEzOC45MzggNjUuMjUgMTM4LjM3NSA2MC43NSAxNzMuMjUiLz48cG9seWdvbiBmaWxsPSIjRUI4RjM1IiBwb2ludHM9IjEyLjkzOCAxODguNDM4IDYwLjc1IDE3My4yNSAzOS4zNzUgMTM4LjkzOCAuNTYzIDE0MS43NSIvPjxwb2x5Z29uIGZpbGw9IiNFODgyMUUiIHBvaW50cz0iODguODc1IDU4LjUgNjQuNjg4IDc4Ljc1IDQ2LjEyNSAxMDEuMjUgOTIuMjUgMTAyLjkzOCIvPjxwb2x5Z29uIGZpbGw9IiNERkNFQzMiIHBvaW50cz0iNjAuNzUgMTczLjI1IDkwLjU2MyAxNTIuNDM4IDg4LjMxMyAxNzAuNDM4IDg4LjMxMyAxODAuNTYzIDY4LjA2MyAxNzYuNjI1Ii8+PHBvbHlnb24gZmlsbD0iI0RGQ0VDMyIgcG9pbnRzPSIxMjEuNSAxNzMuMjUgMTUwLjc1IDE1Mi40MzggMTQ4LjUgMTcwLjQzOCAxNDguNSAxODAuNTYzIDEyOC4yNSAxNzYuNjI1IiB0cmFuc2Zvcm09Im1hdHJpeCgtMSAwIDAgMSAyNzIuMjUgMCkiLz48cG9seWdvbiBmaWxsPSIjMzkzOTM5IiBwb2ludHM9IjcwLjMxMyAxMTIuNSA2NC4xMjUgMTI1LjQzOCA4Ni4wNjMgMTE5LjgxMyIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMTUwLjE4OCAwKSIvPjxwb2x5Z29uIGZpbGw9IiNFODhGMzUiIHBvaW50cz0iMTIuMzc1IC41NjMgODguODc1IDU4LjUgNzUuOTM4IDI3Ii8+PHBhdGggZmlsbD0iIzhFNUEzMCIgZD0iTTEyLjM3NTAwMDIsMC41NjI1MDAwMDggTDIuMjUwMDAwMDMsMzEuNTAwMDAwNSBMNy44NzUwMDAxMiw2NS4yNTAwMDEgTDMuOTM3NTAwMDYsNjcuNTAwMDAxIEw5LjU2MjUwMDE0LDcyLjU2MjUgTDUuMDYyNTAwMDgsNzYuNTAwMDAxMSBMMTEuMjUsODIuMTI1MDAxMiBMNy4zMTI1MDAxMSw4NS41MDAwMDEzIEwxNi4zMTI1MDAyLDk2Ljc1MDAwMTQgTDU4LjUwMDAwMDksODMuODEyNTAxMiBDNzkuMTI1MDAxMiw2Ny4zMTI1MDA0IDg5LjI1MDAwMTMsNTguODc1MDAwMyA4OC44NzUwMDEzLDU4LjUwMDAwMDkgQzg4LjUwMDAwMTMsNTguMTI1MDAwOSA2My4wMDAwMDA5LDM4LjgxMjUwMDYgMTIuMzc1MDAwMiwwLjU2MjUwMDAwOCBaIi8+PGcgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMjExLjUgMCkiPjxwb2x5Z29uIGZpbGw9IiNGODlEMzUiIHBvaW50cz0iMTYuMzEzIDk2LjE4OCAuNTYzIDE0MS43NSAzOS45MzggMTM5LjUgNjUuMjUgMTM5LjUgNjUuMjUgMTE5LjgxMyA2NC4xMjUgNzkuMzEzIDU4LjUgODMuODEzIi8+PHBvbHlnb24gZmlsbD0iI0Q4N0MzMCIgcG9pbnRzPSI0Ni4xMjUgMTAxLjI1IDkyLjI1IDEwMi4zNzUgODcuMTg4IDEyNiA2NS4yNSAxMjAuMzc1Ii8+PHBvbHlnb24gZmlsbD0iI0VBOEQzQSIgcG9pbnRzPSI0Ni4xMjUgMTAxLjgxMyA2NS4yNSAxMTkuODEzIDY1LjI1IDEzNy44MTMiLz48cG9seWdvbiBmaWxsPSIjRjg5RDM1IiBwb2ludHM9IjY1LjI1IDEyMC4zNzUgODcuNzUgMTI2IDk1LjA2MyAxNTAuMTg4IDkwIDE1MyA2NS4yNSAxMzguMzc1Ii8+PHBvbHlnb24gZmlsbD0iI0VCOEYzNSIgcG9pbnRzPSI2NS4yNSAxMzguMzc1IDYwLjc1IDE3My4yNSA5MCAxNTMiLz48cG9seWdvbiBmaWxsPSIjRUE4RTNBIiBwb2ludHM9IjkyLjI1IDEwMi4zNzUgOTUuMDYzIDE1MC4xODggODYuNjI1IDEyNS43MTkiLz48cG9seWdvbiBmaWxsPSIjRDg3QzMwIiBwb2ludHM9IjM5LjM3NSAxMzguOTM4IDY1LjI1IDEzOC4zNzUgNjAuNzUgMTczLjI1Ii8+PHBvbHlnb24gZmlsbD0iI0VCOEYzNSIgcG9pbnRzPSIxMi45MzggMTg4LjQzOCA2MC43NSAxNzMuMjUgMzkuMzc1IDEzOC45MzggLjU2MyAxNDEuNzUiLz48cG9seWdvbiBmaWxsPSIjRTg4MjFFIiBwb2ludHM9Ijg4Ljg3NSA1OC41IDY0LjY4OCA3OC43NSA0Ni4xMjUgMTAxLjI1IDkyLjI1IDEwMi45MzgiLz48cG9seWdvbiBmaWxsPSIjMzkzOTM5IiBwb2ludHM9IjcwLjMxMyAxMTIuNSA2NC4xMjUgMTI1LjQzOCA4Ni4wNjMgMTE5LjgxMyIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMTUwLjE4OCAwKSIvPjxwb2x5Z29uIGZpbGw9IiNFODhGMzUiIHBvaW50cz0iMTIuMzc1IC41NjMgODguODc1IDU4LjUgNzUuOTM4IDI3Ii8+PHBhdGggZmlsbD0iIzhFNUEzMCIgZD0iTTEyLjM3NTAwMDIsMC41NjI1MDAwMDggTDIuMjUwMDAwMDMsMzEuNTAwMDAwNSBMNy44NzUwMDAxMiw2NS4yNTAwMDEgTDMuOTM3NTAwMDYsNjcuNTAwMDAxIEw5LjU2MjUwMDE0LDcyLjU2MjUgTDUuMDYyNTAwMDgsNzYuNTAwMDAxMSBMMTEuMjUsODIuMTI1MDAxMiBMNy4zMTI1MDAxMSw4NS41MDAwMDEzIEwxNi4zMTI1MDAyLDk2Ljc1MDAwMTQgTDU4LjUwMDAwMDksODMuODEyNTAxMiBDNzkuMTI1MDAxMiw2Ny4zMTI1MDA0IDg5LjI1MDAwMTMsNTguODc1MDAwMyA4OC44NzUwMDEzLDU4LjUwMDAwMDkgQzg4LjUwMDAwMTMsNTguMTI1MDAwOSA2My4wMDAwMDA5LDM4LjgxMjUwMDYgMTIuMzc1MDAwMiwwLjU2MjUwMDAwOCBaIi8+PC9nPjwvZz48L3N2Zz4=`,
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
3 changes: 3 additions & 0 deletions packages/core/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IStorageWrapper, LocalStorageWrapper } from "./localStorageStore"
import { pipe } from "./utils"
import { FilterList, filterBy, filterByPreAuthorized } from "./wallet/filter"
import { isWalletObj } from "./wallet/isWalletObject"
import { injectMetamaskBridge } from "./wallet/metamaskBridge"
import { scanObjectForWallets } from "./wallet/scan"
import { Sort, sortBy } from "./wallet/sort"

Expand Down Expand Up @@ -75,6 +76,8 @@ export function getStarknet(
}
const lastConnectedStore = storageFactoryImplementation("gsw-last")

injectMetamaskBridge(windowObject)

return {
getAvailableWallets: async (options = {}) => {
const availableWallets = scanObjectForWallets(
Expand Down
197 changes: 197 additions & 0 deletions packages/core/src/wallet/metamaskBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import type {
IStarknetWindowObject,
RpcMessage,
StarknetWindowObject,
WalletEvents,
} from "../StarknetWindowObject"
import wallets, { WalletProvider } from "../discovery"
import { init, loadRemote } from "@module-federation/runtime"

interface MetaMaskProvider {
isMetaMask: boolean
request(options: { method: string }): Promise<void>
}

function isMetaMaskProvider(obj: unknown): obj is MetaMaskProvider {
return (
obj !== null &&
typeof obj === "object" &&
obj.hasOwnProperty("isMetaMask") &&
obj.hasOwnProperty("request")
)
}

function detectMetaMaskProvider(
windowObject: Record<string, unknown>,
{ timeout = 3000 } = {},
) {
let handled = false
return new Promise<MetaMaskProvider | null>((resolve) => {
if (windowObject.ethereum) {
handleEthereum()
} else {
if (typeof windowObject.addEventListener === "function") {
windowObject.addEventListener("ethereum#initialized", handleEthereum, {
once: true,
})
}
setTimeout(() => {
handleEthereum()
}, timeout)
}
function handleEthereum() {
if (handled) {
return
}
handled = true
if (typeof windowObject.removeEventListener === "function") {
windowObject.removeEventListener("ethereum#initialized", handleEthereum)
}
const { ethereum } = windowObject
if (isMetaMaskProvider(ethereum)) {
resolve(ethereum)
} else {
resolve(null)
}
}
})
}

async function waitForMetaMaskProvider(
windowObject: Record<string, unknown>,
options: { timeout?: number; retries?: number } = {},
): Promise<MetaMaskProvider | null> {
const { timeout = 3000, retries = 0 } = options

let provider: MetaMaskProvider | null = null
try {
provider = await detectMetaMaskProvider(windowObject, { timeout })
} catch {
// Silent error - do nothing
}

if (provider) {
return provider
}

if (retries === 0) {
return null
}

provider = await waitForMetaMaskProvider({ timeout, retries: retries - 1 })
return provider
}

async function detectMetamaskSupport(windowObject: Record<string, unknown>) {
const provider = await waitForMetaMaskProvider(windowObject, { retries: 3 })
return provider
}

function createMetaMaskProviderWrapper(
walletInfo: WalletProvider,
provider: unknown,
): StarknetWindowObject {
let metaMaskSnapWallet: IStarknetWindowObject | undefined
const metaMaskProviderWrapper: IStarknetWindowObject = {
id: walletInfo.id,
name: walletInfo.name,
icon: walletInfo.icon,
get version() {
return metaMaskSnapWallet?.version ?? "0.0.0"
},
get isConnected() {
return metaMaskSnapWallet?.isConnected ?? false
},
get provider() {
return metaMaskSnapWallet?.provider
},
get account() {
return metaMaskSnapWallet?.account
},
get selectedAddress() {
return metaMaskSnapWallet?.selectedAddress
},
get chainId() {
return metaMaskSnapWallet?.chainId
},
request<T extends RpcMessage>(
call: Omit<T, "result">,
): Promise<T["result"]> {
if (!metaMaskSnapWallet) {
throw new Error("Wallet not enabled")
}
return metaMaskSnapWallet.request(call)
},
async enable(): Promise<string[]> {
if (!metaMaskSnapWallet) {
await init({
name: "MetaMaskStarknetSnapWallet",
remotes: [
{
name: "MetaMaskStarknetSnapWallet",
alias: "MetaMaskStarknetSnapWallet",
entry:
"https://snaps.consensys.io/starknet/get-starknet/v1/remoteEntry.js",
},
],
})

const result = await loadRemote("MetaMaskStarknetSnapWallet/index")

const { MetaMaskSnapWallet } = result as {
MetaMaskSnapWallet: any
MetaMaskSnap: any
}

metaMaskSnapWallet = new MetaMaskSnapWallet(provider, "*")
}

return await metaMaskSnapWallet!.enable()
},
isPreauthorized() {
return metaMaskSnapWallet?.isPreauthorized() ?? Promise.resolve(false)
},
on<E extends WalletEvents>(
event: E["type"],
handleEvent: E["handler"],
): void {
if (!metaMaskSnapWallet) {
throw new Error("Wallet not enabled")
}
// @ts-ignore: Metamask currently doesn't support on method
return metaMaskSnapWallet.on(event, handleEvent)
},
off<E extends WalletEvents>(event: E["type"], handleEvent: E["handler"]) {
if (!metaMaskSnapWallet) {
throw new Error("Wallet not enabled")
}
// @ts-ignore: Metamask currently doesn't support off method
return metaMaskSnapWallet.off(event, handleEvent)
},
}

return metaMaskProviderWrapper as StarknetWindowObject
}

async function injectMetamaskBridge(windowObject: Record<string, unknown>) {
if (window.hasOwnProperty("starknet_metamask")) {
return
}

const metamaskWalletInfo = wallets.find((wallet) => wallet.id === "metamask")
if (!metamaskWalletInfo) {
return
}

const provider = await detectMetamaskSupport(windowObject)
if (!provider) {
return
}

window.starknet_metamask = createMetaMaskProviderWrapper(
metamaskWalletInfo,
provider,
)
}

export { injectMetamaskBridge }
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0",
"autoprefixer": "^10.4.7",
"postcss": "^8.4.14",
"postcss": "^8.4.21",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
Expand Down
Loading

0 comments on commit 4d359d8

Please sign in to comment.