Skip to content

Commit

Permalink
feat: devire import identities from seed phrase
Browse files Browse the repository at this point in the history
- [x] Add signature options for identity import
- [x] Minor refactoring
  • Loading branch information
0xmad committed Oct 18, 2023
1 parent 796aac1 commit 2ea495f
Show file tree
Hide file tree
Showing 26 changed files with 485 additions and 74 deletions.
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"eventemitter2": "^6.4.9",
"extension-port-stream": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"idc-nullifier": "^0.0.4",
"json-stable-stringify": "^1.0.2",
"link-preview-js": "^3.0.5",
"lodash": "^4.17.21",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/* eslint-disable @typescript-eslint/unbound-method */
import { EventName } from "@cryptkeeperzk/providers";
import { EWallet, ConnectedIdentityMetadata, ICreateIdentityOptions, IImportIdentityArgs } from "@cryptkeeperzk/types";
import {
EWallet,
type ConnectedIdentityMetadata,
type ICreateIdentityOptions,
type IImportIdentityArgs,
} from "@cryptkeeperzk/types";
import { createNewIdentity } from "@cryptkeeperzk/zk";
import pick from "lodash/pick";
import browser from "webextension-polyfill";
Expand Down Expand Up @@ -643,6 +648,7 @@ describe("background/services/zkIdentity", () => {
name: "Name",
nullifier: mockDefaultNullifier,
trapdoor: mockDefaultTrapdoor,
messageSignature: "signature",
urlOrigin: "http://localhost:3000",
};

Expand Down
20 changes: 19 additions & 1 deletion packages/app/src/background/services/zkIdentity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
} from "@cryptkeeperzk/types";
import { ZkIdentitySemaphore, createNewIdentity } from "@cryptkeeperzk/zk";
import { bigintToHex } from "bigint-conversion";
// import { Prover } from "idc-nullifier";
import omit from "lodash/omit";
import pick from "lodash/pick";
import browser from "webextension-polyfill";

Expand Down Expand Up @@ -58,13 +60,16 @@ export default class ZkIdentityService extends BaseService implements IBackupabl

private lockService: LockerService;

// private idcProver: Prover;

private connectedIdentity?: ZkIdentitySemaphore;

private constructor() {
super();
this.connectedIdentity = undefined;
this.identitiesStore = new SimpleStorage(IDENTITY_KEY);
this.connectedIdentityStore = new SimpleStorage(CONNECTED_IDENTITY_KEY);
// this.idcProver = new Prover();
this.notificationService = NotificationService.getInstance();
this.historyService = HistoryService.getInstance();
this.browserController = BrowserUtils.getInstance();
Expand Down Expand Up @@ -343,7 +348,19 @@ export default class ZkIdentityService extends BaseService implements IBackupabl
};

