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

Backup file after dkg #587

Merged
merged 16 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 9 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
152 changes: 152 additions & 0 deletions apps/router/src/guardian-ui/components/BackupModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
Button,
Flex,
Alert,
AlertIcon,
AlertTitle,
Text,
} from '@chakra-ui/react';
import { useGuardianAdminApi, useGuardianDispatch } from '../../hooks';
import { hexToBlob } from '../utils/api';
import { GUARDIAN_APP_ACTION_TYPE, GuardianStatus } from '../../types/guardian';

interface BackupModalProps {
isOpen: boolean;
onClose: () => void;
}

export const BackupModal: React.FC<BackupModalProps> = ({
isOpen,
onClose,
}) => {
const { t } = useTranslation();
const [hasDownloaded, setHasDownloaded] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadError, setDownloadError] = useState<string | null>(null);
const api = useGuardianAdminApi();
const dispatch = useGuardianDispatch();

useEffect(() => {
if (isOpen) {
setHasDownloaded(false);
setDownloadError(null);
}
}, [isOpen]);

const handleDownload = async () => {
kleysc marked this conversation as resolved.
Show resolved Hide resolved
setIsDownloading(true);
setDownloadError(null);

try {
const response = await api.downloadGuardianBackup();
const blob = hexToBlob(response.tar_archive_bytes, 'application/x-tar');
const url = window.URL.createObjectURL(blob);

await new Promise((resolve, reject) => {
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'guardianBackup.tar');

link.onclick = () => {
setTimeout(() => {
URL.revokeObjectURL(url);
resolve(true);
}, 1000);
};

link.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Download failed'));
};

document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});

setHasDownloaded(true);
} catch (error) {
console.error('Error in handleDownload:', error);
setDownloadError(
t('federation-dashboard.danger-zone.backup.error-download')
);
} finally {
setIsDownloading(false);
}
};
kleysc marked this conversation as resolved.
Show resolved Hide resolved

const handleContinue = () => {
if (hasDownloaded) {
dispatch({
type: GUARDIAN_APP_ACTION_TYPE.SET_STATUS,
payload: GuardianStatus.Admin,
});
onClose();
}
};

