mirror of
https://github.com/getwax/wax.git
synced 2026-01-09 23:27:58 -05:00
Finish hooking in write call for rest of recovery process.
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
16
packages/demos/email-recovery/src/context/AppContext.tsx
Normal file
16
packages/demos/email-recovery/src/context/AppContext.tsx
Normal 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: () => {}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { useContext } from "react";
|
||||
import { appContext } from "./AppContext";
|
||||
|
||||
export const useAppContext = () => useContext(appContext)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user