Skip to content

Commit

Permalink
persist passkey
Browse files Browse the repository at this point in the history
  • Loading branch information
mmv08 committed Jan 11, 2024
1 parent 28f026c commit 897168e
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 66 deletions.
6 changes: 6 additions & 0 deletions examples/safe-4337-passkeys/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
/* text-align: center; */
}

.header {
display: flex;
align-items: center;
justify-content: space-between;
}

.logo {
height: 6em;
will-change: filter;
Expand Down
101 changes: 74 additions & 27 deletions examples/safe-4337-passkeys/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,94 @@
import { useState } from "react"
import ConnectButton from "./components/ConnectButton"
import safeLogo from "/safe-logo.svg"
import { createPasskey, isLocalStoragePasskey, toLocalStorageFormat } from "./logic/passkeys.ts"
import { PasskeyLocalStorageFormat, createPasskey, toLocalStorageFormat } from "./logic/passkeys.ts"
import "./App.css"
import { setItem } from "./logic/storage.ts"

import { useLocalStorageState } from "./hooks/useLocalStorageState.ts"
import ConnectButton from "./components/ConnectButton.tsx"
import { useState } from "react"
import { useWeb3ModalProvider, useWeb3ModalAccount } from "@web3modal/ethers/react"
import { APP_CHAIN_ID } from "./config.ts"
import { switchToMumbai } from "./logic/wallets.ts"
const PASSKEY_LOCALSTORAGE_KEY = "passkeyId"

function App() {
const [savedPasskey] = useState(() => {
const savedData = localStorage.getItem("data")
if (!savedData) {
return null
}
const [passkey, setPasskey] = useLocalStorageState<PasskeyLocalStorageFormat | null>(
PASSKEY_LOCALSTORAGE_KEY,
null
)
const [error, setError] = useState<string>()
const { chainId } = useWeb3ModalAccount()
const { walletProvider } = useWeb3ModalProvider()

const savedDataJson = JSON.parse(savedData)
if (isLocalStoragePasskey(savedDataJson)) {
return savedDataJson
}
const handleCreatePasskeyClick = async () => {
setError(undefined)
try {
const passkey = await createPasskey()

return null
})
setPasskey(toLocalStorageFormat(passkey))
} catch (error) {
if (error instanceof Error) {
setError(error.message)
} else {
setError("Unknown error")
}
}
}

const handleCreatePasskeyClick = async () => {
const passkey = await createPasskey()
const handleSwitchToMumbaiClick = () => {
if (!walletProvider) return

setItem(PASSKEY_LOCALSTORAGE_KEY, toLocalStorageFormat(passkey))
setError(undefined)
try {
switchToMumbai(walletProvider)
} catch (error) {
if (error instanceof Error) {
setError(error.message)
} else {
setError("Unknown error when switching to Mumbai network")
}
}
}

return (
<>
<div>
<header className="header">
<a href="https://safe.global" target="_blank">
<img src={safeLogo} className="logo" alt="Safe logo" />
</a>
</div>

<div className="card">
<ConnectButton />
</div>
</header>
<h1>Safe + 4337 + Passkeys demo</h1>
<div className="card">
<ConnectButton />
</div>
<div className="card">
<button onClick={handleCreatePasskeyClick}>Create Passkey</button>
</div>

{Boolean(walletProvider) && chainId !== APP_CHAIN_ID && (
<div className="card">
<p>Please switch to Mumbai network to continue</p>
<button onClick={handleSwitchToMumbaiClick}>Switch to Mumbai</button>
</div>
)}

{passkey ? (
<div className="card">
<p>
Passkey ID: {passkey.rawId}
<br />
Passkey X: {passkey.pubkeyCoordinates.x}
<br />
Passkey Y: {passkey.pubkeyCoordinates.y}
</p>
</div>
) : (
<div className="card">
<button onClick={handleCreatePasskeyClick}>Create Passkey</button>
</div>
)}

{error && (
<div className="card">
<p>Error: {error}</p>
</div>
)}
</>
)
}
Expand Down
3 changes: 3 additions & 0 deletions examples/safe-4337-passkeys/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const APP_CHAIN_ID = 80001

export { APP_CHAIN_ID }
37 changes: 37 additions & 0 deletions examples/safe-4337-passkeys/src/hooks/useLocalStorageState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useState } from "react"
import { setItem, getItem } from "../logic/storage.ts"

/**
* Custom hook that manages state in local storage.
*
* @template T - The type of the state value.
* @param {string} key - The key used to store the state value in local storage.
* @param {T} initialValue - The initial value of the state.
* @returns {[T, React.Dispatch<React.SetStateAction<T>>]} - An array containing the current state value and a function to update the state.
*/
function useLocalStorageState<T>(
key: string,
initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const [state, setState] = useState<T>(() => {
const storedValue = getItem(key)

if (storedValue) {
try {
return JSON.parse(storedValue) as T
} catch {
// trick eslint with a no-op
}
}

return initialValue
})

useEffect(() => {
setItem(key, JSON.stringify(state))
}, [key, state])

return [state, setState]
}

export { useLocalStorageState }
14 changes: 8 additions & 6 deletions examples/safe-4337-passkeys/src/logic/passkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,15 @@ async function createPasskey(): Promise<PasskeyCredentialWithPubkeyCoordinates>
}