import = async (args: IImportIdentityArgs): Promise<string> => {
const identity = createNewIdentity({ ...args, groups: [], isDeterministic: false });
// const importedIdentity = createNewIdentity({ ...args, groups: [], isDeterministic: false, isImported: true });
const identity = createNewIdentity({
...omit(args, ["trapdoor", "nullifier"]),
groups: [],
isDeterministic: true,
isImported: true,
});
// const proof = this.idcProver.generateProof({
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// // @ts-expect-error
// identity: importedIdentity.zkIdentity,
// externalNullifier: identity.genIdentityCommitment(),
// });
const status = await this.insertIdentity(identity);

if (!status) {
Expand All @@ -369,6 +386,7 @@ export default class ZkIdentityService extends BaseService implements IBackupabl
urlOrigin,
isDeterministic,
nonce: isDeterministic ? options.nonce : undefined,
isImported: false,
name: options.name || `Account # ${numOfIdentities}`,
messageSignature,
};
Expand Down
14 changes: 13 additions & 1 deletion packages/app/src/config/mock/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import BigNumber from "bignumber.js";

import { mockConnector } from "@src/connectors/mock";
import { ConnectorNames, IUseWalletData } from "@src/types";
import { ConnectorNames, type IUseWalletData } from "@src/types";

import type { IUseSignatureOptionsData } from "@src/ui/hooks/wallet/useSignatureOptions";
import type { BrowserProvider } from "ethers";

import { ZERO_ADDRESS } from "../const";
Expand All @@ -29,3 +30,14 @@ export const defaultWalletHookData: IUseWalletData = {
onDisconnect: jest.fn(),
onLock: jest.fn(),
};

export const mockSignatureOptions: IUseSignatureOptionsData = {
options: [
{ id: "ck", title: "Sign with CryptKeeper", checkDisabledItem: () => false },
{
id: "eth",
title: "Sign with MetaMask",
checkDisabledItem: () => false,
},
],
};
25 changes: 16 additions & 9 deletions packages/app/src/ui/components/DropdownButton/DropdownButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import ButtonGroup, { type ButtonGroupProps } from "@mui/material/ButtonGroup";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Grow from "@mui/material/Grow";
import MenuItem from "@mui/material/MenuItem";
Expand All @@ -11,27 +11,34 @@ import Typography from "@mui/material/Typography";

import { useDropdownButton } from "./useDropdownButton";

export interface IDropdownButtonProps {
menuOptions: IDropdownButtonMenuItem[];
export interface IDropdownButtonProps extends Omit<ButtonGroupProps, "onClick"> {
options: IDropdownButtonOption[];
disabled?: boolean;
onClick: (index: number) => void;
}

export interface IDropdownButtonMenuItem {
export interface IDropdownButtonOption {
id: string;
title: string;
checkDisabledItem?: (index: number) => boolean;
}

const DropdownButton = ({ menuOptions, onClick }: IDropdownButtonProps): JSX.Element => {
const DropdownButton = ({ disabled = false, options, onClick, ...rest }: IDropdownButtonProps): JSX.Element => {
const { isMenuOpen, menuRef, selectedIndex, onToggleMenu, onMenuItemClick, onSubmit } = useDropdownButton({
onClick,
});

return (
<>
<ButtonGroup ref={menuRef} sx={{ ml: 1, width: "70%" }} variant="contained">
<Button data-testid="dropdown-button" size="small" sx={{ textTransform: "none", flex: 1 }} onClick={onSubmit}>
{menuOptions[selectedIndex].title}
<ButtonGroup ref={menuRef} variant="contained" {...rest} sx={{ ml: 1, width: "70%", ...rest.sx }}>
<Button
data-testid="dropdown-button"
disabled={disabled}
size="small"
sx={{ textTransform: "none", flex: 1 }}
onClick={onSubmit}
>
{options[selectedIndex].title}
</Button>

<Button data-testid="dropdown-menu-button" size="small" sx={{ width: 5 }} onClick={onToggleMenu}>
Expand Down Expand Up @@ -64,7 +71,7 @@ const DropdownButton = ({ menuOptions, onClick }: IDropdownButtonProps): JSX.Ele
>
<ClickAwayListener onClickAway={onToggleMenu}>
<MenuList autoFocusItem id="split-button-menu">
{menuOptions.map((option, index) => (
{options.map((option, index) => (
<MenuItem
key={option.id}
data-testid={`dropdown-menu-item-${index}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DropdownButton, IDropdownButtonProps } from "..";

describe("ui/components/DropdownButton", () => {
const defaultProps: IDropdownButtonProps = {
menuOptions: [
options: [
{
id: "metamask",
title: "Connect to MetaMask",
Expand Down Expand Up @@ -60,7 +60,7 @@ describe("ui/components/DropdownButton", () => {
const menuItem = await findByTestId("dropdown-menu-item-1");
act(() => fireEvent.click(menuItem));

const selectedItem = await findByText(defaultProps.menuOptions[1].title);
const selectedItem = await findByText(defaultProps.options[1].title);
expect(selectedItem).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion packages/app/src/ui/components/DropdownButton/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { IDropdownButtonMenuItem, IDropdownButtonProps } from "./DropdownButton";
export type { IDropdownButtonOption, IDropdownButtonProps } from "./DropdownButton";
export { default as DropdownButton } from "./DropdownButton";
1 change: 1 addition & 0 deletions packages/app/src/ui/ducks/__tests__/identities.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ describe("ui/ducks/identities", () => {
nullifier: "nullifier",
trapdoor: "trapdoor",
urlOrigin: "http://localhost:3000",
messageSignature: "signature",
};

await Promise.resolve(store.dispatch(importIdentity(args)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @jest-environment jsdom
*/

import { renderHook } from "@testing-library/react";

import { defaultWalletHookData } from "@src/config/mock/wallet";

import { useSignatureOptions } from "..";
import { useEthWallet } from "../useEthWallet";

jest.mock("../useEthWallet", (): unknown => ({
useEthWallet: jest.fn(),
}));

describe("ui/hooks/useSignatureOptions", () => {
const defaultHookArgs = {
isLoading: false,
};

beforeEach(() => {
(useEthWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: true });
});

afterEach(() => {
jest.clearAllMocks();
});

test("should return cryptkeeper and eth wallet options", () => {
const { result } = renderHook(() => useSignatureOptions(defaultHookArgs));

expect(result.current.options[0].id).toBe("ck");
expect(result.current.options[0].title).toBe("Sign with CryptKeeper");
expect(result.current.options[0].checkDisabledItem()).toBe(defaultHookArgs.isLoading);
expect(result.current.options[1].id).toBe("eth");
expect(result.current.options[1].title).toBe("Sign with MetaMask");
expect(result.current.options[1].checkDisabledItem()).toBe(defaultHookArgs.isLoading);
});

test("should return cryptkeeper and connect eth wallet options", () => {
(useEthWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: false });

const { result } = renderHook(() => useSignatureOptions(defaultHookArgs));

expect(result.current.options[0].id).toBe("ck");
expect(result.current.options[0].title).toBe("Sign with CryptKeeper");
expect(result.current.options[0].checkDisabledItem()).toBe(defaultHookArgs.isLoading);
expect(result.current.options[1].id).toBe("eth");
expect(result.current.options[1].title).toBe("Connect to MetaMask");
expect(result.current.options[1].checkDisabledItem()).toBe(defaultHookArgs.isLoading);
});

test("should return cryptkeeper and install eth wallet options", () => {
(useEthWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: false, isInjectedWallet: false });

const { result } = renderHook(() => useSignatureOptions(defaultHookArgs));

expect(result.current.options[0].id).toBe("ck");
expect(result.current.options[0].title).toBe("Sign with CryptKeeper");
expect(result.current.options[0].checkDisabledItem()).toBe(defaultHookArgs.isLoading);
expect(result.current.options[1].id).toBe("eth");
expect(result.current.options[1].title).toBe("Install MetaMask");
expect(result.current.options[1].checkDisabledItem()).toBe(true);
});
});
1 change: 1 addition & 0 deletions packages/app/src/ui/hooks/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { useCryptKeeperWallet } from "./useCryptKeeper";
export { useEthWallet } from "./useEthWallet";
export { useSignatureOptions } from "./useSignatureOptions";
33 changes: 33 additions & 0 deletions packages/app/src/ui/hooks/wallet/useSignatureOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEthWallet } from "./useEthWallet";

export interface IUseSignatureOptionsArgs {
isLoading: boolean;
}

export interface IUseSignatureOptionsData {
options: ISignatureOption[];
}

export interface ISignatureOption {
id: string;
title: string;
checkDisabledItem: () => boolean;
}

export const useSignatureOptions = ({ isLoading }: IUseSignatureOptionsArgs): IUseSignatureOptionsData => {
const ethWallet = useEthWallet();

const { isActive: isWalletConnected, isInjectedWallet: isWalletInstalled } = ethWallet;
const ethWalletTitle = isWalletConnected ? "Sign with MetaMask" : "Connect to MetaMask";

const options = [
{ id: "ck", title: "Sign with CryptKeeper", checkDisabledItem: () => isLoading },
{
id: "eth",
title: isWalletInstalled ? ethWalletTitle : "Install MetaMask",
checkDisabledItem: () => isLoading || !isWalletInstalled,
},
];

return { options };
};
27 changes: 5 additions & 22 deletions packages/app/src/ui/pages/CreateIdentity/CreateIdentity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,14 @@ import { Checkbox } from "@src/ui/components/Checkbox";
import { DropdownButton } from "@src/ui/components/DropdownButton";
import { FullModalContent, FullModalFooter, FullModalHeader } from "@src/ui/components/FullModal";
import { Input } from "@src/ui/components/Input";
import { useSignatureOptions } from "@src/ui/hooks/wallet";

import { useCreateIdentity } from "./useCreateIdentity";

const CreateIdentity = (): JSX.Element => {
const {
isLoading,
isWalletInstalled,
isWalletConnected,
errors,
control,
onCloseModal,
onSign,
onGoToImportIdentity,
} = useCreateIdentity();

const ethWalletTitle = isWalletConnected ? "Sign with MetaMask" : "Connect to MetaMask";

const menuOptions = [
{ id: "ck", title: "Sign with CryptKeeper", checkDisabledItem: () => isLoading },
{
id: "eth",
title: isWalletInstalled ? ethWalletTitle : "Install metamask",
checkDisabledItem: () => isLoading || !isWalletInstalled,
},
];
const { isLoading, errors, control, onCloseModal, onSign, onGoToImportIdentity } = useCreateIdentity();

const { options } = useSignatureOptions({ isLoading });

return (
<Box data-testid="create-identity-page" sx={{ height: "100%" }}>
Expand Down Expand Up @@ -130,7 +113,7 @@ const CreateIdentity = (): JSX.Element => {
Reject
</Button>

<DropdownButton menuOptions={menuOptions} onClick={onSign} />
<DropdownButton options={options} onClick={onSign} />
</Box>

<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { Suspense } from "react";
import { useNavigate } from "react-router-dom";

import { ZERO_ADDRESS } from "@src/config/const";
import { defaultWalletHookData } from "@src/config/mock/wallet";
import { defaultWalletHookData, mockSignatureOptions } from "@src/config/mock/wallet";
import { Paths } from "@src/constants";
import { closePopup } from "@src/ui/ducks/app";
import { useAppDispatch } from "@src/ui/ducks/hooks";
import { createIdentity } from "@src/ui/ducks/identities";
import { useCryptKeeperWallet, useEthWallet } from "@src/ui/hooks/wallet";
import { useCryptKeeperWallet, useEthWallet, useSignatureOptions } from "@src/ui/hooks/wallet";
import { signWithSigner, getMessageTemplate } from "@src/ui/services/identity";

import CreateIdentity from "..";
Expand Down Expand Up @@ -42,6 +42,7 @@ jest.mock("@src/ui/ducks/identities", (): unknown => ({
jest.mock("@src/ui/hooks/wallet", (): unknown => ({
useEthWallet: jest.fn(),
useCryptKeeperWallet: jest.fn(),
useSignatureOptions: jest.fn(),
}));

describe("ui/pages/CreateIdentity", () => {
Expand All @@ -64,6 +65,8 @@ describe("ui/pages/CreateIdentity", () => {

(useCryptKeeperWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: true });

(useSignatureOptions as jest.Mock).mockReturnValue(mockSignatureOptions);

(useAppDispatch as jest.Mock).mockReturnValue(mockDispatch);

(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
Expand Down Expand Up @@ -100,6 +103,17 @@ describe("ui/pages/CreateIdentity", () => {

test("should connect properly to eth wallet", async () => {
(useEthWallet as jest.Mock).mockReturnValue({ ...defaultWalletHookData, isActive: false });
(useSignatureOptions as jest.Mock).mockReturnValue({
...mockSignatureOptions,
options: [
...mockSignatureOptions.options.filter(({ id }) => id === "eth"),
{
id: "eth",
title: "Connect to MetaMask",
checkDisabledItem: () => false,
},
],
});

const { container } = render(
<Suspense>
Expand Down
Loading

0 comments on commit 2ea495f

Please sign in to comment.