diff --git a/.eslintrc.js b/.eslintrc.js index 1514a3e1..ee56fa38 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,7 +9,7 @@ module.exports = { parserOptions: { project: './tsconfig.json' }, - ignorePatterns: ['.eslintrc.js', 'next.config.js', 'next-env.d.ts', 'out'], + ignorePatterns: ['.eslintrc.js', 'next.config.js', 'next-env.d.ts', 'out', '**/examples/**'], rules: { '@typescript-eslint/key-spacing': 0, 'multiline-ternary': 0, diff --git a/.github/styles/config/vocabularies/default/accept.txt b/.github/styles/config/vocabularies/default/accept.txt index 6a9440ee..fb7924c5 100644 --- a/.github/styles/config/vocabularies/default/accept.txt +++ b/.github/styles/config/vocabularies/default/accept.txt @@ -107,6 +107,7 @@ EURe EVM EdgeEVM Edgeware +ESLint EtherLite Etherscan Eurus diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 1e5d1040..3e3e2ace 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 409da8b0..747a4ac2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8 diff --git a/assets/7579-tutorial-2.png b/assets/7579-tutorial-2.png new file mode 100644 index 00000000..9ee441a3 Binary files /dev/null and b/assets/7579-tutorial-2.png differ diff --git a/assets/7579-tutorial-3.png b/assets/7579-tutorial-3.png new file mode 100644 index 00000000..4c81b9de Binary files /dev/null and b/assets/7579-tutorial-3.png differ diff --git a/assets/7579-tutorial.png b/assets/7579-tutorial.png new file mode 100644 index 00000000..0538f965 Binary files /dev/null and b/assets/7579-tutorial.png differ 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/examples/erc-7579/app/layout.tsx b/examples/erc-7579/app/layout.tsx new file mode 100644 index 00000000..1c325b47 --- /dev/null +++ b/examples/erc-7579/app/layout.tsx @@ -0,0 +1,87 @@ +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: ERC-7579', + description: 'Generated by create next app' +} + +export default function RootLayout ({ + children +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + +
+

Schedule Transfers

+ +
+ Create a new 7579 compatible Safe Account and use it to schedule + transactions. +
+
+
+ {children} +
+ + + ) +} diff --git a/examples/erc-7579/app/page.tsx b/examples/erc-7579/app/page.tsx new file mode 100644 index 00000000..f55fc6b5 --- /dev/null +++ b/examples/erc-7579/app/page.tsx @@ -0,0 +1,32 @@ +'use client' + +import { useState } from 'react' + +import { + getSmartAccountClient, + type SafeSmartAccountClient +} from '../lib/permissionless' +import ScheduledTransferForm from '../components/ScheduledTransferForm' + +export default function Home () { + const [safe, setSafe] = useState() + + const handleLoadSafe = async () => { + const safe = await getSmartAccountClient() + setSafe(safe) + } + + return ( + <> + {safe == null ? ( + <> + + + ) : ( + + )} + + ) +} diff --git a/examples/erc-7579/components/ScheduledTransferForm.tsx b/examples/erc-7579/components/ScheduledTransferForm.tsx new file mode 100644 index 00000000..026ae0a3 --- /dev/null +++ b/examples/erc-7579/components/ScheduledTransferForm.tsx @@ -0,0 +1,154 @@ +import { useState, useEffect } from 'react' + +import { SafeSmartAccountClient } from '@/lib/permissionless' +import { + install7579Module, + scheduleTransfer, + scheduledTransfersModuleAddress +} from '@/lib/scheduledTransfers' + +const ScheduledTransferForm: React.FC<{ safe: SafeSmartAccountClient }> = ({ + safe +}) => { + const [recipient, setRecipient] = useState('') + const [amount, setAmount] = useState(0) + const [date, setDate] = useState('') + const [txHash, setTxHash] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(false) + const [is7579Installed, setIs7579Installed] = useState(false) + + useEffect(() => { + const init7579Module = async () => { + const isModuleInstalled = await safe + .isModuleInstalled({ + type: 'executor', + address: scheduledTransfersModuleAddress, + context: '0x' + }) + .catch(() => false) + if (isModuleInstalled) { + setIs7579Installed(true) + } + } + void init7579Module() + }, [safe]) + + return ( + <> +
Your Safe: {safe.account.address}
{' '} +
+ ERC-7579 module installed:{' '} + {is7579Installed + ? 'Yes ✅' + : 'No, schedule a transfer below to install it!'}{' '} +
+
+
+ + setRecipient(e.target.value)} + value={recipient} + /> +
+
+ + setAmount(Number(e.target.value))} + value={amount} + /> +
+
+ + setDate(e.target.value)} + value={date} + /> +
+ + +
+
+ {loading ?

Processing, please wait...

: null} + {error ? ( +

+ There was an error processing the transaction. Please try again. +

+ ) : null} + {txHash ? ( + <> +

+ Success!{' '} + + View on Etherscan + +

+ + ) : null} +
+ + ) +} + +export default ScheduledTransferForm diff --git a/examples/erc-7579/lib/permissionless.ts b/examples/erc-7579/lib/permissionless.ts new file mode 100644 index 00000000..e4587304 --- /dev/null +++ b/examples/erc-7579/lib/permissionless.ts @@ -0,0 +1,78 @@ +import { Hex, createPublicClient, http, Chain, Transport } from 'viem' +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' +import { sepolia } from 'viem/chains' +import { + ENTRYPOINT_ADDRESS_V07, + createSmartAccountClient, + SmartAccountClient +} from 'permissionless' +import { + signerToSafeSmartAccount, + SafeSmartAccount +} from 'permissionless/accounts' +import { erc7579Actions, Erc7579Actions } from 'permissionless/actions/erc7579' +import { + createPimlicoBundlerClient, + createPimlicoPaymasterClient +} from 'permissionless/clients/pimlico' +import { EntryPoint } from 'permissionless/types' + +export type SafeSmartAccountClient = SmartAccountClient< + EntryPoint, + Transport, + Chain, + SafeSmartAccount +> & + Erc7579Actions> + +const pimlicoUrl = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${process.env.NEXT_PUBLIC_PIMLICO_API_KEY}` +const safe4337ModuleAddress = '0x3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2' +const erc7569LaunchpadAddress = '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE' + +const privateKey = + (process.env.NEXT_PUBLIC_PRIVATE_KEY as Hex) ?? + (() => { + const pk = generatePrivateKey() + console.log('Private key to add to .env.local:', `PRIVATE_KEY=${pk}`) + return pk + })() + +const signer = privateKeyToAccount(privateKey) + +const publicClient = createPublicClient({ + transport: http('https://rpc.ankr.com/eth_sepolia') +}) + +const paymasterClient = createPimlicoPaymasterClient({ + transport: http(pimlicoUrl), + entryPoint: ENTRYPOINT_ADDRESS_V07 +}) + +const bundlerClient = createPimlicoBundlerClient({ + transport: http(pimlicoUrl), + entryPoint: ENTRYPOINT_ADDRESS_V07 +}) + +export const getSmartAccountClient = async () => { + const account = await signerToSafeSmartAccount(publicClient, { + entryPoint: ENTRYPOINT_ADDRESS_V07, + signer, + safeVersion: '1.4.1', + saltNonce: 120n, + safe4337ModuleAddress, + erc7569LaunchpadAddress + }) + + const smartAccountClient = createSmartAccountClient({ + chain: sepolia, + account, + bundlerTransport: http(pimlicoUrl), + middleware: { + gasPrice: async () => + (await bundlerClient.getUserOperationGasPrice()).fast, + sponsorUserOperation: paymasterClient.sponsorUserOperation + } + }).extend(erc7579Actions({ entryPoint: ENTRYPOINT_ADDRESS_V07 })) + + return smartAccountClient as SafeSmartAccountClient +} diff --git a/examples/erc-7579/lib/scheduledTransfers.ts b/examples/erc-7579/lib/scheduledTransfers.ts new file mode 100644 index 00000000..666c09b2 --- /dev/null +++ b/examples/erc-7579/lib/scheduledTransfers.ts @@ -0,0 +1,79 @@ +import { + getScheduledTransactionData, + getCreateScheduledTransferAction +} from '@rhinestone/module-sdk' + +export interface ScheduledTransferDataInput { + startDate: number + repeatEvery: number + numberOfRepeats: number + amount: number + recipient: `0x${string}` +} + +export const scheduledTransfersModuleAddress = + '0xF1aE317941efeb1ffB103D959EF58170F1e577E0' +const sepoliaUSDCTokenAddress = '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8' + +export const install7579Module = async ( + safe: any, + scheduledTransferInput: ScheduledTransferDataInput +) => { + const { startDate, repeatEvery, numberOfRepeats, amount, recipient } = + scheduledTransferInput + const scheduledTransaction = { + startDate, + repeatEvery, + numberOfRepeats, + token: { + token_address: sepoliaUSDCTokenAddress as `0x${string}`, + decimals: 6 + }, + amount, + recipient + } + + const scheduledTransactionData = getScheduledTransactionData({ + scheduledTransaction + }) + const txHash = await safe.installModule({ + type: 'executor', + address: scheduledTransfersModuleAddress, + context: scheduledTransactionData + }) + + console.log( + 'Scheduled transfers module is being installed: https://sepolia.etherscan.io/tx/' + + txHash + ) +} + +export const scheduleTransfer = async ( + safe: any, + scheduledTransferInput: ScheduledTransferDataInput +) => { + const { startDate, repeatEvery, numberOfRepeats, amount, recipient } = + scheduledTransferInput + const scheduledTransaction = { + startDate, + repeatEvery, + numberOfRepeats, + token: { + token_address: sepoliaUSDCTokenAddress as `0x${string}`, + decimals: 6 + }, + amount, + recipient + } + + const scheduledTransactionData = getCreateScheduledTransferAction({ + scheduledTransaction + }) + const txHash = await safe.sendTransaction({ + to: scheduledTransactionData.target, + value: scheduledTransactionData.value as bigint, + data: scheduledTransactionData.callData + }) + + return txHash +} diff --git a/pages/advanced/erc-7579/_meta.json b/pages/advanced/erc-7579/_meta.json index 81e1e09a..a3bdbf31 100644 --- a/pages/advanced/erc-7579/_meta.json +++ b/pages/advanced/erc-7579/_meta.json @@ -1,5 +1,6 @@ { "overview": "Overview", "7579-safe": "Safe and ERC-7579", - "developer-tooling": "Developer Tooling" + "developer-tooling": "Developer Tooling", + "tutorials": "Tutorials" } \ No newline at end of file diff --git a/pages/advanced/erc-7579/tutorials/7579-tutorial.mdx b/pages/advanced/erc-7579/tutorials/7579-tutorial.mdx new file mode 100644 index 00000000..349e1e3a --- /dev/null +++ b/pages/advanced/erc-7579/tutorials/7579-tutorial.mdx @@ -0,0 +1,210 @@ +import { Callout } from 'nextra/components' + +# **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](https://docs.safe.global/advanced/erc-7579/overview) 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](https://docs.rhinestone.wtf/module-sdk/modules/scheduled-transfers) 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](../../../../assets/7579-tutorial.png) + +## Prerequisites + +**Prerequisite knowledge:** You will need some basic experience with [React](https://react.dev/learn), [Next.js](https://nextjs.org/docs), [ERC-4337](https://docs.safe.global/home/4337-overview) and [ERC-7579](https://docs.safe.global/advanced/erc-7579/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-7579-tutorial) for this tutorial. + +## **1. Setup a Next.js application** + +Initialize a new Next.js app using pnpm with the following command: + +```shell +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](https://docs.pimlico.io/permissionless) to set up a Safe and interact with it, Rhinestone's [Module SDK](https://docs.rhinestone.wtf/module-sdk) to install and use core modules, and [viem](https://www.npmjs.com/package/viem) 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: + +```shell +pnpm add permissionless viem @rhinestone/module-sdk +``` + +Now, create a file named `.env.local` at the root of your project, and add your Pimlico API key to it: + +```shell +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: + +```shell +pnpm dev +``` + +Go to `http://localhost:3000` in your browser to see the default Next.js application. + +![screenshot-default](../../../../assets/next.png) + +## 2. Initialize `permissionless` client + +Create a `lib` folder at the project root, and add a file `permissionless.ts`: + +```shell +mkdir lib +cd lib +touch permissionless.ts +``` + +Add the code necessary to create Pimlico's `smartAccountClient` by adding this content to `permissionless.ts` : + +```typescript +// from ../../../../examples/erc-7579/lib/permissionless.ts +``` + +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: + +```shell +touch scheduledTransfers.ts +``` + +Add the code necessary to create a scheduled transfer using Rhinestone's `ScheduledTransfers` module: +```typescript +// from ../../../../examples/erc-7579/lib/scheduledTransfers.ts +``` + +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](https://docs.rhinestone.wtf/module-sdk/modules/scheduled-transfers), 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: + +```shell +cd .. +mkdir components +cd components +touch ScheduledTransferForm.tsx +``` + +Add the following code to `ScheduledTransferForm.tsx`: + +```tsx +// from ../../../../examples/erc-7579/components/ScheduledTransferForm.tsx +``` + +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: + +```tsx +// from ../../../../examples/erc-7579/app/page.tsx +``` + +## 5. Add styling (optional) + +We can add some styling to our app by editing the contents of `layout.tsx` in `app` folder: + +```tsx +// from ../../../../examples/erc-7579/app/layout.tsx +``` + +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: + +```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; +} + +input { + padding: 10px; + border-radius: 5px; + border: 1px solid #ccc; + margin: 10px 0; +} + +button:disabled { + background: #ccc; + color: #666; +} +``` + +## **Testing your app** + +That's it! You can find the source code for the example created in this tutorial [on GitHub](https://github.com/5afe/safe-7559-tutorial). You can now return to your browser and see the app displayed. + +![screenshot-finalized](../../../../assets/7579-tutorial-2.png) + +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](../../../../assets/7579-tutorial-3.png) + +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](https://docs.rhinestone.wtf/module-sdk/modules/ownable-validator). Here are some ideas: + +- [Create a dead man switch](https://docs.rhinestone.wtf/module-sdk/modules/deadman-switch) in case your account becomes inactive after a certain amount of time. +- Add [multi-factor validation](https://docs.rhinestone.wtf/module-sdk/modules/mfa-validator) to your Smart Account. +- [Designate an owner](https://docs.rhinestone.wtf/module-sdk/modules/ownable-executor) that can act on your behalf for executing transactions and paying for gas. + +You can also find more inspiration on this list of [ERC-7579 modules](https://erc7579.com/modules). You can also read more about this ERC in our [overview](https://docs.safe.global/advanced/erc-7579/overview) or in the [official documentation](https://erc7579.com/). + +Did you encounter any difficulties? Let us know by opening [an issue](https://github.com/5afe/safe-7559-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/advanced/erc-7579/tutorials/_meta.json b/pages/advanced/erc-7579/tutorials/_meta.json new file mode 100644 index 00000000..8cf0e512 --- /dev/null +++ b/pages/advanced/erc-7579/tutorials/_meta.json @@ -0,0 +1,3 @@ +{ + "7579-tutorial": "Build an app with Safe and ERC-7579" +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index aea04424..6eb3fde3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,5 @@ ".eslintrc.js", "**/scripts/**/*" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", "**/examples/**"] }