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

Support downloading key share(s) after DKG #12

Merged
merged 2 commits into from
Nov 16, 2023
Merged
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
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