Check if connected account is Safe

Remove unusued testing components.
Add safe deployment npm lib to get active safe deployments. Will need to update other abi/addr refs to it.
This commit is contained in:
jacque006
2024-05-01 01:17:20 -06:00
parent a63aa9137b
commit cfcab86fb5
8 changed files with 34 additions and 277 deletions

View File

@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@safe-global/safe-deployments": "^1.35.0",
"@tanstack/react-query": "^5.28.14",
"@wagmi/cli": "^2.1.4",
"axios": "^1.6.8",

View File

@@ -1,4 +1,4 @@
import { createContext, useEffect, useState } from "react";
import { createContext, useState } from "react";
import "./App.css";
import ConnectWallets from "./components/ConnectWallets";
import Navbar from "./components/Navbar";
@@ -8,8 +8,6 @@ import SafeModuleRecovery from "./components/SafeModuleRecovery";
import TriggerAccountRecovery from "./components/TriggerAccountRecovery";
import { STEPS } from "./constants";
import { Web3Provider } from "./providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { useAccount } from "wagmi";
import { AppContextProvider } from "./context/AppContextProvider";
export const StepsContext = createContext(null);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,173 +0,0 @@
import { useState, useCallback, useMemo } from 'react'
import { useAccount, useWriteContract, useReadContract } from 'wagmi'
import { abi as safeAbi } from '../abi/Safe.json'
import { abi as recoveryPluginAbi } from '../abi/SafeZkEmailRecoveryPlugin.json'
import { safeZkSafeZkEmailRecoveryPlugin } from '../../contracts.base-sepolia.json'
import { Button } from './Button'
import { genAccountCode, getRequestGuardianSubject, templateIdx } from '../utils/email'
import { readContract } from 'wagmi/actions'
import { config } from '../providers/config'
import { pad } from 'viem'
import { relayer } from '../services/relayer'
import { useAppContext } from '../context/AppContextHook'
export function ConfigureSafeModule() {
const { address } = useAccount()
const { writeContractAsync } = useWriteContract()
const {
guardianEmail,
setGuardianEmail,
accountCode,
setAccountCode
} = useAppContext()
// TODO 0 sets recovery to default of 2 weeks, likely want a warning here
// Also, better time duration setting component
const [recoveryDelay, setRecoveryDelay] = useState(0)
const { data: isModuleEnabled } = useReadContract({
address,
abi: safeAbi,
functionName: 'isModuleEnabled',
args: [safeZkSafeZkEmailRecoveryPlugin]
});
const { data: safeOwnersData } = useReadContract({
address,
abi: safeAbi,
functionName: 'getOwners',
});
const firstSafeOwner = useMemo(() => {
const safeOwners = safeOwnersData as string[];
if (!safeOwners?.length) {
return;
}
return safeOwners[0];
}, [safeOwnersData]);
// const checkGuardianAcceptance = useCallback(async () => {
// if (!gurdianRequestId) {
// throw new Error('missing guardian request id')
// }
// const resBody = await relayer.requestStatus(gurdianRequestId)
// console.debug('guardian req res body', resBody);
// }, [gurdianRequestId])
const enableEmailRecoveryModule = useCallback(async () => {
if (!address) {
throw new Error('unable to get account address');
}
await writeContractAsync({
abi: safeAbi,
address,
functionName: 'enableModule',
args: [safeZkSafeZkEmailRecoveryPlugin],
})
}, [address, writeContractAsync])
const configureRecoveryAndRequestGuardian = useCallback(async () => {
if (!address) {
throw new Error('unable to get account address');
}
if (!guardianEmail) {
throw new Error('guardian email not set')
}
if (!firstSafeOwner) {
throw new Error('safe owner not found')
}
const acctCode = await genAccountCode();
setAccountCode(accountCode);
const guardianSalt = await relayer.getAccountSalt(acctCode, guardianEmail);
const guardianAddr = await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'computeEmailAuthAddress',
args: [guardianSalt]
})
// TODO Should this be something else?
const previousOwnerInLinkedList = pad("0x1", {
size: 20
})
await writeContractAsync({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'configureRecovery',
args: [
firstSafeOwner,
guardianAddr,
recoveryDelay,
previousOwnerInLinkedList
],
})
console.debug('recovery configured');
const recoveryRouterAddr = await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'getRouterForSafe',
args: [address]
}) as string;
const subject = getRequestGuardianSubject(address);
const { requestId } = await relayer.acceptanceRequest(
recoveryRouterAddr,
guardianEmail,
acctCode,
templateIdx,
subject,
);
console.debug('req guard req id', requestId)
// TODO poll until guard req is complete or fails
}, [
address,
firstSafeOwner,
guardianEmail,
recoveryDelay,
accountCode,
setAccountCode,
writeContractAsync
])
return (
<>
{
isModuleEnabled ?
<div>Recovery Module Enabled</div> :
<Button onClick={enableEmailRecoveryModule}>
1. Enable Email Recovery Module
</Button>
}
<div>
<label>
Guardian's Email
<input disabled ={!isModuleEnabled}
type='email'
onInput={e => setGuardianEmail((e.target as HTMLTextAreaElement).value)}
/>
</label>
<label>
Recovery Delay
<input
disabled={!isModuleEnabled}
type='number'
onInput={e => setRecoveryDelay(parseInt((e.target as HTMLTextAreaElement).value))}
/>
</label>
<Button
disabled={!isModuleEnabled}
onClick={configureRecoveryAndRequestGuardian}>
2. Configure Recovery & Request Guardian
</Button>
</div>
</>
);
}

View File

