Advanced
ERC-7579
Tutorials
Build an app with Safe and ERC-7579

How to build an app with Safe and ERC-7579

The smart account ecosystem was fragmented, with each provider building its own modules often incompatible with other smart account implementations. Developers had to build new modules compatible with their smart accounts or miss out on essential application features.

ERC-7579 (opens in a new tab) aims to ensure interoperability across implementations. It defines the account interface so developers can implement modules for all smart accounts that follow this standard. The Safe7579 Adapter makes your Safe compatible with any ERC-7579 modules. As a developer building with Safe, you get access to a rich ecosystem of modules to make your application feature-rich.

Let's say you want to build an app to enable scheduling transfers for monthly salaries to a team of contributors. However, Safe does not offer a native module for scheduling transfers. With the ERC-7579 compatibility, you can use Rhinestone's Scheduled Transfer module (opens in a new tab) with Safe to build an app to schedule transfers ahead of time.

This tutorial will teach you to build an app that can:

  • Deploy an ERC-7579-compatible Safe Smart Account.
  • Create a scheduled transaction.
  • Execute it at the requested date and time.

screenshot

Prerequisites

Prerequisite knowledge: You will need some basic experience with React (opens in a new tab), Next.js (opens in a new tab), ERC-4337 (opens in a new tab) and ERC-7579 (opens in a new tab).

Before progressing with the tutorial, please make sure you have:

Note: If you wish to follow along using the completed project, you can check out the GitHub repository (opens in a new tab) for this tutorial.

1. Setup a Next.js application

Initialize a new Next.js app using pnpm with the following command:


_10
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 Pimlico's Permissionless.js (opens in a new tab) to set up a Safe and interact with it, Rhinestone's Module SDK (opens in a new tab) to install and use core modules, and viem (opens in a new tab) for some helper functions.

⚠️

As of now, permissionless.js can only be used to deploy single-signer Safe accounts. Multi-signature ERC-7579 Safes will be coming soon.

Run the following command to add all these dependencies to the project:


_10
pnpm add permissionless viem @rhinestone/module-sdk@0.1.4

Now, create a file named .env.local at the root of your project, and add your Pimlico API key to it:


_10
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:


_10
pnpm dev

Go to http://localhost:3000 in your browser to see the default Next.js application.

screenshot-default

2. Initialize permissionless client

Create a lib folder at the project root, and add a file permissionless.ts:


_10
mkdir lib
_10
cd lib
_10
touch permissionless.ts

Add the code necessary to create Pimlico's smartAccountClient by adding this content to permissionless.ts :


