Skip to content

Commit

Permalink
feat: Add cancel recovery feature in burner wallet flow
Browse files Browse the repository at this point in the history
  • Loading branch information
wryonik committed Sep 27, 2024
1 parent 02e446f commit 75056bc
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 90 deletions.
4 changes: 2 additions & 2 deletions src/components/RequestedRecoveries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ const RequestedRecoveries = () => {
toast.success("Recovery Cancelled")
setButtonState(BUTTON_STATES.TRIGGER_RECOVERY)
} catch (err) {
toast.error("Something went wrong while completing recovery process");
toast.error("Something went wrong while cancelling recovery process");
} finally {
setIsCancelRecoveryLoading(false);
}
Expand Down Expand Up @@ -386,7 +386,7 @@ const RequestedRecoveries = () => {
</Grid>
<Grid item xs={12} sm={5.5}>
<InputField
type="email"
type="string"
value={newOwner || ""}
onChange={(e) => setNewOwner(e.target.value)}
label="Requested New Owner Address"
Expand Down
54 changes: 44 additions & 10 deletions src/components/burnerWallet/GuardianSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import {
Typography,
} from "@mui/material";
import {
createSmartAccountClient,
ENTRYPOINT_ADDRESS_V07,
walletClientToSmartAccountSigner,
} from "permissionless";
import { signerToSafeSmartAccount } from "permissionless/accounts";
import { erc7579Actions } from "permissionless/actions/erc7579";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { createWalletClient, custom, WalletClient } from "viem";
import { createWalletClient, custom, http, WalletClient } from "viem";
import { baseSepolia } from "viem/chains";
import { readContract } from "wagmi/actions";
import { publicClient, run } from "./deploy";
import { pimlicoBundlerClient, publicClient, run } from "./deploy";
import {
erc7569LaunchpadAddress,
safe4337ModuleAddress,
Expand All @@ -28,13 +30,15 @@ import { StepsContext } from "../../App";
import infoIcon from "../../assets/infoIcon.svg";
import { STEPS } from "../../constants";
import { useAppContext } from "../../context/AppContextHook";
import { useBurnerAccount } from "../../context/BurnerAccountContext";
import { config } from "../../providers/config";
import { relayer } from "../../services/relayer";
import { genAccountCode, templateIdx } from "../../utils/email";
import { TIME_UNITS } from "../../utils/recoveryDataUtils";
import { useGetSafeAccountAddress } from "../../utils/useGetSafeAccountAddress";
import { Button } from "../Button";
import InputField from "../InputField";
import Loader from "../Loader";

//logic for valid email address check for input
const isValidEmail = (email: string) => {
Expand All @@ -44,6 +48,7 @@ const isValidEmail = (email: string) => {

const GuardianSetup = () => {
const address = useGetSafeAccountAddress();
const { setBurnerAccountClient } = useBurnerAccount();

const { guardianEmail, setGuardianEmail, accountCode, setAccountCode } =
useAppContext();
Expand All @@ -59,7 +64,7 @@ const GuardianSetup = () => {
const [isWalletPresent, setIsWalletPresent] = useState(false);
const [emailError, setEmailError] = useState(false);
const [recoveryDelayUnit, setRecoveryDelayUnit] = useState(
TIME_UNITS.SECS.value,
TIME_UNITS.SECS.value
);
// const [recoveryExpiryUnit, setRecoveryExpiryUnit] = useState(
// TIME_UNITS.DAYS.value,
Expand Down Expand Up @@ -142,7 +147,7 @@ const GuardianSetup = () => {

const guardianSalt = await relayer.getAccountSalt(
acctCode,
guardianEmail,
guardianEmail
);

// The guardian address is generated by sending the user's account address and guardian salt to the computeEmailAuthAddress function
Expand All @@ -153,11 +158,36 @@ const GuardianSetup = () => {
args: [safeAccount.address, guardianSalt],
})) as string;

const smartAccountClient = createSmartAccountClient({
account: safeAccount,
entryPoint: ENTRYPOINT_ADDRESS_V07,
chain: baseSepolia,
bundlerTransport: http(
`https://api.pimlico.io/v2/base-sepolia/rpc?apikey=${import.meta.env.VITE_PIMLICO_API_KEY}`
),
middleware: {
gasPrice: async () =>
(await pimlicoBundlerClient.getUserOperationGasPrice()).fast, // if using pimlico bundler
},
}).extend(erc7579Actions({ entryPoint: ENTRYPOINT_ADDRESS_V07 }));

console.log(safeAccount, smartAccountClient)

localStorage.setItem("safeAccount", JSON.stringify(safeAccount))
localStorage.setItem("smartAccountClient", JSON.stringify(smartAccountClient))

setBurnerAccountClient(smartAccountClient);

// The run function creates a new burner wallet, assigns the current owner as its guardian, installs the recovery module, and returns the wallet's address.
const burnerWalletAddress = await run(client, safeAccount, guardianAddr);
const burnerWalletAddress = await run(
client,
safeAccount,
smartAccountClient,
guardianAddr
);
localStorage.setItem(
"burnerWalletConfig",
JSON.stringify({ burnerWalletAddress }),
JSON.stringify({ burnerWalletAddress })
);
setIsWalletPresent(true);
} catch (error) {
Expand Down Expand Up @@ -234,7 +264,7 @@ const GuardianSetup = () => {
guardianEmail,
localStorageAccountCode,
templateIdx,
subject[0].join().replaceAll(",", " ").replace("{ethAddr}", address),
subject[0].join().replaceAll(",", " ").replace("{ethAddr}", address)
);
} catch (error) {
// retry mechanism as this API call fails for the first time
Expand All @@ -245,7 +275,7 @@ const GuardianSetup = () => {
guardianEmail,
localStorageAccountCode,
templateIdx,
subject[0].join().replaceAll(",", " ").replace("{ethAddr}", address),
subject[0].join().replaceAll(",", " ").replace("{ethAddr}", address)
);
}

Expand All @@ -256,7 +286,7 @@ const GuardianSetup = () => {
} catch (err) {
console.error(err);
toast.error(
err?.shortMessage ?? "Something went wrong, please try again.",
err?.shortMessage ?? "Something went wrong, please try again."
);
setLoading(false);
}
Expand All @@ -267,6 +297,10 @@ const GuardianSetup = () => {
checkIfRecoveryIsConfigured,
]);

if (isAccountInitializedLoading) {
return <Loader />;
}

return (
<Box sx={{ marginX: "auto", marginTop: "100px", marginBottom: "100px" }}>
<Typography variant="h2" sx={{ paddingBottom: "1.5rem" }}>
Expand Down Expand Up @@ -313,7 +347,7 @@ const GuardianSetup = () => {
value={recoveryDelay}
onChange={(e) =>
setRecoveryDelay(
parseInt((e.target as HTMLInputElement).value),
parseInt((e.target as HTMLInputElement).value)
)
}
title="Recovery Delay"
Expand Down
14 changes: 10 additions & 4 deletions src/components/burnerWallet/RequestedRecoveries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { keccak256 } from "viem";
import { readContract } from "wagmi/actions";
import { readContract, writeContract } from "wagmi/actions";

Check failure on line 6 in src/components/burnerWallet/RequestedRecoveries.tsx

View workflow job for this annotation

GitHub Actions / eslint

'writeContract' is defined but never used

Check failure on line 6 in src/components/burnerWallet/RequestedRecoveries.tsx

View workflow job for this annotation

GitHub Actions / eslint

'writeContract' is defined but never used
import {
universalEmailRecoveryModule,
validatorsAddress,
Expand All @@ -27,6 +27,7 @@ import { Button } from "../Button";
import InputField from "../InputField";
import { useWriteContract } from "wagmi";

Check warning on line 28 in src/components/burnerWallet/RequestedRecoveries.tsx

View workflow job for this annotation

GitHub Actions / eslint

`wagmi` import should occur before import of `wagmi/actions`

Check failure on line 28 in src/components/burnerWallet/RequestedRecoveries.tsx

View workflow job for this annotation

GitHub Actions / eslint

'useWriteContract' is defined but never used

Check warning on line 28 in src/components/burnerWallet/RequestedRecoveries.tsx

View workflow job for this annotation

GitHub Actions / eslint

`wagmi` import should occur before import of `wagmi/actions`

Check failure on line 28 in src/components/burnerWallet/RequestedRecoveries.tsx

View workflow job for this annotation

GitHub Actions / eslint

'useWriteContract' is defined but never used
import Loader from "../Loader";
import { useBurnerAccount } from "../../context/BurnerAccountContext";

Check warning on line 30 in src/components/burnerWallet/RequestedRecoveries.tsx

View workflow job for this annotation

GitHub Actions / eslint

`../../context/BurnerAccountContext` import should occur before import of `../../providers/config`

Check warning on line 30 in src/components/burnerWallet/RequestedRecoveries.tsx

View workflow job for this annotation

GitHub Actions / eslint

`../../context/BurnerAccountContext` import should occur before import of `../../providers/config`

const BUTTON_STATES = {
TRIGGER_RECOVERY: "Trigger Recovery",
Expand All @@ -40,7 +41,7 @@ const RequestedRecoveries = () => {
const address = useGetSafeAccountAddress();
const { guardianEmail } = useAppContext();
const navigate = useNavigate();
const { writeContractAsync } = useWriteContract();
const { burnerAccountClient } = useBurnerAccount();

const [newOwner, setNewOwner] = useState<`0x${string}`>();
const safeWalletAddress = address;
Expand Down Expand Up @@ -70,6 +71,8 @@ const RequestedRecoveries = () => {
args: [address],
});

console.log(getRecoveryRequest);

const getGuardianConfig = await readContract(config, {
abi: universalEmailRecoveryModuleAbi,
address: universalEmailRecoveryModule as `0x${string}`,
Expand Down Expand Up @@ -180,14 +183,17 @@ const RequestedRecoveries = () => {

const handleCancelRecovery = useCallback(async () => {
setIsCancelRecoveryLoading(true);
setIsTriggerRecoveryLoading(false)
try {
await writeContractAsync({
await burnerAccountClient.writeContract({
abi: universalEmailRecoveryModuleAbi,
address: universalEmailRecoveryModule as `0x${string}`,
functionName: "cancelRecovery",
args: [],
});

setButtonState(BUTTON_STATES.TRIGGER_RECOVERY)
toast.success("Recovery Cancelled")
console.log("Recovery Cancelled");
} catch (err) {
console.log(err);
Expand Down Expand Up @@ -226,7 +232,7 @@ const RequestedRecoveries = () => {
}
};

console.log(isRecoveryStatusLoading)
console.log(isRecoveryStatusLoading);

// Since we are polling for every actions but only wants to show full screen loader for the initial request
if (
Expand Down
16 changes: 3 additions & 13 deletions src/components/burnerWallet/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ export const pimlicoBundlerClient = createPimlicoBundlerClient({
* @async
* @param {WalletClient} client - The wallet client used for transactions and interactions.
* @param {object} safeAccount - The smart account object containing the address of the account.
* @param {object} smartAccountClient - The safe account client
* @param {string} guardianAddr - The address of the guardian used in the recovery module.
* @returns {Promise<string>} The address of the configured smart account.
*/
export async function run(
client: WalletClient,
safeAccount: object,
smartAccountClient: object,
guardianAddr: string,
) {
const ownableValidatorAddress = validatorsAddress;
Expand All @@ -65,21 +67,9 @@ export async function run(
// generate hash
await client.sendTransaction({
to: safeAccount.address,
value: parseEther("0.003"),
value: parseEther("0.0003"),
});

const smartAccountClient = createSmartAccountClient({
account: safeAccount,
entryPoint: ENTRYPOINT_ADDRESS_V07,
chain: baseSepolia,
bundlerTransport: http(
`https://api.pimlico.io/v2/base-sepolia/rpc?apikey=${import.meta.env.VITE_PIMLICO_API_KEY}`,
),
middleware: {
gasPrice: async () =>
(await pimlicoBundlerClient.getUserOperationGasPrice()).fast, // if using pimlico bundler
},
}).extend(erc7579Actions({ entryPoint: ENTRYPOINT_ADDRESS_V07 }));

// txHash
await smartAccountClient.sendTransaction({
Expand Down
35 changes: 35 additions & 0 deletions src/context/BurnerAccountContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { createContext, useContext, useState, ReactNode } from "react";

Check failure on line 1 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Member 'ReactNode' of the import declaration should be sorted alphabetically

Check failure on line 1 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Member 'ReactNode' of the import declaration should be sorted alphabetically
import "viem/window";

interface BurnerAccountContextType {
burnerAccountClient: any; // Replace 'any' with the actual type if known

Check failure on line 5 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type

Check failure on line 5 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
setBurnerAccountClient: (client: any) => void; // Adjust the type as necessary

Check failure on line 6 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type

Check failure on line 6 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
}

const BurnerAccountContext = createContext<BurnerAccountContextType | null>(
null
);

export const BurnerAccountProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [burnerAccountClient, setBurnerAccountClient] = useState<any>(null); // Adjust type as needed

Check failure on line 16 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type

Check failure on line 16 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type

return (
<BurnerAccountContext.Provider
value={{ burnerAccountClient, setBurnerAccountClient }}
>
{children}
</BurnerAccountContext.Provider>
);
};

export const useBurnerAccount = () => {

Check warning on line 27 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components

Check warning on line 27 in src/context/BurnerAccountContext.tsx

View workflow job for this annotation

GitHub Actions / eslint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const context = useContext(BurnerAccountContext);
if (!context) {
throw new Error(
"useBurnerAccount must be used within a BurnerAccountProvider"
);
}
return context;
};
Loading

0 comments on commit 75056bc

Please sign in to comment.