Skip to main content

Recover a Safe Account with Google using AbstractionKit

This is the second of two guides that demonstrate how to recover a Safe smart wallet using a Google account. The first guide focuses on adding the recovery method, while this guide focuses on executing the actual recovery process. We recommend starting with guide number one which can be found here.

What is AbstractionKit?

AbstractionKit is a Typescript Library that enables developers to easily build on Account Abstraction, with first class support for Safe Accounts. One of the unique use cases enabled by AbstractionKit is the ability for users to add a recovery method(s) as a backup to their account in the case that they lose access to their main signing key.

You can combine Lit with AbstractionKit to enable a powerful social recovery experience for your users while using Smart Accounts for gas sponsorship, transaction batching, and more.

For additional information during this guide:

Complete Code Example

If you would like to see the complete code example, you can find it here.

Installation

Install required dependencies

npm i abstractionkit@0.1.12 @lit-protocol/lit-node-client @lit-protocol/lit-auth-client @lit-protocol/constants

Configure .env file

Configure the following values within the repository .env file:

// Lit
LIT_API_KEY= // Request Relay Server API Key from Lit at https://forms.gle/RNZYtGYTY9BcD9MEA

// Candide
BUNDLER_URL="https://sepolia.voltaire.candidewallet.com/rpc" // Other networks are found here: https://docs.candide.dev/wallet/bundler/rpc-endpoints
PAYMASTER_URL= // Request an API key from Candide on Discord

// Generate a Public/Private Key
OWNER_PUBLIC_ADDRESS=
OWNER_PRIVATE_KEY=
NEW_OWNER_PUBLIC_ADDRESS=

// Network Info
VITE_CHAIN_ID=
JSON_RPC_NODE_PROVIDER= // Get an RPC from a Node provider

Sign in with Google using Lit

Initialize the Lit Network Connection and GoogleProvider

  • Connect to the Lit Network using LitNodeClient.
  • Set up the LitAuthClient for authentication.
  • Initialize a GoogleProvider for Google sign-in.
import { LitNodeClient } from "@lit-protocol/lit-node-client";
import { LitAuthClient, GoogleProvider } from "@lit-protocol/lit-auth-client";
import { ProviderType, LitNetwork } from "@lit-protocol/constants";

const initalizeClientsAndProvider = async () => {
const litNodeClient = new LitNodeClient({
litNetwork: LitNetwork.DatilDev,
debug: true,
});
await litNodeClient.connect();

const litAuthClient = new LitAuthClient({
litRelayConfig: {
relayApiKey: process.env.LIT_API_KEY,
},
litNodeClient,
});

console.log("Connected to Lit Node and Lit Auth Clients ✔️");

const provider = litAuthClient.initProvider<GoogleProvider>(
ProviderType.Google,
{
// redirectUri: The redirect URI Lit's login server should redirect to after a successful login
}
);
return { litNodeClient, litAuthClient, provider };
};

Authentication with Gmail

  • Generate an AuthMethod using the GoogleProvider
  • Check if the user is already authenticated. If not, redirect to Google sign-in
import { AuthMethod } from "@lit-protocol/types";

const generateAuthMethod = async () => {
const url = new URL(window.location.href);
if (!url.searchParams.get("provider")) {
console.log("Signing in with Google...");
provider.signIn((url) => {
window.location.href = url;
});
} else if (url.searchParams.get("provider") === "google") {
const authMethod = await provider.authenticate();
return authMethod;
}
};

const authMethod = await generateAuthMethod();
if (!authMethod) {
return;
}
Mint PKP (Programmable Key Pair)
import { LitAuthClient } from "@lit-protocol/lit-auth-client";

const mintWithGoogle = async (authMethod) => {
const pkp = await litAuthClient.mintPKPWithAuthMethods([authMethod], {
addPkpEthAddressAsPermittedAddress: true
});
console.log("Fetched PKP", pkp);
return pkp;
};

const pkp = await mintWithGoogle(authMethod);
console.log("Minted PKP ✔️");

Get the Google Guardian Signer

import { PKPEthersWallet } from "@lit-protocol/pkp-ethers";
import { LitAbility, LitPKPResource } from "@lit-protocol/auth-helpers";
import { AuthCallbackParams } from "@lit-protocol/types";
import { LIT_RPC } from "@lit-protocol/constants";

const authNeededCallback = async (params: AuthCallbackParams) => {
console.log(`auth needed callback params`, JSON.stringify(params, null, 2));
const response = await litNodeClient.signSessionKey({
statement: params.statement,
authMethods: [authMethod],
resourceAbilityRequests: [
{
resource: new LitPKPResource("*"),
ability: LitAbility.PKPSigning,
},
],
expiration: params.expiration,
resources: params.resources,
chainId: 1,
pkpPublicKey: pkp.pkpPublicKey,
});
return response.authSig;
};

