Skip to content

Commit

Permalink
Passkeys (#503)
Browse files Browse the repository at this point in the history
* docs: Passkeys in the SDK references (#495)

* add passkeys in the doc references

* Update pages/sdk/protocol-kit/reference/safe-factory.md

Co-authored-by: Germán Martínez <[email protected]>

* Update pages/sdk/protocol-kit/reference/safe.md

Co-authored-by: Germán Martínez <[email protected]>

* Update pages/sdk/protocol-kit/reference/safe.md

Co-authored-by: Germán Martínez <[email protected]>

* Update pages/sdk/protocol-kit/reference/safe.md

Co-authored-by: Germán Martínez <[email protected]>

* Update pages/sdk/relay-kit/reference/safe-4337-pack.mdx

Co-authored-by: Germán Martínez <[email protected]>

---------

Co-authored-by: Germán Martínez <[email protected]>

* docs: Add Passkeys section, overview and guide (#498)

* Add Passkeys section

* Add Passkeys overview

* Add passkeys guide

* Create missing pages

* Update steps style in Passkeys guide

* fix: copy edits

* feat: add safe and passkeys section

* fix: vale errors

* fix: formatting error

* fix: updates based on feedback

* feat: add flow diagram

* fix: minor update

* Add passkeys tutorial

* Minor fixes to passkeys tutorial

* Fix vale errors

* Fix screenshots

* Fix screenshots

* fix: minor edits

* fix: minor edits

* Minor fixes

* fix: correct term

* Add passkeys FAQs

* Minor fixes

* feat: add new chains

* Fix typo

* Fix styling

* fix: correct based on feedback

* feat: add api reference for passkeys remove and swap owners (#505)

* Fix typo in tutorial layout

* Get tutorial code examples from github

* Update 7579 code examples

* Move passkeys code examples outside of the /pages folder

* Add developer preview notice in 7579 and passkeys tutorials

---------

Co-authored-by: Dani Somoza <[email protected]>
Co-authored-by: Germán Martínez <[email protected]>
Co-authored-by: Germán Martínez <[email protected]>
Co-authored-by: Tanay Pant <tanaypantprotonmail.com>
Co-authored-by: leonardotc <[email protected]>
  • Loading branch information
5 people authored Jul 5, 2024
1 parent 510c32b commit 2dd756a
Show file tree
Hide file tree
Showing 31 changed files with 1,387 additions and 32 deletions.
62 changes: 62 additions & 0 deletions .github/scripts/generateCodeExamples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require('fs')

const repos = [
{
organization: '5afe',
repo: 'safe-passkeys-tutorial',
destination: './examples/passkeys',
branch: 'main',
files: [
'/lib/constants.ts',
'/lib/utils.ts',
'/lib/passkeys.ts',
'/lib/usdc.ts',
'/components/PasskeyList.tsx',
'/app/page.tsx',
'/app/layout.tsx'
]
},
{
organization: '5afe',
repo: 'safe-7579-tutorial',
destination: './examples/erc-7579',
branch: 'main',
files: [
'/lib/permissionless.ts',
'/lib/scheduledTransfers.ts',
'/components/ScheduledTransferForm.tsx',
'/app/page.tsx',
'/app/layout.tsx'
]
}
]

const generateCodeExamples = async ({
organization,
repo,
branch,
destination,
files
}) => {
const fetch = await import('node-fetch')
files.forEach(async filePath => {
const url = `https://raw.githubusercontent.com/${organization}/${repo}/${branch}${filePath}?token=$(date +%s)`
await fetch
.default(url)
.then(async res => {
if (!res.ok) throw new Error(res.statusText)
const text = await res.text()
const destinationDirectory =
destination + filePath.substring(0, filePath.lastIndexOf('/'))
if (!fs.existsSync(destinationDirectory)) {
fs.mkdirSync(destinationDirectory, { recursive: true })
}
fs.writeFileSync(destination + filePath, text)
})
.catch((res) => {
console.error('Error fetching file for', filePath, ':', res.statusText)
})
})
}

repos.forEach(generateCodeExamples)
2 changes: 2 additions & 0 deletions .github/styles/config/vocabularies/default/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ EIP
EOA
EOAs
ERC
ESLint
EURe
EVM
EdgeEVM
Expand Down Expand Up @@ -243,6 +244,7 @@ npm
onboarding
onchain
pluggable
precompile(?s)
saltNonce
superset
textWrap
Expand Down
Binary file added assets/safe-passkeys-app-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/safe-passkeys-app-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion examples/erc-7579/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function RootLayout ({
<h1>Schedule Transfers</h1>

<div>
Create a new 7579 compatible Safe Account and use it to schedule
Create a new ERC-7579-compatible Safe Smart Account and use it to schedule
transactions.
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion examples/erc-7579/components/ScheduledTransferForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,13 @@ const ScheduledTransferForm: React.FC<{ safe: SafeSmartAccountClient }> = ({
/>
</div>
<div>
<label htmlFor='amount'>Amount:</label>
<label htmlFor='amount'>Amount (integer):</label>
<input
style={{ marginLeft: '20px' }}
id='amount'
type='number'
placeholder='1'
min='0'
onChange={e => setAmount(Number(e.target.value))}
value={amount}
/>
Expand Down
21 changes: 17 additions & 4 deletions examples/erc-7579/lib/scheduledTransfers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {
getScheduledTransactionData,
getInstallScheduledTransfersExecutor,
getCreateScheduledTransferAction
} from '@rhinestone/module-sdk'

import { SafeSmartAccountClient } from './permissionless'

export interface ScheduledTransferDataInput {
startDate: number
repeatEvery: number
Expand All @@ -16,7 +19,7 @@ export const scheduledTransfersModuleAddress =
const sepoliaUSDCTokenAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8'

export const install7579Module = async (
safe: any,
safe: SafeSmartAccountClient,
scheduledTransferInput: ScheduledTransferDataInput
) => {
const { startDate, repeatEvery, numberOfRepeats, amount, recipient } =
Expand All @@ -33,23 +36,33 @@ export const install7579Module = async (
recipient
}

const scheduledTransactionData = getScheduledTransactionData({
const executionData = getScheduledTransactionData({
scheduledTransaction
})

const scheduledTransfersModule = getInstallScheduledTransfersExecutor({
executeInterval: repeatEvery,
numberOfExecutions: numberOfRepeats,
startDate,
executionData
})

const txHash = await safe.installModule({
type: 'executor',
address: scheduledTransfersModuleAddress,
context: scheduledTransactionData
context: scheduledTransfersModule.data as `0x${string}`
})

console.log(
'Scheduled transfers module is being installed: https://sepolia.etherscan.io/tx/' +
txHash
)

return txHash
}

export const scheduleTransfer = async (
safe: any,
safe: SafeSmartAccountClient,
scheduledTransferInput: ScheduledTransferDataInput
) => {
const { startDate, repeatEvery, numberOfRepeats, amount, recipient } =
Expand Down
83 changes: 83 additions & 0 deletions examples/passkeys/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Img from 'next/image'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
title: 'Safe Tutorial: Passkeys',
description: 'Generated by create next app'
}

export default function RootLayout ({
children
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang='en'>
<body className={inter.className}>
<nav
style={{
display: 'flex',
justifyContent: 'space-between',
padding: '1rem'
}}
>
<a href='https://safe.global'>
<Img width={95} height={36} alt='safe-logo' src='/safe.svg' />
</a>
<div style={{ display: 'flex' }}>
<a
href='https://docs.safe.global/home/passkeys-tutorials/safe-passkeys-tutorial'
style={{
display: 'flex',
alignItems: 'center',
marginRight: '1rem'
}}
>
Read tutorial{' '}
<Img
width={20}
height={20}
alt='link-icon'
src='/external-link.svg'
style={{ marginLeft: '0.5rem' }}
/>
</a>
<a
href='https://github.com/5afe/safe-passkeys-tutorial'
style={{ display: 'flex', alignItems: 'center' }}
>
View on GitHub{' '}
<Img
width={24}
height={24}
alt='github-icon'
src='/github.svg'
style={{ marginLeft: '0.5rem' }}
/>
</a>
</div>
</nav>
<div style={{ width: '100%', textAlign: 'center' }}>
<h1>Passkeys tutorial</h1>

<div>Create a new 4337 compatible Safe Account using passkeys</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
marginLeft: '40px',
marginRight: '40px'
}}
>
{children}
</div>
</body>
</html>
)
}
149 changes: 149 additions & 0 deletions examples/passkeys/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use client'

import { useState } from 'react'
import { Safe4337Pack } from '@safe-global/relay-kit'
import Img from 'next/image'

import PasskeyList from '../components/PasskeyList'
import { executeUSDCTransfer } from '../lib/usdc'
import { getPasskeyFromRawId, type PasskeyArgType } from '../lib/passkeys'
import { BUNDLER_URL, CHAIN_NAME, RPC_URL } from '../lib/constants'
import { bufferToString } from '../lib/utils'

function Create4337SafeAccount () {
const [selectedPasskey, setSelectedPasskey] = useState<PasskeyArgType>()
const [safeAddress, setSafeAddress] = useState<string>()
const [isSafeDeployed, setIsSafeDeployed] = useState<boolean>()
const [userOp, setUserOp] = useState<string>()

const selectPasskeySigner = async (rawId: string) => {
console.log('selected passkey signer: ', rawId)

const passkey = await getPasskeyFromRawId(rawId)

const safe4337Pack = await Safe4337Pack.init({
provider: RPC_URL,
rpcUrl: RPC_URL,
signer: passkey,
bundlerUrl: BUNDLER_URL,
options: {
owners: [],
threshold: 1
}
})

const safeAddress = await safe4337Pack.protocolKit.getAddress()
const isSafeDeployed = await safe4337Pack.protocolKit.isSafeDeployed()

setSelectedPasskey(passkey)
setSafeAddress(safeAddress)
setIsSafeDeployed(isSafeDeployed)
}

return (
<>
<div
style={{
width: '50%'
}}
>
{selectedPasskey && (
<>
<h2>Passkey Selected</h2>

<div style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
{bufferToString(selectedPasskey.rawId)}
</div>
</>
)}
<PasskeyList selectPasskeySigner={selectPasskeySigner} />
</div>
{safeAddress && (
<div
style={{
width: '50%'
}}
>
<h2>Safe Account</h2>

<div style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
Address: {safeAddress}
</div>
<div>
Is deployed?:{' '}
{isSafeDeployed ? (
<a
href={`https://app.safe.global/transactions/history?safe=sep:${safeAddress}`}
target='_blank'
rel='noreferrer'
>
Yes{' '}
<Img
src='/external-link.svg'
alt='External link'
width={14}
height={14}
/>
</a>
) : (
'No'
)}
</div>
<div>
{' '}
<a
href='https://faucet.circle.com/'
target='_blank'
rel='noreferrer'
>
Get some test USDC for your Safe{' '}
<Img
src='/external-link.svg'
alt='External link'
width={14}
height={14}
/>
</a>
</div>
{selectedPasskey && (
<button
onClick={async () =>
await executeUSDCTransfer({
signer: selectedPasskey,
safeAddress
}).then(userOpHash => {
setUserOp(userOpHash)
setIsSafeDeployed(true)
})
}
>
Sign transaction with passkey
</button>
)}
{userOp && isSafeDeployed && (
<>
<div>
Done! Check the transaction status on{' '}
<a
href={`https://jiffyscan.xyz/userOpHash/${userOp}?network=${CHAIN_NAME}`}
target='_blank'
rel='noreferrer'
>
Jiffy Scan{' '}
<Img
src='/external-link.svg'
alt='External link'
width={14}
height={14}
/>
</a>
</div>
</>
)}
</div>
)}
</>
)
}

export default Create4337SafeAccount
Loading

0 comments on commit 2dd756a

Please sign in to comment.