ModuleSDK
1: Install and use your first module

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: '0x7579EE8307284F293B1927136486880611F20002', 
  erc7579LaunchpadAddress: '0x7579011aB74c46090561ea277Ba79D510c6C00ff',
  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,
})