Finish hooking in write call for rest of recovery process.

This commit is contained in:
jacque006
2024-04-08 01:08:06 -04:00
parent c8809b0a28
commit 672177dc1a
9 changed files with 136 additions and 174 deletions

View File

@@ -1,6 +1,7 @@
import './App.css'
import { ConfigureSafeModule } from './components/ConfigureSafeModule';
import { PerformRecovery } from './components/PerformRecovery';
import { AppContextProvider } from './context/AppContextProvider';
import { Web3Provider } from "./providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
@@ -9,9 +10,11 @@ function App() {
<>
<h1>Safe Email Recovery Demo</h1>
<Web3Provider>
<ConnectKitButton />
<ConfigureSafeModule />
<PerformRecovery />
<AppContextProvider>
<ConnectKitButton />
<ConfigureSafeModule />
<PerformRecovery />
</AppContextProvider>
</Web3Provider>
</>
)

View File

@@ -9,13 +9,18 @@ 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 [recoveryConfigured, setRecoveryConfigured] = useState(false)
const [guardianEmail, setGuardianEmail] = useState<string>()
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)
@@ -40,6 +45,15 @@ export function ConfigureSafeModule() {
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');
@@ -66,8 +80,10 @@ export function ConfigureSafeModule() {
throw new Error('safe owner not found')
}
const accountCode = await genAccountCode();
const guardianSalt = await relayer.getAccountSalt(accountCode, guardianEmail);
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}`,
@@ -93,7 +109,7 @@ export function ConfigureSafeModule() {
console.debug('recovery configured');
const recoveryRelayerAddr = await readContract(config, {
const recoveryRouterAddr = await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'getRouterForSafe',
@@ -102,33 +118,29 @@ export function ConfigureSafeModule() {
const subject = getRequestGuardianSubject(address);
const { requestId } = await relayer.acceptanceRequest(
recoveryRelayerAddr,
recoveryRouterAddr,
guardianEmail,
accountCode,
acctCode,
templateIdx,
subject,
);
console.debug('req guard req id', requestId)
setRecoveryConfigured(true);
// TODO poll until guard req is complete or fails
}, [
address,
firstSafeOwner,
guardianEmail,
recoveryDelay,
accountCode,
setAccountCode,
writeContractAsync
])
const recoveryCfgEnabled = useMemo(
() => !isModuleEnabled || recoveryConfigured,
[isModuleEnabled, recoveryConfigured]
);
return (
<>
{
isModuleEnabled ?
isModuleEnabled ?
<div>Recovery Module Enabled</div> :
<Button onClick={enableEmailRecoveryModule}>
1. Enable Email Recovery Module
@@ -137,7 +149,7 @@ export function ConfigureSafeModule() {
<div>
<label>
Guardian's Email
<input disabled ={recoveryCfgEnabled}
<input disabled ={!isModuleEnabled}
type='email'
onInput={e => setGuardianEmail((e.target as HTMLTextAreaElement).value)}
/>
@@ -145,13 +157,13 @@ export function ConfigureSafeModule() {
<label>
Recovery Delay
<input
disabled={recoveryCfgEnabled}
disabled={!isModuleEnabled}
type='number'
onInput={e => setRecoveryDelay(parseInt((e.target as HTMLTextAreaElement).value))}
/>
</label>
<Button
disabled={recoveryCfgEnabled}
disabled={!isModuleEnabled}
onClick={configureRecoveryAndRequestGuardian}>
2. Configure Recovery & Request Guardian
</Button>

View File

@@ -1,121 +1,40 @@
import { waitForTransactionReceipt } from '@wagmi/core'
import { useState, useCallback } from 'react'
import { Button } from './Button'
import { relayer } from '../services/relayer'
import { useConfig, useReadContract, useWalletClient } from 'wagmi'
import { abi as proxyAbi, bytecode as proxyBytecode } from '../abi/ERC1967Proxy.json'
import { abi as simpleWalletAbi } from '../abi/SimpleWallet.json'
import { abi as recoveryPluginAbi } from '../abi/SafeZkEmailRecoveryPlugin.json'
import { useReadContract, useAccount } from 'wagmi'
import {
verifier,
dkimRegistry,
emailAuthImpl,
simpleWalletImpl
} from '../../contracts.base-sepolia.json'
import { ethers } from 'ethers'
import {
genAccountCode,
getRequestGuardianSubject,
getRequestsRecoverySubject,
templateIdx
} from '../utils/email'
import { safeZkSafeZkEmailRecoveryPlugin } from '../../contracts.base-sepolia.json'
import { useAppContext } from '../context/AppContextHook'
// TODO Pull from lib
type HexStr = `0x${string}`;
const storageKeys = {
simpleWalletAddress: 'simpleWalletAddress',
guardianEmail: 'guardianEmail',
accountCode: 'accountCode',
}
// TODO Switch back to Safe over SimpleWallet
export function PerformRecovery() {
const cfg = useConfig()
const { data: walletClient } = useWalletClient()
const [simpleWalletAddress, setSimpleWalletAddress] = useState(
localStorage.getItem(storageKeys.simpleWalletAddress)
)
const [guardianEmail, setGuardianEmail] = useState(
localStorage.getItem(storageKeys.guardianEmail)
);
// TODO TEST, probably don't show on FE
const [accountCode, setAccountCode] = useState(
localStorage.getItem(storageKeys.accountCode)
);
const [gurdianRequestId, setGuardianRequestId] = useState<number>()
const { address } = useAccount()
const { guardianEmail } = useAppContext()
const [newOwner, setNewOwner] = useState<string>()
const { data: simpleWalletOwner } = useReadContract({
address: simpleWalletAddress as HexStr,
abi: simpleWalletAbi,
functionName: 'owner',
// TODO pull from recovery module
// const { data: timelock } = useReadContract({
// address: simpleWalletAddress as HexStr,
// abi: simpleWalletAbi,
// functionName: 'timelock',
// });
const timelock = -1
const { data: recoveryRouterAddr } = useReadContract({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: 'getRouterForSafe',
args: [address]
});
const { data: timelock } = useReadContract({
address: simpleWalletAddress as HexStr,
abi: simpleWalletAbi,
functionName: 'timelock',
});
const deploySimpleWallet = useCallback(async() => {
const simpleWalletInterface = new ethers.Interface(simpleWalletAbi);
const data = simpleWalletInterface.encodeFunctionData('initialize', [
walletClient?.account.address, verifier, dkimRegistry, emailAuthImpl
]);
const hash = await walletClient?.deployContract({
abi: proxyAbi,
bytecode: proxyBytecode.object as HexStr,
args: [simpleWalletImpl, data],
}) as HexStr
const { contractAddress } = await waitForTransactionReceipt(cfg, { hash })
if (!contractAddress) {
throw new Error('simplewallet deployment has no contractAddress');
}
setSimpleWalletAddress(contractAddress);
// localStorage.setItem(storageKeys.simpleWalletAddress, contractAddress);
}, [walletClient, cfg])
const requestGuardian = useCallback(async () => {
if (!simpleWalletAddress) {
throw new Error('simple wallet address not set')
}
if (!guardianEmail) {
throw new Error('guardian email not set')
}
const accountCode = await genAccountCode()
const subject = getRequestGuardianSubject(simpleWalletAddress);
const { requestId } = await relayer.acceptanceRequest(
simpleWalletAddress,
guardianEmail,
accountCode,
templateIdx,
subject,
);
setGuardianRequestId(requestId)
setAccountCode(accountCode)
// localStorage.setItem(storageKeys.accountCode, accountCode)
// localStorage.setItem(storageKeys.guardianEmail, guardianEmail)
}, [simpleWalletAddress, guardianEmail])
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 requestRecovery = useCallback(async () => {
if (!simpleWalletAddress) {
throw new Error('simple wallet address not set')
if (!address) {
throw new Error('unable to get account address');
}
if (!guardianEmail) {
@@ -126,60 +45,50 @@ export function PerformRecovery() {
throw new Error('new owner not set')
}
const subject = getRequestsRecoverySubject(simpleWalletAddress, newOwner)
if (!recoveryRouterAddr) {
throw new Error('could not find recovery router for safe')
}
const subject = getRequestsRecoverySubject(address, newOwner)
const { requestId } = await relayer.recoveryRequest(
simpleWalletAddress,
recoveryRouterAddr as string,
guardianEmail,
templateIdx,
subject,
)
console.debug('recovery request id', requestId)
}, [simpleWalletAddress, guardianEmail, newOwner])
}, [recoveryRouterAddr, address, guardianEmail, newOwner])
const completeRecovery = useCallback(async () => {
// TODO Instead, poll relayer.requestStatus until complete recovery is complete
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 (
<>
<div>{`TEST SimplerWallet address: ${simpleWalletAddress}`}</div>
<div>{`TEST SimpleWallet owner ${simpleWalletOwner}`}</div>
<div>{`TEST account code: ${accountCode}`}</div>
<div>{`TEST timelock: ${timelock}`}</div>
<Button disabled={!!simpleWalletAddress} onClick={deploySimpleWallet}>TEST Deploy SimpleWallet</Button>
<div>
<label>
Guardian's Email
<input disabled ={!simpleWalletAddress}
type='email'
onInput={e => setGuardianEmail((e.target as HTMLTextAreaElement).value)}
/>
</label>
<Button
disabled={!simpleWalletAddress || !guardianEmail || !!gurdianRequestId}
onClick={requestGuardian}>
TEST Request Guardian
</Button>
</div>
<div>
<Button onClick={checkGuardianAcceptance}>
TEST Check for guardian acceptance
</Button>
</div>
<label>
New Owner (address)
<input type='text'
onInput={e => setNewOwner((e.target as HTMLTextAreaElement).value)}
/>
</label>
<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}>
4. Complete Recovery (Switch to polling)
TEST Complete Recovery (Switch to polling)
</Button>
</>
);

View File

@@ -0,0 +1,16 @@
import { createContext } from 'react'
type AppContextType = {
accountCode: string,
setAccountCode: (ac: string) => void;
guardianEmail: string;
setGuardianEmail: (ge: string) => void;
}
export const appContext = createContext<AppContextType>({
accountCode: '',
setAccountCode: () => {},
guardianEmail: '',
setGuardianEmail: () => {}
});

View File

@@ -0,0 +1,4 @@
import { useContext } from "react";
import { appContext } from "./AppContext";
export const useAppContext = () => useContext(appContext)

View File

@@ -0,0 +1,23 @@
import { ReactNode, useMemo, useState } from "react";
import { appContext } from "./AppContext";
export const AppContextProvider = ({ children } : { children: ReactNode }) => {
const [accountCode, setAccountCode] = useState('');
const [guardianEmail, setGuardianEmail] = useState('');
const ctxVal = useMemo(() => ({
accountCode,
setAccountCode,
guardianEmail,
setGuardianEmail,
}), [
accountCode,
guardianEmail
])
return (
<appContext.Provider value={ctxVal}>
{children}
</appContext.Provider>
)
}

View File

@@ -2,10 +2,6 @@ import axios from "axios"
// Spec: https://www.notion.so/proofofemail/Email-Sender-Auth-c87063cd6cdc4c5987ea3bc881c68813#d7407d31e1354167be61612f5a16995b
// TODO Consider using a bigint for templateIdx as it *could* overflow JS number, but practically seems unlikely
type RequestIDData = {
request_id: number;
}
class Relayer {
private readonly apiRoute = 'api';
apiUrl: string;
@@ -62,9 +58,7 @@ class Relayer {
templateIdx: number,
subject: string
) {
const {
request_id: requestId
} = await axios<unknown, RequestIDData>({
const { data } = await axios({
method: "POST",
url: `${this.apiUrl}/recoveryRequest`,
data: {
@@ -74,13 +68,14 @@ class Relayer {
subject,
}
})
const { request_id: requestId } = data
return { requestId };
}
async completeRequest(walletEthAddr: string) {
const data = await axios<unknown, unknown>({
async completeRecovery(walletEthAddr: string) {
const data = await axios({
method: "POST",
url: `${this.apiUrl}/completeRequest`,
url: `${this.apiUrl}/completeRecovery`,
data: {
wallet_eth_addr: walletEthAddr,
}
@@ -89,7 +84,7 @@ class Relayer {
}
async getAccountSalt(accountCode: string, emailAddress: string) {
const { data } = await axios<unknown, { data: string }>({
const { data } = await axios({
method: "POST",
url: `${this.apiUrl}/getAccountSalt`,
data: {

View File

@@ -51,4 +51,4 @@ export async function genAccountCode(): Promise<string> {
export const getRequestGuardianSubject = (acctAddr: string) =>
`Accept guardian request for ${acctAddr}`;
export const getRequestsRecoverySubject = (acctAddr: string, newOwner: string) =>
`Set the new signer of ${acctAddr} to ${newOwner}`;
`Update owner to ${newOwner} on account ${acctAddr}`;