Skip to content

Commit

Permalink
Readme improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
mmv08 committed Jan 22, 2024
1 parent e70a755 commit 4fdefa7
Show file tree
Hide file tree
Showing 12 changed files with 84 additions and 33 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion examples/safe-4337-passkeys/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Get projectId at https://cloud.walletconnect.com
VITE_WC_CLOUD_PROJECT_ID=
VITE_WC_4337_BUNDLER_URL
// 4337 Bundler URL. We recommend https://www.pimlico.io/
VITE_WC_4337_BUNDLER_URL=
53 changes: 32 additions & 21 deletions examples/safe-4337-passkeys/README.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions examples/safe-4337-passkeys/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 0 additions & 2 deletions examples/safe-4337-passkeys/src/components/PasskeyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
8 changes: 8 additions & 0 deletions examples/safe-4337-passkeys/src/hooks/useCodeAtAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>('')
const [status, setStatus] = useState<RequestStatus>(RequestStatus.NOT_REQUESTED)
Expand Down
5 changes: 5 additions & 0 deletions examples/safe-4337-passkeys/src/hooks/useFeeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ethers.FeeData>()
const [status, setStatus] = useState<RequestStatus>(RequestStatus.NOT_REQUESTED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<bigint>(0n)
const [status, setStatus] = useState<RequestStatus>(RequestStatus.NOT_REQUESTED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserOpGasLimitEstimation | undefined>(undefined)
const [status, setStatus] = useState<RequestStatus>(RequestStatus.NOT_REQUESTED)
Expand Down
4 changes: 4 additions & 0 deletions examples/safe-4337-passkeys/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 6 additions & 6 deletions examples/safe-4337-passkeys/src/logic/safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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])
}

Expand All @@ -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,
Expand All @@ -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])

Expand All @@ -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])

Expand Down
10 changes: 7 additions & 3 deletions examples/safe-4337-passkeys/src/logic/userOp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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')
Expand Down

0 comments on commit 4fdefa7

Please sign in to comment.