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

feat: Ability to display/export the private key #3290

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@
}
}

.privateKey {
background: transparent;
border: none;
cursor: pointer;
&:hover {
svg {
g,
path {
stroke: var(--primary-color);
}
}
}
}

@media screen and (max-width: 1330px) {
.container {
.balance {
Expand Down
23 changes: 22 additions & 1 deletion packages/neuron-ui/src/components/AddressBook/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'
import { useState as useGlobalState, useDispatch } from 'states'
import Dialog from 'widgets/Dialog'
import CopyZone from 'widgets/CopyZone'
import { Copy } from 'widgets/Icons/icon'
import ViewPrivateKey from 'components/ViewPrivateKey'
import { Copy, PrivateKey } from 'widgets/Icons/icon'
import Table, { TableProps, SortType } from 'widgets/Table'
import { shannonToCKBFormatter, useLocalDescription } from 'utils'
import { HIDE_BALANCE } from 'utils/const'
Expand Down Expand Up @@ -44,6 +45,7 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {

const dispatch = useDispatch()
const { onChangeEditStatus, onSubmitDescription } = useLocalDescription('address', walletId, dispatch)
const [viewPrivateKeyAddress, setViewPrivateKeyAddress] = useState('')

const columns = useMemo<TableProps<State.Address>['columns']>(
() => [
Expand Down Expand Up @@ -149,6 +151,21 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
return 0
},
},
{
title: '',
dataIndex: 'key',
align: 'left',
width: '40px',
render(_, __, { address }) {
return (
<Tooltip tip={t('addresses.view-private-key')} placement="left">
<button type="button" className={styles.privateKey} onClick={() => setViewPrivateKeyAddress(address)}>
<PrivateKey />
</button>
</Tooltip>
)
},
},
],
[t]
)
Expand Down Expand Up @@ -179,6 +196,10 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
}
/>
</div>

{!!viewPrivateKeyAddress && (
<ViewPrivateKey address={viewPrivateKeyAddress} onClose={() => setViewPrivateKeyAddress('')} />
)}
</div>
</Dialog>
)
Expand Down
36 changes: 23 additions & 13 deletions packages/neuron-ui/src/components/Receive/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import Button from 'widgets/Button'
import CopyZone from 'widgets/CopyZone'
import QRCode from 'widgets/QRCode'
import Tooltip from 'widgets/Tooltip'
import { AddressTransform, Download, Copy, Attention, SuccessNoBorder } from 'widgets/Icons/icon'
import ViewPrivateKey from 'components/ViewPrivateKey'
import { AddressTransform, Download, Copy, Attention, SuccessNoBorder, PrivateKey } from 'widgets/Icons/icon'
import VerifyHardwareAddress from './VerifyHardwareAddress'
import styles from './receive.module.scss'
import { useCopyAndDownloadQrCode, useSwitchAddress } from './hooks'
Expand All @@ -29,6 +30,7 @@ export const AddressQrCodeWithCopyZone = ({
)

const [isCopySuccess, setIsCopySuccess] = useState(false)
const [showViewPrivateKey, setShowViewPrivateKey] = useState(false)
const timer = useRef<ReturnType<typeof setTimeout>>()
const { ref, onCopyQrCode, onDownloadQrCode, showCopySuccess } = useCopyAndDownloadQrCode()

Expand Down Expand Up @@ -70,19 +72,27 @@ export const AddressQrCodeWithCopyZone = ({
<CopyZone content={showAddress} className={styles.showAddress}>
{showAddress}
</CopyZone>
<button
type="button"
className={styles.addressToggle}
onClick={onClick}
title={transformLabel}
onFocus={stopPropagation}
onMouseOver={stopPropagation}
onMouseUp={stopPropagation}
>
<AddressTransform />
{transformLabel}
</button>
<div className={styles.actionWrap}>
<button
type="button"
className={styles.addressToggle}
onClick={onClick}
title={transformLabel}
onFocus={stopPropagation}
onMouseOver={stopPropagation}
onMouseUp={stopPropagation}
>
<AddressTransform />
{transformLabel}
</button>
<button type="button" className={styles.privateKey} onClick={() => setShowViewPrivateKey(true)}>
<PrivateKey />
{t('addresses.view-private-key')}
</button>
</div>
</div>

{showViewPrivateKey && <ViewPrivateKey address={showAddress} onClose={() => setShowViewPrivateKey(false)} />}
</div>
)
}
Expand Down
49 changes: 33 additions & 16 deletions packages/neuron-ui/src/components/Receive/receive.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,26 +125,43 @@
color: var(--main-text-color);
}

.addressToggle {
width: 100%;
.actionWrap {
margin-top: 8px;
appearance: none;
border: none;
background: none;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
font-family: PingFang SC;
font-style: normal;
font-weight: 500;
color: var(--primary-color);
line-height: normal;
cursor: pointer;
gap: 32px;

svg {
pointer-events: none;
margin-right: 5px;
button {
appearance: none;
border: none;
background: none;
font-size: 12px;
font-family: PingFang SC;
font-style: normal;
font-weight: 500;
color: var(--primary-color);
line-height: normal;
cursor: pointer;
display: flex;
align-items: center;
}

.addressToggle {
svg {
pointer-events: none;
margin-right: 5px;
}
}

.privateKey {
svg {
width: 16px;
margin-right: 3px;
g,
path {
stroke: var(--primary-color);
}
}
}
}

Expand Down
145 changes: 145 additions & 0 deletions packages/neuron-ui/src/components/ViewPrivateKey/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useState as useGlobalState } from 'states'
import Dialog from 'widgets/Dialog'
import TextField from 'widgets/TextField'
import Alert from 'widgets/Alert'
import { errorFormatter, useCopy, isSuccessResponse } from 'utils'
import { Attention, Copy } from 'widgets/Icons/icon'
import { getPrivateKeyByAddress } from 'services/remote'
import styles from './viewPrivateKey.module.scss'

const ViewPrivateKey = ({ onClose, address }: { onClose?: () => void; address?: string }) => {
const [t] = useTranslation()
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [privateKey, setPrivateKey] = useState('')
const [isLoading, setIsLoading] = useState(false)
const { copied, onCopy, copyTimes } = useCopy()
const {
wallet: { id: walletID = '' },
} = useGlobalState()

useEffect(() => {
setPassword('')
setError('')
}, [setError, setPassword])

const onChange = useCallback(
(e: React.SyntheticEvent<HTMLInputElement>) => {
const { value } = e.target as HTMLInputElement
setPassword(value)
setError('')
},
[setPassword, setError]
)

const onSubmit = useCallback(
async (e?: React.FormEvent) => {
if (e) {
e.preventDefault()
}
if (!password) {
return
}
setIsLoading(true)
try {
const res = await getPrivateKeyByAddress({
walletID,
address,
password,
})

setIsLoading(false)

if (!isSuccessResponse(res)) {
setError(errorFormatter(res.message, t))
return
}
setPrivateKey(res.result)
} catch (err) {
setIsLoading(false)
}
},
[walletID, password, setError, t]
)

if (privateKey) {
return (
<Dialog
show
title={t('addresses.view-private-key')}
onConfirm={onClose}
onCancel={onClose}
showCancel={false}
confirmText={t('common.close')}
className={styles.dialog}
>
<div>
<div className={styles.tip}>
<Attention />
{t('addresses.view-private-key-tip')}
</div>

<TextField
className={styles.passwordInput}
placeholder={t('password-request.placeholder')}
width="100%"
label={<span className={styles.label}>{t('addresses.private-key')}</span>}
value={privateKey}
field="password"
type="password"
disabled
suffix={
<div className={styles.copy}>
<Copy onClick={() => onCopy(privateKey)} />
</div>
}
/>

{copied ? (
<Alert status="success" className={styles.notice} key={copyTimes.toString()}>
{t('common.copied')}
</Alert>
) : null}
</div>
</Dialog>
)
}
return (
<Dialog
show
title={t('addresses.view-private-key')}
onCancel={onClose}
onConfirm={onSubmit}
confirmText={t('wizard.next')}
isLoading={isLoading}
disabled={!password || isLoading}
className={styles.dialog}
>
<div>
<div className={styles.tip}>
<Attention />
{t('addresses.view-private-key-tip')}
</div>

<TextField
className={styles.passwordInput}
placeholder={t('password-request.placeholder')}
width="100%"
label={t('wizard.password')}
value={password}
field="password"
type="password"
onChange={onChange}
autoFocus
error={error}
/>
</div>
</Dialog>
)
}

ViewPrivateKey.displayName = 'ViewPrivateKey'

export default ViewPrivateKey
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@import '../../styles/mixin.scss';

.passwordInput {
margin-top: 16px;
}

.dialog {
width: 700px;
}

.tip {
color: var(--warn-text-color);
background: var(--warn-background-color);
margin: -20px -16px 0;
display: flex;
align-items: center;
justify-content: center;
height: 32px;
font-size: 12px;
gap: 4px;
font-weight: 500;
border-bottom: 1px solid var(--warn-border-color);
}

.label {
font-weight: 500;
color: var(--main-text-color);
font-size: 14px;
}

.copy {
display: flex;
align-items: center;
margin-left: 6px;
}

.notice {
@include dialog-copy-animation;
}
Loading
Loading