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 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 (opens in a new tab) as the service provider, but any other provider compatible with the ERC-4337 can be used.
Note: Please always use a combination of passkeys and other authentication methods to ensure the security of your users' assets.
Prerequisites
- Node.js and npm (opens in a new tab).
- A Pimlico account (opens in a new tab) and an API key.
- Passkeys feature is available only in secure contexts (opens in a new tab) (HTTPS), in some or all supporting browsers (opens in a new tab).
Install dependencies
_10yarn add @safe-global/relay-kit_10yarn add @safe-global/protocol-kit
Steps
Imports
Here are all the necessary imports for the script we implement in this guide.
_10import { Safe4337Pack } from '@safe-global/relay-kit'_10import { extractPasskeyData } 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.
_25const RP_NAME = 'Safe Smart Account'_25const USER_DISPLAY_NAME = 'User display name'_25const USER_NAME = 'User name'_25_25const passkeyCredential = await navigator.credentials.create({_25 publicKey: {_25 pubKeyCredParams: [_25 {_25 alg: -7,_25 type: 'public-key'_25 }_25 ],_25 challenge: crypto.getRandomValues(new Uint8Array(32)),_25 rp: {_25 name: RP_NAME_25 },_25 user: {_25 displayName: USER_DISPLAY_NAME,_25 id: crypto.getRandomValues(new Uint8Array(32)),_25 name: USER_NAME_25 },_25 timeout: 60_000,_25 attestation: 'none',_25 },_25})
After generating the passkeyCredential
object, we need to create a new object with the PasskeyArgType
type that will contain the rawId
and the coordinates
information.
_10if (!passkeyCredential) {_10 throw Error("Passkey creation failed: No credential was returned.");_10}_10_10const passkey = await extractPasskeyData(passkeyCredential);
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.
_19const PIMLICO_API_KEY = // ..._19const RPC_URL = 'https://rpc.ankr.com/eth_sepolia'_19_19const safe4337Pack = await Safe4337Pack.init({_19 provider: RPC_URL,_19 signer: passkey,_19 bundlerUrl: `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}`,_19 paymasterOptions: {_19 isSponsored: true,_19 paymasterUrl: `https://api.pimlico.io/v2/sepolia/rpc?apikey=${PIMLICO_API_KEY}`,_19 paymasterAddress: '0x...',_19 paymasterTokenAddress: '0x...',_19 sponsorshipPolicyId // Optional value to set the sponsorship policy id from Pimlico_19 },_19 options: {_19 owners: [],_19 threshold: 1_19 }_19})
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.
_10// Define the transactions to execute_10const transaction1 = { to, data, value }_10const transaction2 = { to, data, value }_10_10// Build the transaction array_10const transactions = [transaction1, transaction2]_10_10// Create the SafeOperation with all the transactions_10const 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.
_10const signedSafeOperation = await safe4337Pack.signSafeOperation(_10 safeTransaction_10)
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.
_10const userOperationHash = await safe4337Pack.executeTransaction({_10 executable: signedSafeOperation_10})
Check the transaction status
To check the transaction status, we can use the getTransactionReceipt()
method, which returns the transaction receipt after it's executed.
_10let userOperationReceipt = null_10_10while (!userOperationReceipt) {_10 // Wait 2 seconds before checking the status again_10 await new Promise((resolve) => setTimeout(resolve, 2000))_10 userOperationReceipt = await safe4337Pack.getUserOperationReceipt(_10 userOperationHash_10 )_10}
In addition, we can use the getUserOperationByHash()
method with the returned hash to retrieve the user operation object we sent to the bundler.
_10const userOperationPayload = await safe4337Pack.getUserOperationByHash(_10 userOperationHash_10)
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: