Skip to content

Commit

Permalink
Make it possible to export and import accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
brusherru committed Jul 13, 2024
1 parent b19b680 commit 824cc6f
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 78 deletions.
83 changes: 6 additions & 77 deletions src/components/CreateAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useEffect } from 'react';
import { Form, useForm } from 'react-hook-form';
import { z } from 'zod';

import {
Button,
Expand All @@ -18,8 +17,6 @@ import {
import { zodResolver } from '@hookform/resolvers/zod';
import { StdPublicKeys } from '@spacemesh/sm-codec';

import { Bech32AddressSchema } from '../api/schemas/address';
import { HexStringSchema } from '../api/schemas/common';
import { useCurrentHRP } from '../hooks/useNetworkSelectors';
import { useAccountsList } from '../hooks/useWalletSelectors';
import usePassword from '../store/usePassword';
Expand All @@ -30,8 +27,13 @@ import {
GENESIS_VESTING_START,
} from '../utils/constants';
import { noop } from '../utils/func';
import { AnySpawnArguments, getTemplateNameByKey } from '../utils/templates';
import { getTemplateNameByKey } from '../utils/templates';

import {
extractSpawnArgs,
FormSchema,
FormValues,
} from './createAccountSchema';
import FormAddressSelect from './FormAddressSelect';
import FormInput from './FormInput';
import FormKeySelect from './FormKeySelect';
Expand All @@ -43,79 +45,6 @@ type CreateAccountModalProps = {
onClose: () => void;
};

const DisplayNameSchema = z.string().min(2);

const SingleSigSchema = z.object({
displayName: DisplayNameSchema,
templateAddress: z.literal(StdPublicKeys.SingleSig),
publicKey: HexStringSchema,
});

const MultiSigSchema = z.object({
displayName: DisplayNameSchema,
templateAddress: z.literal(StdPublicKeys.MultiSig),
required: z.number().min(0).max(10),
publicKeys: z
.array(HexStringSchema)
.min(1, 'MultiSig account requires at least two parties'),
});

const VaultSchema = z.object({
displayName: DisplayNameSchema,
templateAddress: z.literal(StdPublicKeys.Vault),
owner: Bech32AddressSchema,
totalAmount: z.string().min(0),
initialUnlockAmount: z.string().min(0),
vestingStart: z.number().min(0),
vestingEnd: z.number().min(0),
});

const VestingSchema = z.object({
displayName: DisplayNameSchema,
templateAddress: z.literal(StdPublicKeys.Vesting),
required: z.number().min(0).max(10),
publicKeys: z
.array(HexStringSchema)
.min(1, 'Vesting account requires at least two parties'),
});

const FormSchema = z.discriminatedUnion('templateAddress', [
SingleSigSchema,
MultiSigSchema,
VaultSchema,
VestingSchema,
]);

type FormValues = z.infer<typeof FormSchema>;

const extractSpawnArgs = (data: FormValues): AnySpawnArguments => {
let args;
args = SingleSigSchema.safeParse(data);
if (args.success) {
return { PublicKey: args.data.publicKey };
}
args = MultiSigSchema.safeParse(data);
if (args.success) {
return { Required: args.data.required, PublicKeys: args.data.publicKeys };
}
args = VestingSchema.safeParse(data);
if (args.success) {
return { Required: args.data.required, PublicKeys: args.data.publicKeys };
}
args = VaultSchema.safeParse(data);
if (args.success) {
return {
Owner: args.data.owner,
TotalAmount: args.data.totalAmount,
InitialUnlockAmount: args.data.initialUnlockAmount,
VestingStart: args.data.vestingStart,
VestingEnd: args.data.vestingEnd,
};
}

throw new Error('Cannot get required inputs to create an account');
};

function CreateAccountModal({
isOpen,
onClose,
Expand Down
214 changes: 214 additions & 0 deletions src/components/ImportAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { useEffect, useRef, useState } from 'react';

import {
Button,
Card,
CardBody,
FormControl,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
} from '@chakra-ui/react';
import { StdTemplateKeys } from '@spacemesh/sm-codec';

import usePassword from '../store/usePassword';
import useWallet from '../store/useWallet';
import { Account, AccountWithAddress } from '../types/wallet';
import { isAnyAccount } from '../utils/account';
import { AnySpawnArguments, getTemplateNameByKey } from '../utils/templates';

type ImportAccountModalProps = {
isOpen: boolean;
onClose: () => void;
accounts: AccountWithAddress<AnySpawnArguments>[];
};

function ImportAccountModal({
isOpen,
onClose,
accounts,
}: ImportAccountModalProps): JSX.Element {
const { createAccount } = useWallet();
const { withPassword } = usePassword();

const inputRef = useRef<HTMLInputElement>(null);
const [displayName, setDisplayName] = useState('');
const [accountData, setAccountData] =
useState<Account<AnySpawnArguments> | null>(null);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
if (accountData) {
const dataConflict = accounts.find(
(acc) =>
acc.templateAddress === accountData.templateAddress &&
JSON.stringify(acc.spawnArguments) ===
JSON.stringify(accountData.spawnArguments)
);
if (dataConflict) {
setError(
// eslint-disable-next-line max-len
'You already have this account in the wallet. No need to import it once again.'
);
return;
}
}
const nameConflict = accounts.find(
(acc) => acc.displayName === displayName
);
if (nameConflict) {
setError(
// eslint-disable-next-line max-len
`You have account with such a name in the wallet. Please consider picking another display name`
);
return;
}

if (!displayName && accountData?.displayName) {
setDisplayName(accountData.displayName);
}
setError(null);
}, [displayName, accounts, accountData]);

const close = () => {
if (inputRef.current) {
inputRef.current.value = '';
}
setError(null);
setAccountData(null);
setDisplayName('');
onClose();
};

const readAccountFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
setError(null);

const reader = new FileReader();
reader.onload = () => {
if (inputRef.current) {
inputRef.current.value = '';
}
if (typeof reader.result !== 'string') {
setError('Failed to read file');
return;
}
try {
const newAccount = JSON.parse(reader.result as string);
if (isAnyAccount(newAccount)) {
setAccountData({ ...newAccount, displayName });
} else {
setError(`Cannot parse the account`);
}
} catch (err) {
setError(`Cannot parse the file: ${err}`);
}
};
reader.readAsText(file);
};

const submit = () => {
if (accountData) {
withPassword(
(password) =>
createAccount(
displayName,
accountData.templateAddress as StdTemplateKeys,
accountData.spawnArguments,
password
),
'Creating a new Account',
// eslint-disable-next-line max-len
`Please enter the password to create the new account "${
accountData.displayName
}" of type "${getTemplateNameByKey(accountData.templateAddress)}"`
)
.then(() => close())
.catch((err) => {
setError(`Failed to open wallet file:\n${err}`);
});
}
};

return (
<Modal isOpen={isOpen} onClose={close} isCentered>
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalHeader>Import Account</ModalHeader>
<ModalBody textAlign="center">
<Text mb={4}>Please select the account file to import.</Text>
<Input
ref={inputRef}
type="file"
display="none"
accept=".account"
onChange={readAccountFile}
/>
<Button
size="lg"
onClick={() => inputRef.current?.click()}
variant="solid"
colorScheme="green"
mb={4}
>
Select account file
</Button>
<FormControl mb={4}>
<FormLabel fontSize="sm" mb={0}>
Please set the display name for imported account:
</FormLabel>
<Input
type="text"
onChange={(e) => setDisplayName(e.target.value)}
value={displayName}
/>
</FormControl>
{accountData && (
<Card variant="outline">
<CardBody p={2} overflow="auto">
<Text as="pre" fontSize="xx-small" textAlign="left">
Template: {getTemplateNameByKey(accountData.templateAddress)}
{'\n\n'}
{Object.entries(accountData.spawnArguments).map(
([k, v], i) =>
`${i !== 0 ? '\n' : ''}${k}: ${
v instanceof Array ? `\n${v.join('\n')}` : v
}\n`
)}
</Text>
</CardBody>
</Card>
)}
{error && (
<Text mt={4} color="red">
{error}
</Text>
)}
</ModalBody>
<ModalFooter>
<Button
colorScheme="blue"
onClick={submit}
ml={2}
isDisabled={!!error}
>
Import
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

export default ImportAccountModal;
Loading

0 comments on commit 824cc6f

Please sign in to comment.