Tutorial 1: Install and use your first module
In this tutorial, you will learn how to install and use a module on your account, in this case a Safe7579 account.
We will first set up the smart account, install the Deadman Switch Module, and then take over the account with the guardian.
Install the packages
First, install the required packages. We use the latest version of module sdk, permissionless ^0.2 and viem ^2.21.
npm i viem @rhinestone/module-sdk permissionless
Import the required functions and constants
import { createPublicClient, Hex, http } from 'viem'
import { getAccountNonce } from 'permissionless/actions'
import { createSmartAccountClient } from 'permissionless'
import { toSafeSmartAccount } from 'permissionless/accounts'
import { erc7579Actions } from 'permissionless/actions/erc7579'
import { createPimlicoClient } from 'permissionless/clients/pimlico'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
import {
createPaymasterClient,
entryPoint07Address,
getUserOperationHash,
} from 'viem/account-abstraction'
import {
RHINESTONE_ATTESTER_ADDRESS,
MOCK_ATTESTER_ADDRESS,
getDeadmanSwitch,
getAccount,
getClient,
getDeadmanSwitchValidatorMockSignature,
getTrustAttestersAction,
encodeModuleInstallationData,
encodeValidatorNonce,
} from '@rhinestone/module-sdk'
Create the clients
Create the smart account client, the bundler client and the paymaster client. You will need to add your own urls here.
const publicClient = createPublicClient({
transport: http(rpcUrl),
chain: chain,
})
const pimlicoClient = createPimlicoClient({
transport: http(bundlerUrl),
entryPoint: {
address: entryPoint07Address,
version: '0.7',
},
})
const paymasterClient = createPaymasterClient({
transport: http(paymasterUrl),
})
Create the signer
The Safe account will need to have a signer to sign user operations. In permissionless.js, the default Safe account validates ECDSA signatures.
For example, to create a signer based on a private key:
const owner = privateKeyToAccount(generatePrivateKey())
Create the Safe account
Create the Safe account object using the signer. Note that you should only use the MockAttester
on testnets.
const safeAccount = await toSafeSmartAccount({
client: publicClient,
owners: [owner],
version: '1.4.1',
entryPoint: {
address: entryPoint07Address,
version: '0.7',
},
safe4337ModuleAddress: '0x3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2', // These are not meant to be used in production as of now.
erc7579LaunchpadAddress: '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE',
attesters: [
RHINESTONE_ATTESTER_ADDRESS, // Rhinestone Attester
MOCK_ATTESTER_ADDRESS, // Mock Attester - do not use in production
],
attestersThreshold: 1,
})
Create the smart account client
The smart account client is used to interact with the smart account. You will need to add your own bundler url and the chain that you are using.
const smartAccountClient = createSmartAccountClient({
account: safeAccount,
chain: chain,
bundlerTransport: http(bundlerUrl),
paymaster: paymasterClient,
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast
},
},
}).extend(erc7579Actions())
Install the Deadman Switch Module
Next, we will install the Deadman Switch Module on the Safe account. This requires creating a nominee. Then, we will need to install the module as both a validator and a hook. The second time this installation happens, we do not need to pass the initialization data again, but will instead pass empty data. However, we still need to encode this empty data so that it can be correctly interpreted by the account.
const nominee = privateKeyToAccount(
'0xc171c45f3d35fad832c53cade38e8d21b8d5cc93d1887e867fac626c1c0d6be7',
)
const account = getAccount({
address: safeAccount.address,
type: 'safe',
})
const client = getClient({
rpcUrl,
})
const deadmanSwitch = await getDeadmanSwitch({
account,
client,
nominee: nominee.address,
timeout: 1,
moduleType: 'validator',
})
const opHash1 = await smartAccountClient.installModule(deadmanSwitch)
await pimlicoClient.waitForUserOperationReceipt({
hash: opHash1,
})
const opHash2 = await smartAccountClient.installModule({
type: 'hook',
address: deadmanSwitch.module,
context: encodeModuleInstallationData({
account,
module: {
...deadmanSwitch,
initData: '0x',
type: 'hook',
},
}),
})
await pimlicoClient.waitForUserOperationReceipt({
hash: opHash2,
})
Wait for the timeout to expire
Since we set our timeout to 1 second, we can wait for the timeout to expire. In a production environment, setting a low timeout will mean that it will be easier for a hostile nominee to take over the account.
await new Promise((resolve) => setTimeout(resolve, 10000))
Create the takeover UserOperation
Now, we will create a UserOperation from the nominee. The calldata, in this case to the Module Registry is entirely random and a nominee will be able to do any action.
const nonce = await getAccountNonce(publicClient, {
address: safeAccount.address,
entryPointAddress: entryPoint07Address,
key: encodeValidatorNonce({ account, validator: deadmanSwitch }),
})
const trustAttestersAction = getTrustAttestersAction({
threshold: 1,
attesters: [
RHINESTONE_ATTESTER_ADDRESS, // Rhinestone Attester
],
})
const userOperation = await smartAccountClient.prepareUserOperation({
account: safeAccount,
calls: [trustAttestersAction],
nonce: nonce,
signature: getDeadmanSwitchValidatorMockSignature() as Hex,
})
Sign the taekover UserOperation
Next, the nominee will have to sign the recovery UserOperation.
const userOpHashToSign = getUserOperationHash({
chainId: chain.id,
entryPointAddress: entryPoint07Address,
entryPointVersion: '0.7',
userOperation,
})
userOperation.signature = await nominee.signMessage({
message: { raw: userOpHashToSign },
})
Execute the takeover UserOperation
Finally, we can execute the UserOperation to take over the account.
const userOpHash = await smartAccountClient.sendUserOperation(userOperation)
const receipt = await pimlicoClient.waitForUserOperationReceipt({
hash: userOpHash,
})