Skip to content

Commit f7e99b8

Browse files
committed
feat: Ability to display/export the private key
1 parent 0d64329 commit f7e99b8

File tree

22 files changed

+384
-40
lines changed

22 files changed

+384
-40
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: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,43 @@
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-family: PingFang SC;
140+
font-style: normal;
141+
font-weight: 500;
142+
color: var(--primary-color);
143+
line-height: normal;
144+
cursor: pointer;
145+
display: flex;
146+
align-items: center;
147+
}
148+
149+
.addressToggle {
150+
svg {
151+
pointer-events: none;
152+
margin-right: 5px;
153+
}
154+
}
155+
156+
.privateKey {
157+
svg {
158+
width: 16px;
159+
margin-right: 3px;
160+
g,
161+
path {
162+
stroke: var(--primary-color);
163+
}
164+
}
148165
}
149166
}
150167

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
async (e?: React.FormEvent) => {
39+
if (e) {
40+
e.preventDefault()
41+
}
42+
if (!password) {
43+
return
44+
}
45+
setIsLoading(true)
46+
try {
47+
const res = await getPrivateKeyByAddress({
48+
walletID,
49+
address,
50+
password,
51+
})
52+
53+
setIsLoading(false)
54+
55+
if (!isSuccessResponse(res)) {
56+
setError(errorFormatter(res.message, t))
57+
return
58+
}
59+
setPrivateKey(res.result)
60+
} catch (err) {
61+
setIsLoading(false)
62+
}
63+
},
64+
[walletID, password, setError, t]
65+
)
66+
67+
if (privateKey) {
68+
return (
69+
<Dialog
70+
show
71+
title={t('addresses.view-private-key')}
72+
onConfirm={onClose}
73+
onCancel={onClose}
74+
showCancel={false}
75+
confirmText={t('common.close')}
76+
className={styles.dialog}
77+
>
78+
<div>
79+
<div className={styles.tip}>
80+
<Attention />
81+
{t('addresses.view-private-key-tip')}
82+
</div>
83+
84+
<TextField
85+
className={styles.passwordInput}
86+
placeholder={t('password-request.placeholder')}
87+
width="100%"
88+
label={<span className={styles.label}>{t('addresses.private-key')}</span>}
89+
value={privateKey}
90+
field="password"
91+
type="password"
92+
disabled
93+
suffix={
94+
<div className={styles.copy}>
95+
<Copy onClick={() => onCopy(privateKey)} />
96+
</div>
97+
}
98+
/>
99+
100+
{copied ? (
101+
<Alert status="success" className={styles.notice} key={copyTimes.toString()}>
102+
{t('common.copied')}
103+
</Alert>
104+
) : null}
105+
</div>
106+
</Dialog>
107+
)
108+
}
109+
return (
110+
<Dialog
111+
show
112+
title={t('addresses.view-private-key')}
113+
onCancel={onClose}
114+
onConfirm={onSubmit}
115+
confirmText={t('wizard.next')}
116+
isLoading={isLoading}
117+
disabled={!password || isLoading}
118+
className={styles.dialog}
119+
>
120+
<div>
121+
<div className={styles.tip}>
122+
<Attention />
123+
{t('addresses.view-private-key-tip')}
124+
</div>
125+
126+
<TextField
127+
className={styles.passwordInput}
128+
placeholder={t('password-request.placeholder')}
129+
width="100%"
130+
label={t('wizard.password')}
131+
value={password}
132+
field="password"
133+
type="password"
134+
onChange={onChange}
135+
autoFocus
136+
error={error}
137+
/>
138+
</div>
139+
</Dialog>
140+
)
141+
}
142+
143+
ViewPrivateKey.displayName = 'ViewPrivateKey'
144+
145+
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)