Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: Safe + 4337 + Passkeys example application #217

Merged
merged 15 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR, but I feel like the gas metering project should also be moved here. Maybe there is a more general directory name to “examples” that would fit both workspace packages...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


## Security and Liability

All contracts are WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Expand Down
5 changes: 5 additions & 0 deletions examples/safe-4337-passkeys/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Get projectId at https://cloud.walletconnect.com
VITE_WC_CLOUD_PROJECT_ID=
// 4337 Bundler URL. We recommend https://www.pimlico.io/
VITE_WC_4337_BUNDLER_URL=

8 changes: 8 additions & 0 deletions examples/safe-4337-passkeys/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
ignorePatterns: ['dist', '.eslintrc.cjs'],
extends: ['../../.eslintrc.js', 'plugin:react-hooks/recommended'],
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
}
24 changes: 24 additions & 0 deletions examples/safe-4337-passkeys/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
41 changes: 41 additions & 0 deletions examples/safe-4337-passkeys/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Safe + 4337 + Passkeys example application

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.

## Running the app

### Clone the repository

```bash
git clone https://github.com/safe-global/safe-modules.git
cd safe-modules
```

### Install dependencies

```bash
npm install
```

### Fill in the environment variables

```bash
cp .env.example .env
```

and fill in the variables in `.env` file.

Helpful links:

- 4337 Bundler: https://www.pimlico.io/
- WalletConnect: https://cloud.walletconnect.com/

### Run the app in development mode

```bash
npm run dev -w examples/safe-4337-passkeys
```

## 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.
13 changes: 13 additions & 0 deletions examples/safe-4337-passkeys/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Safe 4337 Passkeys Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions examples/safe-4337-passkeys/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "safe-4337-passkeys",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@web3modal/ethers": "^3.5.7",
"@safe-global/safe-eip4337": "^0.2.0",
"ethers": "^6.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react-swc": "^3.5.0",
"typescript": "^5.3.3",
"vite": "^5.0.12"
}
}
21 changes: 21 additions & 0 deletions examples/safe-4337-passkeys/public/safe-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions examples/safe-4337-passkeys/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#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);
}
99 changes: 99 additions & 0 deletions examples/safe-4337-passkeys/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import safeLogo from '/safe-logo.svg'
import { PasskeyLocalStorageFormat, createPasskey, toLocalStorageFormat } from './logic/passkeys.ts'
import './App.css'
import { useLocalStorageState } from './hooks/useLocalStorageState.ts'
import ConnectButton from './components/ConnectButton.tsx'
import { useState } from 'react'
import { useWeb3ModalProvider, useWeb3ModalAccount } from '@web3modal/ethers/react'
import { APP_CHAIN_ID } from './config.ts'
import { switchToMumbai } from './logic/wallets.ts'
import { PasskeyCard } from './components/PasskeyCard.tsx'
import { SafeCard } from './components/SafeCard.tsx'

const PASSKEY_LOCALSTORAGE_KEY = 'passkeyId'

function App() {
const [passkey, setPasskey] = useLocalStorageState<PasskeyLocalStorageFormat | undefined>(PASSKEY_LOCALSTORAGE_KEY, undefined)
const [error, setError] = useState<string>()
const { chainId } = useWeb3ModalAccount()
const { walletProvider } = useWeb3ModalProvider()
const connectedToWrongChain = Boolean(walletProvider) && chainId !== APP_CHAIN_ID

const handleCreatePasskeyClick = async () => {
setError(undefined)
try {
const passkey = await createPasskey()

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

const handleSwitchToMumbaiClick = () => {
if (!walletProvider) return

setError(undefined)
try {
switchToMumbai(walletProvider)
} catch (error) {
if (error instanceof Error) {
setError(error.message)
} else {
setError('Unknown error when switching to Mumbai network')
}
}
}

let content = (
<>
<PasskeyCard passkey={passkey} handleCreatePasskeyClick={handleCreatePasskeyClick} />

{passkey && walletProvider && <SafeCard passkey={passkey} provider={walletProvider} />}

{error && (
<div className="card">
<p>Error: {error}</p>
</div>
)}
</>
)
if (!walletProvider) {
content = (
<div className="card">
<p>Please connect wallet to continue</p>
</div>
)
}
if (connectedToWrongChain) {
content = (
<div className="card">
<p>Please switch to Mumbai network to continue</p>
<button onClick={handleSwitchToMumbaiClick}>Switch to Mumbai</button>
</div>
)
}

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

<div className="card">
<ConnectButton />
</div>
</header>
<h1>Safe + 4337 + Passkeys demo</h1>

{content}
</>
)
}

export default App
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ConnectButton() {
return <w3m-button />
}
47 changes: 47 additions & 0 deletions examples/safe-4337-passkeys/src/components/OpPrefundCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ethers } from 'ethers'
import { getJsonRpcProviderFromEip1193Provider } from '../logic/wallets'
import { useState } from 'react'

function PrefundCard({
provider,
requiredPrefund,
safeAddress,
}: {
provider: ethers.Eip1193Provider
requiredPrefund: bigint
safeAddress: string
}) {
const [loading, setLoading] = useState(false)

const handlePrefundClick = async () => {
setLoading(true)
const jsonRpcProvider = getJsonRpcProviderFromEip1193Provider(provider)

const signer = await jsonRpcProvider.getSigner()

try {
await signer
.sendTransaction({
to: safeAddress,
value: requiredPrefund,
})
.then((tx) => tx.wait(1))
} catch (error) {
console.error(error)
} finally {
setLoading(false)
}
}

return (
<div className="card">
<p>You need to prefund your safe with {requiredPrefund.toString()} wei. Click the button below to prefund your safe.</p>

<button onClick={handlePrefundClick} disabled={loading}>
{loading ? 'Confirming tx' : 'Prefund'}
</button>
</div>
)
}

export { PrefundCard }
33 changes: 33 additions & 0 deletions examples/safe-4337-passkeys/src/components/PasskeyCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useMemo } from 'react'

import { PasskeyLocalStorageFormat } from '../logic/passkeys'
import { getSignerAddressFromPubkeyCoords } from '../logic/safe'

function PasskeyCard({ passkey, handleCreatePasskeyClick }: { passkey?: PasskeyLocalStorageFormat; handleCreatePasskeyClick: () => void }) {
const predictedSignerAddress = useMemo(() => {
if (!passkey) return undefined

return getSignerAddressFromPubkeyCoords(passkey.pubkeyCoordinates.x, passkey.pubkeyCoordinates.y)
}, [passkey])

return passkey ? (
<div className="card">
<p>
Passkey ID: {passkey.rawId}
<br />
Passkey X: {passkey.pubkeyCoordinates.x}
<br />
Passkey Y: {passkey.pubkeyCoordinates.y}
<br />
Predicted Signer Address: {predictedSignerAddress}
</p>
</div>
) : (
<div className="card">
<p>First, you need to create a passkey which will be used to sign transactions</p>
<button onClick={handleCreatePasskeyClick}>Create Passkey</button>
</div>
)
}

export { PasskeyCard }
Loading
Loading