return (
<Modal
isOpen={isOpen}
onClose={onClose}
closeOnOverlayClick={false}
isCentered
>
<ModalOverlay />
<ModalContent>
<ModalHeader alignSelf='center'>
{t('federation-dashboard.danger-zone.backup.title')}
</ModalHeader>
<ModalBody pb={6}>
<Flex direction='column' gap={4}>
<Alert status='warning'>
<AlertIcon />
<AlertTitle>
{t('federation-dashboard.danger-zone.backup.warning-title')}
</AlertTitle>
</Alert>
<Text mb={4}>
{t('federation-dashboard.danger-zone.backup.warning-text')}
</Text>
{downloadError && (
<Alert status='error'>
<AlertIcon />
<AlertTitle>{downloadError}</AlertTitle>
</Alert>
)}
<Flex justifyContent='center' gap={4} direction={['column', 'row']}>
<Button
variant='ghost'
size={['sm', 'md']}
onClick={handleDownload}
isDisabled={hasDownloaded || isDownloading}
isLoading={isDownloading}
bg='red.500'
color='white'
_hover={{ bg: 'red.600' }}
_active={{ bg: 'red.700' }}
>
{t('federation-dashboard.danger-zone.acknowledge-and-download')}
</Button>
<Button
colorScheme='blue'
size={['sm', 'md']}
onClick={handleContinue}
isDisabled={!hasDownloaded}
>
{t('common.close')}
</Button>
kleysc marked this conversation as resolved.
Show resolved Hide resolved
</Flex>
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { Flex, Heading, Text, Spinner } from '@chakra-ui/react';
import { useTranslation } from '@fedimint/utils';
import {
Expand All @@ -7,6 +7,7 @@ import {
GuardianStatus,
} from '../../../../../types/guardian';
import { useGuardianDispatch } from '../../../../../hooks';
import { BackupModal } from '../../../BackupModal';

interface SetupCompleteProps {
role: GuardianRole;
Expand All @@ -15,40 +16,50 @@ interface SetupCompleteProps {
export const SetupComplete: React.FC<SetupCompleteProps> = ({ role }) => {
const { t } = useTranslation();
const dispatch = useGuardianDispatch();
const [showBackupModal, setShowBackupModal] = useState(true);

useEffect(() => {
const timer = setTimeout(() => {
dispatch({
type: GUARDIAN_APP_ACTION_TYPE.SET_STATUS,
payload: GuardianStatus.Admin,
});
}, 3000);
if (!showBackupModal) {
const timer = setTimeout(() => {
dispatch({
type: GUARDIAN_APP_ACTION_TYPE.SET_STATUS,
payload: GuardianStatus.Admin,
});
}, 3000);

return () => clearTimeout(timer);
}, [dispatch]);
return () => clearTimeout(timer);
}
}, [dispatch, showBackupModal]);

return (
<Flex
direction='column'
justify='center'
align='center'
textAlign='center'
pt={10}
>
<Heading size='sm' fontSize='42px' mb={8}>
{t('setup-complete.header')}
</Heading>
<Heading size='md' fontWeight='medium' mb={2}>
{t('setup-complete.congratulations')}
</Heading>
<Text mb={16} fontWeight='medium'>
{role === GuardianRole.Follower
? t(`setup-complete.follower-message`)
: t(`setup-complete.leader-message`)}
</Text>
<Flex direction='column' align='center'>
<Spinner size='xl' mb={4} />
<>
<Flex
direction='column'
justify='center'
align='center'
textAlign='center'
pt={10}
>
<Heading size='sm' fontSize='42px' mb={8}>
{t('setup-complete.header')}
</Heading>
<Heading size='md' fontWeight='medium' mb={2}>
{t('setup-complete.congratulations')}
</Heading>
<Text mb={16} fontWeight='medium'>
{role === GuardianRole.Follower
? t(`setup-complete.follower-message`)
: t(`setup-complete.leader-message`)}
</Text>
<Flex direction='column' align='center'>
<Spinner size='xl' mb={4} />
</Flex>
</Flex>
</Flex>

<BackupModal
isOpen={showBackupModal}
onClose={() => setShowBackupModal(false)}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
useConsensusPolling,
useGuardianSetupApi,
useGuardianSetupContext,
useTrimmedInputArray,
} from '../../../../../hooks';

interface PeerWithHash {
Expand All @@ -63,7 +64,9 @@ export const VerifyGuardians: React.FC<Props> = ({ next }) => {
const isHost = role === GuardianRole.Host;
const [myHash, setMyHash] = useState('');
const [peersWithHash, setPeersWithHash] = useState<PeerWithHash[]>();
const [enteredHashes, setEnteredHashes] = useState<string[]>([]);
const [enteredHashes, handleHashChange] = useTrimmedInputArray(
peersWithHash ? peersWithHash.map(() => '') : []
);
const [verifiedConfigs, setVerifiedConfigs] = useState<boolean>(false);
const [isStarting, setIsStarting] = useState(false);
const [error, setError] = useState<string>();
Expand All @@ -74,14 +77,6 @@ export const VerifyGuardians: React.FC<Props> = ({ next }) => {
useConsensusPolling();

useEffect(() => {
if (
peers.every(
(peer) => peer.status === GuardianServerStatus.VerifiedConfigs
)
) {
setVerifiedConfigs(true);
}

async function assembleHashInfo() {
if (peers.length === 0) {
return setError(t('verify-guardians.error'));
Expand All @@ -97,7 +92,6 @@ export const VerifyGuardians: React.FC<Props> = ({ next }) => {

try {
const hashes = await api.getVerifyConfigHash();

setMyHash(hashes[ourCurrentId]);
setPeersWithHash(
Object.entries(peers)
Expand All @@ -109,23 +103,23 @@ export const VerifyGuardians: React.FC<Props> = ({ next }) => {
.filter((peer) => peer.id !== ourCurrentId.toString())
);

// If we're already at the VerifiedConfigs state, prefill all the other hashes with the correct values
// Prefill hashes if already verified
if (
peers[ourCurrentId].status === GuardianServerStatus.VerifiedConfigs
) {
const otherPeers = Object.entries(peers).filter(
([id]) => id !== ourCurrentId.toString()
);
setEnteredHashes(
otherPeers.map(([id]) => hashes[id as unknown as number])
);
otherPeers.forEach(([id], idx) => {
handleHashChange(idx, hashes[id as unknown as number]);
});
}
} catch (err) {
setError(formatApiErrorMessage(err));
}
}
assembleHashInfo();
}, [api, peers, ourCurrentId, t]);
}, [api, peers, ourCurrentId, t, handleHashChange]);

useEffect(() => {
// If we're the only guardian, skip this verify other guardians step.
Expand Down Expand Up @@ -207,14 +201,6 @@ export const VerifyGuardians: React.FC<Props> = ({ next }) => {
sm: 'row',
}) as StackDirection | undefined;

const handleChangeHash = useCallback((value: string, index: number) => {
setEnteredHashes((hashes) => {
const newHashes = [...hashes];
newHashes[index] = value;
return newHashes;
});
}, []);

const tableRows = useMemo(() => {
if (!peersWithHash) return [];
return peersWithHash.map(({ peer, hash }, idx) => {
Expand All @@ -239,14 +225,14 @@ export const VerifyGuardians: React.FC<Props> = ({ next }) => {
variant='filled'
value={value}
placeholder={`${t('verify-guardians.verified-placeholder')}`}
onChange={(ev) => handleChangeHash(ev.currentTarget.value, idx)}
onChange={(ev) => handleHashChange(idx, ev.currentTarget.value)}
readOnly={isValid}
/>
</FormControl>
),
};
});
}, [peersWithHash, enteredHashes, handleChangeHash, t]);
}, [peersWithHash, enteredHashes, handleHashChange, t]);

if (error) {
return (
Expand Down Expand Up @@ -344,7 +330,7 @@ export const VerifyGuardians: React.FC<Props> = ({ next }) => {
'verify-guardians.verified-placeholder'
)}`}
onChange={(ev) =>
handleChangeHash(ev.currentTarget.value, idx)
handleHashChange(idx, ev.currentTarget.value)
}
readOnly={isValid}
/>
Expand Down
16 changes: 15 additions & 1 deletion apps/router/src/guardian-ui/setup/FederationSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { VerifyGuardians } from '../components/setup/screens/verifyGuardians/Ver
import { SetupComplete } from '../components/setup/screens/setupComplete/SetupComplete';
import { SetupProgress as SetupStepper } from '../components/setup/SetupProgress';
import { TermsOfService } from '../components/TermsOfService';
import { BackupModal } from '../components/BackupModal';

import { ReactComponent as ArrowLeftIcon } from '../assets/svgs/arrow-left.svg';
import { ReactComponent as CancelIcon } from '../assets/svgs/x-circle.svg';
Expand Down Expand Up @@ -137,7 +138,20 @@ export const FederationSetup: React.FC = () => {
canRestart = true;
break;
case SetupProgress.SetupComplete:
content = <SetupComplete role={role ?? GuardianRole.Follower} />;
content = (
<>
<SetupComplete role={role ?? GuardianRole.Follower} />
<BackupModal
isOpen={true}
onClose={() => {
dispatch({
type: SETUP_ACTION_TYPE.SET_INITIAL_STATE,
payload: null,
});
}}
/>
kleysc marked this conversation as resolved.
Show resolved Hide resolved
</>
);
break;
default:
title = t('setup.progress.error.title');
Expand Down
Loading
Loading