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 11 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
44 changes: 31 additions & 13 deletions packages/neuron-ui/src/components/Receive/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ 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'

type AddressTransformWithCopyZoneProps = {
showAddress: string
assetAccountId?: string
isInShortFormat: boolean
onClick: () => void
}

export const AddressQrCodeWithCopyZone = ({
showAddress,
assetAccountId,
isInShortFormat,
onClick,
}: AddressTransformWithCopyZoneProps) => {
Expand All @@ -29,6 +32,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 +74,33 @@ 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}
assetAccountId={assetAccountId}
onClose={() => setShowViewPrivateKey(false)}
/>
)}
</div>
)
}
Expand Down
48 changes: 32 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,42 @@
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-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
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const SUDTAccountList = () => {
break
}
setReceiveData({
accountId: account.accountId,
address: account.address,
accountName: account.accountName ?? DEFAULT_SUDT_FIELDS.accountName,
tokenName: account.tokenName ?? DEFAULT_SUDT_FIELDS.tokenName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { DEFAULT_SUDT_FIELDS } = CONSTANTS

export interface DataProps {
address: string
accountId: string
devchenyan marked this conversation as resolved.
Show resolved Hide resolved
accountName: string
tokenName: string
symbol: string
Expand All @@ -21,7 +22,7 @@ export interface DataProps {
const SUDTReceiveDialog = ({ data, onClose }: { data: DataProps; onClose?: () => void }) => {
const [t] = useTranslation()
const [isInShortFormat, setIsInShortFormat] = useState(false)
const { address, accountName, tokenName, symbol } = data
const { address, accountId, accountName, tokenName, symbol } = data

const displayedAddr = isInShortFormat ? addressToAddress(address, { deprecated: true }) : address

Expand Down Expand Up @@ -52,6 +53,7 @@ const SUDTReceiveDialog = ({ data, onClose }: { data: DataProps; onClose?: () =>

<AddressQrCodeWithCopyZone
showAddress={displayedAddr}
assetAccountId={accountId}
isInShortFormat={isInShortFormat}
onClick={() => setIsInShortFormat(is => !is)}
/>
Expand Down
152 changes: 152 additions & 0 deletions packages/neuron-ui/src/components/ViewPrivateKey/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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,
assetAccountId,
}: {
onClose?: () => void
address?: string
assetAccountId?: 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(
(e?: React.FormEvent) => {
if (e) {
e.preventDefault()
}
if (!password) {
return
}
setIsLoading(true)
getPrivateKeyByAddress({
walletID,
assetAccountId,
address,
password,
})
.then(res => {
if (!isSuccessResponse(res)) {
setError(errorFormatter(res.message, t))
return
}
setPrivateKey(res.result)
})
.finally(() => {
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
Loading
Loading