_78
import { Hex, createPublicClient, http, Chain, Transport } from 'viem'
_78
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
_78
import { sepolia } from 'viem/chains'
_78
import {
_78
ENTRYPOINT_ADDRESS_V07,
_78
createSmartAccountClient,
_78
SmartAccountClient
_78
} from 'permissionless'
_78
import {
_78
signerToSafeSmartAccount,
_78
SafeSmartAccount
_78
} from 'permissionless/accounts'
_78
import { erc7579Actions, Erc7579Actions } from 'permissionless/actions/erc7579'
_78
import {
_78
createPimlicoBundlerClient,
_78
createPimlicoPaymasterClient
_78
} from 'permissionless/clients/pimlico'
_78
import { EntryPoint } from 'permissionless/types'
_78
_78
export type SafeSmartAccountClient = SmartAccountClient<
_78
EntryPoint,
_78
Transport,
_78
Chain,
_78
SafeSmartAccount<EntryPoint>
_78
> &
_78
Erc7579Actions<EntryPoint, SafeSmartAccount<EntryPoint>>
_78
_78
const pimlicoUrl = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${process.env.NEXT_PUBLIC_PIMLICO_API_KEY}`
_78
const safe4337ModuleAddress = '0x3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2'
_78
const erc7569LaunchpadAddress = '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE'
_78
_78
const privateKey =
_78
(process.env.NEXT_PUBLIC_PRIVATE_KEY as Hex) ??
_78
(() => {
_78
const pk = generatePrivateKey()
_78
console.log('Private key to add to .env.local:', `PRIVATE_KEY=${pk}`)
_78
return pk
_78
})()
_78
_78
const signer = privateKeyToAccount(privateKey)
_78
_78
const publicClient = createPublicClient({
_78
transport: http('https://rpc.ankr.com/eth_sepolia')
_78
})
_78
_78
const paymasterClient = createPimlicoPaymasterClient({
_78
transport: http(pimlicoUrl),
_78
entryPoint: ENTRYPOINT_ADDRESS_V07
_78
})
_78
_78
const bundlerClient = createPimlicoBundlerClient({
_78
transport: http(pimlicoUrl),
_78
entryPoint: ENTRYPOINT_ADDRESS_V07
_78
})
_78
_78
export const getSmartAccountClient = async () => {
_78
const account = await signerToSafeSmartAccount(publicClient, {
_78
entryPoint: ENTRYPOINT_ADDRESS_V07,
_78
signer,
_78
safeVersion: '1.4.1',
_78
saltNonce: 120n,
_78
safe4337ModuleAddress,
_78
erc7569LaunchpadAddress
_78
})
_78
_78
const smartAccountClient = createSmartAccountClient({
_78
chain: sepolia,
_78
account,
_78
bundlerTransport: http(pimlicoUrl),
_78
middleware: {
_78
gasPrice: async () =>
_78
(await bundlerClient.getUserOperationGasPrice()).fast,
_78
sponsorUserOperation: paymasterClient.sponsorUserOperation
_78
}
_78
}).extend(erc7579Actions({ entryPoint: ENTRYPOINT_ADDRESS_V07 }))
_78
_78
return smartAccountClient as SafeSmartAccountClient
_78
}

It will:

  • Load a PRIVATE_KEY from .env.local (or generate one if it doesn't exist);
  • Create the publicClient, paymasterClient and bundlerClient necessary to initialize the permissionless library;
  • Create an ERC-7579-compatible Safe Smart Account from the generated private key
  • Pass the Safe Smart Account object to createSmartAccountClient to generate a Permissionless client.

We can then call getSmartAccountClient() wherever we need to interact with the Safe via Pimlico.

3. Add Rhinestone module functionality

Create a new file scheduledTransfers.ts in the lib folder:


_10
touch scheduledTransfers.ts

Add the code necessary to create a scheduled transfer using Rhinestone's ScheduledTransfers module:


_92
import {
_92
getScheduledTransactionData,
_92
getInstallScheduledTransfersExecutor,
_92
getCreateScheduledTransferAction
_92
} from '@rhinestone/module-sdk'
_92
_92
import { SafeSmartAccountClient } from './permissionless'
_92
_92
export interface ScheduledTransferDataInput {
_92
startDate: number
_92
repeatEvery: number
_92
numberOfRepeats: number
_92
amount: number
_92
recipient: `0x${string}`
_92
}
_92
_92
export const scheduledTransfersModuleAddress =
_92
'0xF1aE317941efeb1ffB103D959EF58170F1e577E0'
_92
const sepoliaUSDCTokenAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8'
_92
_92
export const install7579Module = async (
_92
safe: SafeSmartAccountClient,
_92
scheduledTransferInput: ScheduledTransferDataInput
_92
) => {
_92
const { startDate, repeatEvery, numberOfRepeats, amount, recipient } =
_92
scheduledTransferInput
_92
const scheduledTransaction = {
_92
startDate,
_92
repeatEvery,
_92
numberOfRepeats,
_92
token: {
_92
token_address: sepoliaUSDCTokenAddress as `0x${string}`,
_92
decimals: 6
_92
},
_92
amount,
_92
recipient
_92
}
_92
_92
const executionData = getScheduledTransactionData({
_92
scheduledTransaction
_92
})
_92
_92
const scheduledTransfersModule = getInstallScheduledTransfersExecutor({
_92
executeInterval: repeatEvery,
_92
numberOfExecutions: numberOfRepeats,
_92
startDate,
_92
executionData
_92
})
_92
_92
const txHash = await safe.installModule({
_92
type: 'executor',
_92
address: scheduledTransfersModuleAddress,
_92
context: scheduledTransfersModule.data as `0x${string}`
_92
})
_92
_92
console.log(
_92
'Scheduled transfers module is being installed: https://sepolia.etherscan.io/tx/' +
_92
txHash
_92
)
_92
_92
return txHash
_92
}
_92
_92
export const scheduleTransfer = async (
_92
safe: SafeSmartAccountClient,
_92
scheduledTransferInput: ScheduledTransferDataInput
_92
) => {
_92
const { startDate, repeatEvery, numberOfRepeats, amount, recipient } =
_92
scheduledTransferInput
_92
const scheduledTransaction = {
_92
startDate,
_92
repeatEvery,
_92
numberOfRepeats,
_92
token: {
_92
token_address: sepoliaUSDCTokenAddress as `0x${string}`,
_92
decimals: 6
_92
},
_92
amount,
_92
recipient
_92
}
_92
_92
const scheduledTransactionData = getCreateScheduledTransferAction({
_92
scheduledTransaction
_92
})
_92
const txHash = await safe.sendTransaction({
_92
to: scheduledTransactionData.target,
_92
value: scheduledTransactionData.value as bigint,
_92
data: scheduledTransactionData.callData
_92
})
_92
_92
return txHash
_92
}

This file contains two functions:

  • install7579Module will install the module to a Safe and schedule its first transfer;
  • scheduleTransfer to schedule subsequent transfers in a Safe where the module has been previously installed.

In the UI, we can then detect whether the Safe has the module installed when a user tries to schedule a transfer. If not, it will run install7579Module; and if it does, it will run scheduleTransfer.

For brevity, we are only covering a simple use case of the ScheduledTransfers module. You can find more information about the module's functionalities in the Rhinestone documentation (opens in a new tab), such as the capacity to schedule recurring transfers, with a pre-determined number of repeats.

4. Add UI components

Now that we have the logic necessary to set up a safe and schedule a transfer, let's create a simple UI to interact with it. Create a new file ScheduledTransferForm.tsx in the components folder:


_10
cd ..
_10
mkdir components
_10
cd components
_10
touch ScheduledTransferForm.tsx

Add the following code to ScheduledTransferForm.tsx:


_155
import { useState, useEffect } from 'react'
_155
_155
import { SafeSmartAccountClient } from '@/lib/permissionless'
_155
import {
_155
install7579Module,
_155
scheduleTransfer,
_155
scheduledTransfersModuleAddress
_155
} from '@/lib/scheduledTransfers'
_155
_155
const ScheduledTransferForm: React.FC<{ safe: SafeSmartAccountClient }> = ({
_155
safe
_155
}) => {
_155
const [recipient, setRecipient] = useState('')
_155
const [amount, setAmount] = useState(0)
_155
const [date, setDate] = useState('')
_155
const [txHash, setTxHash] = useState('')
_155
const [loading, setLoading] = useState(false)
_155
const [error, setError] = useState(false)
_155
const [is7579Installed, setIs7579Installed] = useState(false)
_155
_155
useEffect(() => {
_155
const init7579Module = async () => {
_155
const isModuleInstalled = await safe
_155
.isModuleInstalled({
_155
type: 'executor',
_155
address: scheduledTransfersModuleAddress,
_155
context: '0x'
_155
})
_155
.catch(() => false)
_155
if (isModuleInstalled) {
_155
setIs7579Installed(true)
_155
}
_155
}
_155
void init7579Module()
_155
}, [safe])
_155
_155
return (
_155
<>
_155
<div style={{ marginTop: '40px' }}>Your Safe: {safe.account.address}</div>{' '}
_155
<div style={{ marginTop: '10px' }}>
_155
ERC-7579 module installed:{' '}
_155
{is7579Installed
_155
? 'Yes ✅'
_155
: 'No, schedule a transfer below to install it!'}{' '}
_155
</div>
_155
<div
_155
style={{
_155
width: '100%',
_155
display: 'flex',
_155
justifyContent: 'space-between',
_155
alignItems: 'center',
_155
marginTop: '40px',
_155
marginBottom: '40px'
_155
}}
_155
>
_155
<div>
_155
<label htmlFor='address'>Address:</label>
_155
<input
_155
style={{ marginLeft: '20px' }}
_155
id='address'
_155
placeholder='0x...'
_155
onChange={e => setRecipient(e.target.value)}
_155
value={recipient}
_155
/>
_155
</div>
_155
<div>
_155
<label htmlFor='amount'>Amount (integer):</label>
_155
<input
_155
style={{ marginLeft: '20px' }}
_155
id='amount'
_155
type='number'
_155
placeholder='1'
_155
min='0'
_155
onChange={e => setAmount(Number(e.target.value))}
_155
value={amount}
_155
/>
_155
</div>
_155
<div>
_155
<label htmlFor='date'>Date/Time:</label>
_155
<input
_155
style={{ marginLeft: '20px' }}
_155
id='date'
_155
type='datetime-local'
_155
onChange={e => setDate(e.target.value)}
_155
value={date}
_155
/>
_155
</div>
_155
_155
<button
_155
disabled={!recipient || !amount || !date || loading}
_155
onClick={async () => {
_155
setLoading(true)
_155
setError(false)
_155
const startDate = new Date(date).getTime() / 1000
_155
const transferInputData = {
_155
startDate: 1710759572,
_155
repeatEvery: 60 * 60 * 24,
_155
numberOfRepeats: 1,
_155
amount,
_155
recipient: recipient as `0x${string}`
_155
}
_155
_155
await (!is7579Installed ? install7579Module : scheduleTransfer)(
_155
safe,
_155
transferInputData
_155
)
_155
.then(txHash => {
_155
setTxHash(txHash)
_155
setLoading(false)
_155
setRecipient('')
_155
setAmount(0)
_155
setDate('')
_155
setIs7579Installed(true)
_155
})
_155
.catch(err => {
_155
console.error(err)
_155
setLoading(false)
_155
setError(true)
_155
})
_155
}}
_155
>
_155
Schedule Transfer
_155
</button>
_155
</div>
_155
<div>
_155
{loading ? <p>Processing, please wait...</p> : null}
_155
{error ? (
_155
<p>
_155
There was an error processing the transaction. Please try again.
_155
</p>
_155
) : null}
_155
{txHash ? (
_155
<>
_155
<p>
_155
Success!{' '}
_155
<a
_155
href={`https://sepolia.etherscan.io/tx/${txHash}`}
_155
target='_blank'
_155
rel='noreferrer'
_155
style={{
_155
textDecoration: 'underline',
_155
fontSize: '14px'
_155
}}
_155
>
_155
View on Etherscan
_155
</a>
_155
</p>
_155
</>
_155
) : null}
_155
</div>
_155
</>
_155
)
_155
}
_155
_155
export default ScheduledTransferForm