// Create a PasskeyCredentialWithPubkeyCoordinates object
const passkeyWithCoordinates: PasskeyCredentialWithPubkeyCoordinates = Object.assign({
pubkeyCoordinates: {
x: Buffer.from(exportedKeyWithXYCoordinates.x, "base64").toString("hex"),
y: Buffer.from(exportedKeyWithXYCoordinates.y, "base64").toString("hex"),
},
const passkeyWithCoordinates: PasskeyCredentialWithPubkeyCoordinates = Object.assign(
passkeyCredential,
})
{
pubkeyCoordinates: {
x: Buffer.from(exportedKeyWithXYCoordinates.x, "base64").toString("hex"),
y: Buffer.from(exportedKeyWithXYCoordinates.y, "base64").toString("hex"),
},
}
)

return passkeyWithCoordinates
}
Expand Down
Empty file.
63 changes: 63 additions & 0 deletions examples/safe-4337-passkeys/src/logic/wallets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createWeb3Modal, defaultConfig } from "@web3modal/ethers/react"
import { ethers } from "ethers"
import { numberToUnpaddedHex } from "../utils"

// 1. Get projectId at https://cloud.walletconnect.com
const projectId = import.meta.env.VITE_WC_CLOUD_PROJECT_ID

if (!projectId) {
throw new Error("Walletconnect Project ID is missing")
}

// 2. Set chains
const mumbai = {
chainId: 80001,
name: "Polygon Mumbai",
currency: "MATIC",
explorerUrl: "https://mumbai.polygonscan.com",
rpcUrl: "https://rpc-mumbai.maticvigil.com",
}

// 3. Create modal
const metadata = {
name: "Safe 4337 Passkeys Example",
description:
"An example application to deploy a 4337-compatible Safe Account with Passkeys signer",
url: "https://safe.global",
icons: ["https://app.safe.global/favicons/favicon.ico"],
}

createWeb3Modal({
ethersConfig: defaultConfig({ metadata }),
// defaultChain: mumbai,
chains: [mumbai],
projectId,
})

async function switchToMumbai(provider: ethers.Eip1193Provider) {
return provider
.request({
method: "wallet_addEthereumChain",
params: [
{
chainId: numberToUnpaddedHex(mumbai.chainId),
blockExplorerUrls: [mumbai.explorerUrl],
chainName: mumbai.name,
nativeCurrency: {
name: mumbai.currency,
symbol: mumbai.currency,
decimals: 18,
},
rpcUrls: [mumbai.rpcUrl],
},
],
})
.catch(() => {
provider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: numberToUnpaddedHex(mumbai.chainId) }],
})
})
}

export { switchToMumbai }
2 changes: 1 addition & 1 deletion examples/safe-4337-passkeys/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client"
import App from "./App.tsx"
// Keep the import of the wallets file to eval so w3m package can initialise
// the required globals
import "./wallets.ts"
import "./logic/wallets.ts"
import "./index.css"

const rootElement = document.getElementById("root")
Expand Down
14 changes: 14 additions & 0 deletions examples/safe-4337-passkeys/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Converts a number to an unpadded hexadecimal string.
* Metamask requires unpadded chain id for wallet_switchEthereumChain and wallet_addEthereumChain.
*
* @param n - The number to convert.
* @param withPrefix - Whether to include the "0x" prefix in the result. Default is true.
* @returns The unpadded hexadecimal string representation of the number.
*/
function numberToUnpaddedHex(n: number, withPrefix = true): string {
const hex = n.toString(16)
return `${withPrefix ? "0x" : ""}${hex}`
}

export { numberToUnpaddedHex }
32 changes: 0 additions & 32 deletions examples/safe-4337-passkeys/src/wallets.ts

This file was deleted.

0 comments on commit 897168e

Please sign in to comment.