Please switch to Ethereum Sepolia test network to continue
-
- {error &&
Error: {error}
}
-
- )
-}
-
-export { SwitchNetwork }
diff --git a/examples/4337-passkeys/src/config.ts b/examples/4337-passkeys/src/config.ts
deleted file mode 100644
index 25a4df593..000000000
--- a/examples/4337-passkeys/src/config.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-// 11155111 = Sepolia testnet chain id
-const APP_CHAIN_ID = 11155111
-
-// Sep testnet shortname
-// https://eips.ethereum.org/EIPS/eip-3770
-const APP_CHAIN_SHORTNAME = 'sep'
-
-/*
- Some of the contracts used in the PoC app are still experimental, and not included in
- the production deployment packages, thus we need to hardcode their addresses here.
- Deployment commit: https://github.com/safe-global/safe-modules/commit/3853f34f31837e0a0aee47a4452564278f8c62ba
-*/
-const SAFE_SIGNER_LAUNCHPAD_ADDRESS = '0x2804BAA6635d97281FB4d1F011B4BF55DD7A5325'
-
-const SAFE_4337_MODULE_ADDRESS = '0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'
-
-const SAFE_MODULE_SETUP_ADDRESS = '0x2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47'
-
-const WEBAUTHN_SIGNER_FACTORY_ADDRESS = '0xc40156AbFEE908E2e3269DA84fa9609bcCDDec60'
-
-const P256_VERIFIER_ADDRESS = '0xcA89CBa4813D5B40AeC6E57A30d0Eeb500d6531b' // FCLP256Verifier
-
-const SAFE_PROXY_FACTORY_ADDRESS = '0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67'
-
-const SAFE_SINGLETON_ADDRESS = '0x29fcB43b46531BcA003ddC8FCB67FFE91900C762'
-
-const ENTRYPOINT_ADDRESS = '0x0000000071727De22E5E9d8BAf0edAc6f37da032'
-
-const XANDER_BLAZE_NFT_ADDRESS = '0xBb9ebb7b8Ee75CDBf64e5cE124731A89c2BC4A07'
-
-export {
- SAFE_MODULE_SETUP_ADDRESS,
- APP_CHAIN_ID,
- ENTRYPOINT_ADDRESS,
- SAFE_SIGNER_LAUNCHPAD_ADDRESS,
- SAFE_4337_MODULE_ADDRESS,
- SAFE_PROXY_FACTORY_ADDRESS,
- SAFE_SINGLETON_ADDRESS,
- XANDER_BLAZE_NFT_ADDRESS,
- WEBAUTHN_SIGNER_FACTORY_ADDRESS,
- P256_VERIFIER_ADDRESS,
- APP_CHAIN_SHORTNAME,
-}
diff --git a/examples/4337-passkeys/src/hooks/UseOutletContext.tsx b/examples/4337-passkeys/src/hooks/UseOutletContext.tsx
deleted file mode 100644
index d1241fbfb..000000000
--- a/examples/4337-passkeys/src/hooks/UseOutletContext.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useOutletContext as useOutletContextRRD } from 'react-router'
-import { ethers } from 'ethers'
-
-type OutletContext = {
- walletProvider: ethers.Eip1193Provider
-}
-
-function useOutletContext() {
- return useOutletContextRRD()
-}
-
-export { useOutletContext }
diff --git a/examples/4337-passkeys/src/hooks/useCodeAtAddress.ts b/examples/4337-passkeys/src/hooks/useCodeAtAddress.ts
deleted file mode 100644
index 53c230e72..000000000
--- a/examples/4337-passkeys/src/hooks/useCodeAtAddress.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { ethers } from 'ethers'
-import { useEffect, useState } from 'react'
-import { getJsonRpcProviderFromEip1193Provider } from '../logic/wallets'
-import { RequestStatus } from '../utils'
-
-type Options = {
- pollInterval?: number
-}
-
-/**
- * Custom hook that retrieves the code at a given address using an Eip1193Provider.
- *
- * @param provider - The Eip1193Provider instance.
- * @param address - The address to retrieve the code from.
- * @param opts - Optional configuration options.
- * @returns An array containing the code and the request status.
- */
-function useCodeAtAddress(provider: ethers.Eip1193Provider, address: string, opts?: Options): [string, RequestStatus] {
- const [code, setCode] = useState('')
- const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED)
-
- useEffect(() => {
- let cancelled = false
-
- async function updateBalance() {
- try {
- const jsonRpcProvider = getJsonRpcProviderFromEip1193Provider(provider)
- const balance = await jsonRpcProvider.getCode(address)
- if (!cancelled) {
- setCode(balance)
- setStatus(RequestStatus.SUCCESS)
- }
- } catch (e) {
- if (!cancelled) {
- setStatus(RequestStatus.ERROR)
- }
- }
- }
-
- if (provider) {
- const pollInterval = opts?.pollInterval || 5000
-
- updateBalance()
- const interval = setInterval(updateBalance, pollInterval)
-
- return () => {
- cancelled = true
- clearInterval(interval)
- }
- }
- }, [provider, address, opts?.pollInterval])
-
- return [code, status]
-}
-
-export { useCodeAtAddress }
diff --git a/examples/4337-passkeys/src/hooks/useEntryPointAccountBalance.ts b/examples/4337-passkeys/src/hooks/useEntryPointAccountBalance.ts
deleted file mode 100644
index d94f20c54..000000000
--- a/examples/4337-passkeys/src/hooks/useEntryPointAccountBalance.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Eip1193Provider } from 'ethers'
-import { getAccountEntryPointBalance } from '../logic/userOp.ts'
-import { getJsonRpcProviderFromEip1193Provider } from '../logic/wallets.ts'
-import { useEffect, useState } from 'react'
-import { RequestStatus } from '../utils.ts'
-
-function useEntryPointAccountBalance(
- walletProvider: Eip1193Provider,
- safeAddress: string,
- opts: { pollInterval: number } = { pollInterval: 5000 },
-): [bigint | null, RequestStatus] {
- const [nonce, setNonce] = useState(null)
- const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED)
-
- useEffect(() => {
- let cancelled = false
-
- const fetchBalance = async () => {
- if (cancelled) return
- setStatus(RequestStatus.LOADING)
- try {
- const provider = getJsonRpcProviderFromEip1193Provider(walletProvider)
- const newNonce = await getAccountEntryPointBalance(provider, safeAddress)
- if (!cancelled) {
- setNonce(newNonce)
- setStatus(RequestStatus.SUCCESS)
- }
- } catch (error) {
- if (!cancelled) {
- setStatus(RequestStatus.ERROR)
- console.error('Error fetching nonce:', error)
- }
- }
- }
-
- fetchBalance()
-
- const interval = setInterval(fetchBalance, opts.pollInterval)
- return () => {
- cancelled = true
- clearInterval(interval)
- }
- }, [walletProvider, safeAddress, opts.pollInterval])
-
- return [nonce, status]
-}
-
-export { useEntryPointAccountBalance }
diff --git a/examples/4337-passkeys/src/hooks/useEntryPointAccountNonce.ts b/examples/4337-passkeys/src/hooks/useEntryPointAccountNonce.ts
deleted file mode 100644
index 142506348..000000000
--- a/examples/4337-passkeys/src/hooks/useEntryPointAccountNonce.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Eip1193Provider } from 'ethers'
-import { getNonceFromEntryPoint } from '../logic/userOp.ts'
-import { getJsonRpcProviderFromEip1193Provider } from '../logic/wallets.ts'
-import { useEffect, useState } from 'react'
-import { RequestStatus } from '../utils.ts'
-
-function useEntryPointAccountNonce(
- walletProvider: Eip1193Provider,
- safeAddress: string,
- opts: { pollInterval: number } = { pollInterval: 5000 },
-): [bigint | null, RequestStatus] {
- const [nonce, setNonce] = useState(null)
- const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED)
-
- useEffect(() => {
- let cancelled = false
-
- const fetchNonce = async () => {
- if (cancelled) return
- setStatus(RequestStatus.LOADING)
- try {
- const provider = getJsonRpcProviderFromEip1193Provider(walletProvider)
- const newNonce = await getNonceFromEntryPoint(provider, safeAddress)
- if (!cancelled) {
- setNonce(newNonce)
- setStatus(RequestStatus.SUCCESS)
- }
- } catch (error) {
- if (!cancelled) {
- setStatus(RequestStatus.ERROR)
- console.error('Error fetching nonce:', error)
- }
- }
- }
-
- fetchNonce()
-
- const interval = setInterval(fetchNonce, opts.pollInterval)
- return () => {
- cancelled = true
- clearInterval(interval)
- }
- }, [walletProvider, safeAddress, opts.pollInterval])
-
- return [nonce, status]
-}
-
-export { useEntryPointAccountNonce }
diff --git a/examples/4337-passkeys/src/hooks/useFeeData.ts b/examples/4337-passkeys/src/hooks/useFeeData.ts
deleted file mode 100644
index 81e174778..000000000
--- a/examples/4337-passkeys/src/hooks/useFeeData.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { ethers } from 'ethers'
-import { useEffect, useState } from 'react'
-import { getJsonRpcProviderFromEip1193Provider } from '../logic/wallets'
-import { RequestStatus } from '../utils'
-
-type FeeData = Omit
-
-/**
- * Applies a multiplier to the provided fee data. Pimlico bundler started requiring
- * a 1.2x multiplier on gas prices. Customize it for your bundler or use their proprietary
- * method that will return the prices with the multiplier applied.
- * @param feeData The fee data to apply the multiplier to.
- * @param multiplier The multiplier to apply to the fee data.
- * @returns The fee data with the multiplier applied.
- */
-function applyMultiplier(feeData: FeeData, multiplier: bigint = 120n): FeeData {
- if (!feeData.gasPrice || !feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) return feeData
-
- return {
- gasPrice: (feeData.gasPrice * multiplier) / 100n,
- maxFeePerGas: (feeData.maxFeePerGas * multiplier) / 100n,
- maxPriorityFeePerGas: (feeData.maxPriorityFeePerGas * multiplier) / 100n,
- }
-}
-
-/**
- * Custom hook that fetches fee data using the provided Eip1193Provider.
- * @param provider The Eip1193Provider instance.
- * @returns A tuple containing the fee data and the request status.
- */
-function useFeeData(provider: ethers.Eip1193Provider): [FeeData | undefined, RequestStatus] {
- const [feeData, setFeeData] = useState()
- const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED)
-
- useEffect(() => {
- let isMounted = true
-
- setStatus(RequestStatus.LOADING)
- const jsonRpcProvider = getJsonRpcProviderFromEip1193Provider(provider)
- jsonRpcProvider
- .getFeeData()
- .then((feeData) => {
- if (!isMounted) return
- setFeeData(applyMultiplier(feeData))
- setStatus(RequestStatus.SUCCESS)
- })
- .catch((error) => {
- if (!isMounted) return
- console.error(error)
- setStatus(RequestStatus.ERROR)
- })
-
- return () => {
- isMounted = false
- }
- }, [provider])
-
- return [feeData, status]
-}
-
-export { useFeeData }
diff --git a/examples/4337-passkeys/src/hooks/useLocalStorageState.ts b/examples/4337-passkeys/src/hooks/useLocalStorageState.ts
deleted file mode 100644
index 06682fcbd..000000000
--- a/examples/4337-passkeys/src/hooks/useLocalStorageState.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-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>]} - An array containing the current state value and a function to update the state.
- */
-function useLocalStorageState(key: string, initialValue: T): [T, React.Dispatch>] {
- const [state, setState] = useState(() => {
- const storedValue = getItem(key)
-
- // this naive hook might write 'undefined' or 'null' to local storage as a string
- if (storedValue && storedValue !== 'undefined' && storedValue !== 'null') {
- try {
- return JSON.parse(storedValue) as T
- } catch {
- // trick eslint with a no-op
- }
- }
-
- return initialValue
- })
-
- useEffect(() => {
- setItem(key, state)
- }, [key, state])
-
- return [state, setState]
-}
-
-export { useLocalStorageState }
diff --git a/examples/4337-passkeys/src/hooks/useNativeTokenBalance.ts b/examples/4337-passkeys/src/hooks/useNativeTokenBalance.ts
deleted file mode 100644
index 188a02a9c..000000000
--- a/examples/4337-passkeys/src/hooks/useNativeTokenBalance.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { ethers } from 'ethers'
-import { useEffect, useState } from 'react'
-import { getJsonRpcProviderFromEip1193Provider } from '../logic/wallets'
-import { RequestStatus } from '../utils'
-
-type Options = {
- pollInterval?: number
-}
-
-/**
- * Custom hook to fetch the balance of the native token for a given address.
- * @param provider The Eip1193Provider instance.
- * @param address The address for which to fetch the balance.
- * @param opts Optional configuration options.
- * @returns An array containing the balance as a bigint and the request status.
- */
-function useNativeTokenBalance(provider: ethers.Eip1193Provider, address: string, opts?: Options): [bigint, RequestStatus] {
- const [balance, setBalance] = useState(0n)
- const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED)
-
- useEffect(() => {
- let cancelled = false
-
- async function updateBalance() {
- try {
- const jsonRpcProvider = getJsonRpcProviderFromEip1193Provider(provider)
- const balance = await jsonRpcProvider.getBalance(address)
- if (!cancelled) {
- setBalance(balance)
- setStatus(RequestStatus.SUCCESS)
- }
- } catch (e) {
- if (!cancelled) {
- setStatus(RequestStatus.ERROR)
- }
- }
- }
-
- if (provider) {
- const pollInterval = opts?.pollInterval || 5000
-
- updateBalance()
- const interval = setInterval(updateBalance, pollInterval)
-
- return () => {
- cancelled = true
- clearInterval(interval)
- }
- }
- }, [provider, address, opts?.pollInterval])
-
- return [balance, status]
-}
-
-export { useNativeTokenBalance }
diff --git a/examples/4337-passkeys/src/hooks/useSignerAddressFromPubkeyCoords.ts b/examples/4337-passkeys/src/hooks/useSignerAddressFromPubkeyCoords.ts
deleted file mode 100644
index 2c8a0e667..000000000
--- a/examples/4337-passkeys/src/hooks/useSignerAddressFromPubkeyCoords.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { getSignerAddressFromPubkeyCoords } from '../logic/safe.ts'
-import { RequestStatus } from '../utils.ts'
-import { ethers } from 'ethers'
-import { useEffect, useState } from 'react'
-import { getJsonRpcProviderFromEip1193Provider } from '../logic/wallets.ts'
-
-/**
- * Custom hook for getting the signer address from the pubkey coordinates.
- * @param x The x coordinate of the pubkey.
- * @param y The y coordinate of the pubkey.
- * @returns The signer address.
- */
-function useSignerAddressFromPubkeyCoords(provider: ethers.Eip1193Provider, x: string, y: string): [string | null, RequestStatus] {
- const [signerAddress, setSignerAddress] = useState(null)
- const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED)
-
- useEffect(() => {
- let isMounted = true
- async function getSignerAddress() {
- setStatus(RequestStatus.LOADING)
-
- try {
- const address = await getSignerAddressFromPubkeyCoords(getJsonRpcProviderFromEip1193Provider(provider), x, y)
- if (!isMounted) return
- setSignerAddress(address)
- setStatus(RequestStatus.SUCCESS)
- } catch (error) {
- if (!isMounted) return
- console.error(error)
- setStatus(RequestStatus.ERROR)
- }
- }
-
- getSignerAddress()
-
- return () => {
- isMounted = false
- }
- }, [provider, x, y])
-
- return [signerAddress, status]
-}
-
-export { useSignerAddressFromPubkeyCoords }
diff --git a/examples/4337-passkeys/src/hooks/useUserOpGasEstimation.ts b/examples/4337-passkeys/src/hooks/useUserOpGasEstimation.ts
deleted file mode 100644
index 875ae5a9d..000000000
--- a/examples/4337-passkeys/src/hooks/useUserOpGasEstimation.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useState, useEffect } from 'react'
-import { UnsignedPackedUserOperation, UserOpGasLimitEstimation, estimateUserOpGasLimit } from '../logic/userOp'
-import { RequestStatus } from '../utils'
-
-/**
- * Custom hook for estimating the gas limit of a user operation.
- * @param userOp The unsigned user operation.
- * @returns An object containing the user operation gas limit estimation and the request status.
- */
-function useUserOpGasLimitEstimation(userOp: UnsignedPackedUserOperation): {
- userOpGasLimitEstimation: UserOpGasLimitEstimation | undefined
- status: RequestStatus
-}
-function useUserOpGasLimitEstimation(
- userOp: UnsignedPackedUserOperation,
- signerAddress: string,
-): {
- userOpGasLimitEstimation: UserOpGasLimitEstimation | undefined
- status: RequestStatus
-}
-function useUserOpGasLimitEstimation(userOp: UnsignedPackedUserOperation, signerAddress?: string) {
- const [userOpGasLimitEstimation, setUserOpGasLimitEstimation] = useState(undefined)
- const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED)
-
- useEffect(() => {
- let isMounted = true
- async function estimate() {
- setStatus(RequestStatus.LOADING)
-
- try {
- const estimation = await estimateUserOpGasLimit(userOp, signerAddress)
- if (!isMounted) return
- // Increase the gas limit by 50%, otherwise the user op will fail during simulation with "verification more than gas limit" error
- estimation.verificationGasLimit = '0x' + ((BigInt(estimation.verificationGasLimit) * 15n) / 10n).toString(16)
- setUserOpGasLimitEstimation(estimation)
- setStatus(RequestStatus.SUCCESS)
- } catch (error) {
- if (!isMounted) return
- console.error(error)
- setStatus(RequestStatus.ERROR)
- }
- }
-
- estimate()
-
- return () => {
- isMounted = false
- }
- }, [userOp, signerAddress])
-
- return { userOpGasLimitEstimation, status }
-}
-
-export { useUserOpGasLimitEstimation }
diff --git a/examples/4337-passkeys/src/index.css b/examples/4337-passkeys/src/index.css
deleted file mode 100644
index cb3fdcdb7..000000000
--- a/examples/4337-passkeys/src/index.css
+++ /dev/null
@@ -1,100 +0,0 @@
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- justify-content: flex-start;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-
- @media screen and (max-width: 768px) {
- font-size: 2.4em;
- }
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
-
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.logo {
- height: 6em;
- will-change: filter;
- transition: filter 300ms;
-
- @media screen and (max-width: 768px) {
- height: 3em;
- }
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
diff --git a/examples/4337-passkeys/src/logic/erc721.ts b/examples/4337-passkeys/src/logic/erc721.ts
deleted file mode 100644
index b06f450b2..000000000
--- a/examples/4337-passkeys/src/logic/erc721.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ethers } from 'ethers'
-import { getRandomUint256 } from '../utils'
-
-/**
- * Encodes the data for a safe mint operation.
- * @param to The address to mint the token to.
- * @param tokenId The ID of the token to mint.
- * @returns The encoded data for the safe mint operation.
- */
-function encodeSafeMintData(to: string, tokenId: ethers.BigNumberish = getRandomUint256()): string {
- const abi = ['function safeMint(address to, uint256 tokenId) external']
- return new ethers.Interface(abi).encodeFunctionData('safeMint', [to, tokenId])
-}
-
-export { encodeSafeMintData }
diff --git a/examples/4337-passkeys/src/logic/passkeys.ts b/examples/4337-passkeys/src/logic/passkeys.ts
deleted file mode 100644
index 028082931..000000000
--- a/examples/4337-passkeys/src/logic/passkeys.ts
+++ /dev/null
@@ -1,255 +0,0 @@
-import { ethers } from 'ethers'
-import { hexStringToUint8Array } from '../utils.ts'
-import { getItem, setItem } from './storage.ts'
-
-type PasskeyCredential = {
- id: 'string'
- rawId: ArrayBuffer
- response: {
- clientDataJSON: ArrayBuffer
- attestationObject: ArrayBuffer
- getPublicKey(): ArrayBuffer
- }
- type: 'public-key'
-}
-
-type PasskeyCredentialWithPubkeyCoordinates = PasskeyCredential & {
- pubkeyCoordinates: {
- x: string
- y: string
- }
-}
-
-const PASSKEY_LOCALSTORAGE_KEY = 'passkeyId'
-
-/**
- * Creates a passkey for signing.
- *
- * @returns A promise that resolves to a PasskeyCredentialWithPubkeyCoordinates object, which includes the passkey credential information and its public key coordinates.
- * @throws Throws an error if the passkey generation fails or if the credential received is null.
- */
-async function createPasskey(): Promise {
- // Generate a passkey credential using WebAuthn API
- const passkeyCredential = (await navigator.credentials.create({
- publicKey: {
- pubKeyCredParams: [
- {
- // ECDSA w/ SHA-256: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1
- alg: -7,
- type: 'public-key',
- },
- ],
- challenge: crypto.getRandomValues(new Uint8Array(32)),
- rp: {
- name: 'Safe Wallet',
- },
- user: {
- displayName: 'Safe Owner',
- id: crypto.getRandomValues(new Uint8Array(32)),
- name: 'safe-owner',
- },
- timeout: 60000,
- attestation: 'none',
- },
- })) as PasskeyCredential | null
-
- if (!passkeyCredential) {
- throw new Error('Failed to generate passkey. Received null as a credential')
- }
-
- // Import the public key to later export it to get the XY coordinates
- const key = await crypto.subtle.importKey(
- 'spki',
- passkeyCredential.response.getPublicKey(),
- {
- name: 'ECDSA',
- namedCurve: 'P-256',
- hash: { name: 'SHA-256' },
- },
- true, // boolean that marks the key as an exportable one
- ['verify'],
- )
-
- // Export the public key in JWK format and extract XY coordinates
- const exportedKeyWithXYCoordinates = await crypto.subtle.exportKey('jwk', key)
- if (!exportedKeyWithXYCoordinates.x || !exportedKeyWithXYCoordinates.y) {
- throw new Error('Failed to retrieve x and y coordinates')
- }
-
- // Create a PasskeyCredentialWithPubkeyCoordinates object
- const passkeyWithCoordinates: PasskeyCredentialWithPubkeyCoordinates = Object.assign(passkeyCredential, {
- pubkeyCoordinates: {
- x: '0x' + Buffer.from(exportedKeyWithXYCoordinates.x, 'base64').toString('hex'),
- y: '0x' + Buffer.from(exportedKeyWithXYCoordinates.y, 'base64').toString('hex'),
- },
- })
-
- return passkeyWithCoordinates
-}
-
-/**
- * Extracts the signature into R and S values from the authenticator response.
- *
- * See:
- * -
- * -
- */
-function extractSignature(response: AuthenticatorAssertionResponse): [bigint, bigint] {
- const check = (x: boolean) => {
- if (!x) {
- throw new Error('invalid signature encoding')
- }
- }
-
- // Decode the DER signature. Note that we assume that all lengths fit into 8-bit integers,
- // which is true for the kinds of signatures we are decoding but generally false. I.e. this
- // code should not be used in any serious application.
- const view = new DataView(response.signature)
-
- // check that the sequence header is valid
- check(view.getUint8(0) === 0x30)
- check(view.getUint8(1) === view.byteLength - 2)
-
- // read r and s
- const readInt = (offset: number) => {
- check(view.getUint8(offset) === 0x02)
- const len = view.getUint8(offset + 1)
- const start = offset + 2
- const end = start + len
- const n = BigInt(ethers.hexlify(new Uint8Array(view.buffer.slice(start, end))))
- check(n < ethers.MaxUint256)
- return [n, end] as const
- }
- const [r, sOffset] = readInt(2)
- const [s] = readInt(sOffset)
-
- return [r, s]
-}
-
-type Assertion = {
- response: AuthenticatorAssertionResponse
-}
-
-export type PasskeyLocalStorageFormat = {
- rawId: string
- pubkeyCoordinates: {
- x: string
- y: string
- }
-}
-
-/**
- * Compute the additional client data JSON fields. This is the fields other than `type` and
- * `challenge` (including `origin` and any other additional client data fields that may be
- * added by the authenticator).
- *
- * See
- */
-function extractClientDataFields(response: AuthenticatorAssertionResponse): string {
- const clientDataJSON = new TextDecoder('utf-8').decode(response.clientDataJSON)
- const match = clientDataJSON.match(/^\{"type":"webauthn.get","challenge":"[A-Za-z0-9\-_]{43}",(.*)\}$/)
-
- if (!match) {
- throw new Error('challenge not found in client data JSON')
- }
-
- const [, fields] = match
- return ethers.hexlify(ethers.toUtf8Bytes(fields))
-}
-
-/**
- * Converts a PasskeyCredentialWithPubkeyCoordinates object to a format that can be stored in the local storage.
- * The rawId is required for signing and pubkey coordinates are for our convenience.
- * @param passkey - The passkey to be converted.
- * @returns The passkey in a format that can be stored in the local storage.
- */
-function toLocalStorageFormat(passkey: PasskeyCredentialWithPubkeyCoordinates): PasskeyLocalStorageFormat {
- return {
- rawId: Buffer.from(passkey.rawId).toString('hex'),
- pubkeyCoordinates: passkey.pubkeyCoordinates,
- }
-}
-
-/**
- * Retrieves a passkey from local storage.
- *
- * @returns The retrieved passkey in the format of a {@link PasskeyLocalStorageFormat}, or null if no valid passkey is found.
- */
-function getPasskeyFromLocalStorage(): PasskeyLocalStorageFormat | null {
- const passkey = getItem(PASSKEY_LOCALSTORAGE_KEY)
- if (!passkey) return null
-
- try {
- const passkeyJson = JSON.parse(passkey)
- return isLocalStoragePasskey(passkeyJson) ? passkeyJson : null
- } catch {
- console.error('Failed to parse passkey from local storage')
- return null
- }
-}
-
-/**
- * Stores a passkey in local storage.
- *
- * This function takes a passkey of type PasskeyCredentialWithPubkeyCoordinates, converts it to a format that can be stored in local storage,
- * and then stores it in local storage using a predefined key.
- *
- * @param passkey - The passkey to be stored in local storage. It must be of type PasskeyCredentialWithPubkeyCoordinates.
- */
-function storePasskeyInLocalStorage(passkey: PasskeyCredentialWithPubkeyCoordinates): void {
- setItem(PASSKEY_LOCALSTORAGE_KEY, toLocalStorageFormat(passkey))
-}
-/**
- * Signs data with a passkey.
- *
- * This function uses the WebAuthn API to sign data with a passkey. The passkey is identified by its ID.
- * The function throws an error if the signing operation fails.
- *
- * @param passkeyId - The ID of the passkey to use for signing.
- * @param data - The data to sign.
- * @returns A promise that resolves to the signed data. The signed data is encoded using ethers.js's default ABI coder.
- * @throws Throws an error if the signing operation fails.
- */
-async function signWithPasskey(passkeyId: string, data: string): Promise {
- const assertion = (await navigator.credentials.get({
- publicKey: {
- challenge: ethers.getBytes(data),
- allowCredentials: [{ type: 'public-key', id: hexStringToUint8Array(passkeyId) }],
- userVerification: 'required',
- },
- })) as Assertion | null
-
- if (!assertion) {
- throw new Error('Failed to sign user operation')
- }
-
- return ethers.AbiCoder.defaultAbiCoder().encode(
- ['bytes', 'bytes', 'uint256[2]'],
- [
- new Uint8Array(assertion.response.authenticatorData),
- extractClientDataFields(assertion.response),
- extractSignature(assertion.response),
- ],
- )
-}
-
-/**
- * Checks if the provided value is in the format of a Local Storage Passkey.
- * @param x The value to check.
- * @returns A boolean indicating whether the value is in the format of a Local Storage Passkey.
- */
-function isLocalStoragePasskey(x: unknown): x is PasskeyLocalStorageFormat {
- return typeof x === 'object' && x !== null && 'rawId' in x && 'pubkeyCoordinates' in x
-}
-
-export type { Assertion }
-export {
- createPasskey,
- toLocalStorageFormat,
- isLocalStoragePasskey,
- extractSignature,
- extractClientDataFields,
- signWithPasskey,
- getPasskeyFromLocalStorage,
- storePasskeyInLocalStorage,
-}
diff --git a/examples/4337-passkeys/src/logic/safe.ts b/examples/4337-passkeys/src/logic/safe.ts
deleted file mode 100644
index 559104e7b..000000000
--- a/examples/4337-passkeys/src/logic/safe.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import { ethers } from 'ethers'
-import { abi as SafeSignerLaunchpadAbi } from '@safe-global/safe-passkey/build/artifacts/contracts/4337/experimental/SafeSignerLaunchpad.sol/SafeSignerLaunchpad.json'
-import { abi as SafeWebAuthnSignerFactoryAbi } from '@safe-global/safe-passkey/build/artifacts/contracts/SafeWebAuthnSignerFactory.sol/SafeWebAuthnSignerFactory.json'
-import { abi as SetupModuleSetupAbi } from '@safe-global/safe-4337/build/artifacts/contracts/SafeModuleSetup.sol/SafeModuleSetup.json'
-import { abi as Safe4337ModuleAbi } from '@safe-global/safe-4337/build/artifacts/contracts/Safe4337Module.sol/Safe4337Module.json'
-import { abi as SafeProxyFactoryAbi } from '@safe-global/safe-4337/build/artifacts/@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol/SafeProxyFactory.json'
-import type { Safe4337Module, SafeModuleSetup, SafeProxyFactory } from '@safe-global/safe-4337/dist/typechain-types/'
-import type { SafeSignerLaunchpad, SafeWebAuthnSignerFactory } from '@safe-global/safe-passkey/dist/typechain-types/'
-
-import {
- P256_VERIFIER_ADDRESS,
- SAFE_PROXY_FACTORY_ADDRESS,
- SAFE_SIGNER_LAUNCHPAD_ADDRESS,
- WEBAUTHN_SIGNER_FACTORY_ADDRESS,
-} from '../config'
-import { PackedUserOperation } from './userOp'
-
-// Hardcoded because we cannot easily install @safe-global/safe-contracts because of conflicting ethers.js versions
-const SafeProxyBytecode =
- '0x608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f7669646564'
-
-function getWebAuthnSignerFactoryContract(provider: ethers.JsonRpcApiProvider): SafeWebAuthnSignerFactory {
- return new ethers.Contract(
- WEBAUTHN_SIGNER_FACTORY_ADDRESS,
- SafeWebAuthnSignerFactoryAbi,
- provider,
- ) as unknown as SafeWebAuthnSignerFactory
-}
-
-/**
- * Calculates the signer address from the given public key coordinates.
- * @param provider The provider to use for the contract call.
- * @param x The x-coordinate of the public key.
- * @param y The y-coordinate of the public key.
- * @returns The signer address.
- */
-async function getSignerAddressFromPubkeyCoords(provider: ethers.JsonRpcApiProvider, x: string, y: string): Promise {
- const WebAuthSignerFactory = getWebAuthnSignerFactoryContract(provider)
- const signerAddress = await WebAuthSignerFactory.getSigner(x, y, P256_VERIFIER_ADDRESS)
-
- return signerAddress
-}
-
-type SafeInitializer = {
- singleton: string
- signerFactory: string
- signerX: string
- signerY: string
- signerVerifiers: string
- setupTo: string
- setupData: string
- fallbackHandler: string
-}
-
-function getLaunchpadInitializer(initializer: SafeInitializer): string {
- const safeSignerLaunchpadInterface = new ethers.Interface(SafeSignerLaunchpadAbi) as unknown as SafeSignerLaunchpad['interface']
-
- const launchpadInitializer = safeSignerLaunchpadInterface.encodeFunctionData('setup', [
- initializer.singleton,
- initializer.signerFactory,
- initializer.signerX,
- initializer.signerY,
- initializer.signerVerifiers,
- initializer.setupTo,
- initializer.setupData,
- initializer.fallbackHandler,
- ])
-
- return launchpadInitializer
-}
-
-/**
- * Generates the deployment data for creating a new Safe contract proxy with a specified singleton address, initializer, and salt nonce.
- * @param singleton The address of the singleton contract.
- * @param initializer The initialization data for the Safe contract.
- * @param saltNonce The salt nonce for the Safe contract.
- * @returns The deployment data for creating the Safe contract proxy.
- */
-function getSafeDeploymentData(singleton: string, initializer = '0x', saltNonce = ethers.ZeroHash): string {
- const safeProxyFactoryInterface = new ethers.Interface(SafeProxyFactoryAbi) as unknown as SafeProxyFactory['interface']
- const deploymentData = safeProxyFactoryInterface.encodeFunctionData('createProxyWithNonce', [singleton, initializer, saltNonce])
-
- return deploymentData
-}
-
-/**
- * Calculates the address of a safe contract based on the initializer, factory address, singleton address, and salt nonce.
- * @param initializer - The initializer bytes.
- * @param factoryAddress - The factory address used to create the safe contract. Defaults to SAFE_PROXY_FACTORY_ADDRESS.
- * @param singleton - The singleton address used for the safe contract. Defaults to SAFE_SIGNER_LAUNCHPAD_ADDRESS.
- * @param saltNonce - The salt nonce used for the safe contract. Defaults to ethers.ZeroHash.
- * @returns The address of the safe contract.
- */
-function getSafeAddress(
- initializer: string,
- factoryAddress = SAFE_PROXY_FACTORY_ADDRESS,
- singleton = SAFE_SIGNER_LAUNCHPAD_ADDRESS,
- saltNonce: ethers.BigNumberish = ethers.ZeroHash,
-): string {
- const deploymentCode = ethers.solidityPacked(['bytes', 'uint256'], [SafeProxyBytecode, singleton])
- const salt = ethers.solidityPackedKeccak256(['bytes32', 'uint256'], [ethers.solidityPackedKeccak256(['bytes'], [initializer]), saltNonce])
- return ethers.getCreate2Address(factoryAddress, salt, ethers.keccak256(deploymentCode))
-}
-
-/**
- * Encodes the function call to enable modules in the SafeModuleSetup contract.
- *
- * @param modules - An array of module addresses.
- * @returns The encoded function call data.
- */
-function encodeSafeModuleSetupCall(modules: string[]): string {
- const safeModuleSetupInterface = new ethers.Interface(SetupModuleSetupAbi) as unknown as SafeModuleSetup['interface']
- return safeModuleSetupInterface.encodeFunctionData('enableModules', [modules])
-}
-
-/**
- * Encodes the necessary data for initializing a Safe contract and performing a user operation.
- * @param setup - The SafeInitializer object containing the initialization parameters.
- * @param to The address of the recipient of the operation.
- * @param value The amount of value to be transferred in the operation.
- * @param data The data payload of the operation.
- * @param operation The type of operation (0 for CALL, 1 for DELEGATECALL).
- * @returns The encoded data for initializing the Safe contract and performing the user operation.
- */
-function getPromoteAccountAndExecuteUserOpData(
- initializer: SafeInitializer,
- to: string,
- value: ethers.BigNumberish,
- data: string,
- operation: 0 | 1,
-): string {
- const safeSignerLaunchpadInterface = new ethers.Interface(SafeSignerLaunchpadAbi) as unknown as SafeSignerLaunchpad['interface']
-
- const initializeThenUserOpData = safeSignerLaunchpadInterface.encodeFunctionData('promoteAccountAndExecuteUserOp', [
- initializer.signerFactory,
- initializer.signerX,
- initializer.signerY,
- initializer.signerVerifiers,
- to,
- value,
- data,
- operation,
- ])
-
- return initializeThenUserOpData
-}
-
-/**
- * Encodes the user operation data for validating a user operation.
- * @param userOp The packed user operation to be validated.
- * @param userOpHash The hash of the user operation.
- * @param missingAccountFunds The amount of missing account funds.
- * @returns The encoded data for validating the user operation.
- */
-function getValidateUserOpData(userOp: PackedUserOperation, userOpHash: string, missingAccountFunds: ethers.BigNumberish): string {
- const safe4337ModuleInterface = new ethers.Interface(Safe4337ModuleAbi) as unknown as Safe4337Module['interface']
-
- const validateUserOpData = safe4337ModuleInterface.encodeFunctionData('validateUserOp', [userOp, userOpHash, missingAccountFunds])
-
- return validateUserOpData
-}
-
-/**
- * Encodes the parameters of a user operation for execution on Safe4337Module.
- * @param to The address of the recipient of the operation.
- * @param value The amount of value to be transferred in the operation.
- * @param data The data payload of the operation.
- * @param operation The type of operation (0 for CALL, 1 for DELEGATECALL).
- * @returns The encoded data for the user operation.
- */
-function getExecuteUserOpData(to: string, value: ethers.BigNumberish, data: string, operation: 0 | 1): string {
- const safe4337ModuleInterface = new ethers.Interface(Safe4337ModuleAbi) as unknown as Safe4337Module['interface']
-
- const executeUserOpData = safe4337ModuleInterface.encodeFunctionData('executeUserOpWithErrorString', [to, value, data, operation])
-
- return executeUserOpData
-}
-
-export type { SafeInitializer }
-export {
- getLaunchpadInitializer,
- getPromoteAccountAndExecuteUserOpData,
- getSignerAddressFromPubkeyCoords,
- getSafeDeploymentData,
- getSafeAddress,
- getValidateUserOpData,
- getExecuteUserOpData,
- encodeSafeModuleSetupCall,
-}
diff --git a/examples/4337-passkeys/src/logic/safeWalletApp.ts b/examples/4337-passkeys/src/logic/safeWalletApp.ts
deleted file mode 100644
index a605991fe..000000000
--- a/examples/4337-passkeys/src/logic/safeWalletApp.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { APP_CHAIN_SHORTNAME } from '../config.ts'
-
-function getSafeWalletAppSafeDashboardLink(safeAddress: string, chainShortName = APP_CHAIN_SHORTNAME): string {
- return `https://app.safe.global/home?safe=${chainShortName}:${safeAddress}`
-}
-
-export { getSafeWalletAppSafeDashboardLink }
diff --git a/examples/4337-passkeys/src/logic/storage.ts b/examples/4337-passkeys/src/logic/storage.ts
deleted file mode 100644
index 8535db3b6..000000000
--- a/examples/4337-passkeys/src/logic/storage.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Sets an item in the local storage.
- * @param key - The key to set the item with.
- * @param value - The value to be stored. It will be converted to a string using JSON.stringify.
- * @template T - The type of the value being stored.
- */
-function setItem(key: string, value: T) {
- // to prevent silly mistakes with double stringifying
- if (typeof value === 'string') {
- localStorage.setItem(key, value)
- } else {
- localStorage.setItem(key, JSON.stringify(value))
- }
-}
-
-/**
- * Retrieves the value associated with the specified key from the local storage.
- *
- * @param key - The key of the item to retrieve.
- * @returns The value associated with the key, or null if the key does not exist.
- */
-function getItem(key: string): string | null {
- return localStorage.getItem(key)
-}
-
-export { setItem, getItem }
diff --git a/examples/4337-passkeys/src/logic/userOp.ts b/examples/4337-passkeys/src/logic/userOp.ts
deleted file mode 100644
index 198e70116..000000000
--- a/examples/4337-passkeys/src/logic/userOp.ts
+++ /dev/null
@@ -1,556 +0,0 @@
-import { ethers } from 'ethers'
-import { abi as EntryPointAbi } from '@account-abstraction/contracts/artifacts/EntryPoint.json'
-import { IEntryPoint } from '@safe-global/safe-4337/dist/typechain-types'
-import {
- SafeInitializer,
- getPromoteAccountAndExecuteUserOpData,
- getExecuteUserOpData,
- getLaunchpadInitializer,
- getSafeAddress,
- getSafeDeploymentData,
- getValidateUserOpData,
- getSignerAddressFromPubkeyCoords,
-} from './safe'
-import {
- APP_CHAIN_ID,
- ENTRYPOINT_ADDRESS,
- SAFE_4337_MODULE_ADDRESS,
- SAFE_PROXY_FACTORY_ADDRESS,
- SAFE_SIGNER_LAUNCHPAD_ADDRESS,
- XANDER_BLAZE_NFT_ADDRESS,
-} from '../config'
-import { encodeSafeMintData } from './erc721'
-import { PasskeyLocalStorageFormat, signWithPasskey } from './passkeys'
-import { calculateSafeOperationHash, unpackGasParameters, SafeUserOperation } from '@safe-global/safe-4337/dist/src/utils/userOp.js'
-import {
- PackedUserOperation as PackedUserOperationOgType,
- UserOperation as UserOperationOgType,
-} from '@safe-global/safe-4337/dist/src/utils/userOp'
-import { buildSignatureBytes } from '@safe-global/safe-4337/dist/src/utils/execution'
-
-type UserOperation = UserOperationOgType & { sender: string }
-type PackedUserOperation = PackedUserOperationOgType & { sender: string }
-type UnsignedPackedUserOperation = Omit
-
-/**
- * Dummy client data JSON fields. This can be used for gas estimations, as it pads the fields enough
- * to account for variations in WebAuthn implementations.
- */
-export const DUMMY_CLIENT_DATA_FIELDS = [
- `"origin":"http://safe.global"`,
- `"padding":"This pads the clientDataJSON so that we can leave room for additional implementation specific fields for a more accurate 'preVerificationGas' estimate."`,
-].join(',')
-
-/**
- * Dummy authenticator data. This can be used for gas estimations, as it ensures that the correct
- * authenticator flags are set.
- */
-export const DUMMY_AUTHENTICATOR_DATA = new Uint8Array(37)
-// Authenticator data is the concatenation of:
-// - 32 byte SHA-256 hash of the relying party ID
-// - 1 byte for the user verification flag
-// - 4 bytes for the signature count
-// We fill it all with `0xfe` and set the appropriate user verification flag.
-DUMMY_AUTHENTICATOR_DATA.fill(0xfe)
-DUMMY_AUTHENTICATOR_DATA[32] = 0x04
-
-/**
- * Encodes the given WebAuthn signature into a string. This computes the ABI-encoded signature parameters:
- * ```solidity
- * abi.encode(authenticatorData, clientDataFields, r, s);
- * ```
- *
- * @param authenticatorData - The authenticator data as a Uint8Array.
- * @param clientDataFields - The client data fields as a string.
- * @param r - The value of r as a bigint.
- * @param s - The value of s as a bigint.
- * @returns The encoded string.
- */
-export function getSignatureBytes({
- authenticatorData,
- clientDataFields,
- r,
- s,
-}: {
- authenticatorData: Uint8Array
- clientDataFields: string
- r: bigint
- s: bigint
-}): string {
- // Helper functions
- // Convert a number to a 64-byte hex string with padded upto Hex string with 32 bytes
- const encodeUint256 = (x: bigint | number) => x.toString(16).padStart(64, '0')
- // Calculate the byte size of the dynamic data along with the length parameter alligned to 32 bytes
- const byteSize = (data: Uint8Array) => 32 * (Math.ceil(data.length / 32) + 1) // +1 is for the length parameter
- // Encode dynamic data padded with zeros if necessary in 32 bytes chunks
- const encodeBytes = (data: Uint8Array) => `${encodeUint256(data.length)}${ethers.hexlify(data).slice(2)}`.padEnd(byteSize(data) * 2, '0')
-
- // authenticatorData starts after the first four words.
- const authenticatorDataOffset = 32 * 4
- // clientDataFields starts immediately after the authenticator data.
- const clientDataFieldsOffset = authenticatorDataOffset + byteSize(authenticatorData)
-
- return (
- '0x' +
- encodeUint256(authenticatorDataOffset) +
- encodeUint256(clientDataFieldsOffset) +
- encodeUint256(r) +
- encodeUint256(s) +
- encodeBytes(authenticatorData) +
- encodeBytes(new TextEncoder().encode(clientDataFields))
- )
-}
-
-// Dummy signature for gas estimation. We require the 12 bytes of validity timestamp data
-// so that the estimation doesn't revert. But we also want to use a dummy signature for
-// more accurate `verificationGasLimit` (We want to run the P256 signature verification
-// code) & `preVerificationGas` (The signature length in bytes should be accurate) estimate.
-// The challenge is neither P256 Verification Gas nor signature length are stable, so we make
-// a calculated guess.
-const DUMMY_SIGNATURE_LAUNCHPAD = ethers.solidityPacked(
- ['uint48', 'uint48', 'bytes'],
- [
- 0,
- 0,
- ethers.AbiCoder.defaultAbiCoder().encode(
- ['bytes', 'string', 'uint256', 'uint256'],
- [
- DUMMY_AUTHENTICATOR_DATA, // authenticatorData without any extensions/attestated credential data is always 37 bytes long.
- DUMMY_CLIENT_DATA_FIELDS,
- `0x${'ec'.repeat(32)}`,
- `0x${'d5a'.repeat(21)}f`,
- ],
- ),
- ],
-)
-
-/**
- * Generates a dummy signature for a user operation.
- *
- * @param signer - The Ethereum address of the signer.
- * @returns The dummy signature for a user operation.
- */
-function dummySignatureUserOp(signer: string) {
- return ethers.solidityPacked(
- ['uint48', 'uint48', 'bytes'],
- [
- 0,
- 0,
- buildSignatureBytes([
- {
- signer,
- data: getSignatureBytes({
- authenticatorData: DUMMY_AUTHENTICATOR_DATA,
- clientDataFields: DUMMY_CLIENT_DATA_FIELDS,
- r: BigInt(`0x${'ec'.repeat(32)}`),
- s: BigInt(`0x${'d5a'.repeat(21)}f`),
- }),
- dynamic: true,
- },
- ]),
- ],
- )
-}
-
-/**
- * Generates the user operation initialization code.
- * @param proxyFactory - The address of the proxy factory.
- * @param deploymentData - The deployment data.
- * @returns The user operation initialization code.
- */
-function getUserOpInitCode(proxyFactory: string, deploymentData: string): string {
- const userOpInitCode = ethers.solidityPacked(['address', 'bytes'], [proxyFactory, deploymentData])
- return userOpInitCode
-}
-
-type UserOpCall = {
- to: string
- data: string
- value: ethers.BigNumberish
- operation: 0 | 1
-}
-
-/**
- * Prepares a user operation with initialization.
- *
- * @param proxyFactoryAddress - The address of the proxy factory.
- * @param initializer - The safe initializer.
- * @param afterInitializationOpCall - Optional user operation call to be executed after initialization.
- * @param saltNonce - The salt nonce.
- * @returns The unsigned user operation.
- */
-function prepareUserOperationWithInitialisation(
- proxyFactoryAddress: string,
- initializer: SafeInitializer,
- afterInitializationOpCall?: UserOpCall,
- saltNonce = ethers.ZeroHash,
-): UnsignedPackedUserOperation {
- const launchpadInitializer = getLaunchpadInitializer(initializer)
- const predictedSafeAddress = getSafeAddress(launchpadInitializer, SAFE_PROXY_FACTORY_ADDRESS, SAFE_SIGNER_LAUNCHPAD_ADDRESS, saltNonce)
- const safeDeploymentData = getSafeDeploymentData(SAFE_SIGNER_LAUNCHPAD_ADDRESS, launchpadInitializer, saltNonce)
- const userOpCall = afterInitializationOpCall ?? {
- to: XANDER_BLAZE_NFT_ADDRESS,
- data: encodeSafeMintData(predictedSafeAddress),
- value: 0,
- operation: 0,
- }
-
- const userOp = {
- sender: predictedSafeAddress,
- nonce: ethers.toBeHex(0),
- initCode: getUserOpInitCode(proxyFactoryAddress, safeDeploymentData),
- callData: getPromoteAccountAndExecuteUserOpData(initializer, userOpCall.to, userOpCall.value, userOpCall.data, userOpCall.operation),
- ...packGasParameters({
- callGasLimit: ethers.toBeHex(2000000),
- verificationGasLimit: ethers.toBeHex(2000000),
- maxFeePerGas: ethers.toBeHex(10000000000),
- maxPriorityFeePerGas: ethers.toBeHex(10000000000),
- }),
- preVerificationGas: ethers.toBeHex(2000000),
- paymasterAndData: '0x',
- }
-
- if (import.meta.env.DEV) {
- console.log('Safe deployment data: ', safeDeploymentData)
- console.log(
- 'validateUserOp data for estimation: ',
- getValidateUserOpData({ ...userOp, signature: DUMMY_SIGNATURE_LAUNCHPAD }, ethers.ZeroHash, 10000000000),
- )
- }
-
- return userOp
-}
-
-function getUnsignedUserOperation(call: UserOpCall, safeAddress: string, nonce: ethers.BigNumberish): UnsignedPackedUserOperation {
- return {
- sender: safeAddress,
- nonce,
- initCode: '0x',
- callData: getExecuteUserOpData(call.to, call.value, call.data, call.operation),
- accountGasLimits: ethers.solidityPacked(['uint128', 'uint128'], [2000000, 2000000]),
- preVerificationGas: 2000000,
- gasFees: ethers.solidityPacked(['uint128', 'uint128'], [10000000000, 10000000000]),
- paymasterAndData: '0x',
- }
-}
-
-/**
- * Retrieves the EIP-4337 bundler provider.
- * @returns The EIP-4337 bundler provider.
- */
-function getEip4337BundlerProvider(): ethers.JsonRpcProvider {
- const provider = new ethers.JsonRpcProvider(import.meta.env.VITE_WC_4337_BUNDLER_URL, undefined, {
- batchMaxCount: 1,
- })
- return provider
-}
-
-/**
- * Returns an instance of the EntryPoint contract.
- * @param provider - The ethers.js JsonRpcProvider to use for interacting with the Ethereum network.
- * @param address - The Ethereum address of the deployed EntryPoint contract.
- * @returns An instance of the EntryPoint contract.
- */
-function getEntryPointContract(provider: ethers.JsonRpcApiProvider, address: string): IEntryPoint {
- return new ethers.Contract(address, EntryPointAbi, provider) as unknown as IEntryPoint
-}
-
-/**
- * Retrieves the nonce from the entry point.
- *
- * @param provider - The ethers.js JsonRpcProvider to use for interacting with the Ethereum network.
- * @param safeAddress - The Ethereum address of the safe for which to retrieve the nonce.
- * @param entryPoint - The Ethereum address of the entry point. Defaults to {@link ENTRYPOINT_ADDRESS} if not provided.
- * @returns A promise that resolves to the nonce for the provided safe address.
- */
-async function getNonceFromEntryPoint(
- provider: ethers.JsonRpcApiProvider,
- safeAddress: string,
- entryPoint = ENTRYPOINT_ADDRESS,
-): Promise {
- const entrypoint = getEntryPointContract(provider, entryPoint)
- const nonce = await entrypoint.getNonce(safeAddress, 0)
-
- return nonce
-}
-
-async function getAccountEntryPointBalance(
- provider: ethers.JsonRpcApiProvider,
- safeAddress: string,
- entryPoint = ENTRYPOINT_ADDRESS,
-): Promise {
- const entrypoint = getEntryPointContract(provider, entryPoint)
- const balance = await entrypoint.balanceOf(safeAddress)
-
- return balance
-}
-
-type UserOpGasLimitEstimation = {
- preVerificationGas: string
- callGasLimit: string
- verificationGasLimit: string
- paymasterVerificationGasLimit: string
- paymasterPostOpGasLimit: string
-}
-
-/**
- * Estimates the gas limit for a user operation. A dummy signature will be used.
- * @param userOp - The user operation to estimate gas limit for.
- * @param signerAddress - The signer address.
- * @param entryPointAddress - The entry point address. Default value is ENTRYPOINT_ADDRESS.
- * @returns A promise that resolves to the estimated gas limit for the user operation.
- */
-async function estimateUserOpGasLimit(
- userOp: UnsignedPackedUserOperation,
- signerAddress?: string,
- entryPointAddress = ENTRYPOINT_ADDRESS,
-): Promise {
- const provider = getEip4337BundlerProvider()
-
- const placeholderSignature =
- (userOp.initCode.length > 0 && userOp.initCode !== '0x') || !signerAddress
- ? DUMMY_SIGNATURE_LAUNCHPAD
- : dummySignatureUserOp(signerAddress)
-
- const rpcUserOp = unpackUserOperationForRpc(userOp, placeholderSignature)
- const estimation = await provider.send('eth_estimateUserOperationGas', [rpcUserOp, entryPointAddress])
-
- return estimation
-}
-
-/**
- * Unpacks a user operation for use over the bundler RPC.
- * @param userOp The user operation to unpack.
- * @param signature The signature bytes for the user operation.
- * @returns An unpacked `UserOperation` that can be used over bundler RPC.
- */
-function unpackUserOperationForRpc(userOp: UnsignedPackedUserOperation, signature: ethers.BytesLike): UserOperation {
- const initFields =
- ethers.dataLength(userOp.initCode) > 0
- ? {
- factory: ethers.getAddress(ethers.dataSlice(userOp.initCode, 0, 20)),
- factoryData: ethers.dataSlice(userOp.initCode, 20),
- }
- : {}
- const paymasterFields =
- ethers.dataLength(userOp.paymasterAndData) > 0
- ? {
- paymaster: ethers.getAddress(ethers.dataSlice(userOp.initCode, 0, 20)),
- paymasterVerificationGasLimit: ethers.toBeHex(ethers.dataSlice(userOp.paymasterAndData, 20, 36)),
- paymasterPostOpGasLimit: ethers.toBeHex(ethers.dataSlice(userOp.paymasterAndData, 36, 52)),
- paymasterData: ethers.dataSlice(userOp.paymasterAndData, 52),
- }
- : {}
- return {
- sender: ethers.getAddress(userOp.sender),
- nonce: ethers.toBeHex(userOp.nonce),
- ...initFields,
- callData: ethers.hexlify(userOp.callData),
- callGasLimit: ethers.toBeHex(ethers.dataSlice(userOp.accountGasLimits, 16, 32)),
- verificationGasLimit: ethers.toBeHex(ethers.dataSlice(userOp.accountGasLimits, 0, 16)),
- preVerificationGas: ethers.toBeHex(userOp.preVerificationGas),
- maxFeePerGas: ethers.toBeHex(ethers.dataSlice(userOp.gasFees, 16, 32)),
- maxPriorityFeePerGas: ethers.toBeHex(ethers.dataSlice(userOp.gasFees, 0, 16)),
- ...paymasterFields,
- signature: ethers.hexlify(signature),
- }
-}
-
-/**
- * Calculates the missing funds for an account.
- * Missing funds is the amount of funds required by the entry point to execute the user operation.
- *
- * @param maxFeePerGas - The maximum fee per gas for the user operation.
- * @param userOpGasLimitEstimation - The gas limit estimation for the user operation.
- * @param currentEntryPointDeposit - The current deposit at the entry point. Defaults to 0n if not provided.
- * @param multiplier - The multiplier used in the calculation. Defaults to 12n if not provided.
- * @returns The missing funds for the account.
- */
-function getMissingAccountFunds(
- maxFeePerGas: bigint,
- userOpGasLimitEstimation: UserOpGasLimitEstimation,
- currentEntryPointDeposit = 0n,
- multiplier = 12n,
-): bigint {
- return (
- (BigInt(maxFeePerGas) *
- (BigInt(userOpGasLimitEstimation.preVerificationGas) +
- BigInt(userOpGasLimitEstimation.callGasLimit) +
- BigInt(userOpGasLimitEstimation.verificationGasLimit) +
- BigInt(userOpGasLimitEstimation.paymasterVerificationGasLimit) +
- BigInt(userOpGasLimitEstimation.paymasterPostOpGasLimit)) *
- multiplier) /
- 10n -
- currentEntryPointDeposit
- )
-}
-
-/**
- * Packs a user operation gas parameters.
- * @param op The UserOperation gas parameters to pack.
- * @returns The packed UserOperation parameters.
- */
-function packGasParameters(
- op: Pick,
-): Pick {
- return {
- accountGasLimits: ethers.solidityPacked(['uint128', 'uint128'], [op.verificationGasLimit, op.callGasLimit]),
- gasFees: ethers.solidityPacked(['uint128', 'uint128'], [op.maxPriorityFeePerGas, op.maxFeePerGas]),
- }
-}
-
-/**
- * Packs a UserOperation object into a string using the defaultAbiCoder.
- * @param op The UserOperation object to pack.
- * @returns The packed UserOperation as a string.
- */
-function packUserOpData(op: UnsignedPackedUserOperation): string {
- return ethers.AbiCoder.defaultAbiCoder().encode(
- [
- 'address', // sender
- 'uint256', // nonce
- 'bytes32', // initCode
- 'bytes32', // callData
- 'bytes32', // accountGasLimits
- 'uint256', // preVerificationGas
- 'bytes32', // gasFees
- 'bytes32', // paymasterAndData
- ],
- [
- op.sender,
- op.nonce,
- ethers.keccak256(op.initCode),
- ethers.keccak256(op.callData),
- op.accountGasLimits,
- op.preVerificationGas,
- op.gasFees,
- ethers.keccak256(op.paymasterAndData),
- ],
- )
-}
-
-/**
- * Off-chain replication of the function to calculate user operation hash from the entrypoint.
- * @param op The user operation.
- * @param entryPoint The entry point.
- * @param chainId The chain ID.
- * @returns The hash of the user operation.
- */
-function getEntryPointUserOpHash(
- op: UnsignedPackedUserOperation,
- entryPoint: string = ENTRYPOINT_ADDRESS,
- chainId: ethers.BigNumberish = APP_CHAIN_ID,
-): string {
- const userOpHash = ethers.keccak256(packUserOpData(op))
- const enc = ethers.AbiCoder.defaultAbiCoder().encode(['bytes32', 'address', 'uint256'], [userOpHash, entryPoint, chainId])
- return ethers.keccak256(enc)
-}
-
-/**
- * Signs and sends a user operation to the specified entry point on the blockchain.
- * @param userOp The unsigned user operation to sign and send.
- * @param passkey The passkey used for signing the user operation.
- * @param entryPoint The entry point address on the blockchain. Defaults to ENTRYPOINT_ADDRESS if not provided.
- * @param chainId The chain ID of the blockchain. Defaults to APP_CHAIN_ID if not provided.
- * @returns User Operation hash promise.
- * @throws An error if signing the user operation fails.
- */
-async function signAndSendDeploymentUserOp(
- userOp: UnsignedPackedUserOperation,
- passkey: PasskeyLocalStorageFormat,
- entryPoint: string = ENTRYPOINT_ADDRESS,
- chainId: ethers.BigNumberish = APP_CHAIN_ID,
-): Promise {
- const userOpHash = getEntryPointUserOpHash(userOp, entryPoint, chainId)
-
- const safeInitOp = {
- userOpHash,
- validAfter: 0,
- validUntil: 0,
- entryPoint: ENTRYPOINT_ADDRESS,
- }
-
- const safeInitOpHash = ethers.TypedDataEncoder.hash(
- { verifyingContract: SAFE_SIGNER_LAUNCHPAD_ADDRESS, chainId },
- {
- SafeInitOp: [
- { type: 'bytes32', name: 'userOpHash' },
- { type: 'uint48', name: 'validAfter' },
- { type: 'uint48', name: 'validUntil' },
- { type: 'address', name: 'entryPoint' },
- ],
- },
- safeInitOp,
- )
-
- const passkeySignature = await signWithPasskey(passkey.rawId, safeInitOpHash)
-
- const signature = ethers.solidityPacked(['uint48', 'uint48', 'bytes'], [safeInitOp.validAfter, safeInitOp.validUntil, passkeySignature])
-
- const rpcUserOp = unpackUserOperationForRpc(userOp, signature)
- return await getEip4337BundlerProvider().send('eth_sendUserOperation', [rpcUserOp, entryPoint])
-}
-
-/**
- * Signs and sends a user operation to the specified entry point on the blockchain.
- * @param userOp The unsigned user operation to sign and send.
- * @param passkey The passkey used for signing the user operation.
- * @param provider The ethers.js JsonRpcProvider to use for interacting with the Ethereum network.
- * @param entryPoint The entry point address on the blockchain. Defaults to ENTRYPOINT_ADDRESS if not provided.
- * @param chainId The chain ID of the blockchain. Defaults to APP_CHAIN_ID if not provided.
- * @returns User Operation hash promise.
- * @throws An error if signing the user operation fails.
- */
-async function signAndSendUserOp(
- provider: ethers.JsonRpcApiProvider,
- userOp: UnsignedPackedUserOperation,
- passkey: PasskeyLocalStorageFormat,
- entryPoint: string = ENTRYPOINT_ADDRESS,
- chainId: ethers.BigNumberish = APP_CHAIN_ID,
-): Promise {
- const safeOp: SafeUserOperation = {
- callData: userOp.callData,
- nonce: userOp.nonce,
- initCode: userOp.initCode,
- paymasterAndData: userOp.paymasterAndData,
- preVerificationGas: userOp.preVerificationGas,
- entryPoint,
- validAfter: 0,
- validUntil: 0,
- safe: userOp.sender,
- ...unpackGasParameters(userOp),
- }
-
- const safeOpHash = calculateSafeOperationHash(SAFE_4337_MODULE_ADDRESS, safeOp, chainId)
- const passkeySignature = await signWithPasskey(passkey.rawId, safeOpHash)
- const signatureBytes = buildSignatureBytes([
- {
- signer: await getSignerAddressFromPubkeyCoords(provider, passkey.pubkeyCoordinates.x, passkey.pubkeyCoordinates.y),
- data: passkeySignature,
- dynamic: true,
- },
- ])
-
- const signature = ethers.solidityPacked(['uint48', 'uint48', 'bytes'], [0, 0, signatureBytes])
- if (import.meta.env.DEV) {
- console.log('validateUserOp data for estimation: ', getValidateUserOpData({ ...userOp, signature }, ethers.ZeroHash, 10000000000))
- }
-
- const rpcUserOp = unpackUserOperationForRpc(userOp, signature)
- return await getEip4337BundlerProvider().send('eth_sendUserOperation', [rpcUserOp, entryPoint])
-}
-
-export type { PackedUserOperation, UnsignedPackedUserOperation, UserOperation, UserOpGasLimitEstimation }
-
-export {
- prepareUserOperationWithInitialisation,
- packGasParameters,
- getEip4337BundlerProvider,
- getNonceFromEntryPoint,
- getUnsignedUserOperation,
- estimateUserOpGasLimit,
- getMissingAccountFunds,
- getAccountEntryPointBalance,
- signAndSendDeploymentUserOp,
- signAndSendUserOp,
-}
diff --git a/examples/4337-passkeys/src/logic/wallets.ts b/examples/4337-passkeys/src/logic/wallets.ts
deleted file mode 100644
index 81c9382db..000000000
--- a/examples/4337-passkeys/src/logic/wallets.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { createWeb3Modal, defaultConfig } from '@web3modal/ethers/react'
-import { ethers } from 'ethers'
-import { numberToUnpaddedHex } from '../utils'
-
-const projectId = import.meta.env.VITE_WC_CLOUD_PROJECT_ID
-
-const sepolia = {
- chainId: 11155111,
- name: 'Sepolia test network',
- currency: 'ETH',
- explorerUrl: 'https://sepolia.etherscan.io',
- rpcUrl: 'https://sepolia.gateway.tenderly.co',
-}
-
-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 }),
- chains: [sepolia],
- projectId,
-})
-
-/**
- * Switches the Ethereum provider to the Ethereum Sepolia test network.
- * @param provider The Ethereum provider.
- * @returns A promise that resolves to an unknown value.
- */
-async function switchToSepolia(provider: ethers.Eip1193Provider): Promise {
- return provider
- .request({
- method: 'wallet_addEthereumChain',
- params: [
- {
- chainId: numberToUnpaddedHex(sepolia.chainId),
- blockExplorerUrls: [sepolia.explorerUrl],
- chainName: sepolia.name,
- nativeCurrency: {
- name: sepolia.currency,
- symbol: sepolia.currency,
- decimals: 18,
- },
- rpcUrls: [sepolia.rpcUrl],
- },
- ],
- })
- .catch(() =>
- provider.request({
- method: 'wallet_switchEthereumChain',
- params: [{ chainId: numberToUnpaddedHex(sepolia.chainId) }],
- }),
- )
-}
-
-/**
- * Converts an Eip1193Provider to a JsonRpcApiProvider.
- * @param provider The Eip1193Provider to convert.
- * @returns The converted JsonRpcApiProvider.
- */
-function getJsonRpcProviderFromEip1193Provider(provider: ethers.Eip1193Provider): ethers.JsonRpcApiProvider {
- return new ethers.BrowserProvider(provider)
-}
-
-export { switchToSepolia, getJsonRpcProviderFromEip1193Provider }
diff --git a/examples/4337-passkeys/src/main.tsx b/examples/4337-passkeys/src/main.tsx
deleted file mode 100644
index 74c9da0db..000000000
--- a/examples/4337-passkeys/src/main.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import { createBrowserRouter, RouterProvider } from 'react-router-dom'
-
-// Keep the import of the wallets file to eval so w3m package can initialise
-// the required globals
-import './logic/wallets.ts'
-import './index.css'
-import { Root } from './routes/Root.tsx'
-import { Home, loader as homeLoader } from './routes/Home.tsx'
-import { DeploySafe, loader as deploySafeLoader } from './routes/DeploySafe.tsx'
-import { CreatePasskey } from './routes/CreatePasskey.tsx'
-import { CREATE_PASSKEY_ROUTE, DEPLOY_SAFE_ROUTE, HOME_ROUTE, SAFE_ROUTE } from './routes/constants.ts'
-import { Safe, loader as safeLoader } from './routes/Safe.tsx'
-
-const router = createBrowserRouter([
- {
- path: HOME_ROUTE,
- element: ,
- children: [
- { index: true, loader: homeLoader, element: },
- { path: CREATE_PASSKEY_ROUTE, element: },
- { path: DEPLOY_SAFE_ROUTE, loader: deploySafeLoader, element: },
- { path: SAFE_ROUTE, loader: safeLoader, element: },
- ],
- },
-])
-
-const rootElement = document.getElementById('root')
-
-if (!rootElement) {
- throw new Error('Root element not found')
-}
-
-ReactDOM.createRoot(rootElement).render(
-
-
- ,
-)
diff --git a/examples/4337-passkeys/src/routes/CreatePasskey.tsx b/examples/4337-passkeys/src/routes/CreatePasskey.tsx
deleted file mode 100644
index 5b4fe39aa..000000000
--- a/examples/4337-passkeys/src/routes/CreatePasskey.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useState } from 'react'
-import { redirect, useNavigate } from 'react-router-dom'
-import { DEPLOY_SAFE_ROUTE, HOME_ROUTE } from './constants.ts'
-import { createPasskey, getPasskeyFromLocalStorage, storePasskeyInLocalStorage } from '../logic/passkeys.ts'
-
-async function loader() {
- const passkey = getPasskeyFromLocalStorage()
- if (passkey) {
- return redirect(HOME_ROUTE)
- }
-
- return null
-}
-
-function CreatePasskey() {
- const [error, setError] = useState()
- const navigate = useNavigate()
-
- const handleCreatePasskeyClick = async () => {
- setError(undefined)
- try {
- const passkey = await createPasskey()
-
- storePasskeyInLocalStorage(passkey)
-
- navigate(DEPLOY_SAFE_ROUTE, { replace: true })
- } catch (error) {
- if (error instanceof Error) {
- setError(error.message)
- } else {
- setError('Unknown error')
- }
- }
- }
-
- return (
-
-
First, create a passkey to sign transactions
-
- Passkey is a secure authentication method that replaces traditional passwords. It uses public key cryptography and unique
- cryptographic keys to securely log users into websites and apps. To access a passkey, users must unlock their devices using
- biometrics like a fingerprint, Face ID or a device PIN. This makes passkeys much more secure than passwords and resistant to
- phishing attacks.
-
- For the Safe Account, passkeys serve as credentials for verifying user operations on-chain. This added layer of authentication
- ensures that only the passkey holder can access and perform actions with the account.
-
- Your Safe is being deployed. Track the user operation on{' '}
- jiffyscan. Once deployed, the page will
- automatically redirect to the Safe dashboard.⏳
-