ModuleSDK Guide
ModuleSDK is a TypeScript library for using ERC7579 smart account modules. It allows you to install and uninstall modules for any ERC7579-compatible account, interact with said modules using dedicated utilities, and can be used with existing SDKs such as permissionless.js, Biconomy, ZeroDev and more.
You can see the full implementation used in this guide here.
Prerequisites
Setting Up the Environment
First, install the necessary packages by running:
npm install @rhinestone/module-sdk permissionless viem wagmi axios circomlibjs
Import the required modules in your script:
import { createSmartAccountClient } from "permissionless";
import { entryPoint07Address } from "viem/account-abstraction";
import { toSafeSmartAccount } from "permissionless/accounts";
import { erc7579Actions } from "permissionless/actions/erc7579";
import {
createPublicClient,
encodeAbiParameters,
encodeFunctionData,
http,
keccak256,
parseAbiParameters,
bytesToHex,
parseAbi,
toFunctionSelector,
toHex,
} from "viem";
import { baseSepolia } from "viem/chains";
import { readContract } from "wagmi/actions";
import axios from "axios";
import { buildPoseidon } from "circomlibjs";
import { getOwnableValidator, getUniversalEmailRecoveryExecutor } from "@rhinestone/module-sdk";
Configuring the Clients
Set up your API keys and URLs:
const owner = "YOUR_OWNER_ADDRESS";
const owner2 = "YOUR_OWNER_2_ADDRESS";
const apiKey = "YOUR_PIMLICO_API_KEY";
const rpcApiKey = "YOUR_ALCHEMY_RPC_API_KEY";
const bundlerUrl = `https://api.pimlico.io/v2/basesepolia/rpc?apikey=${apiKey}`;
const rpcUrl = `https://base-sepolia.g.alchemy.com/v2/${rpcApiKey}`;
Create the public client and the Pimlico client:
const publicClient = createPublicClient({
transport: http(rpcUrl),
});
const pimlicoClient = createSmartAccountClient({
transport: http(bundlerUrl),
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
});
Setting Up the Safe Account
Initialize your Safe account with the necessary parameters:
const ownableValidator = getOwnableValidator({
owners: [owner, owner2],
threshold: 2,
});
const safeAccount = await toSafeSmartAccount({
client: publicClient,
owners: [owner],
version: "1.4.1",
entryPoint: {
address: entryPoint07Address,
version: "0.7",
},
safe4337ModuleAddress: "0x7579EE8307284F293B1927136486880611F20002",
erc7579LaunchpadAddress: "0x7579011aB74c46090561ea277Ba79D510c6C00ff",
attesters: ["0x000000333034E9f539ce08819E12c1b8Cb29084d"], // Rhinestone's attester address
attestersThreshold: 1,
validators: [
{
address: ownableValidator.address,
context: ownableValidator.initData,
},
],
});
const safeWalletAddress = "YOUR_SAFE_WALLET_ADDRESS";
Create the smart account client and extend it with ERC-7579 actions:
const smartAccountClient = createSmartAccountClient({
account: safeAccount,
chain: baseSepolia,
bundlerTransport: http(bundlerUrl),
paymaster: pimlicoClient,
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast;
},
},
}).extend(erc7579Actions());
Installing the Module and Configuring Recovery
Set up the Universal Email Recovery Module:
const universalEmailRecoveryModuleAddress = "0x636632FA22052d2a4Fb6e3Bab84551B620b9C1F9";
const guardianEmail = "guardian@gmail.com";
Generate a random account code using Poseidon:
const poseidon = await buildPoseidon();
const accountCodeBytes: Uint8Array = poseidon.F.random();
const accountCode = bytesToHex(accountCodeBytes.reverse());
Fetch the guardian salt by sending a POST request:
const { guardianSalt } = await axios.post(`${relayerApiUrl}/getAccountSalt`, {
account_code: accountCode.slice(2),
email_addr: guardianEmail,
});
Compute the guardian address:
const guardianAddr = await readContract({
abi: universalEmailRecoveryModuleAbi,
address: universalEmailRecoveryModuleAddress,
functionName: "computeEmailAuthAddress",
args: [safeWalletAddress, guardianSalt],
});
Prepare the module data for installation:
const validator = ownableValidator.address;
const isInstalledContext = toHex(0);
const initialSelector = toFunctionSelector("setThreshold(uint256)");
const guardians = [guardianAddr];
const weights = [1n];
const threshold = 1n;
const delay = 6n * 60n * 60n; // 6 hours
const expiry = 2n * 7n * 24n * 60n * 60n; // 2 weeks in seconds
const emailRecoveryModule = getUniversalEmailRecoveryExecutor({
validator,
isInstalledContext,
initialSelector,
guardians,
weights,
threshold,
delay,
expiry,
chainId: baseSepolia.id,
});
Install the module:
const userOpHash = await smartAccountClient.installModule(emailRecoveryModule);
const receipt = await pimlicoClient.waitForUserOperationReceipt({
hash: userOpHash,
});
Handling Acceptance
Fetch the acceptance command template:
const subject = await readContract({
abi: universalEmailRecoveryModuleAbi,
address: universalEmailRecoveryModuleAddress,
functionName: "acceptanceCommandTemplates",
args: [],
});
const templateIdx = 0;
const handleAcceptanceCommand = subject[0]
.join(" ")
.replace("{ethAddr}", safeWalletAddress);
Send the acceptance request:
const { data: handleAcceptanceData } = await axios.post(
`${relayerApiUrl}/acceptanceRequest`,
{
controller_eth_addr: universalEmailRecoveryModuleAddress,
guardian_email_addr: guardianEmail,
account_code: accountCode,
template_idx: templateIdx,
command: handleAcceptanceCommand,
}
);
const { request_id: requestId } = handleAcceptanceData;
Handling Recovery
Fetch the recovery command template:
const processRecoveryCommand = await readContract({
abi: universalEmailRecoveryModuleAbi,
address: universalEmailRecoveryModuleAddress,
functionName: "recoveryCommandTemplates",
args: [],
});
Send the recovery request:
See the following example for how to construct the recovery command. We assume that the user has lost access to single signer but still has access to the second one. In this case, the easiest way to recover the OwnableValidator is to reduce the threshold to 1 and then the user can add another signer instead.
const { data: processRecoveryData } = await axios.post(
`${relayerApiUrl}/recoveryRequest`,
{
controller_eth_addr: universalEmailRecoveryModuleAddress,
guardian_email_addr: guardianEmail,
template_idx: templateIdx,
command: processRecoveryCommand,
}
);
const { request_id: processRecoveryDataRequestId } = processRecoveryData;
Completing the Recovery
Set up the parameters for reducing the threshold:
const ownableValidatorAbi = parseAbi([
"function setThreshold(uint256 _threshold) external",
]);
const recoveryCallData = encodeFunctionData({
abi: ownableValidatorAbi,
functionName: "setThreshold",
args: [1n],
});
const recoveryData = encodeAbiParameters(
parseAbiParameters("address, bytes"),
[safeWalletAddress, recoveryCallData]
);
Complete the recovery process:
const { data } = await axios.post(`${relayerApiUrl}/completeRequest`, {
controller_eth_addr: universalEmailRecoveryModuleAddress,
account_eth_addr: safeWalletAddress,
complete_calldata: recoveryData,
});