-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add 7579 tutorial * Move example code in its own top-level folder * Ignore type validation and linter for example files * Fix vale errors * Fix typo * Implement requested changes * Edit screenshots * Update pages/advanced/erc-7579/tutorials/7579-tutorial.mdx Co-authored-by: Konrad <[email protected]> * Implement requested changes * Implement requested changes * Implement requested changes * Fix deploy workflows * Fix typo --------- Co-authored-by: Konrad <[email protected]>
- Loading branch information
Showing
17 changed files
with
650 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,6 +107,7 @@ EURe | |
EVM | ||
EdgeEVM | ||
Edgeware | ||
ESLint | ||
EtherLite | ||
Etherscan | ||
Eurus | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<html lang='en'> | ||
<body className={inter.className}> | ||
<nav | ||
style={{ | ||
display: 'flex', | ||
justifyContent: 'space-between', | ||
padding: '1rem' | ||
}} | ||
> | ||
<a href='https://safe.global'> | ||
<Img width={95} height={36} alt='safe-logo' src='/safe.svg' /> | ||
</a> | ||
<div style={{ display: 'flex' }}> | ||
<a | ||
href='https://docs.safe.global/advanced/erc-7579/tutorials/7579-tutorial' | ||
style={{ | ||
display: 'flex', | ||
alignItems: 'center', | ||
marginRight: '1rem' | ||
}} | ||
> | ||
Read tutorial{' '} | ||
<Img | ||
width={20} | ||
height={20} | ||
alt='link-icon' | ||
src='/external-link.svg' | ||
style={{ marginLeft: '0.5rem' }} | ||
/> | ||
</a> | ||
<a | ||
href='https://github.com/5afe/safe-tutorial-7579' | ||
style={{ display: 'flex', alignItems: 'center' }} | ||
> | ||
View on GitHub{' '} | ||
<Img | ||
width={24} | ||
height={24} | ||
alt='github-icon' | ||
src='/github.svg' | ||
style={{ marginLeft: '0.5rem' }} | ||
/> | ||
</a> | ||
</div> | ||
</nav> | ||
<div style={{ width: '100%', textAlign: 'center' }}> | ||
<h1>Schedule Transfers</h1> | ||
|
||
<div> | ||
Create a new 7579 compatible Safe Account and use it to schedule | ||
transactions. | ||
</div> | ||
</div> | ||
<div | ||
style={{ | ||
display: 'flex', | ||
alignItems: 'center', | ||
justifyContent: 'space-between', | ||
marginLeft: '40px', | ||
marginRight: '40px', | ||
flexDirection: 'column' | ||
}} | ||
> | ||
{children} | ||
</div> | ||
</body> | ||
</html> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SafeSmartAccountClient | undefined>() | ||
|
||
const handleLoadSafe = async () => { | ||
const safe = await getSmartAccountClient() | ||
setSafe(safe) | ||
} | ||
|
||
return ( | ||
<> | ||
{safe == null ? ( | ||
<> | ||
<button onClick={handleLoadSafe} style={{ marginTop: '40px' }}> | ||
Create Safe | ||
</button> | ||
</> | ||
) : ( | ||
<ScheduledTransferForm safe={safe} /> | ||
)} | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<> | ||
<div style={{ marginTop: '40px' }}>Your Safe: {safe.account.address}</div>{' '} | ||
<div style={{ marginTop: '10px' }}> | ||
ERC-7579 module installed:{' '} | ||
{is7579Installed | ||
? 'Yes ✅' | ||
: 'No, schedule a transfer below to install it!'}{' '} | ||
</div> | ||
<div | ||
style={{ | ||
width: '100%', | ||
display: 'flex', | ||
justifyContent: 'space-between', | ||
alignItems: 'center', | ||
marginTop: '40px', | ||
marginBottom: '40px' | ||
}} | ||
> | ||
<div> | ||
<label htmlFor='address'>Address:</label> | ||
<input | ||
style={{ marginLeft: '20px' }} | ||
id='address' | ||
placeholder='0x...' | ||
onChange={e => setRecipient(e.target.value)} | ||
value={recipient} | ||
/> | ||
</div> | ||
<div> | ||
<label htmlFor='amount'>Amount:</label> | ||
<input | ||
style={{ marginLeft: '20px' }} | ||
id='amount' | ||
type='number' | ||
placeholder='1' | ||
onChange={e => setAmount(Number(e.target.value))} | ||
value={amount} | ||
/> | ||
</div> | ||
<div> | ||
<label htmlFor='date'>Date/Time:</label> | ||
<input | ||
style={{ marginLeft: '20px' }} | ||
id='date' | ||
type='datetime-local' | ||
onChange={e => setDate(e.target.value)} | ||
value={date} | ||
/> | ||
</div> | ||
|
||
<button | ||
disabled={!recipient || !amount || !date || loading} | ||
onClick={async () => { | ||
setLoading(true) | ||
setError(false) | ||
const startDate = new Date(date).getTime() / 1000 | ||
const transferInputData = { | ||
startDate: 1710759572, | ||
repeatEvery: 60 * 60 * 24, | ||
numberOfRepeats: 1, | ||
amount, | ||
recipient: recipient as `0x${string}` | ||
} | ||
|
||
await (!is7579Installed ? install7579Module : scheduleTransfer)( | ||
safe, | ||
transferInputData | ||
) | ||
.then(txHash => { | ||
setTxHash(txHash) | ||
setLoading(false) | ||
setRecipient('') | ||
setAmount(0) | ||
setDate('') | ||
setIs7579Installed(true) | ||
}) | ||
.catch(err => { | ||
console.error(err) | ||
setLoading(false) | ||
setError(true) | ||
}) | ||
}} | ||
> | ||
Schedule Transfer | ||
</button> | ||
</div> | ||
<div> | ||
{loading ? <p>Processing, please wait...</p> : null} | ||
{error ? ( | ||
<p> | ||
There was an error processing the transaction. Please try again. | ||
</p> | ||
) : null} | ||
{txHash ? ( | ||
<> | ||
<p> | ||
Success!{' '} | ||
<a | ||
href={`https://sepolia.etherscan.io/tx/${txHash}`} | ||
target='_blank' | ||
rel='noreferrer' | ||
style={{ | ||
textDecoration: 'underline', | ||
fontSize: '14px' | ||
}} | ||
> | ||
View on Etherscan | ||
</a> | ||
</p> | ||
</> | ||
) : null} | ||
</div> | ||
</> | ||
) | ||
} | ||
|
||
export default ScheduledTransferForm |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<EntryPoint> | ||
> & | ||
Erc7579Actions<EntryPoint, SafeSmartAccount<EntryPoint>> | ||
|
||
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 | ||
} |
Oops, something went wrong.