const guardianSigner = new PKPEthersWallet({
litNodeClient,
authContext: {
getSessionSigsProps: {
chain: "ethereum",
expiration: new Date(Date.now() + 60_000 * 60).toISOString(),
resourceAbilityRequests: [
{
resource: new LitPKPResource("*"),
ability: LitAbility.PKPSigning,
},
],
authNeededCallback: authNeededCallback,
},
},
pkpPubKey: pkp.pkpPublicKey,
rpc: LIT_RPC.CHRONICLE_YELLOWSTONE,
});
console.log("Created PKPEthersWallet using the PKP ✔️");

Start the Recovery Process

Initilize the Safe Account Class

import { SafeAccountV0_2_0 as SafeAccount } from "abstractionkit";

const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress]);
console.log("Smart Account Address: ", smartAccount.accountAddress);
Repare The Recovery Transaction
import { SocialRecoveryModule } from "abstractionkit";

const srm = new SocialRecoveryModule();

const initiateRecoveryMetaTx = srm.createConfirmRecoveryMetaTransaction(
smartAccount.accountAddress,
[newOwnerPublicAddress],
1, // new threshold
true // whether to auto-start execution of recovery
);

let userOperationRecovery = await guardianSmartAccount.createUserOperation(
[initiateRecoveryMetaTx],
process.env.JSON_RPC_NODE_PROVIDER,
process.env.BUNDLER_URL
);
import { CandidePaymaster } from "abstractionkit";
import ethers from "ethers"

// Sponsor the recovery transaction using the paymaster
const paymasterUrl = process.env.PAYMASTER_URL;
const paymaster = new CandidePaymaster(paymasterUrl);

userOperationRecovery = await paymaster.createSponsorPaymasterUserOperation(
userOperationRecovery,
process.env.BUNDLER_URL
);

Sign and Submit UserOperation

// Sign
const domain = {
chainId: process.env.CHAIN_ID,
verifyingContract: smartAccount.safe4337ModuleAddress,
};
const types = SafeAccount.EIP712_SAFE_OPERATION_TYPE;
// formate according to EIP712 Safe Operation Type
const { sender, ...userOp } = userOperation;
const safeUserOperation = {
...userOp,
safe: userOperation.sender,
validUntil: BigInt(0),
validAfter: BigInt(0),
entryPoint: smartAccount.entrypointAddress,
};
const signature = await guardianSigner.signTypedData(domain, types, safeUserOperation);
const formatedSig = SafeAccount.formatEip712SignaturesToUseroperationSignature([ownerPublicAddress], [signature]);
userOperationRecovery.signature = signature;

// Submit
const sendUserOpResponseRecovery =
await guardianSmartAccount.sendUserOperation(
userOperationRecovery,
process.env.BUNDLER_URL
);

Monitor UserOp

// Wait for receipt
const userOpReceiptResultRecovery = await sendUserOpResponseRecovery.included();

console.log(userOpReceiptResultRecovery);

Finalize the Recovery

After the grace period is over, you can finalize the recovery

Prepare the Finalization UserOp

const finalizeRecoveryMetaTx = srm.createFinalizeRecoveryMetaTransaction(
smartAccount.accountAddress
);

let userOperationFinalizeRecovery = await guardianSmartAccount.createUserOperation(
[finalizeRecoveryMetaTx],
process.env.JSON_RPC_NODE_PROVIDER,
process.env.BUNDLER_URL
);
// Add gas sponsorship info using paymaster
userOperationFinalizeRecovery = await paymaster.createSponsorPaymasterUserOperation(
userOperationRecovery,
process.env.BUNDLER_URL
);

Sign and Submit

// Sign userOperation
const domain = {
chainId: process.env.CHAIN_ID,
verifyingContract: smartAccount.safe4337ModuleAddress,
};

const types = SafeAccount.EIP712_SAFE_OPERATION_TYPE;

// formate according to EIP712 Safe Operation Type
const { sender, ...userOp } = userOperation;
const safeUserOperation = {
...userOp,
safe: userOperation.sender,
validUntil: BigInt(0),
validAfter: BigInt(0),
entryPoint: smartAccount.entrypointAddress,
};

const signature = await guardianSigner.signTypedData(domain, types, safeUserOperation);
const formatedSig = SafeAccount.formatEip712SignaturesToUseroperationSignature([ownerPublicAddress], [signature]);

userOperationRecovery.signature = signature;


// Submit userOperation
const sendUserOperationResponseRecovery = await guardianSmartAccount.sendUserOperation(
userOperationRecovery,
process.env.BUNDLER_URL
);

Monitor UserOperation

const userOperationReceiptResultRecovery = await sendUserOperationResponseRecovery.included();

console.log(userOperationReceiptResultRecovery);

That's it! You've successfully recovered an account with a Google Account using Lit.

Find here the complete doc page for Account Recovery.