diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx index 85d1e832..1e515119 100644 --- a/src/app/account/page.tsx +++ b/src/app/account/page.tsx @@ -7,6 +7,7 @@ import { TabHeaderButton } from 'src/components/buttons/TabHeaderButton'; import { Section } from 'src/components/layout/Section'; import { Amount, formatNumberString } from 'src/components/numbers/Amount'; import { useBalance, useVoteSignerToAccount } from 'src/features/account/hooks'; +import Settings from 'src/features/account/Settings'; import { DelegationsTable } from 'src/features/delegation/components/DelegationsTable'; import { useDelegatees } from 'src/features/delegation/hooks/useDelegatees'; import { useDelegationBalances } from 'src/features/delegation/hooks/useDelegationBalances'; @@ -214,7 +215,7 @@ function TableTabs({ addressToDelegatee?: AddressTo; activateStake: (g: Address) => void; }) { - const tabs = ['stakes', 'rewards', 'delegations', 'history'] as const; + const tabs = ['stakes', 'rewards', 'delegations', 'history', 'settings'] as const; const { tab, onTabChange } = useTabs<(typeof tabs)[number]>('stakes'); return ( @@ -248,6 +249,7 @@ function TableTabs({ /> )} {tab === 'history' && } + {tab === 'settings' && } ); } diff --git a/src/features/account/Settings.tsx b/src/features/account/Settings.tsx new file mode 100644 index 00000000..875e02b9 --- /dev/null +++ b/src/features/account/Settings.tsx @@ -0,0 +1,262 @@ +import { useState } from 'react'; +import { SolidButton } from 'src/components/buttons/SolidButton'; +import { Modal, useModal } from 'src/components/menus/Modal'; +import { ZERO_ADDRESS } from 'src/config/consts'; +import { useGetVoteSignerFor, useVoteSignerToAccount } from 'src/features/account/hooks'; +import { useLockedStatus } from 'src/features/locking/useLockedStatus'; +import { getTotalLockedCelo } from 'src/features/locking/utils'; +import useAddressToLabel from 'src/utils/useAddressToLabel'; +import { isAddress, isHex } from 'viem'; +import { useAccount } from 'wagmi'; + +export default function Settings() { + const account = useAccount(); + const address = account?.address; + + const { data: voteSigner } = useGetVoteSignerFor(address); + const { signingFor, isVoteSigner } = useVoteSignerToAccount(address); + const { lockedBalances } = useLockedStatus(signingFor); + const addressToLabel = useAddressToLabel(); + + const totalLocked = getTotalLockedCelo(lockedBalances); + const hasVoteSigner = voteSigner && voteSigner !== ZERO_ADDRESS && voteSigner !== address; + const hasLockedCelo = totalLocked && totalLocked > 0n; + + const { + isModalOpen: isPrepareModalOpen, + openModal: openPrepareModal, + closeModal: closePrepareModal, + } = useModal(); + + const { + isModalOpen: isAuthorizeModalOpen, + openModal: openAuthorizeModal, + closeModal: closeAuthorizeModal, + } = useModal(); + + const [principalAddress, setPrincipalAddress] = useState(''); + const [proofOfPossession, setProofOfPossession] = useState(''); + + const isValidPrincipalAddress = (address: string): boolean => { + return isAddress(address.trim()); + }; + + const isValidProofOfPossession = (hex: string): boolean => { + const trimmed = hex.trim(); + return trimmed.length > 2 && isHex(trimmed, { strict: true }); + }; + + const prepareVoteSigner = () => { + openPrepareModal(); + }; + + const handleAuthorizeVoteSigner = () => { + openAuthorizeModal(); + }; + + const handlePrepareSign = () => { + // TODO: Implement generate proof-of-possession functionality + console.log('Generate proof-of-possession for principal:', principalAddress); + closePrepareModal(); + }; + + const handleAuthorizeSign = () => { + // TODO: Implement authorize vote signer functionality + console.log('Authorize vote signer with PoP:', proofOfPossession); + closeAuthorizeModal(); + }; + + return ( +
+
+
+

Vote Signer Setup

+ +
+

How Vote Signing Works

+

+ Vote signing allows you to separate your locked CELO (voting rights) from the account + that actually submits votes. +

+
+
+ Principal Account: Has locked CELO and voting rights, authorizes + another account +
+
+ Vote Signer Account: Has minimal CELO for gas fees, votes on behalf + of the Principal +
+
+ + {address && ( +
+
+ Your current account:{' '} + {isVoteSigner ? ( + + Vote Signer Account (voting for {addressToLabel(signingFor!)}) + + ) : hasLockedCelo ? ( + + Principal Account (has{' '} + {totalLocked ? (Number(totalLocked) / 1e18).toFixed(2) : '0'} locked CELO) + + ) : ( + Regular Account (no locked CELO) + )} +
+
+ )} +
+ +
+
+
+ + 1 + +

Generate Proof of Possession

+
+

+ The account that will vote (vote signer) must first generate a cryptographic proof + of possession. +

+ {!hasLockedCelo && !isVoteSigner && ( +
+ Use this step if: This account will vote on behalf of another + account with locked CELO +
+ )} + {hasLockedCelo && ( +
+ Note: Switch to the account that will vote (without locked CELO) + to generate the proof +
+ )} + + Prepare Vote Signer + +
+ +
+
+ + 2 + +

Authorize Vote Signer

+
+

+ The account with locked CELO (Principal) uses the proof of possession to authorize + the vote signer. +

+ {hasLockedCelo && ( +
+ Perfect! This account has locked CELO and can authorize a vote + signer +
+ )} + {!hasLockedCelo && !isVoteSigner && ( +
+ Note: Switch to the account with locked CELO to authorize a vote + signer +
+ )} +
+ + Authorize Vote Signer + + {hasVoteSigner && ( + + Current: {addressToLabel(voteSigner)} + + )} +
+
+
+
+
+ + +
+

+ Enter the address of the Principal Account (the account with locked CELO that you want + to vote for): +

+
+ + setPrincipalAddress(e.target.value)} + placeholder="0x..." + className="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + Sign + + +
+
+
+ + +
+

+ Enter the Proof of Possession (PoP) generated by the Vote Signer Account: +

+
+ +