Skip to content

Commit 9a77b74

Browse files
authored
feat: Ability to display/export the private key (nervosnetwork#3290)
* feat: Ability to display/export the private key * feat: test * fix: comment * fix: check * fix: test * fix: test * fix: check * fix: check * fix: comment * fix: comment
1 parent 7cc4585 commit 9a77b74

File tree

23 files changed

+445
-41
lines changed

23 files changed

+445
-41
lines changed

packages/neuron-ui/src/components/AddressBook/addressBook.module.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@
168168
}
169169
}
170170

171+
.privateKey {
172+
background: transparent;
173+
border: none;
174+
cursor: pointer;
175+
&:hover {
176+
svg {
177+
g,
178+
path {
179+
stroke: var(--primary-color);
180+
}
181+
}
182+
}
183+
}
184+
171185
@media screen and (max-width: 1330px) {
172186
.container {
173187
.balance {

packages/neuron-ui/src/components/AddressBook/index.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'
33
import { useState as useGlobalState, useDispatch } from 'states'
44
import Dialog from 'widgets/Dialog'
55
import CopyZone from 'widgets/CopyZone'
6-
import { Copy } from 'widgets/Icons/icon'
6+
import ViewPrivateKey from 'components/ViewPrivateKey'
7+
import { Copy, PrivateKey } from 'widgets/Icons/icon'
78
import Table, { TableProps, SortType } from 'widgets/Table'
89
import { shannonToCKBFormatter, useLocalDescription } from 'utils'
910
import { HIDE_BALANCE } from 'utils/const'
@@ -44,6 +45,7 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
4445

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

4850
const columns = useMemo<TableProps<State.Address>['columns']>(
4951
() => [
@@ -149,6 +151,21 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
149151
return 0
150152
},
151153
},
154+
{
155+
title: '',
156+
dataIndex: 'key',
157+
align: 'left',
158+
width: '40px',
159+
render(_, __, { address }) {
160+
return (
161+
<Tooltip tip={t('addresses.view-private-key')} placement="left">
162+
<button type="button" className={styles.privateKey} onClick={() => setViewPrivateKeyAddress(address)}>
163+
<PrivateKey />
164+
</button>
165+
</Tooltip>
166+
)
167+
},
168+
},
152169
],
153170
[t]
154171
)
@@ -179,6 +196,10 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
179196
}
180197
/>
181198
</div>
199+
200+
{!!viewPrivateKeyAddress && (
201+
<ViewPrivateKey address={viewPrivateKeyAddress} onClose={() => setViewPrivateKeyAddress('')} />
202+
)}
182203
</div>
183204
</Dialog>
184205
)

packages/neuron-ui/src/components/Receive/index.tsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import Button from 'widgets/Button'
77
import CopyZone from 'widgets/CopyZone'
88
import QRCode from 'widgets/QRCode'
99
import Tooltip from 'widgets/Tooltip'
10-
import { AddressTransform, Download, Copy, Attention, SuccessNoBorder } from 'widgets/Icons/icon'
10+
import ViewPrivateKey from 'components/ViewPrivateKey'
11+
import { AddressTransform, Download, Copy, Attention, SuccessNoBorder, PrivateKey } from 'widgets/Icons/icon'
1112
import VerifyHardwareAddress from './VerifyHardwareAddress'
1213
import styles from './receive.module.scss'
1314
import { useCopyAndDownloadQrCode, useSwitchAddress } from './hooks'
@@ -29,6 +30,7 @@ export const AddressQrCodeWithCopyZone = ({
2930
)
3031

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

@@ -70,19 +72,27 @@ export const AddressQrCodeWithCopyZone = ({
7072
<CopyZone content={showAddress} className={styles.showAddress}>
7173
{showAddress}
7274
</CopyZone>
73-
<button
74-
type="button"
75-
className={styles.addressToggle}
76-
onClick={onClick}
77-
title={transformLabel}
78-
onFocus={stopPropagation}
79-
onMouseOver={stopPropagation}
80-
onMouseUp={stopPropagation}
81-
>
82-
<AddressTransform />
83-
{transformLabel}
84-
</button>
75+
<div className={styles.actionWrap}>
76+
<button
77+
type="button"
78+
className={styles.addressToggle}
79+
onClick={onClick}
80+
title={transformLabel}
81+
onFocus={stopPropagation}
82+
onMouseOver={stopPropagation}
83+
onMouseUp={stopPropagation}
84+
>
85+
<AddressTransform />
86+
{transformLabel}
87+
</button>
88+
<button type="button" className={styles.privateKey} onClick={() => setShowViewPrivateKey(true)}>
89+
<PrivateKey />
90+
{t('addresses.view-private-key')}
91+
</button>
92+
</div>
8593
</div>
94+
95+
{showViewPrivateKey && <ViewPrivateKey address={showAddress} onClose={() => setShowViewPrivateKey(false)} />}
8696
</div>
8797
)
8898
}

packages/neuron-ui/src/components/Receive/receive.module.scss

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,42 @@
125125
color: var(--main-text-color);
126126
}
127127