This component will provide a form to allow the user to input the amount, receiver address, and date and time for the scheduled transfer. It will detect whether the Safe has the module installed, and then call either install7579Module or scheduleTransfer from the scheduledTransfers.ts file.

Now, edit app/page.tsx to include the ScheduledTransferForm component:


_32
'use client'
_32
_32
import { useState } from 'react'
_32
_32
import {
_32
getSmartAccountClient,
_32
type SafeSmartAccountClient
_32
} from '../lib/permissionless'
_32
import ScheduledTransferForm from '../components/ScheduledTransferForm'
_32
_32
export default function Home () {
_32
const [safe, setSafe] = useState<SafeSmartAccountClient | undefined>()
_32
_32
const handleLoadSafe = async () => {
_32
const safe = await getSmartAccountClient()
_32
setSafe(safe)
_32
}
_32
_32
return (
_32
<>
_32
{safe == null ? (
_32
<>
_32
<button onClick={handleLoadSafe} style={{ marginTop: '40px' }}>
_32
Create Safe
_32
</button>
_32
</>
_32
) : (
_32
<ScheduledTransferForm safe={safe} />
_32
)}
_32
</>
_32
)
_32
}

5. Add styling (optional)

We can add some styling to our app by editing the contents of layout.tsx in app folder:


_87
import type { Metadata } from 'next'
_87
import { Inter } from 'next/font/google'
_87
import Img from 'next/image'
_87
import './globals.css'
_87
_87
const inter = Inter({ subsets: ['latin'] })
_87
_87
export const metadata: Metadata = {
_87
title: 'Safe Tutorial: ERC-7579',
_87
description: 'Generated by create next app'
_87
}
_87
_87
export default function RootLayout ({
_87
children
_87
}: Readonly<{
_87
children: React.ReactNode
_87
}>) {
_87
return (
_87
<html lang='en'>
_87
<body className={inter.className}>
_87
<nav
_87
style={{
_87
display: 'flex',
_87
justifyContent: 'space-between',
_87
padding: '1rem'
_87
}}
_87
>
_87
<a href='https://safe.global'>
_87
<Img width={95} height={36} alt='safe-logo' src='/safe.svg' />
_87
</a>
_87
<div style={{ display: 'flex' }}>
_87
<a
_87
href='https://docs.safe.global/advanced/erc-7579/tutorials/7579-tutorial'
_87
style={{
_87
display: 'flex',
_87
alignItems: 'center',
_87
marginRight: '1rem'
_87
}}
_87
>
_87
Read tutorial{' '}
_87
<Img
_87
width={20}
_87
height={20}
_87
alt='link-icon'
_87
src='/external-link.svg'
_87
style={{ marginLeft: '0.5rem' }}
_87
/>
_87
</a>
_87
<a
_87
href='https://github.com/5afe/safe-tutorial-7579'
_87
style={{ display: 'flex', alignItems: 'center' }}
_87
>
_87
View on GitHub{' '}
_87
<Img
_87
width={24}
_87
height={24}
_87
alt='github-icon'
_87
src='/github.svg'
_87
style={{ marginLeft: '0.5rem' }}
_87
/>
_87
</a>
_87
</div>
_87
</nav>
_87
<div style={{ width: '100%', textAlign: 'center' }}>
_87
<h1>Schedule Transfers</h1>
_87
_87
<div>
_87
Create a new ERC-7579-compatible Safe Smart Account and use it to schedule
_87
transactions.
_87
</div>
_87
</div>
_87
<div
_87
style={{
_87
display: 'flex',
_87
alignItems: 'center',
_87
justifyContent: 'space-between',
_87
marginLeft: '40px',
_87
marginRight: '40px',
_87
flexDirection: 'column'
_87
}}
_87
>
_87
{children}
_87
</div>
_87
</body>
_87
</html>
_87
)
_87
}

