diff --git a/.github/styles/config/vocabularies/default/accept.txt b/.github/styles/config/vocabularies/default/accept.txt index b3cb440c..88e295d0 100644 --- a/.github/styles/config/vocabularies/default/accept.txt +++ b/.github/styles/config/vocabularies/default/accept.txt @@ -102,6 +102,7 @@ EIP EOA EOAs ERC +ESLint EURe EVM EdgeEVM @@ -241,6 +242,7 @@ npm onboarding onchain pluggable +precompile(?s) saltNonce textWrap trace_block diff --git a/assets/next.png b/assets/next.png new file mode 100644 index 00000000..4b82dd7d Binary files /dev/null and b/assets/next.png differ diff --git a/assets/safe-passkeys-app-1.png b/assets/safe-passkeys-app-1.png new file mode 100644 index 00000000..c7475065 Binary files /dev/null and b/assets/safe-passkeys-app-1.png differ diff --git a/assets/safe-passkeys-app-2.png b/assets/safe-passkeys-app-2.png new file mode 100644 index 00000000..c5d581da Binary files /dev/null and b/assets/safe-passkeys-app-2.png differ diff --git a/pages/home/4337-guides/permissionless-detailed.mdx b/pages/home/4337-guides/permissionless-detailed.mdx index 9fdfb49e..e1b322a2 100644 --- a/pages/home/4337-guides/permissionless-detailed.mdx +++ b/pages/home/4337-guides/permissionless-detailed.mdx @@ -19,10 +19,14 @@ This guide focuses on how user operations are built and what happens under the h Install [viem](https://npmjs.com/viem) and [permissionless](https://npmjs.com/permissionless) dependencies by running the following command: +{/* */} + ```bash pnpm install viem permissionless ``` +{/* */} + ## Steps diff --git a/pages/home/_meta.json b/pages/home/_meta.json index 355e4ccd..39770c6b 100644 --- a/pages/home/_meta.json +++ b/pages/home/_meta.json @@ -12,5 +12,20 @@ "4337-overview": "Overview", "4337-safe": "Safe and ERC-4337", "4337-supported-networks": "Supported Networks", - "4337-guides": "Guides" + "4337-guides": "Guides", + "-- Passkeys": { + "type": "separator", + "title": "Passkeys" + }, + "passkeys-overview": "Overview", + "passkeys-safe": "Safe and Passkeys", + "passkeys-supported-networks": "Supported Networks", + "passkeys-guides": "Guides", + "passkeys-tutorials": "Tutorials", + "passkeys-faqs": { + "title": "FAQs", + "theme": { + "toc": false + } + } } diff --git a/pages/home/passkeys-faqs.mdx b/pages/home/passkeys-faqs.mdx new file mode 100644 index 00000000..2209efe4 --- /dev/null +++ b/pages/home/passkeys-faqs.mdx @@ -0,0 +1,26 @@ +# Passkeys FAQs + +## Which devices support passkeys? + +Apple and Android devices both support passkeys and syncing. If a device uses [Cross-device authentication (CDA)](https://passkeys.dev/docs/reference/terms/#cross-device-authentication-cda), its passkeys will be portable to other devices. You can read more about device support [here](https://passkeys.dev/device-support/#matrix). + +## How can I sync a passkey across devices? + +Passkeys can be synced across devices through secure cloud services provided by device manufacturers and operating system vendors. Most platforms support passkey syncing natively and automatically, meaning that a passkey used to authenticate a user on one device will also be available on this user’s other devices, for example, via Apple ID or Google Account. + +Only device-bound passkeys (a specific type of passkeys) created on Windows will be available solely on the device they were created on. + +## How can I recover an account with passkeys? + +Account recovery with passkeys typically involves using your synced devices or recovery methods provided by the cloud service where the passkeys are stored. The device manufacturer usually does this automatically, but services like password managers can also be used to store and access passkeys securely, independently from the manufacturer. + +## Do passkeys and web3 use the same encryption schemes? + +Passkeys and web3 share common principles of encryption but may use different schemes depending on the specific implementation. Generally, passkeys use public key cryptography, which is also a foundational element in many web3 protocols: + +- Ethereum primarily uses the `secp256k1` elliptic curve to generate public-private key pairs. This curve was chosen for its properties that offer a good balance of security and performance in blockchain applications. +- Passkeys can support `secp256r1` (also known as `P-256`). `P-256` is commonly used for its strong security properties and compatibility with web technologies. + +## Can I use passkeys with ERC-4337? + +Passkeys can be integrated with ERC-4337, providing enhanced security and user experience in managing web3 accounts. See [our tutorial to build your own implementation](https://docs.safe.global/home/passkeys-tutorials/safe-passkeys-tutorial), or check out [4337 support contract for passkeys](https://github.com/safe-global/safe-modules/tree/main/modules/passkey/contracts/4337) for more information. \ No newline at end of file diff --git a/pages/home/passkeys-guides/_meta.json b/pages/home/passkeys-guides/_meta.json new file mode 100644 index 00000000..27c0385b --- /dev/null +++ b/pages/home/passkeys-guides/_meta.json @@ -0,0 +1,3 @@ +{ + "safe-sdk": "Passkeys with the Safe{Core} SDK" +} diff --git a/pages/home/passkeys-guides/safe-sdk.mdx b/pages/home/passkeys-guides/safe-sdk.mdx new file mode 100644 index 00000000..bca9906b --- /dev/null +++ b/pages/home/passkeys-guides/safe-sdk.mdx @@ -0,0 +1,225 @@ +import { Steps, Tabs } from 'nextra/components' + +# Passkeys with the Safe\{Core\} SDK + +This guide will teach you how to create and execute multiple Safe transactions grouped in a batch from a Safe Smart Account that uses a passkey as an owner. To have a good user experience, we will use an [ERC-4337 compatible Safe](../4337-guides/safe-sdk.mdx) with sponsored transactions using Pimlico infrastructure. During this guide, we will create a new passkey, add it to the Safe as an owner, and use it to sign the user operations. + +This guide uses [Pimlico](https://pimlico.io) as the service provider, but any other provider compatible with the ERC-4337 can be used. + +## Prerequisites + +- [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). +- A [Pimlico account](https://dashboard.pimlico.io) and an API key. +- Passkeys feature is available only in [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (HTTPS), in some or all [supporting browsers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API#browser_compatibility). + +## Install dependencies + +{/* */} + +```bash +yarn add @safe-global/relay-kit +yarn add @safe-global/protocol-kit +``` + +{/* */} + +## Steps + + + + ### Imports + + Here are all the necessary imports for the script we implement in this guide. + + {/* */} + + ```typescript + import { Safe4337Pack } from '@safe-global/relay-kit' + import { PasskeyArgType } from '@safe-global/protocol-kit' + ``` + + {/* */} + + ### Create a passkey + + Firstly, we need to generate a passkey credential using the WebAuthn API in a supporting browser environment. + + {/* */} + + ```typescript + const RP_NAME = 'Safe Smart Account' + const USER_DISPLAY_NAME = 'User display name' + const USER_NAME = 'User name' + + const passkeyCredential = await navigator.credentials.create({ + publicKey: { + pubKeyCredParams: [ + { + alg: -7, + type: 'public-key' + } + ], + challenge: crypto.getRandomValues(new Uint8Array(32)), + rp: { + name: RP_NAME + }, + user: { + displayName: USER_DISPLAY_NAME, + id: crypto.getRandomValues(new Uint8Array(32)), + name: USER_NAME + }, + timeout: 60_000, + attestation: 'none', + }, + }) + ``` + + {/* */} + + After generating the `passkeyCredential` object, we need to create a new object with the `PasskeyArgType` type that will contain the `rawId` and the `publicKey` information. + + {/* */} + + ```typescript + const passkeyObject = passkeyCredential as PublicKeyCredential + const attestationResponse = + passkeyObject.response as AuthenticatorAttestationResponse + + const rawId = passkeyObject.rawId + const publicKey = attestationResponse.getPublicKey() + + const passkey: PasskeyArgType = { + rawId, + publicKey + } + ``` + + {/* */} + + At this point, it's critical to securely store the information in the `passkey` object in a persistent service. Losing access to this data will result in the user being unable to access their passkey and, therefore, their Safe Smart Account. + + ### Initialize the Safe4337Pack + + Once the passkey is created and secured, we can use the `Safe4337Pack` class exported from the Relay Kit to create, sign, and submit Safe user operations. + + To instantiate this class, the static `init()` method allows connecting existing Safe accounts (as long as they have the `Safe4337Module` enabled) or setting a custom configuration to deploy a new Safe account at the time where the first Safe transaction is submitted. For this guide, we will deploy a new Safe account, configure the paymaster options to get all the transactions sponsored and connect our passkey to add it as the only owner. + + {/* */} + + ```typescript + const PIMLICO_API_KEY = // ... + const RPC_URL = 'https://rpc.ankr.com/eth_sepolia' + + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + rpcUrl: RPC_URL, + signer: passkey, + bundlerUrl: `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}`, + paymasterOptions: { + isSponsored: true, + paymasterUrl: `https://api.pimlico.io/v2/sepolia/rpc?apikey=${PIMLICO_API_KEY}`, + paymasterAddress: '0x...', + paymasterTokenAddress: '0x...', + sponsorshipPolicyId // Optional value to set the sponsorship policy id from Pimlico + }, + options: { + owners: [], + threshold: 1 + } + }) + ``` + + {/* */} + + ### Create a user operation + + To create a Safe user operation, use the `createTransaction()` method, which takes the array of transactions to execute and returns a `SafeOperation` object. + + {/* */} + + ```typescript + // Define the transactions to execute + const transaction1 = { to, data, value } + const transaction2 = { to, data, value } + + // Build the transaction array + const transactions = [transaction1, transaction2] + + // Create the SafeOperation with all the transactions + const safeOperation = await safe4337Pack.createTransaction({ transactions }) + ``` + + {/* */} + + The `safeOperation` object has the `data` and `signatures` properties, which contain all the information about the transaction batch and the signatures of the Safe owners, respectively. + + ### Sign a user operation + + Before sending the user operation to the bundler, the `safeOperation` object must be signed with the connected passkey. The user is now requested to authenticate with the associated device and sign in with a biometric sensor, PIN, or gesture. + + The `signSafeOperation()` method, which receives a `SafeOperation` object, generates a signature that will be checked when the `Safe4337Module` validates the user operation. + + {/* */} + + ```typescript + const signedSafeOperation = await safe4337Pack.signSafeOperation( + safeTransaction + ) + ``` + + {/* */} + + ### Submit the user operation + + Once the `safeOperation` object is signed with the passkey, we can call the `executeTransaction()` method to submit the user operation to the bundler. + + {/* */} + + ```typescript + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + ``` + + {/* */} + + ### Check the transaction status + + To check the transaction status, we can use the `getTransactionReceipt()` method, which returns the transaction receipt after it's executed. + + {/* */} + + ```typescript + let userOperationReceipt = null + + while (!userOperationReceipt) { + // Wait 2 seconds before checking the status again + await new Promise((resolve) => setTimeout(resolve, 2000)) + userOperationReceipt = await safe4337Pack.getUserOperationReceipt( + userOperationHash + ) + } + ``` + + {/* */} + + In addition, we can use the `getUserOperationByHash()` method with the returned hash to retrieve the user operation object we sent to the bundler. + + {/* */} + + ```typescript + const userOperationPayload = await safe4337Pack.getUserOperationByHash( + userOperationHash + ) + ``` + + {/* */} + + + +## Recap and further reading + +After following this guide, we are able to deploy a new ERC-4337 compatible Safe Smart Account setup with a passkey and create, sign, and execute Safe transactions signing them with the passkey. Learn more about passkeys and how Safe supports them in detail by following these links: + +- [Safe\{Core\} SDK demo](https://github.com/5afe/passkey-sdk-demo-dapp) +- [Safe Passkeys contracts](https://github.com/safe-global/safe-modules/tree/main/modules/passkey) diff --git a/pages/home/passkeys-overview.mdx b/pages/home/passkeys-overview.mdx new file mode 100644 index 00000000..d693f966 --- /dev/null +++ b/pages/home/passkeys-overview.mdx @@ -0,0 +1,53 @@ +import { Grid } from '@mui/material' +import CustomCard from '../../components/CustomCard' + +# What are passkeys? + +Passkeys are a standard authentication method designed to avoid using traditional passwords, providing a more secure and user-friendly experience. + +Passkeys are based on public and private key pairs to secure user authentication. The public key is stored on the server side, while the private key is secured in the user's device. The user is authenticated by proving ownership of the private key, usually with biometric sensors, without extracting it from the device at any time. This method ensures that sensitive information remains protected and reduces the risk of credential theft. + +## Why do we need passkeys? + +Passkeys offer significant security improvements over traditional passwords. In the context of web3, where secure key management is paramount, passkeys provide an efficient alternative to seed phrases, which are often considered both a security liability and a subpar user experience. + + + + + + + + + + + + + +Safe offers the capability to sign into your wallet using passkeys by implementing a dedicated module that verifies the integrity of the key provided. + +## Further reading + +- [The official W3C standard](https://www.w3.org/TR/webauthn) +- [WebAuthn API specification](https://webauthn.wtf/how-it-works/basics) +- [Passkeys 101 by FIDO Alliance](https://fidoalliance.org/passkeys) diff --git a/pages/home/passkeys-safe.mdx b/pages/home/passkeys-safe.mdx new file mode 100644 index 00000000..e8ec63df --- /dev/null +++ b/pages/home/passkeys-safe.mdx @@ -0,0 +1,63 @@ +import { Callout } from 'nextra/components' + +# Safe and Passkeys + + + Passkeys are compatible with Safe versions `≥1.3.0`. + + +Safe's standard-agnostic nature allows adding or removing user flows, such as custom signature verification logic. This flexibility facilitates the integration of a Passkeys-based execution flow into a Safe. Safe passkey contracts conform to both ERC-1271 and WebAuthn standards, enabling the verification of signatures for WebAuthn credentials that use the `secp256r1` curve. + +These contracts can utilize EIP-7212 precompiles for signature verification on supported networks or alternatively employ any verifier contract as a fallback mechanism. + +## Passkey contracts + + + This section covers implementation details of the passkeys with Safe. If you'd rather get straight to building, head over to our [guides](./passkeys-guides/safe-sdk) and [tutorials](./passkeys-tutorials/safe-passkeys-tutorial) sections. + + +### `SafeWebAuthnSignerProxy` + +A proxy contract is uniquely deployed for each `Passkey` signer. The signer information, such as Public key coordinates, Verifier address, and Singleton address, is immutable. All calls to the signer are forwarded to the `SafeWebAuthnSignerSingleton` contract. + +`SafeWebAuthnSignerProxy` provides gas savings compared to the whole contract deployment for each signer creation. + +`SafeWebAuthnSignerProxy` and `SafeWebAuthnSignerSingleton` use no storage slots to avoid storage access violations defined in ERC-4337. Check [this PR](https://github.com/safe-global/safe-modules/pull/370) for details on gas savings. This non-standard proxy contract appends signer information, like public key coordinates and verifier data, to the call data before forwarding the calls to the singleton contract. + +### `SafeWebAuthnSignerSingleton` + +`SafeWebAuthnSignerSingleton` is a singleton contract that implements the ERC-1271 interface to support signature verification. It enables signature data to be forwarded from a Safe to the `WebAuthn` library. This contract expects the caller to append public key coordinates and the verifier address (inspired by [ERC-2771](https://eips.ethereum.org/EIPS/eip-2771)). + +### `SafeWebAuthnSignerFactory` + +The `SafeWebAuthnSignerFactory` contract deploys the `SafeWebAuthnSignerProxy` contract with the public key coordinates and verifier information. The factory contract also supports signature verification for the public key and signature information without deploying the signer contract, which is used during the validation of ERC-4337 user operations by the experimental `SafeSignerLaunchpad` contract. + +New signers can be deployed using the [ISafeSignerFactory](https://github.com/safe-global/safe-modules/blob/466a9b8ef169003c5df856c6ecd295e6ecb9e99d/modules/passkey/contracts/interfaces/ISafeSignerFactory.sol) interface and this factory contract address. + +### `WebAuthn` + +This library generates a signing message, hashing it, and forwards the call to the verifier contract. The `WebAuthn` library defines a `Signature` struct containing `authenticatorData` and `clientDataFields`, followed by the ECDSA signature's `r` and `s` components. + +The `authenticatorData` and `clientDataFields` are required for generating the signing message. The `bytes` signature received in the `verifySignature(...)` function is cast to the `Signature` struct, so the caller has to take into account formatting the signature bytes as expected by the `WebAuthn` library. +The code snippet below shows signature encoding for verification using the WebAuthn library. + +``` +bytes authenticatorData = ...; +string clientDataFields = ...; +uint256 r = ...; +uint256 s = ...; +// Encode the signature data +bytes memory signature = abi.encode(authenticatorData, clientDataFields, r, s); +``` + +### `P256` + +`P256` is a library for P256 signature verification with contracts that follow the EIP-7212 EC verify precompile interface. This library defines a custom type `Verifiers`, which encodes two addresses into a single `uint176`. The first address (2 bytes) is a precompile address dedicated to verification, and the second (20 bytes) is a fallback address. + +This setup allows the library to support networks where the precompile is not yet available. It seamlessly transitions to the precompile when it becomes active while relying on a fallback contract address in the meantime. + +## Further reading + +- [Passkeys module](https://github.com/safe-global/safe-modules/blob/466a9b8ef169003c5df856c6ecd295e6ecb9e99d/modules/passkey/README.md) +- [Safe and Passkeys demo application](https://github.com/safe-global/safe-modules/tree/main/examples/4337-passkeys) +- [4337 support for passkeys](https://github.com/safe-global/safe-modules/tree/main/modules/passkey/contracts/4337) \ No newline at end of file diff --git a/pages/home/passkeys-supported-networks.mdx b/pages/home/passkeys-supported-networks.mdx new file mode 100644 index 00000000..e8a713a0 --- /dev/null +++ b/pages/home/passkeys-supported-networks.mdx @@ -0,0 +1,9 @@ +# Supported Networks + +The Safe Passkeys Module `v0.2.0` is deployed in the following networks: + +| Network | `SafeWebAuthnSignerFactory` Address | `DaimoP256Verifier` Address | `FCLP256Verifier` Address | +| ---------------------------- | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | +| Sepolia | 0xF7488fFbe67327ac9f37D5F722d83Fc900852Fbf | 0xc2b78104907F722DABAc4C69f826a522B2754De4 | 0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765 | + +To add additional deployments please follow the [deployment instructions](https://github.com/safe-global/safe-modules/tree/main/modules/passkey#deploy) in the Safe Modules repository. diff --git a/pages/home/passkeys-tutorials/_meta.json b/pages/home/passkeys-tutorials/_meta.json new file mode 100644 index 00000000..3f800121 --- /dev/null +++ b/pages/home/passkeys-tutorials/_meta.json @@ -0,0 +1,4 @@ +{ + "safe-passkeys-tutorial": "Build an app with Safe and passkeys" +} + \ No newline at end of file diff --git a/pages/home/passkeys-tutorials/safe-passkeys-tutorial.mdx b/pages/home/passkeys-tutorials/safe-passkeys-tutorial.mdx new file mode 100644 index 00000000..13582f9f --- /dev/null +++ b/pages/home/passkeys-tutorials/safe-passkeys-tutorial.mdx @@ -0,0 +1,717 @@ +# How to build an app with Safe and passkeys + +An increasing number of applications rely on passkeys to authenticate users securely and with little friction. Security and user-friendliness are crucial to making web3 a reality for the next billion users. +Being able to unlock a Safe Smart Account with your fingerprints or Face ID, sending transactions without worrying about third-party wallet interfaces, phishing attempts, or securing seed phrases will bring new forms of ownership to the connected world. +Today, we'll learn how to make this a reality using [Safe\{Core\} SDK](../../sdk/overview.mdx), [Pimlico](https://www.pimlico.io/), and [Next.js](https://nextjs.org/docs). + +This tutorial will demonstrate creating a web app for using [passkeys](../passkeys-overview.mdx) in your Safe. This app will allow you to: +- Create a new passkey secured by the user's device. +- Deploy a new Safe on Ethereum Sepolia for free. +- Sign a transaction to send USDC using the previously created passkey. + +![safe-passkeys-app-2.png](../../../assets/safe-passkeys-app-2.png) + +## **What you'll need** + +**Prerequisite knowledge:** You will need some basic experience with [React](https://react.dev/learn), Next.js, and [ERC-4337](../4337-overview). + +Before progressing with the tutorial, please make sure you have: + +- Downloaded and installed [Node.js](https://nodejs.org/en/download/package-manager) and [pnpm](https://pnpm.io/installation). +- Created an API key from [Pimlico](https://www.pimlico.io/). + +**Note:** If you wish to follow along using the completed project, you can [check out the GitHub repository](https://github.com/5afe/safe-passkeys-tutorial) for this tutorial. + +## 1. Setup a Next.js application + +Initialize a new Next.js app using pnpm with the following command: + +```bash +pnpm create next-app +``` + +When prompted by the CLI: + +- Select `yes` to TypeScript, ESLint, and App router. +- Select `no` to all other questions (Tailwind, `src` directory, and import aliases). + +### Install dependencies + +For this project, we'll use the [Relay Kit from the Safe\{Core\} SDK](../../sdk/relay-kit.mdx) to set up a Safe, sponsor a USDC transaction, and use [viem](https://www.npmjs.com/package/viem) and [buffer](https://www.npmjs.com/package/buffer) for some helper functions. + +Run the following command to add all these dependencies to the project: + +```bash +pnpm add @safe-global/relay-kit@3.1.0-alpha.0 buffer viem +``` + +Now, create a file named `.env.local` at the root of your project, and add your Pimlico API key to it: + +```bash +echo "NEXT_PUBLIC_PIMLICO_API_KEY='your_pimlico_api_key_goes_here'" > .env.local +``` + +### Run the development server + +Run the local development server with the following command: + +```bash +pnpm dev +``` + +Go to `http://localhost:3000` in your browser to see the default Next.js application. + +![next.png](../../../assets/next.png) + +## 2. Add project constants and utilities + +Create a `lib` folder at the project root and add a file `constants.ts` containing common constants used throughout the project: + +```bash +mkdir lib +cd lib +touch constants.ts +``` + +Add the following code to the `constants.ts` file: + +```tsx +export const STORAGE_PASSKEY_LIST_KEY = 'passkeyList' // Variable name that passkeys will be stored under in local storage +export const RPC_URL = 'https://ethereum-sepolia-rpc.publicnode.com' +export const CHAIN_NAME = 'sepolia' +export const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // SEPOLIA +export const paymasterAddress = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA +export const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${process.env.NEXT_PUBLIC_PIMLICO_API_KEY}` +export const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${process.env.NEXT_PUBLIC_PIMLICO_API_KEY}` +``` + +In the same `lib` folder, create a `utils.ts` file: + +```bash +touch utils.ts +``` + +Add the following code to the `utils.ts` file: + +```tsx +import { Buffer } from 'buffer' + +export const bufferToString = (buffer: ArrayBuffer): string => + Buffer.from(buffer).toString('hex') + +export function hexStringToUint8Array(hexString: string): Uint8Array { + const arr = [] + for (let i = 0; i < hexString.length; i += 2) { + arr.push(parseInt(hexString.substr(i, 2), 16)) + } + return new Uint8Array(arr) +} +``` + +This file contains two utilities for manipulating passkey objects from the native [`navigator.credentials`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/credentials) API: + +- `bufferToString`: Helps us read the passkeys properties (returned by the browser) as strings. +- `hexStringToUint8Array`: Helps us pass string arguments to the `credentials` API. + +## 3. Add passkeys functionality + +In the `lib` folder, create a file called `passkeys.ts` : + +```bash +touch passkeys.ts +``` + +This file will contain all the logic required to operate passkey: + +- Create and recover them using the user's device. +- Store and retrieve them from/to the local storage. + +**Note:** You can also store the passkeys on a remote database or the user's device. + +```tsx +import { PasskeyArgType } from '@safe-global/protocol-kit' + +import { STORAGE_PASSKEY_LIST_KEY } from './constants' +import { bufferToString, hexStringToUint8Array } from './utils' + +export type PasskeyItemType = { rawId: string; publicKey: string } + +export async function createPasskey(): Promise { + const label = 'Safe Owner' // This can be customized to match, for example, a user name. + // 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 SmartAccount' + }, + user: { + displayName: label, + id: crypto.getRandomValues(new Uint8Array(32)), + name: label + }, + timeout: 60_000, + attestation: 'none' + } + }) + + if (!passkeyCredential) { + throw Error('Passkey creation failed: No credential was returned.') + } + + console.log('passkeyCredential: ', passkeyCredential) + + const passkey = passkeyCredential as PublicKeyCredential + const attestationResponse = + passkey.response as AuthenticatorAttestationResponse + + const rawId = passkey.rawId + const publicKey = attestationResponse.getPublicKey() + + if (!publicKey) { + throw new Error('getPublicKey error') + } + + return { + rawId, + publicKey + } +} + +export function storePasskey(passkey: PasskeyArgType) { + const passkeys = loadPasskeys() + + const newPasskeyItem = { + rawId: bufferToString(passkey.rawId), + publicKey: bufferToString(passkey.publicKey) + } + + passkeys.push(newPasskeyItem) + + localStorage.setItem(STORAGE_PASSKEY_LIST_KEY, JSON.stringify(passkeys)) +} + +export function loadPasskeys(): PasskeyItemType[] { + const passkeysStored = localStorage.getItem(STORAGE_PASSKEY_LIST_KEY) + + const passkeyIds = passkeysStored ? JSON.parse(passkeysStored) : [] + + return passkeyIds +} + +function getPublicKeyFromLocalStorage(passkeyRawId: string): ArrayBuffer { + const passkeys = loadPasskeys() + + const { publicKey } = passkeys.find( + (passkey: PasskeyItemType) => passkey.rawId === passkeyRawId + )! + + return hexStringToUint8Array(publicKey) +} + +export async function getPasskeyFromRawId( + passkeyRawId: string +): Promise { + const passkeyCredentials = (await navigator.credentials.get({ + publicKey: { + allowCredentials: [ + { + id: hexStringToUint8Array(passkeyRawId), + type: 'public-key' + } + ], + challenge: crypto.getRandomValues(new Uint8Array(32)), + userVerification: 'required' + } + })) as PublicKeyCredential + + const publicKey = getPublicKeyFromLocalStorage(passkeyRawId) + + const passkey = { + rawId: passkeyCredentials.rawId, + publicKey + } + + return passkey +} +``` + +In this file, we have four functions: + +- `createPasskey`, which helps create a new passkey. +- `storePasskeyInLocalStorage`, which helps store it in the browser's local storage. +- `loadPasskeysFromLocalStorage`, which helps load a passkey from local storage. +- `getPublicKeyFromLocalStorage`, which helps find a passkey in the local storage corresponding to a given `rawId` and returns this passkey's public key. +- `getPasskeyFromRawId`, which helps reconstruct a full passkey from a `rawId` and a public key stored in local storage. + +## 4. Add USDC transaction functionality + +Create a `usdc.ts` file in the `lib` folder to add functions to prepare and send a transaction transferring USDC from our yet-to-come Safe. + +```tsx +touch usdc.ts +``` + +Add the following code to the `usdc.ts` file: + +```tsx +import { encodeFunctionData, parseAbi } from 'viem' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { PasskeyArgType } from '@safe-global/protocol-kit' + +import { + BUNDLER_URL, + CHAIN_NAME, + PAYMASTER_URL, + RPC_URL, + paymasterAddress, + usdcTokenAddress +} from '../constants' + +const paymasterOptions = { + isSponsored: true, + paymasterAddress, + paymasterUrl: PAYMASTER_URL +} + +const generateTransferCallData = (to: `0x${string}`, value: bigint) => { + const abi = parseAbi([”function transfer(address _to, address _from) returns (bool)”]) + return encodeFunctionData({ + abi, + functionName: 'transfer', + args: [to, value] + }) +} + +export const executeUSDCTransfer = async ({ + signer, + safeAddress +}: { + signer: PasskeyArgType + safeAddress: string +}) => { + const safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + rpcUrl: RPC_URL, + signer, + bundlerUrl: BUNDLER_URL, + paymasterOptions, + options: { + owners: [ + /* Other owners... */ + ], + threshold: 1 + } + }) + + const usdcAmount = 100_000n // 0.1 USDC + + const transferUSDC = { + to: usdcTokenAddress, + data: generateTransferCallData(safeAddress!, usdcAmount), + value: '0' + } + + const safeOperation = await safe4337Pack.createTransaction({ + transactions: [transferUSDC] + }) + + const signedSafeOperation = await safe4337Pack.signSafeOperation( + safeOperation + ) + + console.log('SafeOperation', signedSafeOperation) + + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: signedSafeOperation + }) + + console.log( + `https://jiffyscan.xyz/userOpHash/${userOperationHash}?network=${CHAIN_NAME}` + ) +} +``` + +With this configuration, a new Safe will be created (but not yet deployed) when a passkey is selected. This Safe will be deployed when its first transaction is executed. + +**Note:** Transferring USDC was chosen here just as an example, and any other transaction would have the same effect. + +## 5. Add UI components + +Let's add a user interface to create and store a passkey on the user's device, deploy a safe, and sign the USDC transaction. + +Create a `components` folder at the project root with a file named `PasskeyList.tsx`: + +```bash +cd .. +mkdir components +cd components +touch Passkeylist.tsx +``` + +Add the following code to the `PasskeyList.tsx` file: + +```tsx +import { useEffect, useState } from 'react' + +import { + createPasskey, + loadPasskeysFromLocalStorage, + storePasskeyInLocalStorage, + type PasskeyItemType +} from '../lib/passkeys' + +type Props = { + selectPasskeySigner: (rawId: string) => void +} + +function PasskeyList ({ selectPasskeySigner }: Props) { + const [passkeyList, setPasskeyList] = useState([]) + + async function handleSubmit () { + const passkey = await createPasskey() + storePasskeyInLocalStorage(passkey) + refreshPasskeyList() + } + + function refreshPasskeyList () { + const passkeys = loadPasskeysFromLocalStorage() + setPasskeyList(passkeys) + } + + useEffect(() => { + refreshPasskeyList() + }, []) + + return ( + <> +

Create new Passkey

+ {' '} + {passkeyList.length > 0 && ( + <> +

Passkey List

+ {passkeyList.map(passkey => ( +
+ Id: {passkey.rawId}{' '} + +
+ ))} + + )} + + ) +} + +export default PasskeyList + +``` + +This component displays a list of previously created passkeys and a button for creating new ones. + +Lastly, replace the content of the `page.tsx` file, within the `app` folder, with this code: + +```tsx +'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() + const [safeAddress, setSafeAddress] = useState() + const [isSafeDeployed, setIsSafeDeployed] = useState() + const [userOp, setUserOp] = useState() + + 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 ( + <> +
+ {selectedPasskey && ( + <> +

Passkey Selected

+ +
+ {bufferToString(selectedPasskey.rawId)} +
+ + )} + +
+ {safeAddress && ( +
+

Safe Account

+ +
+ Address: {safeAddress} +
+
+ Is deployed?:{' '} + {isSafeDeployed ? ( + + Yes{' '} + External link + + ) : ( + 'No' + )} +
+ + {selectedPasskey && ( + + )} + {userOp && isSafeDeployed && ( + <> +
+ Done! Check the transaction status on{' '} + + Jiffy Scan{' '} + External link + +
+ + )} +
+ )} + + ) +} + +export default Create4337SafeAccount + +``` + +This UI will put everything we built in the previous steps into a coherent application with all the functionality required to let you create a passkey, select it, and use it to sign a transaction. + +## 6. (Optional) Add styling to the app + +Because a web app is nothing without good styling, let's add some Safe design to our project 💅. + +Still within the `app` folder, replace the existing content of the file `layout.ts`with this code: + +```tsx +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 ( + + + +
+

Passkeys tutorial

+ +
Create a new 4337 compatible Safe Account using passkeys
+
+
+ {children} +
+ + + ) +} + +``` + +In the same folder, add some margin to the titles, by adding this code at the end of the `globals.css` file: + +```css +h1, +h2, +h3 { + margin-top: 40px; + margin-bottom: 10px; +} + +button { + cursor: pointer; + border: none; + background: #00E673; + color: black; + padding: 10px 20px; + border-radius: 5px; + margin: 10px 0; +} +``` + +Finally, in the `public` folder, add these three icons. You can find them in the project's GitHub repository: [`safe.svg`](https://github.com/5afe/safe-passkeys-tutorial/blob/main/public/safe.svg), [`github.svg`](https://github.com/5afe/safe-passkeys-tutorial/blob/main/public/github.svg/), and [`external-link.svg`](https://github.com/5afe/safe-passkeys-tutorial/blob/main/public/external-link.svg). + +## Testing your Safe passkeys app + +That's it! You can find the source code for the example created in this tutorial [on GitHub](https://github.com/5afe/safe-passkeys-tutorial). You can now return to your browser and see the app displayed 🎉. + +![safe-passkeys-app-1.png](../../../assets/safe-passkeys-app-1.png) + +Click the **Add New Passkey** button to prompt a browser pop-up asking you to confirm the creation of a new passkey. This passkey will be stored in your browser's local storage and displayed in the list above the button. + +Once confirmed, select this passkey by clicking **Select** next to it. This will prompt another pop-up window, this time asking to confirm the use of the previously created passkey. + +![safe-passkeys-app-2.png](../../../assets/safe-passkeys-app-2.png) + +At this stage, the app will have created a safe object awaiting deployment. Send some USDC to your future safe by clicking the link to [Circle's USDC faucet](https://faucet.circle.com/) for Sepolia and entering the Safe's address. By clicking **Sign transaction with passkey**, the deployment of this safe will then be embedded in a batch transaction, along with the transfer of USDC. +Open the console to see the UserOp that was sent or click the link provided to Jiffy scan for more complete information. + +## Do more with Safe and passkeys + +Today, we learned how to use passkeys (create them, store them, and use them securely) and how they can interact with a Safe (deploy it and send transactions). We hope you enjoyed this tutorial and that the combination of passkeys and 4337 will unlock new forms of ownership for your project and users. + +You can now integrate passkeys with more transactions and functionalities of the Safe ecosystem. You can read more about passkeys in our [overview](../passkeys-overview) or in the [WebAuthn API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API). + +Did you encounter any difficulties? Let us know by opening [an issue](https://github.com/5afe/safe-passkeys-tutorial/issues/new) or asking a question on [Stack Exchange](https://ethereum.stackexchange.com/questions/tagged/safe-core) with the `safe-core` tag. diff --git a/pages/sdk/protocol-kit/reference/safe-factory.md b/pages/sdk/protocol-kit/reference/safe-factory.md index a344e889..bb11b961 100644 --- a/pages/sdk/protocol-kit/reference/safe-factory.md +++ b/pages/sdk/protocol-kit/reference/safe-factory.md @@ -15,6 +15,25 @@ const safeFactory = await SafeFactory.init({ }) ``` +- The `signer` property + + A passkey object can be passed as a signer to initialize an instance of the Safe Factory. + +```typescript +import { SafeFactory, PasskeyArgType } from '@safe-global/protocol-kit' + +const passkey: PasskeyArgType = { + rawId, + publicKey, +} + +const safeFactory = await SafeFactory.init({ + provider, + signer: passkey +}) +``` + + - The `isL1SafeSingleton` flag Two versions of the Safe contracts are available: [Safe.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/Safe.sol) that doesn't trigger events to save gas and [SafeL2.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/SafeL2.sol) that does, which is more appropriate for L2 networks. @@ -52,6 +71,7 @@ const safeFactory = await SafeFactory.init({ signMessageLibAddress: '', createCallAddress: '', simulateTxAccessorAddress: '', + safeWebAuthnSignerFactoryAddress:'', safeSingletonAbi: '', // Optional. Only needed with web3.js safeProxyFactoryAbi: '', // Optional. Only needed with web3.js multiSendAbi: '', // Optional. Only needed with web3.js @@ -60,6 +80,7 @@ const safeFactory = await SafeFactory.init({ signMessageLibAbi: '', // Optional. Only needed with web3.js createCallAbi: '', // Optional. Only needed with web3.js simulateTxAccessorAbi: '' // Optional. Only needed with web3.js + safeWebAuthnSignerFactoryAbi: '' // Optional. Only needed with web3.js } } diff --git a/pages/sdk/protocol-kit/reference/safe.md b/pages/sdk/protocol-kit/reference/safe.md index 69e55b5c..dd570e19 100644 --- a/pages/sdk/protocol-kit/reference/safe.md +++ b/pages/sdk/protocol-kit/reference/safe.md @@ -37,6 +37,26 @@ const protocolKit = await Safe.init({ }) ``` +- The `signer` property + + A passkey object can be passed as a signer to initialize an instance of the Protocol Kit. + +```typescript +import Safe from '@safe-global/protocol-kit' + +const passkey: PasskeyArgType = { + rawId, + publicKey, +} + +const protocolKit = await Safe.init({ + provider, + signer: passkey, + // safeAddress or predictedSafe +}) +``` + + - The `isL1SafeSingleton` flag Two versions of the Safe contracts are available: [Safe.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/Safe.sol) that doesn't trigger events to save gas and [SafeL2.sol](https://github.com/safe-global/safe-contracts/blob/v1.4.1/contracts/SafeL2.sol) that does, which is more appropriate for L2 networks. @@ -75,6 +95,7 @@ const protocolKit = await Safe.init({ signMessageLibAddress: '', createCallAddress: '', simulateTxAccessorAddress: '', + safeWebAuthnSignerFactoryAddress:'', safeSingletonAbi: '', // Optional. Only needed with web3.js safeProxyFactoryAbi: '', // Optional. Only needed with web3.js multiSendAbi: '', // Optional. Only needed with web3.js @@ -83,6 +104,7 @@ const protocolKit = await Safe.init({ signMessageLibAbi: '', // Optional. Only needed with web3.js createCallAbi: '', // Optional. Only needed with web3.js simulateTxAccessorAbi: '' // Optional. Only needed with web3.js + safeWebAuthnSignerFactoryAbi: '' // Optional. Only needed with web3.js } } @@ -174,6 +196,7 @@ protocolKit = await protocolKit.connect({ predictedSafe }) signMessageLibAddress: '', createCallAddress: '', simulateTxAccessorAddress: '', + safeWebAuthnSignerFactoryAddress:'', safeSingletonAbi: '', // Optional. Only needed with web3.js safeProxyFactoryAbi: '', // Optional. Only needed with web3.js multiSendAbi: '', // Optional. Only needed with web3.js @@ -182,6 +205,7 @@ protocolKit = await protocolKit.connect({ predictedSafe }) signMessageLibAbi: '', // Optional. Only needed with web3.js createCallAbi: '', // Optional. Only needed with web3.js simulateTxAccessorAbi: '' // Optional. Only needed with web3.js + safeWebAuthnSignerFactoryAbi: '' // Optional. Only needed with web3.js } } @@ -513,6 +537,22 @@ const txResponse = await protocolKit.executeTransaction(safeTransaction) await txResponse.transactionResponse?.wait() ``` +Instead of using an address, this method also supports the use of a passkey to set the address of the new owner: + +```typescript +const passkey: PasskeyArgType = { + rawId, + publicKey, +} +const params: AddPasskeyOwnerTxParams = { + passkey, + threshold // Optional. If `threshold` isn't provided the current threshold won't change. +} +const safeTransaction = await protocolKit.createAddOwnerTx(params) +const txResponse = await protocolKit.executeTransaction(safeTransaction) +await txResponse.transactionResponse?.wait() +``` + This method can optionally receive the `options` parameter: ```typescript @@ -591,6 +631,17 @@ Checks if a specific address is an owner of the current Safe. const isOwner = await protocolKit.isOwner(address) ``` +A passkey can also be used to check if the signer account is an owner of the current Safe. + +```typescript +const passkey: PasskeyArgType = { + rawId, + publicKey, +} + +const isOwner = await protocolKit.isOwner(passkey) +``` + ## Threshold ### `createChangeThresholdTx` diff --git a/pages/sdk/relay-kit/reference/safe-4337-pack.mdx b/pages/sdk/relay-kit/reference/safe-4337-pack.mdx index 9d963667..8e2f1d4c 100644 --- a/pages/sdk/relay-kit/reference/safe-4337-pack.mdx +++ b/pages/sdk/relay-kit/reference/safe-4337-pack.mdx @@ -40,7 +40,7 @@ The `Safe4337InitOptions` used in the `init()` method are: ```typescript Safe4337InitOptions = { provider: Eip1193Provider | HttpTransport | SocketTransport - signer?: HexAddress | PrivateKey + signer?: HexAddress | PrivateKey | PasskeyArgType bundlerUrl: string rpcUrl: string safeModulesVersion?: string @@ -89,7 +89,7 @@ PaymasterOptions = { ``` - **`provider`** : The EIP-1193 compatible provider or RPC URL of the selected chain. -- **`signer`** : The signer private address if the `provider` doesn't resolve to a signer account. If the `provider` resolves to multiple signer addresses, the `signer` property can be used to specify which account to connect, otherwise the first address returned will be used. +- **`signer`** : A passkey or the signer private key if the `provider` doesn't resolve to a signer account. If the `provider` resolves to multiple signer addresses, the `signer` property can be used to specify which account to connect, otherwise the first address returned will be used. - **`rpcUrl`** : The RPC URL of the selected chain. - **`bundlerUrl`** : The bundler's URL. - **`safeModulesVersion`** : The version of the [Safe Modules contract](https://github.com/safe-global/safe-modules-deployments/tree/main/src/assets/safe-4337-module).