128-
.addressToggle {
129-
width: 100%;
128+
.actionWrap {
130129
margin-top: 8px;
131-
appearance: none;
132-
border: none;
133-
background: none;
134130
display: flex;
135131
justify-content: center;
136-
align-items: center;
137-
font-size: 12px;
138-
font-family: PingFang SC;
139-
font-style: normal;
140-
font-weight: 500;
141-
color: var(--primary-color);
142-
line-height: normal;
143-
cursor: pointer;
132+
gap: 32px;
144133

145-
svg {
146-
pointer-events: none;
147-
margin-right: 5px;
134+
button {
135+
appearance: none;
136+
border: none;
137+
background: none;
138+
font-size: 12px;
139+
font-style: normal;
140+
font-weight: 500;
141+
color: var(--primary-color);
142+
line-height: normal;
143+
cursor: pointer;
144+
display: flex;
145+
align-items: center;
146+
}
147+
148+
.addressToggle {
149+
svg {
150+
pointer-events: none;
151+
margin-right: 5px;
152+
}
153+
}
154+
155+
.privateKey {
156+
svg {
157+
width: 16px;
158+
margin-right: 3px;
159+
g,
160+
path {
161+
stroke: var(--primary-color);
162+
}
163+
}
148164
}
149165
}
150166

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import React, { useCallback, useEffect, useState } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { useState as useGlobalState } from 'states'
4+
import Dialog from 'widgets/Dialog'
5+
import TextField from 'widgets/TextField'
6+
import Alert from 'widgets/Alert'
7+
import { errorFormatter, useCopy, isSuccessResponse } from 'utils'
8+
import { Attention, Copy } from 'widgets/Icons/icon'
9+
import { getPrivateKeyByAddress } from 'services/remote'
10+
import styles from './viewPrivateKey.module.scss'
11+
12+
const ViewPrivateKey = ({ onClose, address }: { onClose?: () => void; address?: string }) => {
13+
const [t] = useTranslation()
14+
const [password, setPassword] = useState('')
15+
const [error, setError] = useState('')
16+
const [privateKey, setPrivateKey] = useState('')
17+
const [isLoading, setIsLoading] = useState(false)
18+
const { copied, onCopy, copyTimes } = useCopy()
19+
const {
20+
wallet: { id: walletID = '' },
21+
} = useGlobalState()
22+
23+
useEffect(() => {
24+
setPassword('')
25+
setError('')
26+
}, [setError, setPassword])
27+
28+
const onChange = useCallback(
29+
(e: React.SyntheticEvent<HTMLInputElement>) => {
30+
const { value } = e.target as HTMLInputElement
31+
setPassword(value)
32+
setError('')
33+
},
34+
[setPassword, setError]
35+
)
36+
37+
const onSubmit = useCallback(
38+
(e?: React.FormEvent) => {
39+
if (e) {
40+
e.preventDefault()
41+
}
42+
if (!password) {
43+
return
44+
}
45+
setIsLoading(true)
46+
getPrivateKeyByAddress({
47+
walletID,
48+
address,
49+
password,
50+
})
51+
.then(res => {
52+
if (!isSuccessResponse(res)) {
53+
setError(errorFormatter(res.message, t))
54+
return
55+
}
56+
setPrivateKey(res.result)
57+
})
58+
.finally(() => {
59+
setIsLoading(false)
60+
})
61+
},
62+
[walletID, password, setError, t]
63+
)
64+
65+
if (privateKey) {
66+
return (
67+
<Dialog
68+
show
69+
title={t('addresses.view-private-key')}
70+
onConfirm={onClose}
71+
onCancel={onClose}
72+
showCancel={false}
73+
confirmText={t('common.close')}
74+
className={styles.dialog}
75+
>
76+
<div>
77+
<div className={styles.tip}>
78+
<Attention />
79+
{t('addresses.view-private-key-tip')}
80+
</div>
81+
82+
<TextField
83+
className={styles.passwordInput}
84+
placeholder={t('password-request.placeholder')}
85+
width="100%"
86+
label={<span className={styles.label}>{t('addresses.private-key')}</span>}
87+
value={privateKey}
88+
field="password"
89+
type="password"
90+
disabled
91+
suffix={
92+
<div className={styles.copy}>
93+
<Copy onClick={() => onCopy(privateKey)} />
94+
</div>
95+
}
96+
/>
97+
98+
{copied ? (
99+
<Alert status="success" className={styles.notice} key={copyTimes.toString()}>
100+
{t('common.copied')}
101+
</Alert>
102+
) : null}
103+
</div>
104+
</Dialog>
105+
)
106+
}
107+
return (
108+
<Dialog
109+
show
110+
title={t('addresses.view-private-key')}
111+
onCancel={onClose}
112+
onConfirm={onSubmit}
113+
confirmText={t('wizard.next')}
114+
isLoading={isLoading}
115+
disabled={!password || isLoading}
116+
className={styles.dialog}
117+
>
118+
<div>
119+
<div className={styles.tip}>
120+
<Attention />
121+
{t('addresses.view-private-key-tip')}
122+
</div>
123+
124+
<TextField
125+
className={styles.passwordInput}
126+
placeholder={t('password-request.placeholder')}
127+
width="100%"
128+
label={t('wizard.password')}
129+
value={password}
130+
field="password"
131+
type="password"
132+
onChange={onChange}
133+
autoFocus
134+
error={error}
135+
/>
136+
</div>
137+
</Dialog>
138+
)
139+
}
140+
141+
ViewPrivateKey.displayName = 'ViewPrivateKey'
142+
143+
export default ViewPrivateKey
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
@import '../../styles/mixin.scss';
2+
3+
.passwordInput {
4+
margin-top: 16px;
5+
}
6+
7+
.dialog {
8+
width: 700px;
9+
}
10+
11+
.tip {
12+
color: var(--warn-text-color);
13+
background: var(--warn-background-color);
14+
margin: -20px -16px 0;
15+
display: flex;
16+
align-items: center;
17+
justify-content: center;
18+
height: 32px;
19+
font-size: 12px;
20+
gap: 4px;
21+
font-weight: 500;
22+
border-bottom: 1px solid var(--warn-border-color);
23+
}
24+
25+
.label {
26+
font-weight: 500;
27+
color: var(--main-text-color);
28+
font-size: 14px;
29+
}
30+
31+
.copy {
32+
display: flex;
33+
align-items: center;
34+
margin-left: 6px;
35+
}
36+
37+
.notice {
38+
@include dialog-copy-animation;
39+
}

0 commit comments

Comments
 (0)