This will add some basic styling to the app, including a header. You can also add some custom CSS to globals.css in the same folder:


_28
h1,
_28
h2,
_28
h3 {
_28
margin-top: 40px;
_28
margin-bottom: 10px;
_28
}
_28
_28
button {
_28
cursor: pointer;
_28
border: none;
_28
background: #00e673;
_28
color: black;
_28
padding: 10px 20px;
_28
border-radius: 5px;
_28
margin: 10px 0;
_28
}
_28
_28
input {
_28
padding: 10px;
_28
border-radius: 5px;
_28
border: 1px solid #ccc;
_28
margin: 10px 0;
_28
}
_28
_28
button:disabled {
_28
background: #ccc;
_28
color: #666;
_28
}

Testing your app

That's it! You can find the source code for the example created in this tutorial on GitHub (opens in a new tab). You can now return to your browser and see the app displayed.

screenshot-finalized

Click the Create Safe button to initialize the Permissionless client with the private key you stored on .env.local. It will deploy an ERC-7579-compatible Safe Smart Account on its first transaction.

screenshot

Once loaded, you will be able to choose an amount to send, a receiver address and select a date and time for your scheduled payment. Click Schedule Transfer to send the transaction. The first time you do this, it will deploy the Safe to Sepolia test network and install the ScheduledTransfers module.

Do more with Safe and ERC-7579

We learned how to deploy an ERC-7579-compatible Safe Smart Account and use an ERC-7579-compatible module, the Scheduled Transfer from Rhinestone. We hope you enjoyed this tutorial and that the combination of Safe and 7579 will allow you to tap into new functionalities for your decentralized apps.

As a next step, you can add more functionalities to your app using other ERC-7579-compatible modules (opens in a new tab). Here are some ideas:

You can also find more inspiration on this list of ERC-7579 modules (opens in a new tab). You can also read more about this ERC in our overview (opens in a new tab) or in the official documentation (opens in a new tab).

Did you encounter any difficulties? Let us know by opening an issue (opens in a new tab) or asking a question on Stack Exchange (opens in a new tab) with the safe-core tag.

Was this page helpful?