Skip to content

Commit

Permalink
Support downloading key share(s) after DKG (#12)
Browse files Browse the repository at this point in the history
* Support downloading key share after key DKG.

* Format code.
  • Loading branch information
tmpfs committed Nov 16, 2023
1 parent 6a0ceb3 commit 9dab9e7
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 92 deletions.
4 changes: 2 additions & 2 deletions components/AccountsLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Loader from "@/components/Loader";
import { AppDispatch } from "@/app/store";
import { accountsSelector, setAccounts } from "@/app/store/accounts";
import { listAccounts } from "@/lib/keyring";
import guard from '@/lib/guard';
import guard from "@/lib/guard";

export default function AccountsLoader() {
const { toast } = useToast();
Expand All @@ -21,7 +21,7 @@ export default function AccountsLoader() {
await dispatch(setAccounts(accounts));
// Load any saved key information
setLoading(false);
}, toast );
}, toast);
};
if (!loaded) {
onLoadKeys();
Expand Down
22 changes: 17 additions & 5 deletions components/AddressBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@ import Icons from "@/components/Icons";

import { abbreviateAddress, copyWithToast } from "@/lib/utils";

export default function AddressBadge({address, className}: {address: string, className?: string}) {
export default function AddressBadge({
address,
className,
}: {
address: string;
className?: string;
}) {
const { toast } = useToast();
const copyAddress = async () => {
await copyWithToast(address, toast);
};

return <Badge onClick={copyAddress} className={className ?? 'cursor-pointer'} variant="secondary">
<Icons.copy className="h-4 w-4 mr-2" />
{abbreviateAddress(address)}
</Badge>;
return (
<Badge
onClick={copyAddress}
className={className ?? "cursor-pointer"}
variant="secondary"
>
<Icons.copy className="h-4 w-4 mr-2" />
{abbreviateAddress(address)}
</Badge>
);
}
64 changes: 30 additions & 34 deletions components/DeleteAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export default function DeleteAccount({
buttonText,
keyShareId,
}: {
account: KeyringAccount
onDeleted: (accountDeleted: boolean) => void
buttonText?: string
keyShareId?: string
account: KeyringAccount;
onDeleted: (accountDeleted: boolean) => void;
buttonText?: string;
keyShareId?: string;
}) {
const dispatch = useDispatch();
const { toast } = useToast();
Expand All @@ -52,35 +52,33 @@ export default function DeleteAccount({
}, toast);
};

const icon = isKeyShare
? (
<Icons.remove
className={`h-4 w-4 ${buttonText !== undefined ? "mr-2" : ""}`} />
)
: (
<Icons.trash
className={`h-4 w-4 ${buttonText !== undefined ? "mr-2" : ""}`} />
);
const icon = isKeyShare ? (
<Icons.remove
className={`h-4 w-4 ${buttonText !== undefined ? "mr-2" : ""}`}
/>
) : (
<Icons.trash
className={`h-4 w-4 ${buttonText !== undefined ? "mr-2" : ""}`}
/>
);

const title = isKeyShare
? (
<AlertDialogTitle>Delete key share</AlertDialogTitle>
)
: (
<AlertDialogTitle>Delete account</AlertDialogTitle>
);
const title = isKeyShare ? (
<AlertDialogTitle>Delete key share</AlertDialogTitle>
) : (
<AlertDialogTitle>Delete account</AlertDialogTitle>
);

const description = isKeyShare
? (
<AlertDialogDescription>
Deleting a key share is permananent you should export a backup copy of the key share first if you want to be able to import it later.
</AlertDialogDescription>
)
: (
<AlertDialogDescription>
Deleting an account is permanent consider exporting the account key shares first.
</AlertDialogDescription>
);
const description = isKeyShare ? (
<AlertDialogDescription>
Deleting a key share is permananent you should export a backup copy of the
key share first if you want to be able to import it later.
</AlertDialogDescription>
) : (
<AlertDialogDescription>
Deleting an account is permanent consider exporting the account key shares
first.
</AlertDialogDescription>
);

return (
<AlertDialog>
Expand All @@ -98,9 +96,7 @@ export default function DeleteAccount({
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction asChild onClick={removeAccount}>
<Button variant="destructive">
Continue
</Button>
<Button variant="destructive">Continue</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
Expand Down
99 changes: 78 additions & 21 deletions components/ExportAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,87 @@ import {
import Icons from "@/components/Icons";

import { exportAccount } from "@/lib/keyring";
import { KeyShares } from "@/lib/types";
import { PrivateKey, KeyShares } from "@/lib/types";
import { toUint8Array, download, keyShareAddress } from "@/lib/utils";
import guard from "@/lib/guard";

function downloadKeyShares(
address: string,
privateKey: KeyShares,
keyShareId?: string,
) {
const exportedAccount = { privateKey };
let fileName = `${address}.json`;
if (keyShareId) {
fileName = `${address}-${keyShareId}.json`;
}
const value = JSON.stringify(exportedAccount, undefined, 2);
download(fileName, toUint8Array(value));
}

function AlertHeader() {
return (
<AlertDialogHeader>
<AlertDialogTitle>Export account</AlertDialogTitle>
<AlertDialogDescription>
Exporting this account will download the private key to your computer
unencrypted; you should copy the file to safe encrypted storage such as
a password manager and delete the downloaded file from your disc.
</AlertDialogDescription>
</AlertDialogHeader>
);
}

export function DownloadKeyShare({
keyShare,
buttonText,
}: {
keyShare: PrivateKey;
buttonText?: string;
}) {
const { toast } = useToast();

const downloadKeyShare = async () => {
await guard(async () => {
const { address } = keyShare;
const { i: keyShareId } = keyShare.privateKey;
const keyShares: KeyShares = {};
keyShares[keyShareId.toString()] = keyShare;
await downloadKeyShares(address, keyShares, keyShareId.toString());
}, toast);
};

return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<Icons.download
className={`h-4 w-4 ${buttonText !== undefined ? "mr-2" : ""}`}
/>
{buttonText}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertHeader />
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={downloadKeyShare}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

export default function ExportAccount({
account,
buttonText,
keyShareId,
}: {
account: KeyringAccount
buttonText?: string
keyShareId?: string
account: KeyringAccount;
buttonText?: string;
keyShareId?: string;
}) {
const { toast } = useToast();

Expand All @@ -50,34 +119,22 @@ export default function ExportAccount({
privateKey[keyShareId] = keySharePrivateKey;
}

const exportedAccount = {
privateKey,
};

const fileName = `${address}.json`;
const value = JSON.stringify(exportedAccount, undefined, 2);
download(fileName, toUint8Array(value));
downloadKeyShares(address, privateKey, keyShareId);
}, toast);
};

return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<Icons.download className={`h-4 w-4 ${buttonText !== undefined ? "mr-2" : ""}`} />
<Icons.download
className={`h-4 w-4 ${buttonText !== undefined ? "mr-2" : ""}`}
/>
{buttonText}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Export account</AlertDialogTitle>
<AlertDialogDescription>
Exporting this account will download the private key to your
computer unencrypted; you should copy the file to safe encrypted
storage such as a password manager and delete the downloaded file
from your disc.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertHeader />
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={downloadAccount}>
Expand Down
12 changes: 3 additions & 9 deletions components/SaveKeyShare.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import KeyAlert from "@/components/KeyAlert";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";

import { DownloadKeyShare } from "@/components/ExportAccount";

import { createAccount } from "@/lib/keyring";
import { invalidateAccounts } from "@/app/store/accounts";
import { PrivateKey } from "@/lib/types";
Expand All @@ -21,12 +23,6 @@ export default function SaveKeyShare({
const dispatch = useDispatch();
const { toast } = useToast();

const downloadKeyShare = async () => {
await guard(async () => {
//await createAccount(keyShare, name);
}, toast);
};

const saveKeyShare = async () => {
await guard(async () => {
await createAccount(keyShare, name);
Expand All @@ -46,9 +42,7 @@ export default function SaveKeyShare({
description="Your key share is ready, now you just need to save it in MetaMask or download and save it to safe encrypted storage such as a password maanager or encrypted disc."
/>
<div className="flex justify-end space-x-4">
<Button variant="outline" onClick={downloadKeyShare}>
Download
</Button>
<DownloadKeyShare keyShare={keyShare} buttonText="Download" />
<Button onClick={saveKeyShare}>Save to MetaMask</Button>
</div>
</div>
Expand Down
8 changes: 7 additions & 1 deletion components/SharesBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import type { KeyringAccount } from "@metamask/keyring-api";
import { Parameters } from "@/lib/types";
import { Badge } from "@/components/ui/badge";

export default function SharesBadge({ account, className }: { account: KeyringAccount, className?: string }) {
export default function SharesBadge({
account,
className,
}: {
account: KeyringAccount;
className?: string;
}) {
const { shares, parameters } = account.options as {
shares: string[];
parameters: Parameters;
Expand Down
5 changes: 4 additions & 1 deletion lib/keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export async function deleteAccount(id: string): Promise<void> {
return await client.deleteAccount(id);
}

export async function deleteKeyShare(id: string, keyShareId: string): Promise<boolean> {
export async function deleteKeyShare(
id: string,
keyShareId: string,
): Promise<boolean> {
return (await ethereum.request({
method: "wallet_invokeSnap",
params: {
Expand Down
6 changes: 4 additions & 2 deletions pages/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ function AccountContent({
<DeleteAccount
account={account}
buttonText="Delete"
onDeleted={onDeleted} />
onDeleted={onDeleted}
/>
</div>
</div>
<ChainBadge className="mt-2" />
Expand Down Expand Up @@ -102,7 +103,8 @@ export default function Account() {
<DeleteAccount
account={account}
keyShareId={keyShareId}
onDeleted={onDeleted} />
onDeleted={onDeleted}
/>
</div>
</div>
);
Expand Down
25 changes: 15 additions & 10 deletions pages/CreateKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ import guard from "@/lib/guard";
import { keygen, WebassemblyWorker } from "@/lib/client";
import { convertRawKey } from "@/lib/utils";

function BackButton({onClick}: {onClick: () => void}) {
function BackButton({ onClick }: { onClick: () => void }) {
// NOTE: the type="button" is required so form submission on
// NOTE: enter key works as expected.
return <Button
type="button" variant="outline" onClick={onClick}>
Back
</Button>
return (
<Button type="button" variant="outline" onClick={onClick}>
Back
</Button>
);
}

function CreateKeyContent({ children }: { children: React.ReactNode }) {
Expand Down Expand Up @@ -148,21 +149,25 @@ export default function CreateKey() {
);
}

const backToAccounts = () => navigate('/');
const backToAccounts = () => navigate("/");
const defaultBack = () => setStep(step - 1);

if (step == 0) {
return (
<CreateKeyContent>
<KeyShareAudienceForm onNext={onKeyAudience}
back={<BackButton onClick={backToAccounts} />} />
<KeyShareAudienceForm
onNext={onKeyAudience}
back={<BackButton onClick={backToAccounts} />}
/>
</CreateKeyContent>
);
} else if (step == 1) {
return (
<CreateKeyContent>
<KeyShareNameForm onNext={onKeyName}
back={<BackButton onClick={defaultBack} />} />
<KeyShareNameForm
onNext={onKeyName}
back={<BackButton onClick={defaultBack} />}
/>
</CreateKeyContent>
);
} else if (step == 2) {
Expand Down
Loading

0 comments on commit 9dab9e7

Please sign in to comment.