Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add warpcast recovery flow to Hats Procotol integration #517

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pages/accounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ export default function Accounts() {
<Card>
<CardHeader>
<CardTitle className="text-2xl">Create a shared Farcaster account</CardTitle>
<CardDescription className="text-lg">
<CardDescription className="text-md leading-tight">
Follow these steps to create a shared Farcaster account. Shared accounts are powered by Hats Protocol
🧢.
</CardDescription>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ const ConnectFarcasterAccountViaHatsProtocol = () => {
<Card>
<CardHeader>
<CardTitle className="text-2xl">Connect to a shared account</CardTitle>
<CardDescription className="text-lg">{state.description}</CardDescription>
<CardDescription className="text-md leading-tight">{state.description}</CardDescription>
</CardHeader>
<CardContent className="w-full max-w-lg">
{getCardContent()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,26 +72,12 @@ export const SharedAccountOwnershipSetup = ({
setDelegatorContractAddress={setDelegatorContractAddress}
/>
</div>
<div className="mt-4 lg:w-1/2 lg:mt-0">
<div className="mx-0 max-w-2xl">
<h3 className="text-lg font-semibold tracking-tight text-foreground">How to get your Hats IDs</h3>
<p className="mt-2 text-md leading-8 text-foreground/70">
Go to the{' '}
<a href="https://app.hatsprotocol.xyz" className="underline" target="_blank" rel="noreferrer">
Hats app
</a>{' '}
and click on the tree you want to use. In the top right corner, you will see the tree ID and the Hats ID.
You will need to use the Hats ID for the admin role and for the caster role.
</p>
</div>
<WarpcastImage url="https://i.imgur.com/pgl0n75.gif" />
</div>
</div>
);

const renderGoBack = () =>
state !== OwnershipSetupSteps.unknown && (
<Button className="mt-8" variant="default" onClick={() => setState(OwnershipSetupSteps.unknown)}>
<Button className="mt-8" variant="outline" onClick={() => setState(OwnershipSetupSteps.unknown)}>
Go back
</Button>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import SwitchWalletButton from '../SwitchWalletButton';
import { Label } from '@/components/ui/label';
import { User } from '@neynar/nodejs-sdk/build/neynar-api/v2';
import { optimism } from 'viem/chains';
import includes from 'lodash.includes';
import ClickToCopyText from '../ClickToCopyText';

const readNonces = async (account: `0x${string}`) => {
if (!account) return BigInt(0);
Expand All @@ -31,7 +33,7 @@ const readNonces = async (account: `0x${string}`) => {

enum TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS {
'CONNECT_WALLET',
'EXECUTE_PREPARE_TO_RECEIVE',
'PENDING_USER_SET_WARPCAST_RECOVERY_ADDRESS',
'PENDING_PREPARE_TO_RECEIVE_CONFIRMATION',
'GENERATE_SIGNATURE',
'PENDING_SIGNATURE_CONFIRMATION',
Expand All @@ -56,9 +58,9 @@ const TransferAccountToHatsDelegatorSteps: TransferAccountToHatsDelegatorStepTyp
idx: 0,
},
{
state: TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.EXECUTE_PREPARE_TO_RECEIVE,
title: 'Prepare to receive',
description: 'Prepare your Hats Protocol Delegator contract instance to receive the Farcaster account',
state: TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_USER_SET_WARPCAST_RECOVERY_ADDRESS,
title: 'Set your recovery address in Warpcast',
description: '',
idx: 1,
},
{
Expand Down Expand Up @@ -122,6 +124,7 @@ const TransferAccountToHatsDelegator = ({
const [deadline, setDeadline] = useState<bigint>(BigInt(0));
const [nonce, setNonce] = useState<bigint>(BigInt(0));
const [onchainTransactionHash, setOnchainTransactionHash] = useState<`0x${string}`>('0x');
const [isRecoveryAddressSet, setIsRecoveryAddressSet] = useState(false);

const { signTypedDataAsync } = useSignTypedData();
const fid = BigInt(user.fid);
Expand All @@ -142,6 +145,19 @@ const TransferAccountToHatsDelegator = ({
hash: onchainTransactionHash,
});

const {
data: recoveryAddress,
error: recoveryAddressError,
status: recoveryAddressStatus,
} = useReadContract({
abi: idRegistryABI,
address: ID_REGISTRY_ADDRESS,
functionName: 'recoveryOf',
args: [fid],
});

console.log('recoveryAddress', recoveryAddress, recoveryAddressError, recoveryAddressStatus);

const {
data: isFidReceivable,
error,
Expand All @@ -163,6 +179,14 @@ const TransferAccountToHatsDelegator = ({
}
}, [isFidReceivable, status, step]);

// when the recovery address is set to the address that the user is connected with,
// we can move to the next step
useEffect(() => {
if (recoveryAddress && address && recoveryAddress === address) {
setIsRecoveryAddressSet(true);
}
}, [recoveryAddress, address]);

const setStepToKey = (key: TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS) => {
const newStep = TransferAccountToHatsDelegatorSteps.find((step) => step.state === key);
if (newStep) setStep(newStep);
Expand Down Expand Up @@ -201,25 +225,6 @@ const TransferAccountToHatsDelegator = ({
}
}, [onchainTransactionHash, transactionResult]);

const onExecutePrepareToReceive = async () => {
try {
const tx = await writeContract(config, {
abi: HatsFarcasterDelegatorAbi,
address: toAddress,
functionName: 'prepareToReceive',
args: [fid],
});

const result = await waitForTransactionReceipt(config, { hash: tx });
setStep(TransferAccountToHatsDelegatorSteps[2]);
setStepToKey(TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_PREPARE_TO_RECEIVE_CONFIRMATION);
} catch (e) {
console.error('onExecutePrepareToReceive error', e);
setErrorMessage(e?.message || e);
setStepToKey(TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.ERROR);
}
};

const getTransferTypeData = () => ({
domain: ID_REGISTRY_EIP_712_DOMAIN,
types: {
Expand Down Expand Up @@ -303,8 +308,8 @@ const TransferAccountToHatsDelegator = ({
switch (step.state) {
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.CONNECT_WALLET:
return 'Connect wallet';
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.EXECUTE_PREPARE_TO_RECEIVE:
return 'Prepare for transfer';
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_USER_SET_WARPCAST_RECOVERY_ADDRESS:
return isRecoveryAddressSet ? 'Next step' : 'Waiting...';
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.GENERATE_SIGNATURE:
return `Generate signature`;
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_PREPARE_TO_RECEIVE_CONFIRMATION:
Expand Down Expand Up @@ -332,8 +337,7 @@ const TransferAccountToHatsDelegator = ({

const onClick = () => {
switch (step.state) {
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.EXECUTE_PREPARE_TO_RECEIVE:
onExecutePrepareToReceive();
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_USER_SET_WARPCAST_RECOVERY_ADDRESS:
break;
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.GENERATE_SIGNATURE:
if (!address) return;
Expand All @@ -353,7 +357,7 @@ const TransferAccountToHatsDelegator = ({
break;
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.ERROR:
setErrorMessage('');
setStepToKey(TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.EXECUTE_PREPARE_TO_RECEIVE);
setStepToKey(TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_USER_SET_WARPCAST_RECOVERY_ADDRESS);
break;
default:
break;
Expand All @@ -362,30 +366,19 @@ const TransferAccountToHatsDelegator = ({

const getCardContent = () => {
switch (step.state) {
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.EXECUTE_PREPARE_TO_RECEIVE:
case TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_USER_SET_WARPCAST_RECOVERY_ADDRESS:
return (
<>
<div className="flex flex-col">
<span>
Prepare delegator contract{' '}
<a
className="underline"
href={`https://optimistic.etherscan.io/address/${toAddress}`}
target="_blank"
rel="noopener noreferrer"
>
{toAddress}
</a>{' '}
to receive your Farcaster account.
Got to Warpcast and set the recovery address to your connected wallet address.
<br />
<br />
<span className="flex flex-col">
{address}
<ClickToCopyText className="w-20" text={address!} size="sm" />
</span>
<Button
className="mt-8 w-1/2"
variant="outline"
onClick={() => setStepToKey(TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.GENERATE_SIGNATURE)}
>
Skip
</Button>
<Label className="mt-2">Skip if you already prepared the contract.</Label>
<br />
<p>recovery address is {recoveryAddress}</p>
</div>
</>
);
Expand Down Expand Up @@ -450,14 +443,22 @@ const TransferAccountToHatsDelegator = ({
)}
</div>
)}
{step.state !== TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.CONFIRMED && <SwitchWalletButton className="w-1/2" />}
{!includes(
[
TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_USER_SET_WARPCAST_RECOVERY_ADDRESS,
TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.CONFIRMED,
],
step.state
) && <SwitchWalletButton className="w-1/2" />}
<Button
className="w-1/2"
variant="default"
disabled={
!toAddress ||
!address ||
!fid ||
(step.state === TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_USER_SET_WARPCAST_RECOVERY_ADDRESS &&
!isRecoveryAddressSet) ||
(step.state === TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.EXECUTE_ONCHAIN &&
!connectedAddressOwnsFarcasterAccount) ||
step.state === TRANSFER_ACCOUNT_TO_HATS_DELEGATOR_STEPS.PENDING_SIGNATURE_CONFIRMATION ||
Expand Down
9 changes: 1 addition & 8 deletions src/common/components/SwitchWalletButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import React, { useEffect, useState } from 'react';

import { Button } from '@/components/ui/button';
import { useAccountModal, useConnectModal } from '@rainbow-me/rainbowkit';
import { useAccount, useDisconnect } from 'wagmi';
import { useAccount } from 'wagmi';
import { cn } from '@/lib/utils';

type SwitchWalletButtonProps = {
className?: string;
};

const SwitchWalletButton = ({ className }: SwitchWalletButtonProps) => {
const { disconnect } = useDisconnect();
const { openConnectModal } = useConnectModal();
const { openAccountModal } = useAccountModal();

Expand All @@ -23,12 +22,6 @@ const SwitchWalletButton = ({ className }: SwitchWalletButtonProps) => {

return (
<div className={cn('flex flex-col', className)}>
{isClient && isConnected && (
<Button variant="outline" className="border-red-700" onClick={() => disconnect()}>
Disconnect
</Button>
)}

<Button
type="button"
variant="outline"
Expand Down
4 changes: 2 additions & 2 deletions src/common/helpers/hats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ export async function createInitialTree(
],
});

if (res.status === 'success') {
if (res?.status === 'success') {
return { casterHat: casterHatId, adminHat: casterAdminHatId };
} else {
throw new Error('Tree creation failed');
throw new Error(`Tree creation failed`);
}
}