@@ -1,94 +0,0 @@
import { useState, useCallback } from 'react'
import { Button } from './Button'
import { relayer } from '../services/relayer'
import { abi as recoveryPluginAbi } from '../abi/SafeZkEmailRecoveryPlugin.json'
import { useReadContract, useAccount } from 'wagmi'
import {
getRequestsRecoverySubject,
templateIdx
} from '../utils/email'
import { safeZkSafeZkEmailRecoveryPlugin } from '../../contracts.base-sepolia.json'
import { useAppContext } from '../context/AppContextHook'
export function PerformRecovery() {
const { address } = useAccount()
const { guardianEmail } = useAppContext()
const [newOwner, setNewOwner] = useState<string>()
// TODO pull from recovery module
// const { data: timelock } = useReadContract({
// address: simpleWalletAddress as HexStr,
// abi: simpleWalletAbi,
// functionName: 'timelock',
// });
const { data: recoveryRouterAddr } = useReadContract({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'getRouterForSafe',
args: [address]
});
const requestRecovery = useCallback(async () => {
if (!address) {
throw new Error('unable to get account address');
}
if (!guardianEmail) {
throw new Error('guardian email not set')
}
if (!newOwner) {
throw new Error('new owner not set')
}
if (!recoveryRouterAddr) {
throw new Error('could not find recovery router for safe')
}
const subject = getRequestsRecoverySubject(address, newOwner)
const { requestId } = await relayer.recoveryRequest(
recoveryRouterAddr as string,
guardianEmail,
templateIdx,
subject,
)
console.debug('recovery request id', requestId)
}, [recoveryRouterAddr, address, guardianEmail, newOwner])
const completeRecovery = useCallback(async () => {
if (!recoveryRouterAddr) {
throw new Error('could not find recovery router for safe')
}
console.debug('recovery router addr', recoveryRouterAddr);
const res = relayer.completeRecovery(
recoveryRouterAddr as string
);
console.debug('complete recovery res', res)
}, [recoveryRouterAddr]);
return (
<>
<label>
New Owner (address)
<input type='text'
onInput={e => setNewOwner((e.target as HTMLTextAreaElement).value)}
/>
</label>
<Button onClick={requestRecovery}>
3. Request Recovery
</Button>
{/* <div>{`TEST timelock: ${timelock}`}</div> */}
<Button onClick={completeRecovery}>
TEST Complete Recovery (Switch to polling)
</Button>
</>
);
}

View File

@@ -1,13 +1,16 @@
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import { useAccount, useReadContract, useWriteContract } from "wagmi";
import { useAccount, useBytecode, useChainId, useReadContract, useWriteContract } from "wagmi";
import { safeZkSafeZkEmailRecoveryPlugin } from "../../contracts.base-sepolia.json";
import { abi as safeAbi } from "../abi/Safe.json";
import { useCallback, useContext, useEffect, useState } from "react";
import { abi as proxyAbi } from "../abi/IProxy.json";
import { useCallback, useContext, useEffect, useState, useMemo } from "react";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
import { getSafeL2SingletonDeployment } from "@safe-global/safe-deployments";
const SafeModuleRecovery = () => {
const chainId = useChainId();
const { address } = useAccount();
const { writeContractAsync } = useWriteContract();
const stepsContext = useContext(StepsContext);
@@ -19,6 +22,18 @@ const SafeModuleRecovery = () => {
}
}, [address, stepsContext]);
// See https://ethereum.stackexchange.com/a/141258
const { data: addressBytecode } = useBytecode({ address });
const { data: masterCopy } = useReadContract({
address,
abi: proxyAbi,
functionName: "masterCopy",
});
const isSafeAccount = useMemo(() => {
const safeL2SingletonAddr = getSafeL2SingletonDeployment({ network: `${chainId}` });
return addressBytecode && masterCopy === safeL2SingletonAddr;
}, [chainId, addressBytecode, masterCopy])
const { data: isModuleEnabled } = useReadContract({
address,
abi: safeAbi,
@@ -26,8 +41,6 @@ const SafeModuleRecovery = () => {
args: [safeZkSafeZkEmailRecoveryPlugin],
});
console.log(isModuleEnabled);
if (isModuleEnabled) {
console.log("Module is enabled");
setLoading(false);
@@ -53,11 +66,14 @@ const SafeModuleRecovery = () => {
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
Connected wallet: <ConnectKitButton />
</div>
{!isModuleEnabled ? (
{!isSafeAccount && (
<div>Connected account is not a Safe, please connect a Safe</div>
)}
{isSafeAccount && !isModuleEnabled && (
<Button disabled={loading} onClick={enableEmailRecoveryModule}>
Enable Email Recovery Module
</Button>
) : null}
)}
</div>
);
};

View File

@@ -1589,6 +1589,13 @@
"@safe-global/safe-gateway-typescript-sdk" "^3.5.3"
viem "^1.0.0"
"@safe-global/safe-deployments@^1.35.0":
version "1.35.0"
resolved "https://registry.npmjs.org/@safe-global/safe-deployments/-/safe-deployments-1.35.0.tgz#6930a86a006526a9791ebd2a11cf8f5d8358563b"
integrity sha512-Of8WQEcvL5Fm+xxnCDjah6Hkw+sNdzcApQnzr+OsPBxYtZL0RRtbmesypj36oOD8BQmyrH54V8DVN+pYjrfJ9g==
dependencies:
semver "^7.6.0"
"@safe-global/safe-gateway-typescript-sdk@^3.5.3":
version "3.19.0"
resolved "https://registry.npmjs.org/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.19.0.tgz#18637c205c83bfc0a6be5fddbf202d6bb4927302"
@@ -5671,7 +5678,7 @@ semver@^6.3.1:
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.8, semver@^7.5.4:
semver@^7.3.8, semver@^7.5.4, semver@^7.6.0:
version "7.6.0"
resolved "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==