Skip to content

Commit

Permalink
Fix cell manage with hard wallet (#3069)
Browse files Browse the repository at this point in the history
* fix: Adapt hard wallet for lock and unlock live cell.

* fix: Add tx hash for cell info dialog
  • Loading branch information
yanguoyu authored Mar 14, 2024
1 parent f94bb2a commit 4055ab6
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import '../../styles/mixin.scss';

.cellInfoDialog {
min-width: 650px;
}
Expand Down Expand Up @@ -270,3 +272,7 @@
top: 4px;
cursor: pointer;
}

.notice {
@include dialog-copy-animation;
}
34 changes: 31 additions & 3 deletions packages/neuron-ui/src/components/CellInfoDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { useMemo, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import Dialog from 'widgets/Dialog'
import { calculateUsedCapacity, shannonToCKBFormatter } from 'utils'
import { calculateUsedCapacity, getExplorerUrl, shannonToCKBFormatter, truncateMiddle, useCopy } from 'utils'
import { useTranslation } from 'react-i18next'
import Tabs from 'widgets/Tabs'
import { type TFunction } from 'i18next'
import { Script } from '@ckb-lumos/base'
import Switch from 'widgets/Switch'
import { Copy, ExplorerIcon } from 'widgets/Icons/icon'
import Alert from 'widgets/Alert'
import { openExternal } from 'services/remote'
import styles from './cellInfoDialog.module.scss'

type ScriptRenderType = 'table' | 'raw'
Expand Down Expand Up @@ -108,13 +111,26 @@ const useTabs = ({ t, output }: { t: TFunction; output?: State.DetailedOutput })
}
}

