Skip to content

Commit

Permalink
feature(auth): Lock a wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
sim committed Jan 17, 2022
1 parent bbaf9a7 commit c43a179
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 66 deletions.
1 change: 1 addition & 0 deletions src/app/sections/ConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const ConnectWallet = ({ renderButton }: Props) => {
<ModalButton
title={t("Connect wallet")}
renderButton={renderButton ?? defaultRenderButton}
maxHeight
>
<Grid gap={20}>
<SwitchWallet />
Expand Down
1 change: 1 addition & 0 deletions src/auth/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type LocalWallet = SingleWallet | MultisigWallet // wallet with name
interface SingleWallet {
address: string
name: string
lock?: boolean
}

interface MultisigWallet extends SingleWallet {
Expand Down
2 changes: 2 additions & 0 deletions src/auth/components/AuthButton.module.scss
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
.button {
border: var(--border-width) solid var(--card-border);
border-radius: var(--border-radius);
color: inherit;
font-weight: var(--normal);
transition: all var(--transition);

&:hover,
&.active {
background: var(--bg-muted);
text-decoration: none;
}

&.active {
Expand Down
20 changes: 12 additions & 8 deletions src/auth/components/AuthButton.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { ButtonHTMLAttributes } from "react"
import { Link, LinkProps } from "react-router-dom"
import classNames from "classnames/bind"
import styles from "./AuthButton.module.scss"

const cx = classNames.bind(styles)

interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
interface ButtonAttrs extends ButtonHTMLAttributes<HTMLButtonElement> {
active: boolean
}

interface LinkAttrs extends LinkProps {
active: boolean
}

type Props = ButtonAttrs | LinkAttrs

const AuthButton = ({ active, ...attrs }: Props) => {
return (
<button
{...attrs}
type="button"
className={cx(styles.button, { active }, attrs.className)}
/>
)
const className = cx(styles.button, { active }, attrs.className)
if (active) return <span {...attrs} className={className} />
if ("to" in attrs) return <Link {...attrs} className={className} />
return <button {...attrs} type="button" className={className} />
}

export default AuthButton
23 changes: 16 additions & 7 deletions src/auth/components/AuthList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@ import { ReactNode } from "react"
import { Link } from "react-router-dom"
import styles from "./AuthList.module.scss"

interface Item {
to: string
children: string
icon: ReactNode
}
type Item =
| { to: string; children: string; icon: ReactNode }
| { onClick: () => void; children: string; icon: ReactNode }

const AuthList = ({ list }: { list: Item[] }) => {
const renderItem = ({ to, children, icon }: Item) => {
const renderItem = ({ children, icon, ...props }: Item) => {
if ("onClick" in props) {
const { onClick } = props
return (
<button className={styles.link} onClick={onClick} key={children}>
{children}
{icon}
</button>
)
}

const { to } = props
return (
<Link to={to} className={styles.link} key={to}>
<Link to={to} className={styles.link} key={children}>
{children}
{icon}
</Link>
Expand Down
34 changes: 23 additions & 11 deletions src/auth/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { useLCDClient } from "data/queries/lcdClient"
import is from "../scripts/is"
import { PasswordError } from "../scripts/keystore"
import { getDecryptedKey, testPassword } from "../scripts/keystore"
import { getWallet, storeWallet, clearWallet } from "../scripts/keystore"
import { getWallet, storeWallet } from "../scripts/keystore"
import { clearWallet, lockWallet } from "../scripts/keystore"
import { getStoredWallet, getStoredWallets } from "../scripts/keystore"
import encrypt from "../scripts/encrypt"
import * as ledger from "../ledger/ledger"
Expand All @@ -28,11 +29,13 @@ const useAuth = () => {
const [wallet, setWallet] = useRecoilState(walletState)
const wallets = getStoredWallets()

/* connect | disconnect */
/* connect */
const connect = useCallback(
(name: string) => {
const storedWallet = getStoredWallet(name)
const { address } = storedWallet
const { address, lock } = storedWallet

if (lock) throw new Error("Wallet is locked")

const wallet = is.multisig(storedWallet)
? { name, address, multisig: true }
Expand All @@ -53,22 +56,30 @@ const useAuth = () => {
[setWallet]
)

const disconnect = useCallback(() => {
clearWallet()
setWallet(undefined)
}, [setWallet])

/* helpers */
/* connected */
const connectedWallet = useMemo(() => {
if (!is.local(wallet)) return
return wallet
}, [wallet])

const getConnectedWallet = () => {
const getConnectedWallet = useCallback(() => {
if (!connectedWallet) throw new Error("Wallet is not defined")
return connectedWallet
}
}, [connectedWallet])

/* disconnected */
const disconnect = useCallback(() => {
clearWallet()
setWallet(undefined)
}, [setWallet])

const lock = useCallback(() => {
const { name } = getConnectedWallet()
lockWallet(name)
disconnect()
}, [disconnect, getConnectedWallet])

/* helpers */
const getKey = (password: string) => {
const { name } = getConnectedWallet()
return getDecryptedKey({ name, password })
Expand Down Expand Up @@ -198,6 +209,7 @@ const useAuth = () => {
connect,
connectLedger,
disconnect,
lock,
available,
encodeEncryptedWallet,
validatePassword,
Expand Down
4 changes: 2 additions & 2 deletions src/auth/modules/Auth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Route, Routes } from "react-router-dom"

/* connect */
import UnlockPage from "./select/UnlockPage"
import AccessWithLedgerPage from "../ledger/AccessWithLedgerPage"

/* create */
Expand All @@ -14,14 +15,14 @@ import ManageWallets from "./manage/ManageWallets"
import ExportWalletPage from "./manage/ExportWalletPage"
import ChangePasswordPage from "./manage/ChangePasswordPage"
import DeleteWalletPage from "./manage/DeleteWalletPage"
import Disconnect from "./manage/Disconnect"

const Auth = () => {
return (
<Routes>
<Route index element={<ManageWallets />} />

{/* connect */}
<Route path="unlock/:name" element={<UnlockPage />} />
<Route path="ledger" element={<AccessWithLedgerPage />} />

{/* create */}
Expand All @@ -34,7 +35,6 @@ const Auth = () => {
<Route path="export" element={<ExportWalletPage />} />
<Route path="password" element={<ChangePasswordPage />} />
<Route path="delete" element={<DeleteWalletPage />} />
<Route path="disconnect" element={<Disconnect />} />
</Routes>
)
}
Expand Down
17 changes: 0 additions & 17 deletions src/auth/modules/manage/Disconnect.tsx

This file was deleted.

31 changes: 23 additions & 8 deletions src/auth/modules/manage/ManageWallets.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import QrCodeIcon from "@mui/icons-material/QrCode"
import PasswordIcon from "@mui/icons-material/Password"
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"
import FactCheckOutlinedIcon from "@mui/icons-material/FactCheckOutlined"
import LogoutIcon from "@mui/icons-material/Logout"
import LockOutlinedIcon from "@mui/icons-material/LockOutlined"
import { Col, Page } from "components/layout"
import is from "../../scripts/is"
import useAuth from "../../hooks/useAuth"
Expand All @@ -12,7 +14,8 @@ import ConnectedWallet from "./ConnectedWallet"

export const useManageWallet = () => {
const { t } = useTranslation()
const { wallet } = useAuth()
const navigate = useNavigate()
const { wallet, disconnect, lock } = useAuth()

const toExport = {
to: "/auth/export",
Expand Down Expand Up @@ -44,19 +47,31 @@ export const useManageWallet = () => {
icon: <FactCheckOutlinedIcon />,
}

const toDisconnect = {
to: "/auth/disconnect",
const disconnectWallet = {
onClick: () => {
disconnect()
navigate("/", { replace: true })
},
children: t("Disconnect"),
icon: <LogoutIcon />,
}

const lockWallet = {
onClick: () => {
lock()
navigate("/", { replace: true })
},
children: t("Lock"),
icon: <LockOutlinedIcon />,
}

if (!wallet) return

return is.ledger(wallet)
? [toSignMultisig, toDisconnect]
: is.multisig(wallet)
? [toPostMultisig, toDelete, toDisconnect]
: [toExport, toPassword, toDelete, toSignMultisig, toDisconnect]
return is.multisig(wallet)
? [toPostMultisig, toDelete, disconnectWallet]
: is.ledger(wallet)
? [toSignMultisig, disconnectWallet]
: [toExport, toPassword, toDelete, toSignMultisig, lockWallet]
}

const ManageWallets = () => {
Expand Down
35 changes: 23 additions & 12 deletions src/auth/modules/select/SwitchWallet.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import LockOutlinedIcon from "@mui/icons-material/LockOutlined"
import { truncate } from "@terra.kitchen/utils"
import classNames from "classnames/bind"
import { Flex } from "components/layout"
Expand All @@ -15,22 +16,32 @@ const SwitchWallet = () => {
return !wallets.length ? null : (
<ul className={styles.list}>
{wallets.map((wallet) => {
const { name, address } = wallet
const { name, address, lock } = wallet
const active = name === connectedWallet?.name
const children = (
<>
<Flex gap={4}>
{is.multisig(wallet) && <MultisigBadge />}
<strong>{name}</strong>
</Flex>

{lock ? (
<LockOutlinedIcon fontSize="inherit" className="muted" />
) : (
truncate(address)
)}
</>
)

const attrs = { className: cx(styles.wallet), active, children }

return (
<li key={name}>
<AuthButton
className={cx(styles.wallet)}
onClick={() => connect(name)}
active={active}
>
<Flex gap={4}>
{is.multisig(wallet) && <MultisigBadge />}
<strong>{name}</strong>
</Flex>
{truncate(address)}
</AuthButton>
{lock ? (
<AuthButton {...attrs} to={`/auth/unlock/${name}`} />
) : (
<AuthButton {...attrs} onClick={() => connect(name)} />
)}
</li>
)
})}
Expand Down
56 changes: 56 additions & 0 deletions src/auth/modules/select/UnlockForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useTranslation } from "react-i18next"
import { useNavigate, useParams } from "react-router-dom"
import { useForm } from "react-hook-form"
import { Form, FormItem } from "components/form"
import { Input, Submit } from "components/form"
import { testPassword, unlockWallet } from "../../scripts/keystore"
import useAuth from "../../hooks/useAuth"

interface Values {
password: string
}

const UnlockForm = () => {
const { t } = useTranslation()
const { name } = useParams()
const navigate = useNavigate()
const { connect } = useAuth()

if (!name) throw new Error("Invalid path")

/* form */
const form = useForm<Values>({ mode: "onChange" })
const { register, handleSubmit, formState } = form
const { errors, isValid } = formState

/* submit */
const submit = ({ password }: Values) => {
unlockWallet(name, password)
connect(name)
navigate("/wallet", { replace: true })
}

return (
<Form onSubmit={handleSubmit(submit)}>
<FormItem label={t("Password")} error={errors.password?.message}>
<Input
{...register("password", {
validate: (password) => {
try {
testPassword({ name, password })
} catch {
return "Incorrect password"
}
},
})}
type="password"
autoFocus
/>
</FormItem>

<Submit disabled={!isValid} />
</Form>
)
}

export default UnlockForm
Loading

0 comments on commit c43a179

Please sign in to comment.