Skip to content

Commit

Permalink
Add import ledger feature for web
Browse files Browse the repository at this point in the history
  • Loading branch information
serjonya-trili committed Aug 28, 2024
1 parent f403f31 commit 5db5c19
Show file tree
Hide file tree
Showing 14 changed files with 345 additions and 110 deletions.
21 changes: 14 additions & 7 deletions apps/desktop/src/components/Onboarding/FakeAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { Button, FormControl, FormLabel, Input } from "@chakra-ui/react";
import { RpcClient } from "@taquito/rpc";
import type { IDP } from "@umami/social-auth";
import { useRestoreLedger, useRestoreSocial } from "@umami/state";
import { GHOSTNET, defaultDerivationPathTemplate, makeDerivationPath } from "@umami/tezos";
import {
GHOSTNET,
defaultDerivationPathTemplate,
makeDerivationPath,
parseImplicitPkh,
} from "@umami/tezos";
import { useForm } from "react-hook-form";

import { ModalContentWrapper } from "./ModalContentWrapper";
Expand Down Expand Up @@ -31,13 +36,15 @@ export const FakeAccount = ({ onClose }: { onClose: () => void }) => {
if (idp) {
restoreSocial(pk, pkh, name, idp);
} else {
restoreLedger(
defaultDerivationPathTemplate,
makeDerivationPath(defaultDerivationPathTemplate, 0),
restoreLedger({
type: "ledger",
derivationPathTemplate: defaultDerivationPathTemplate,
derivationPath: makeDerivationPath(defaultDerivationPathTemplate, 0),
pk,
pkh,
name
);
address: parseImplicitPkh(pkh),
label: name,
curve: "ed25519",
});
}
onClose();
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { mockToast } from "@umami/state";
import { defaultDerivationPathTemplate, getLedgerPublicKeyPair } from "@umami/tezos";
import {
defaultDerivationPathTemplate,
getLedgerPublicKeyPair,
mockImplicitAddress,
} from "@umami/tezos";

import { RestoreLedger } from "./RestoreLedger";
import { act, render, screen, userEvent } from "../../../mocks/testUtils";
Expand All @@ -25,7 +29,7 @@ const fixture = () => {
describe("<RestoreLedger />", () => {
test("success", async () => {
const user = userEvent.setup();
getPkMock.mockResolvedValue({ pk: "test", pkh: "test" });
getPkMock.mockResolvedValue({ pk: "test", pkh: mockImplicitAddress(0).pkh });
render(fixture());

const confirmBtn = screen.getByRole("button", {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, ListItem, OrderedList, VStack, useToast } from "@chakra-ui/react";
import { withTimeout } from "@umami/core";
import { useAsyncActionHandler, useRestoreLedger } from "@umami/state";
import { getLedgerPublicKeyPair, makeDerivationPath } from "@umami/tezos";
import { getLedgerPublicKeyPair, makeDerivationPath, parseImplicitPkh } from "@umami/tezos";

import { USBIcon } from "../../../assets/icons";
import { ModalContentWrapper } from "../ModalContentWrapper";
Expand Down Expand Up @@ -49,9 +49,19 @@ export const RestoreLedger = ({

const derivationPath = account.derivationPathTemplate
? makeDerivationPath(account.derivationPathTemplate, 0)
: account.derivationPath;
const { pk, pkh } = await getLedgerPublicKeyPair(derivationPath);
restoreLedger(account.derivationPathTemplate, derivationPath!, pk, pkh, account.label);
: account.derivationPath!;
// TODO: add support for other curves
const curve = "ed25519";
const { pk, pkh } = await getLedgerPublicKeyPair(derivationPath, curve);
restoreLedger({
type: "ledger",
derivationPath,
pk,
address: parseImplicitPkh(pkh),
curve,
label: account.label,
derivationPathTemplate: account.derivationPathTemplate,
});
toast.close(toastId);
toast({ description: "Account successfully created!", status: "success" });
closeModal();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Button } from "@chakra-ui/react";
import { type Curves } from "@taquito/signer";
import { defaultDerivationPathTemplate } from "@umami/tezos";
import { FormProvider, useForm } from "react-hook-form";

import { AdvancedAccountSettings, CURVES } from "./AdvancedAccountSettings";
import { act, fireEvent, render, screen, userEvent, waitFor } from "../../../testUtils";

const TestComponent = ({ onSubmit }: { onSubmit: () => void }) => {
const form = useForm<{ derivationPath: string; curve: Curves }>({
mode: "onBlur",
defaultValues: {
derivationPath: defaultDerivationPathTemplate,
curve: "ed25519",
},
});

return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<AdvancedAccountSettings />
<Button type="submit">Submit</Button>
</form>
</FormProvider>
);
};

describe("<AdvancedAccountSettings />", () => {
it("requires a derivation path", async () => {
const user = userEvent.setup();
render(<TestComponent onSubmit={() => {}} />);

await act(() => user.click(screen.getByText("Advanced")));
const input = screen.getByLabelText("Derivation Path");
await act(() => user.clear(input));
fireEvent.blur(input);

await waitFor(() => expect(screen.getByText("Derivation path is required")).toBeVisible());
});

it.each(CURVES)("has %s curve option", async curve => {
const user = userEvent.setup();

render(<TestComponent onSubmit={() => {}} />);

await act(() => user.click(screen.getByText("Advanced")));

await waitFor(() => expect(screen.getByRole("button", { name: curve })).toBeVisible());
});

it("restores the default derivation on reset click", async () => {
const user = userEvent.setup();
render(<TestComponent onSubmit={() => {}} />);

const input = screen.getByLabelText("Derivation Path");
await act(() => user.clear(input));
await act(() => user.click(screen.getByText("Reset")));

await waitFor(() => expect(input).toHaveValue("44'/1729'/?'/0'"));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Button,
Flex,
FormControl,
FormErrorMessage,
FormLabel,
Heading,
Input,
InputGroup,
InputRightElement,
} from "@chakra-ui/react";
import { type Curves } from "@taquito/signer";
import { useFormContext } from "react-hook-form";

import { useColor } from "../../../styles/useColor";
import { RadioButtons } from "../../RadioButtons";

export const CURVES = ["ed25519", "secp256k1", "p256"];

export const AdvancedAccountSettings = () => {
const color = useColor();

const form = useFormContext<{ derivationPath: string; curve: Curves }>();

const {
formState: { errors },
register,
resetField,
} = form;

return (
<Accordion marginTop="6px" allowToggle data-testid="advanced-section">
<AccordionItem>
<AccordionButton justifyContent="center" color={color("900")}>
<Heading size="md">Advanced</Heading>
<AccordionIcon />
</AccordionButton>

<AccordionPanel>
<Flex flexDirection="column" gap="24px">
<FormControl isInvalid={!!errors.curve}>
<FormLabel>Elliptic Curve</FormLabel>
<Flex gap="8px">
<RadioButtons fontSize="sm" fontWeight="400" inputName="curve" options={CURVES} />
</Flex>
</FormControl>

<FormControl isInvalid={!!errors.derivationPath}>
<FormLabel>Derivation Path</FormLabel>

<InputGroup>
<Input
{...register("derivationPath", {
required: "Derivation path is required",
})}
placeholder="m/44'/1729'/?'/0' (default)"
/>
<InputRightElement>
<Button
marginRight="10px"
color={color("200")}
fontWeight="600"
background={color("black")}
borderRadius="4px"
_hover={{ background: color("400") }}
onClick={() => resetField("derivationPath")}
size="sm"
>
Reset
</Button>
</InputRightElement>
</InputGroup>
{errors.derivationPath && (
<FormErrorMessage>{errors.derivationPath.message}</FormErrorMessage>
)}
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>
</Accordion>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./AdvancedAccountSettings";
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ jest.mock("../../../utils/persistor", () => ({
persistor: { pause: jest.fn(), resume: jest.fn() },
}));

jest.setTimeout(15000);

describe("<ImportBackupTab />", () => {
it("requires a file", async () => {
render(<ImportBackupTab />);
Expand Down Expand Up @@ -76,6 +78,8 @@ describe("<ImportBackupTab />", () => {
await act(() => user.type(screen.getByLabelText("Password"), password));
await act(() => user.click(screen.getByText("Import Wallet")));

await waitFor(() => expect(store.getState().accounts.items.length).toEqual(1));
await waitFor(() => expect(store.getState().accounts.items.length).toEqual(1), {
timeout: 10000,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@chakra-ui/react";

import { ImportBackupTab } from "./ImportBackupTab";
import { LedgerTab } from "./LedgerTab";
import { SecretKeyTab } from "./SecretKeyTab";
import { SeedPhraseTab } from "./SeedPhraseTab";
import { LoginIcon } from "../../../assets/icons";
Expand All @@ -31,7 +32,7 @@ export const ImportWallet = () => {
</Center>
</ModalHeader>
<ModalBody>
<Tabs variant="onboarding">
<Tabs isLazy variant="onboarding">
<TabSwitch options={["Seed Phrase", "Secret Key", "Backup", "Ledger"]} />

<TabPanels padding="30px 0 0 0">
Expand All @@ -44,7 +45,9 @@ export const ImportWallet = () => {
<TabPanel>
<ImportBackupTab />
</TabPanel>
<TabPanel>Ledger</TabPanel>
<TabPanel>
<LedgerTab />
</TabPanel>
</TabPanels>
</Tabs>
</ModalBody>
Expand Down
Loading

1 comment on commit 5db5c19

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.91% (1789/2132) 78.94% (840/1064) 78.57% (451/574)
apps/web Coverage: 83%
83.91% (1789/2132) 78.94% (840/1064) 78.57% (451/574)
packages/components Coverage: 96%
96.89% (125/129) 98.07% (51/52) 84.21% (32/38)
packages/core Coverage: 82%
83.05% (196/236) 73.55% (89/121) 82.14% (46/56)
packages/crypto Coverage: 100%
100% (28/28) 100% (3/3) 100% (5/5)
packages/data-polling Coverage: 98%
96.55% (140/145) 95.45% (21/22) 92.85% (39/42)
packages/multisig Coverage: 98%
98.4% (123/125) 89.47% (17/19) 100% (33/33)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 84%
84.26% (766/909) 80.4% (160/199) 79.33% (288/363)
packages/tezos Coverage: 86%
85.57% (89/104) 89.47% (17/19) 82.75% (24/29)
packages/tzkt Coverage: 86%
84.05% (58/69) 81.25% (13/16) 76.92% (30/39)

Please sign in to comment.