const CellInfoDialog = ({ onCancel, output }: { onCancel: () => void; output?: State.DetailedOutput }) => {
const CellInfoDialog = ({
onCancel,
output,
isMainnet,
}: {
onCancel: () => void
output?: State.DetailedOutput
isMainnet: boolean
}) => {
const [t] = useTranslation()

const { tabs, currentTab, setCurrentTab, scriptRenderType, setScriptRenderType } = useTabs({
t,
output,
})
const { copied, copyTimes, onCopy } = useCopy()
const onOpenTx = useCallback(() => {
const explorerUrl = getExplorerUrl(isMainnet)
openExternal(`${explorerUrl}/transaction/${output?.outPoint.txHash}`)
}, [isMainnet, output?.outPoint.txHash])
if (!output) {
return null
}
Expand All @@ -124,6 +140,13 @@ const CellInfoDialog = ({ onCancel, output }: { onCancel: () => void; output?: S
title={
<div className={styles.title}>
<span>{t('cell-manage.cell-detail-dialog.title')}</span>
<div className={styles.outPoint}>
{t('cell-manage.cell-detail-dialog.transaction-hash')}
:&nbsp;&nbsp;
{truncateMiddle(output.outPoint.txHash, 10, 10)}
<Copy onClick={() => onCopy(output.outPoint.txHash)} />
<ExplorerIcon onClick={onOpenTx} />
</div>
</div>
}
onCancel={onCancel}
Expand All @@ -149,6 +172,11 @@ const CellInfoDialog = ({ onCancel, output }: { onCancel: () => void; output?: S
</div>
) : null}
</div>
{copied ? (
<Alert status="success" className={styles.notice} key={copyTimes.toString()}>
{t('common.copied')}
</Alert>
) : null}
</Dialog>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@
@include checkbox;

.multiActions {
position: absolute;
position: fixed;
bottom: 24px;
left: 50%;
left: calc(50% + 80px);
transform: translateX(-50%);
padding: 12px 40px 12px 40px;
border-radius: 40px;
Expand Down Expand Up @@ -235,4 +235,22 @@
margin-right: 4px;
}
}

.hardWalletImg {
width: 88px;
height: 88px;
margin: 16px 0 24px 0;
}

.lockActions {
margin-top: 24px;
display: flex;
justify-content: center;
gap: 16px;
}

.hardwalletErr {
justify-content: center;
margin-top: 12px;
}
}
142 changes: 138 additions & 4 deletions packages/neuron-ui/src/components/CellManagement/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { CkbAppNotFoundException, DeviceNotFoundException } from 'exceptions'
import { TFunction } from 'i18next'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
connectDevice,
getDeviceCkbAppVersion,
getDevices,
getLiveCells,
getPlatform,
updateLiveCellsLocalInfo,
updateLiveCellsLockStatus as updateLiveCellsLockStatusAPI,
updateWallet,
} from 'services/remote'
import { ControllerResponse } from 'services/remote/remoteApiWrapper'
import { AppActions, useDispatch } from 'states'
import { LockScriptCategory, RoutePath, TypeScriptCategory, isSuccessResponse, outPointToStr } from 'utils'
import { ErrorCode, LockScriptCategory, RoutePath, TypeScriptCategory, isSuccessResponse, outPointToStr } from 'utils'
import { SortType } from 'widgets/Table'

const cellTypeOrder: Record<string, number> = {
Expand Down Expand Up @@ -204,6 +212,7 @@ export const useAction = ({
resetPassword,
setError,
password,
verifyDeviceStatus,
}: {
liveCells: State.LiveCellWithLocalInfo[]
currentPageLiveCells: State.LiveCellWithLocalInfo[]
Expand All @@ -212,39 +221,43 @@ export const useAction = ({
resetPassword: () => void
setError: (error: string) => void
password: string
verifyDeviceStatus: () => Promise<boolean>
}) => {
const dispatch = useDispatch()
const navigate = useNavigate()
const [action, setAction] = useState<undefined | Actions>()
const [operateCells, setOperateCells] = useState<State.LiveCellWithLocalInfo[]>([])
const [loading, setLoading] = useState(false)
const onOpenActionDialog = useCallback(
(e: React.SyntheticEvent<SVGSVGElement, MouseEvent>) => {
async (e: React.SyntheticEvent<SVGSVGElement, MouseEvent>) => {
e.stopPropagation()
const { action: curAction, index } = e.currentTarget.dataset as { action: Actions; index: string }
if (!curAction || index === undefined || !currentPageLiveCells[+index]) return
const operateCell = currentPageLiveCells[+index]
setOperateCells([operateCell])
setAction(curAction)
resetPassword()
await verifyDeviceStatus()
},
[currentPageLiveCells, setOperateCells, dispatch, navigate]
)
const onMultiAction = useCallback(
(e: React.SyntheticEvent<HTMLButtonElement, MouseEvent>) => {
async (e: React.SyntheticEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
const { action: curAction } = e.currentTarget.dataset as { action: Actions }
if (!curAction || !selectedOutPoints.size) return
setOperateCells(liveCells.filter(v => selectedOutPoints.has(outPointToStr(v.outPoint))))
setAction(curAction)
resetPassword()
await verifyDeviceStatus()
},
[liveCells, selectedOutPoints, setOperateCells, dispatch, navigate]
)
const onActionConfirm = useCallback(() => {
const onActionConfirm = useCallback(async () => {
switch (action) {
case 'lock':
case 'unlock':
if (!(await verifyDeviceStatus())) return
setLoading(true)
updateLiveCellsLockStatus({
outPoints: operateCells.map(v => v.outPoint),
Expand Down Expand Up @@ -362,3 +375,124 @@ export const usePassword = () => {
resetPassword,
}
}

export const useHardWallet = ({ wallet, t }: { wallet: State.WalletIdentity; t: TFunction }) => {
const isWin32 = useMemo(() => {
return getPlatform() === 'win32'
}, [])
const [error, setError] = useState<ErrorCode | string | undefined>()
const isNotAvailable = useMemo(() => {
return error === ErrorCode.DeviceNotFound || error === ErrorCode.CkbAppNotFound
}, [error])

const [deviceInfo, setDeviceInfo] = useState(wallet.device)
const [isReconnecting, setIsReconnecting] = useState(false)

const ensureDeviceAvailable = useCallback(
async (device: State.DeviceInfo) => {
try {
const connectionRes = await connectDevice(device)
let { descriptor } = device
if (!isSuccessResponse(connectionRes)) {
// for win32, opening or closing the ckb app changes the HID descriptor(deviceInfo),
// so if we can't connect to the device, we need to re-search device automatically.
// for unix, the descriptor never changes unless user plugs the device into another USB port,
// in that case, mannauly re-search device one time will do.
if (isWin32) {
setIsReconnecting(true)
const devicesRes = await getDevices(device)
setIsReconnecting(false)
if (isSuccessResponse(devicesRes) && Array.isArray(devicesRes.result) && devicesRes.result.length > 0) {
const [updatedDeviceInfo] = devicesRes.result
descriptor = updatedDeviceInfo.descriptor
setDeviceInfo(updatedDeviceInfo)
} else {
throw new DeviceNotFoundException()
}
} else {
throw new DeviceNotFoundException()
}
}

// getDeviceCkbAppVersion will halt forever while in win32 sleep mode.
const ckbVersionRes = await Promise.race([
getDeviceCkbAppVersion(descriptor),
new Promise<ControllerResponse>((_, reject) => {
setTimeout(() => reject(), 1000)
}),
]).catch(() => {
return { status: ErrorCode.DeviceInSleep }
})

if (!isSuccessResponse(ckbVersionRes)) {
if (ckbVersionRes.status !== ErrorCode.DeviceInSleep) {
throw new CkbAppNotFoundException()
} else {
throw new DeviceNotFoundException()
}
}
setError(undefined)
return true
} catch (err) {
if (err instanceof CkbAppNotFoundException || err instanceof DeviceNotFoundException) {
setError(err.code)
}
return false
}
},
[isWin32]
)

const reconnect = useCallback(async () => {
if (!deviceInfo) return
setError(undefined)
setIsReconnecting(true)
try {
const res = await getDevices(deviceInfo)
if (isSuccessResponse(res) && Array.isArray(res.result) && res.result.length > 0) {
const [device] = res.result
setDeviceInfo(device)
if (device.descriptor !== deviceInfo.descriptor) {
await updateWallet({
id: wallet.id,
device,
})
}
await ensureDeviceAvailable(device)
} else {
setError(ErrorCode.DeviceNotFound)
}
} catch (err) {
setError(ErrorCode.DeviceNotFound)
} finally {
setIsReconnecting(false)
}
}, [deviceInfo, ensureDeviceAvailable, wallet.id])

const verifyDeviceStatus = useCallback(async () => {
if (deviceInfo) {
return ensureDeviceAvailable(deviceInfo)
}
return true
}, [ensureDeviceAvailable, deviceInfo])

const errorMessage = useMemo(() => {
switch (error) {
case ErrorCode.DeviceNotFound:
return t('hardware-verify-address.status.disconnect')
case ErrorCode.CkbAppNotFound:
return t(CkbAppNotFoundException.message)
default:
return error
}
}, [error, t])
return {
deviceInfo,
isReconnecting,
isNotAvailable,
reconnect,
verifyDeviceStatus,
errorMessage,
setError,
}
}
Loading

2 comments on commit 4055ab6

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Packaging for test is done in 8275589096

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Packaging for test is done in 8275589847

Please sign in to comment.