diff --git a/README.md b/README.md index e4120d23..d1553406 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ This repository contains a collection of modules for the [Safe Smart Account](ht - [4337 Module](./modules/4337) - [Allowance Module](./modules/allowances) +## Examples + +- [Safe + 4337 + Passkeys](./examples/safe-4337-passkeys) + ## Security and Liability All contracts are WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. diff --git a/examples/safe-4337-passkeys/.env.example b/examples/safe-4337-passkeys/.env.example index a2b36b7a..60814f88 100644 --- a/examples/safe-4337-passkeys/.env.example +++ b/examples/safe-4337-passkeys/.env.example @@ -1,3 +1,4 @@ // Get projectId at https://cloud.walletconnect.com VITE_WC_CLOUD_PROJECT_ID= -VITE_WC_4337_BUNDLER_URL \ No newline at end of file +// 4337 Bundler URL. We recommend https://www.pimlico.io/ +VITE_WC_4337_BUNDLER_URL= \ No newline at end of file diff --git a/examples/safe-4337-passkeys/README.md b/examples/safe-4337-passkeys/README.md index 0d6babed..3145e63b 100644 --- a/examples/safe-4337-passkeys/README.md +++ b/examples/safe-4337-passkeys/README.md @@ -1,30 +1,41 @@ -# React + TypeScript + Vite +# Safe + 4337 + Passkeys example application -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +This minimalistic example application demonstrates a Safe{Core} Smart Account deployment leveraging 4337 and Passkeys. It uses experimental and unaudited (at the moment of writing) contracts: [SafeSignerLaunchpad](https://github.com/safe-global/safe-modules/blob/959b0d3b420ce6d7d15811363e4b8fcc4640ae32/modules/4337/contracts/experimental/SafeSignerLaunchpad.sol) and [WebAuthnSigner](https://github.com/safe-global/safe-modules/blob/main/modules/4337/contracts/experimental/WebAuthnSigner.sol), which uses [FreshCryptoLib](https://github.com/rdubois-crypto/FreshCryptoLib/) under the hood. -Currently, two official plugins are available: +## Running the app -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +### Clone the repository -## Expanding the ESLint configuration +```bash +git clone https://github.com/safe-global/safe-modules.git +cd safe-modules +``` + +### Install dependencies + +```bash +npm install +``` + +### Fill in the environment variables -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: +```bash +cp .env.example .env +``` + +and fill in the variables in `.env` file. + +Helpful links: -- Configure the top-level `parserOptions` property like this: +- 4337 Bundler: https://www.pimlico.io/ +- WalletConnect: https://cloud.walletconnect.com/ -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} +### Run the app in development mode + +```bash +npm run dev -w examples/safe-4337-passkeys ``` -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list +## Config adjustments + +The application depends on a specific set of contracts deployed on a specific network. If you want to use your own contracts, you need to adjust the configuration in `src/config.ts` file. diff --git a/examples/safe-4337-passkeys/src/App.css b/examples/safe-4337-passkeys/src/App.css index 9d4305f0..6b381520 100644 --- a/examples/safe-4337-passkeys/src/App.css +++ b/examples/safe-4337-passkeys/src/App.css @@ -15,6 +15,10 @@ 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); diff --git a/examples/safe-4337-passkeys/src/components/PasskeyCard.tsx b/examples/safe-4337-passkeys/src/components/PasskeyCard.tsx index 0e7b4fe1..4e124f1b 100644 --- a/examples/safe-4337-passkeys/src/components/PasskeyCard.tsx +++ b/examples/safe-4337-passkeys/src/components/PasskeyCard.tsx @@ -7,8 +7,6 @@ function PasskeyCard({ passkey, handleCreatePasskeyClick }: { passkey?: PasskeyL const predictedSignerAddress = useMemo(() => { if (!passkey) return undefined - console.log({ passkey }) - return getSignerAddressFromPubkeyCoords(passkey.pubkeyCoordinates.x, passkey.pubkeyCoordinates.y) }, [passkey]) diff --git a/examples/safe-4337-passkeys/src/hooks/useCodeAtAddress.ts b/examples/safe-4337-passkeys/src/hooks/useCodeAtAddress.ts index e382f467..53c230e7 100644 --- a/examples/safe-4337-passkeys/src/hooks/useCodeAtAddress.ts +++ b/examples/safe-4337-passkeys/src/hooks/useCodeAtAddress.ts @@ -7,6 +7,14 @@ 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) diff --git a/examples/safe-4337-passkeys/src/hooks/useFeeData.ts b/examples/safe-4337-passkeys/src/hooks/useFeeData.ts index a3ff2691..973e28cc 100644 --- a/examples/safe-4337-passkeys/src/hooks/useFeeData.ts +++ b/examples/safe-4337-passkeys/src/hooks/useFeeData.ts @@ -3,6 +3,11 @@ import { useEffect, useState } from 'react' import { getJsonRpcProviderFromEip1193Provider } from '../logic/wallets' import { RequestStatus } from '../utils' +/** + * 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): [ethers.FeeData | undefined, RequestStatus] { const [feeData, setFeeData] = useState() const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED) diff --git a/examples/safe-4337-passkeys/src/hooks/useNativeTokenBalance.ts b/examples/safe-4337-passkeys/src/hooks/useNativeTokenBalance.ts index a06a0d23..188a02a9 100644 --- a/examples/safe-4337-passkeys/src/hooks/useNativeTokenBalance.ts +++ b/examples/safe-4337-passkeys/src/hooks/useNativeTokenBalance.ts @@ -7,6 +7,13 @@ 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) diff --git a/examples/safe-4337-passkeys/src/hooks/useUserOpGasEstimation.ts b/examples/safe-4337-passkeys/src/hooks/useUserOpGasEstimation.ts index f66bcdc0..d3c660e6 100644 --- a/examples/safe-4337-passkeys/src/hooks/useUserOpGasEstimation.ts +++ b/examples/safe-4337-passkeys/src/hooks/useUserOpGasEstimation.ts @@ -2,6 +2,11 @@ import { useState, useEffect } from 'react' import { UnsignedUserOperation, 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: UnsignedUserOperation) { const [userOpGasLimitEstimation, setUserOpGasLimitEstimation] = useState(undefined) const [status, setStatus] = useState(RequestStatus.NOT_REQUESTED) diff --git a/examples/safe-4337-passkeys/src/index.css b/examples/safe-4337-passkeys/src/index.css index 69919eb7..dd0c354f 100644 --- a/examples/safe-4337-passkeys/src/index.css +++ b/examples/safe-4337-passkeys/src/index.css @@ -33,6 +33,10 @@ body { h1 { font-size: 3.2em; line-height: 1.1; + + @media screen and (max-width: 768px) { + font-size: 2.4em; + } } button { diff --git a/examples/safe-4337-passkeys/src/logic/safe.ts b/examples/safe-4337-passkeys/src/logic/safe.ts index ffbeb8a1..b7427648 100644 --- a/examples/safe-4337-passkeys/src/logic/safe.ts +++ b/examples/safe-4337-passkeys/src/logic/safe.ts @@ -111,7 +111,7 @@ function getInitHash(safeInitializer: SafeInitializer, chainId: ethers.BigNumber } function getLaunchpadInitializer(safeInitHash: string, optionalCallAddress = ethers.ZeroAddress, optionalCalldata = '0x'): string { - const safeSignerLaunchpadInterface = new ethers.Interface(SafeSignerLaunchpadAbi) as SafeSignerLaunchpad['interface'] + const safeSignerLaunchpadInterface = new ethers.Interface(SafeSignerLaunchpadAbi) as unknown as SafeSignerLaunchpad['interface'] const launchpadInitializer = safeSignerLaunchpadInterface.encodeFunctionData('preValidationSetup', [ safeInitHash, @@ -130,7 +130,7 @@ function getLaunchpadInitializer(safeInitHash: string, optionalCallAddress = eth * @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 SafeProxyFactory['interface'] + const safeProxyFactoryInterface = new ethers.Interface(SafeProxyFactoryAbi) as unknown as SafeProxyFactory['interface'] const deploymentData = safeProxyFactoryInterface.encodeFunctionData('createProxyWithNonce', [singleton, initializer, saltNonce]) return deploymentData @@ -162,7 +162,7 @@ function getSafeAddress( * @returns The encoded function call data. */ function encodeAddModuleLibCall(modules: string[]): string { - const addModulesLibInterface = new ethers.Interface(AddModulesLibAbi) as AddModulesLib['interface'] + const addModulesLibInterface = new ethers.Interface(AddModulesLibAbi) as unknown as AddModulesLib['interface'] return addModulesLibInterface.encodeFunctionData('enableModules', [modules]) } @@ -173,7 +173,7 @@ function encodeAddModuleLibCall(modules: string[]): string { * @returns The encoded data for initializing the Safe contract and performing the user operation. */ function getLaunchpadInitializeThenUserOpData(initializer: SafeInitializer, encodedUserOp: string): string { - const safeSignerLaunchpadInterface = new ethers.Interface(SafeSignerLaunchpadAbi) as SafeSignerLaunchpad['interface'] + const safeSignerLaunchpadInterface = new ethers.Interface(SafeSignerLaunchpadAbi) as unknown as SafeSignerLaunchpad['interface'] const initializeThenUserOpData = safeSignerLaunchpadInterface.encodeFunctionData('initializeThenUserOp', [ initializer.singleton, @@ -197,7 +197,7 @@ function getLaunchpadInitializeThenUserOpData(initializer: SafeInitializer, enco * @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 Safe4337Module['interface'] + const safe4337ModuleInterface = new ethers.Interface(Safe4337ModuleAbi) as unknown as Safe4337Module['interface'] const executeUserOpData = safe4337ModuleInterface.encodeFunctionData('executeUserOp', [to, value, data, operation]) @@ -212,7 +212,7 @@ function getExecuteUserOpData(to: string, value: ethers.BigNumberish, data: stri * @returns The encoded data for validating the user operation. */ function getValidateUserOpData(userOp: UserOperation, userOpHash: string, missingAccountFunds: ethers.BigNumberish): string { - const safe4337ModuleInterface = new ethers.Interface(Safe4337ModuleAbi) as Safe4337Module['interface'] + const safe4337ModuleInterface = new ethers.Interface(Safe4337ModuleAbi) as unknown as Safe4337Module['interface'] const validateUserOpData = safe4337ModuleInterface.encodeFunctionData('validateUserOp', [userOp, userOpHash, missingAccountFunds]) diff --git a/examples/safe-4337-passkeys/src/logic/userOp.ts b/examples/safe-4337-passkeys/src/logic/userOp.ts index fc8a4d51..47a31318 100644 --- a/examples/safe-4337-passkeys/src/logic/userOp.ts +++ b/examples/safe-4337-passkeys/src/logic/userOp.ts @@ -169,7 +169,7 @@ function getRequiredPrefund(maxFeePerGas: bigint, userOpGasLimitEstimation: User * @param op The UserOperation object to pack. * @returns The packed UserOperation as a string. */ -function packUserOp(op: UserOperation): string { +function packUserOp(op: UnsignedUserOperation): string { return ethers.AbiCoder.defaultAbiCoder().encode( [ 'address', // sender @@ -286,6 +286,10 @@ function extractSignature(response: AuthenticatorAssertionResponse): [bigint, bi return [r, s] } +type Assertion = { + response: AuthenticatorAssertionResponse +} + /** * Signs and sends a user operation to the specified entry point on the blockchain. * @param userOp The unsigned user operation to sign and send. @@ -323,12 +327,12 @@ async function signAndSendUserOp( safeInitOp, ) - const assertion = await navigator.credentials.get({ + const assertion = (await navigator.credentials.get({ publicKey: { challenge: ethers.getBytes(safeInitOpHash), allowCredentials: [{ type: 'public-key', id: hexStringToUint8Array(passkey.rawId) }], }, - }) + })) as Assertion | null if (!assertion) { throw new Error('Failed to sign user operation')