diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8aef31da7e..84574240e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+# 0.35.0-rc1 (2020-12-19)
+
+This is a release candidate to preview the changes in the next official release and may not be stable. Welcome any questions or suggestions.
+
+[CKB v0.35.1](https://github.com/nervosnetwork/ckb/releases/tag/v0.35.1) was released on Sept. 14th, 2020. This version of CKB node is now bundled and preconfigured in Neuron.
+
+### New features
+
+* Enable hardware wallet of Ledger.
+* Support address verification on hardware wallet device.
+
+
# 0.34.0 (2020-12-16)
[CKB v0.35.1](https://github.com/nervosnetwork/ckb/releases/tag/v0.35.1) was released on Sept. 14th, 2020. This version of CKB node is now bundled and preconfigured in Neuron.
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 190e0cbdd7..05c27db908 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -26,8 +26,8 @@ stages:
vmImage: 'macos-10.14'
strategy:
matrix:
- node_12_x:
- node_version: 12.x
+ node_14_x:
+ node_version: 14.x
steps:
- task: NodeTool@0
inputs:
@@ -47,13 +47,16 @@ stages:
vmImage: 'ubuntu-18.04'
strategy:
matrix:
- node_12_x:
- node_version: 12.x
+ node_14_x:
+ node_version: 14.x
steps:
- task: NodeTool@0
inputs:
versionSpec: $(node_version)
displayName: 'Install Node.js'
+ - script: |
+ sudo apt-get install -y libudev-dev
+ displayName: Install libudev
- script: |
yarn global add lerna
yarn bootstrap
@@ -71,8 +74,8 @@ stages:
vmImage: 'vs2017-win2016'
strategy:
matrix:
- node_12_x:
- node_version: 12.x
+ node_14_x:
+ node_version: 14.x
steps:
- task: NodeTool@0
inputs:
@@ -101,7 +104,7 @@ stages:
steps:
- task: NodeTool@0
inputs:
- versionSpec: 12.x
+ versionSpec: 14.x
displayName: 'Install Node.js'
- script: |
yarn global add lerna
@@ -131,8 +134,11 @@ stages:
steps:
- task: NodeTool@0
inputs:
- versionSpec: 12.x
+ versionSpec: 14.x
displayName: 'Install Node.js'
+ - script: |
+ sudo apt-get install -y libudev-dev
+ displayName: Install libudev
- script: |
yarn global add lerna
yarn bootstrap
@@ -152,7 +158,7 @@ stages:
steps:
- task: NodeTool@0
inputs:
- versionSpec: 12.x
+ versionSpec: 14.x
displayName: 'Install Node.js'
- script: yarn global add lerna
displayName: 'Install lerna'
diff --git a/lerna.json b/lerna.json
index 06451e9d1a..995ae7790e 100644
--- a/lerna.json
+++ b/lerna.json
@@ -2,7 +2,7 @@
"packages": [
"packages/*"
],
- "version": "0.34.0",
+ "version": "0.35.0-rc1",
"npmClient": "yarn",
"useWorkspaces": true
}
diff --git a/package.json b/package.json
index c3885c466f..7fc703dfa3 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "neuron",
"productName": "Neuron",
"description": "CKB Neuron Wallet",
- "version": "0.34.0",
+ "version": "0.35.0-rc1",
"private": true,
"author": {
"name": "Nervos Core Dev",
@@ -22,7 +22,7 @@
"packages/*"
],
"scripts": {
- "bootstrap": "yarn policies set-version 1.19.2 && npx cross-env LUMOS_NODE_RUNTIME=electron LUMOS_NODE_RUNTIME_VERSION=9.0.2 lerna bootstrap && lerna link",
+ "bootstrap": "npx cross-env LUMOS_NODE_RUNTIME=electron LUMOS_NODE_RUNTIME_VERSION=9.0.2 lerna bootstrap && lerna link",
"start:ui": "cd packages/neuron-ui && yarn run start",
"start:wallet": "cd packages/neuron-wallet && yarn run start:dev",
"start": "concurrently \"cross-env BROWSER=none yarn run start:ui\" \"wait-on http://localhost:3000 && yarn run start:wallet\"",
diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json
index d05aa7bbc6..9e0e148e86 100644
--- a/packages/neuron-ui/package.json
+++ b/packages/neuron-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "neuron-ui",
- "version": "0.34.0",
+ "version": "0.35.0-rc1",
"private": true,
"author": {
"name": "Nervos Core Dev",
diff --git a/packages/neuron-ui/src/components/ImportHardware/confirming.tsx b/packages/neuron-ui/src/components/ImportHardware/confirming.tsx
index 3c78ddca56..b7d42b655c 100644
--- a/packages/neuron-ui/src/components/ImportHardware/confirming.tsx
+++ b/packages/neuron-ui/src/components/ImportHardware/confirming.tsx
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { RouteComponentProps } from 'react-router-dom'
import Button from 'widgets/Button'
import { ReactComponent as PendingIcon } from 'widgets/Icons/Pending.svg'
-import { getDevicePublickey } from 'services/remote'
+import { getDeviceExtendedPublickey } from 'services/remote'
import { isSuccessResponse, useDidMount } from 'utils'
import { RoutePath, LocationState } from './common'
@@ -17,7 +17,7 @@ const Confirming = ({ history, location }: RouteComponentProps<{}, {}, LocationS
}, [history, entryPath])
useDidMount(() => {
- getDevicePublickey().then(res => {
+ getDeviceExtendedPublickey().then(res => {
if (isSuccessResponse(res)) {
history.push({
pathname: entryPath + RoutePath.NameWallet,
diff --git a/packages/neuron-ui/src/components/Receive/index.tsx b/packages/neuron-ui/src/components/Receive/index.tsx
index 2af32d5596..75194c5e6e 100644
--- a/packages/neuron-ui/src/components/Receive/index.tsx
+++ b/packages/neuron-ui/src/components/Receive/index.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useMemo } from 'react'
+import React, { useCallback, useMemo, useState } from 'react'
import { useRouteMatch, useHistory } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import Button from 'widgets/Button'
@@ -6,18 +6,19 @@ import QRCode from 'widgets/QRCode'
import CopyZone from 'widgets/CopyZone'
import { RoutePath } from 'utils'
import { useState as useGlobalState, useDispatch } from 'states'
+import VerifyHardwareAddress from 'components/VerifyHardwareAddress'
import styles from './receive.module.scss'
const Receive = () => {
- const {
- wallet: { addresses = [] },
- } = useGlobalState()
+ const { wallet } = useGlobalState()
const dispatch = useDispatch()
const [t] = useTranslation()
const {
params: { address },
} = useRouteMatch()
const history = useHistory()
+ const [displayVerifyDialog, setDisplayVerifyDialog] = useState(false)
+ const { addresses } = wallet
const isSingleAddress = addresses.length === 1
const accountAddress = useMemo(() => {
@@ -31,6 +32,10 @@ const Receive = () => {
history.push(RoutePath.Addresses)
}, [history])
+ const onVerifyAddressClick = useCallback(() => {
+ setDisplayVerifyDialog(true)
+ }, [])
+
if (!accountAddress) {
return
{t('receive.address-not-found')}
}
@@ -58,6 +63,23 @@ const Receive = () => {
onClick={onAddressBookClick}
/>
)}
+ {isSingleAddress && (
+
+ )}
+ {displayVerifyDialog && (
+ {
+ setDisplayVerifyDialog(false)
+ }}
+ />
+ )}
)
}
diff --git a/packages/neuron-ui/src/components/VerifyHardwareAddress/index.tsx b/packages/neuron-ui/src/components/VerifyHardwareAddress/index.tsx
new file mode 100644
index 0000000000..ed1a5ea83d
--- /dev/null
+++ b/packages/neuron-ui/src/components/VerifyHardwareAddress/index.tsx
@@ -0,0 +1,233 @@
+import React, { useCallback, useRef, useState, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import Button from 'widgets/Button'
+import { ControllerResponse } from 'services/remote/remoteApiWrapper'
+import Spinner from 'widgets/Spinner'
+import { ReactComponent as HardWalletIcon } from 'widgets/Icons/HardWallet.svg'
+import { ReactComponent as VerifiedIcon } from 'widgets/Icons/Success.svg'
+import {
+ connectDevice,
+ getDevices,
+ getDeviceCkbAppVersion,
+ getDevicePublicKey,
+ DeviceInfo,
+ updateWallet,
+ getPlatform,
+} from 'services/remote'
+import { ErrorCode, errorFormatter, isSuccessResponse, useDidMount } from 'utils'
+import { CkbAppNotFoundException, DeviceNotFoundException } from 'exceptions'
+import CopyZone from 'widgets/CopyZone'
+import styles from './verifyHardwareAddress.module.scss'
+import VerifyError from './verify-error'
+
+export interface VerifyHardwareAddressProps {
+ address: string
+ wallet: State.WalletIdentity
+ onDismiss: () => void
+}
+
+const VerifyHardwareAddress = ({ address, wallet, onDismiss }: VerifyHardwareAddressProps) => {
+ const [t] = useTranslation()
+ const dialogRef = useRef(null)
+ // const dispatch = useDispatch()
+ const onCancel = useCallback(() => {
+ onDismiss()
+ }, [onDismiss])
+ const isWin32 = useMemo(() => {
+ return getPlatform() === 'win32'
+ }, [])
+ const [status, setStatus] = useState('')
+ const [error, setError] = useState('')
+ const connectStatus = t('hardware-verify-address.status.connect')
+ const userInputStatus = t('hardware-verify-address.status.user-input')
+ const disconnectStatus = t('hardware-verify-address.status.disconnect')
+ const verifiedStatus = t('hardware-verify-address.verified')
+ const invalidStatus = t('hardware-verify-address.invalid')
+ const ckbAppNotFoundStatus = t(CkbAppNotFoundException.message)
+ const isNotAvailableToVerify = useMemo(() => {
+ return status === disconnectStatus || status === ckbAppNotFoundStatus
+ }, [status, disconnectStatus, ckbAppNotFoundStatus])
+
+ const [deviceInfo, setDeviceInfo] = useState(wallet.device!)
+ const [isReconnecting, setIsReconnecting] = useState(false)
+ const isLoading = useMemo(() => {
+ return status === userInputStatus || isReconnecting
+ }, [status, userInputStatus, isReconnecting])
+
+ const productName = `${wallet.device!.manufacturer} ${wallet.device!.product}`
+
+ const ensureDeviceAvailable = useCallback(
+ async (device: DeviceInfo) => {
+ try {
+ const conectionRes = await connectDevice(device)
+ let { descriptor } = device
+ if (!isSuccessResponse(conectionRes)) {
+ // 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((_, reject) => setTimeout(() => reject(), 1000)),
+ ]).catch(() => {
+ return { status: ErrorCode.DeviceInSleep }
+ })
+
+ if (!isSuccessResponse(ckbVersionRes)) {
+ if (ckbVersionRes.status !== ErrorCode.DeviceInSleep) {
+ throw new CkbAppNotFoundException()
+ } else {
+ throw new DeviceNotFoundException()
+ }
+ }
+ setStatus(connectStatus)
+ } catch (err) {
+ if (err.code === ErrorCode.CkbAppNotFound) {
+ setStatus(ckbAppNotFoundStatus)
+ } else {
+ setStatus(disconnectStatus)
+ }
+ }
+ },
+ [connectStatus, disconnectStatus, ckbAppNotFoundStatus, isWin32]
+ )
+
+ const reconnect = useCallback(async () => {
+ 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)
+ }
+ } catch (err) {
+ setStatus(disconnectStatus)
+ } finally {
+ setIsReconnecting(false)
+ }
+ }, [deviceInfo, disconnectStatus, ensureDeviceAvailable, wallet.id])
+
+ const verify = useCallback(async () => {
+ await ensureDeviceAvailable(deviceInfo)
+ setStatus(userInputStatus)
+ const res = await getDevicePublicKey()
+ if (isSuccessResponse(res)) {
+ const { result } = res
+ if (result?.address === address) {
+ setStatus(verifiedStatus)
+ } else {
+ setStatus(invalidStatus)
+ }
+ } else {
+ setError(errorFormatter(res.message, t))
+ }
+ }, [deviceInfo, address, userInputStatus, verifiedStatus, invalidStatus, ensureDeviceAvailable, t])
+
+ useDidMount(() => {
+ // eslint-disable-next-line no-unused-expressions
+ dialogRef.current?.showModal()
+ ensureDeviceAvailable(deviceInfo)
+ })
+
+ const dialogClass = `${styles.dialog}`
+
+ let container = (
+
+
{t('hardware-verify-address.title')}
+
+
+
+
+ {t('hardware-verify-address.device')} |
+
+
+ {productName}
+ |
+
+
+ {t('hardware-verify-address.address')} |
+
+
+ {address}
+
+ |
+
+
+ {t('hardware-verify-address.status.label')} |
+
+ {status === verifiedStatus && }
+ {status}
+ |
+
+
+
+
+
+
+ )
+
+ if (error) {
+ container =
+ }
+
+ return (
+
+ )
+}
+
+VerifyHardwareAddress.displayName = 'VerifyHardwareAddress'
+
+export default VerifyHardwareAddress
diff --git a/packages/neuron-ui/src/components/VerifyHardwareAddress/verify-error.tsx b/packages/neuron-ui/src/components/VerifyHardwareAddress/verify-error.tsx
new file mode 100644
index 0000000000..948396d9c0
--- /dev/null
+++ b/packages/neuron-ui/src/components/VerifyHardwareAddress/verify-error.tsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import Button from 'widgets/Button'
+import { ReactComponent as FailedInfo } from 'widgets/Icons/FailedInfo.svg'
+import { errorFormatter } from 'utils'
+import CopyZone from 'widgets/CopyZone'
+
+import styles from './verifyHardwareAddress.module.scss'
+
+const VerifyError = ({ error, onCancel }: { error: string; onCancel: () => void }) => {
+ const [t] = useTranslation()
+ const errorMsg = errorFormatter(error, t)
+ return (
+
+ )
+}
+
+VerifyError.displayName = 'VerifyError'
+
+export default VerifyError
diff --git a/packages/neuron-ui/src/components/VerifyHardwareAddress/verifyHardwareAddress.module.scss b/packages/neuron-ui/src/components/VerifyHardwareAddress/verifyHardwareAddress.module.scss
new file mode 100644
index 0000000000..5136ad4e9e
--- /dev/null
+++ b/packages/neuron-ui/src/components/VerifyHardwareAddress/verifyHardwareAddress.module.scss
@@ -0,0 +1,185 @@
+@import '../../styles/mixin.scss';
+
+.dialog {
+ @include dialog-container;
+ padding: 30px 50px;
+ min-height: 225px;
+ height: 240px;
+ width: 600px;
+
+ .container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ .action {
+ @extend .main;
+ text-align: center;
+ .message {
+ margin-top: 26px;
+ font-weight: bold;
+ }
+
+ +.footer {
+ justify-content: center;
+ }
+
+ svg {
+ width: 56px;
+ height: 56px;
+ }
+
+ .rotating {
+ svg {
+ animation: rotating 3s linear infinite;
+ }
+ }
+ }
+
+ .main {
+ flex: 1;
+ tr {
+ td {
+ font-size: 14px;
+ svg {
+ width: 14px;
+ height: 14px;
+ margin-right: 4px;
+ position: relative;
+ top: 2px;
+ }
+ }
+ .first {
+ padding-right: 30px;
+ }
+ }
+ }
+ }
+
+ &::backdrop {
+ @include overlay;
+ }
+
+ .footer {
+ @include dialog-footer;
+ flex-shrink: 0;
+
+ button {
+ margin-left: 10px;
+ }
+
+ .left {
+ button {
+ margin: 0;
+ }
+ }
+
+ .right {
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ }
+ }
+}
+
+.table {
+ overflow: auto;
+}
+
+.hd {
+ max-width: 700px;
+ width: 700px;
+ max-height: 400px;
+ height: 400px;
+}
+
+.sign {
+ table {
+ border-collapse: collapse;
+ }
+
+ thead {
+ border-bottom: 1px solid #e3e3e3;
+ }
+
+ tbody tr:hover {
+ background-color: #f5f5f5;
+ }
+
+ td {
+ height: 1.75rem;
+ word-wrap: none;
+ word-break: keep-all;
+ & > div {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ width: 100%;
+ font-size: 13px;
+ }
+ }
+
+ span,
+ td,
+ th {
+ font-size: 0.875rem;
+ letter-spacing: 0.5px;
+ padding: 0;
+ }
+
+ th,
+ td {
+ &:first-of-type {
+ padding-left: 10px;
+ width: 80px;
+ }
+
+ &:last-of-type {
+ padding-right: 10px;
+ width: 336px;
+ text-align: right;
+ }
+ }
+}
+
+.active {
+ background-color: #eeeeee;
+}
+
+.signed {
+ color: #888888;
+}
+
+.tabs {
+ width: 100%;
+ display: flex;
+ margin-top: 15px;
+ flex-direction: row;
+
+ button {
+ flex: 1;
+ cursor: pointer;
+ font-family: 'SourceCodePro-Regular', 'SourceHanSansCN-Regular', monospace;
+ font-size: 14px;
+ border: none;
+ background: none;
+ height: 30px;
+
+ &.active {
+ font-weight: bold;
+ border-bottom: 4px solid #3cc68a;
+ }
+ }
+}
+
+.warning {
+ color: #d03a3a;
+}
+
+.title {
+ font-size: 1.125rem;
+ line-height: 1.375rem;
+ font-weight: bold;
+ letter-spacing: 0.9px;
+ margin: 0;
+ margin-bottom: 26px;
+}
diff --git a/packages/neuron-ui/src/components/WalletSetting/index.tsx b/packages/neuron-ui/src/components/WalletSetting/index.tsx
index c612d5357f..c8730305ab 100644
--- a/packages/neuron-ui/src/components/WalletSetting/index.tsx
+++ b/packages/neuron-ui/src/components/WalletSetting/index.tsx
@@ -43,16 +43,13 @@ const buttons = [
url: RoutePath.ImportKeystore,
icon: ,
},
-]
-
-if (process.env.NODE_ENV === 'development') {
- buttons.push({
+ {
label: 'wizard.hardware-wallet',
ariaLabel: 'import from hardware wallet',
url: RoutePath.ImportHardware,
icon: ,
- })
-}
+ },
+]
const WalletSetting = ({
wallet: { id: currentID = '' },
diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json
index fbadfadbd1..8361dc8a1f 100644
--- a/packages/neuron-ui/src/locales/en.json
+++ b/packages/neuron-ui/src/locales/en.json
@@ -58,12 +58,12 @@
},
"hardware-sign": {
"cancel": "Cancel",
- "title": "Sign via hardware walllet",
+ "title": "Sign via hardware wallet",
"device": "Device:",
"status": {
"label": "Status:",
"connect": "Connected, ready for signing.",
- "user-input": "Connected, waiting for user input...",
+ "user-input": "Connected, waiting for confirmation on device...",
"disconnect": "Disconnected, please check your connection."
},
"inputs": "Inputs(signing {{index}} of {{length}})",
@@ -74,6 +74,25 @@
"rescan": "Rescan"
}
},
+ "hardware-verify-address": {
+ "title": "Verify address via hardware wallet",
+ "device": "Device:",
+ "address": "Address:",
+ "verified": "Verified",
+ "invalid": "Invalid",
+ "status": {
+ "label": "Status:",
+ "connect": "Connected, ready for verifying.",
+ "user-input": "Connected, waiting for confirmation on device...",
+ "disconnect": "Disconnected, please check your connection."
+ },
+ "actions": {
+ "close": "Close",
+ "rescan": "Rescan",
+ "copy-address": "Copy Address",
+ "verify": "Verify"
+ }
+ },
"offline-sign": {
"title": "Offline Sign",
"json-file": "JSON file:",
@@ -200,7 +219,8 @@
"address-not-found": "Address not found",
"prompt": "Neuron picks a new receiving address for better privacy. Please go to the Address Book if you want to use a previously used receiving address.",
"address-qrcode": "Address QR Code",
- "address": "{{network}} Address"
+ "address": "{{network}} Address",
+ "verify-address": "Verify Address"
},
"history": {
"meta": "Meta",
diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json
index d58dfbe791..8db3d4101e 100644
--- a/packages/neuron-ui/src/locales/zh-tw.json
+++ b/packages/neuron-ui/src/locales/zh-tw.json
@@ -62,11 +62,30 @@
"disconnect": "未連接,請檢查設備連接。"
},
"actions": {
- "close": "关闭",
+ "close": "關閉",
"success": "交易成功",
"rescan": "再次檢測"
}
},
+ "hardware-verify-address": {
+ "title": "使用硬件錢包進行地址驗證",
+ "device": "設備:",
+ "address": "地址:",
+ "verified": "驗證通過",
+ "invalid": "地址不正確",
+ "status": {
+ "label": "狀態:",
+ "connect": "已連接",
+ "user-input": "已連接,等待確認...",
+ "disconnect": "未連接,請檢查設備連接。"
+ },
+ "actions": {
+ "close": "關閉",
+ "rescan": "再次檢測",
+ "copy-address": "复制地址",
+ "verify": "驗證"
+ }
+ },
"offline-sign": {
"title": "離線簽名",
"json-file": "JSON 檔案:",
@@ -193,7 +212,8 @@
"address-not-found": "未找到地址",
"prompt": "為了保護隱私,Neuron 會自動選擇一個新收款地址。如果您想使用舊的收款地址,請訪問地址簿頁面。",
"address-qrcode": "地址二維碼",
- "address": "{{network}} 地址"
+ "address": "{{network}} 地址",
+ "verify-address": "驗證地址"
},
"history": {
"meta": "元信息",
diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json
index bb71cf775c..50e966c815 100644
--- a/packages/neuron-ui/src/locales/zh.json
+++ b/packages/neuron-ui/src/locales/zh.json
@@ -67,6 +67,25 @@
"rescan": "再次检测"
}
},
+ "hardware-verify-address": {
+ "title": "使用硬件钱包进行地址验证",
+ "device": "设备:",
+ "address": "地址:",
+ "verified": "验证通过",
+ "invalid": "地址不正确",
+ "status": {
+ "label": "状态:",
+ "connect": "已连接",
+ "user-input": "已连接,等待确认...",
+ "disconnect": "未连接,请检查设备连接。"
+ },
+ "actions": {
+ "close": "关闭",
+ "rescan": "再次检测",
+ "copy-address": "复制地址",
+ "verify": "验证"
+ }
+ },
"offline-sign": {
"title": "离线签名",
"json-file": "JSON 文件:",
@@ -193,7 +212,8 @@
"address-not-found": "未找到地址",
"prompt": "为了保护隐私,Neuron 会自动选择一个新收款地址。如果您想使用旧的收款地址,请访问地址簿页面。",
"address-qrcode": "地址二维码",
- "address": "{{network}} 地址"
+ "address": "{{network}} 地址",
+ "verify-address": "验证地址"
},
"history": {
"meta": "元信息",
diff --git a/packages/neuron-ui/src/services/remote/hardware.ts b/packages/neuron-ui/src/services/remote/hardware.ts
index 153c27dfb9..da01bae6d0 100644
--- a/packages/neuron-ui/src/services/remote/hardware.ts
+++ b/packages/neuron-ui/src/services/remote/hardware.ts
@@ -21,13 +21,20 @@ export interface ExtendedPublicKey {
chainCode: string
}
+export interface PublicKey {
+ publicKey: string
+ lockArg: string
+ address: string
+}
+
export type Descriptor = string
export type Version = string
export const getDevices = remoteApi('detect-device')
export const getDeviceCkbAppVersion = remoteApi('get-device-ckb-app-version')
export const getDeviceFirmwareVersion = remoteApi('get-device-firmware-version')
-export const getDevicePublickey = remoteApi('get-device-public-key')
+export const getDeviceExtendedPublickey = remoteApi('get-device-extended-public-key')
+export const getDevicePublicKey = remoteApi('get-device-public-key')
export const connectDevice = remoteApi('connect-device')
export const createHardwareWallet = remoteApi(
'create-hardware-wallet'
diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts
index bebc226d42..9944b7f502 100644
--- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts
+++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts
@@ -107,6 +107,7 @@ type Action =
| 'get-device-ckb-app-version'
| 'get-device-firmware-version'
| 'get-device-public-key'
+ | 'get-device-extended-public-key'
| 'connect-device'
| 'create-hardware-wallet'
// offline-signature
diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json
index b7c0bd0d7c..dee34a9f97 100644
--- a/packages/neuron-wallet/package.json
+++ b/packages/neuron-wallet/package.json
@@ -3,7 +3,7 @@
"productName": "Neuron",
"description": "CKB Neuron Wallet",
"homepage": "https://www.nervos.org/",
- "version": "0.34.0",
+ "version": "0.35.0-rc1",
"private": true,
"author": {
"name": "Nervos Core Dev",
@@ -50,14 +50,14 @@
"electron-updater": "4.2.0",
"electron-window-state": "5.0.3",
"elliptic": "6.5.3",
- "hw-app-ckb": "0.1.1",
+ "hw-app-ckb": "0.1.2",
"i18next": "17.0.13",
"leveldown": "5.4.1",
"levelup": "4.3.2",
"reflect-metadata": "0.1.13",
"rxjs": "6.5.3",
"sha3": "2.0.7",
- "sqlite3": "4.1.1",
+ "sqlite3": "5.0.0",
"subleveldown": "^4.1.4",
"typeorm": "0.2.20",
"uuid": "3.3.3"
@@ -85,7 +85,7 @@
"electron-notarize": "0.2.1",
"jest-when": "2.7.2",
"lint-staged": "9.2.5",
- "neuron-ui": "0.34.0",
+ "neuron-ui": "0.35.0-rc1",
"rimraf": "3.0.0",
"ttypescript": "1.5.10"
}
diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts
index a27ef56ad5..8bab05afcb 100644
--- a/packages/neuron-wallet/src/controllers/api.ts
+++ b/packages/neuron-wallet/src/controllers/api.ts
@@ -462,6 +462,10 @@ export default class ApiController {
return this.hardwareController.getFirmwareVersion()
})
+ handle('get-device-extended-public-key', async () => {
+ return this.hardwareController.getExtendedPublicKey()
+ })
+
handle('get-device-public-key', async () => {
return this.hardwareController.getPublicKey()
})
diff --git a/packages/neuron-wallet/src/controllers/app/menu.ts b/packages/neuron-wallet/src/controllers/app/menu.ts
index 643e0a1402..ac54b542fe 100644
--- a/packages/neuron-wallet/src/controllers/app/menu.ts
+++ b/packages/neuron-wallet/src/controllers/app/menu.ts
@@ -82,12 +82,12 @@ const navigateTo = (url: string) => {
}
}
-// const importHardware = (url: string) => {
-// const window = BrowserWindow.getFocusedWindow()
-// if (window) {
-// CommandSubject.next({ winID: window.id, type: 'import-hardware', payload: url, dispatchToUI: true })
-// }
-// }
+const importHardware = (url: string) => {
+ const window = BrowserWindow.getFocusedWindow()
+ if (window) {
+ CommandSubject.next({ winID: window.id, type: 'import-hardware', payload: url, dispatchToUI: true })
+ }
+}
const loadTransaction = (url: string, json: OfflineSignJSON, filePath: string) => {
const window = BrowserWindow.getFocusedWindow()
@@ -208,14 +208,13 @@ const updateApplicationMenu = (mainWindow: BrowserWindow | null) => {
}
}
},
- // COMMENT OUT UNTIL ledger app is approved
- // {
- // id: 'import-with-hardware',
- // label: t('application-menu.wallet.import-hardware'),
- // click: () => {
- // importHardware(URL.ImportHardware)
- // }
- // }
+ {
+ id: 'import-with-hardware',
+ label: t('application-menu.wallet.import-hardware'),
+ click: () => {
+ importHardware(URL.ImportHardware)
+ }
+ }
],
},
separator,
diff --git a/packages/neuron-wallet/src/controllers/hardware.ts b/packages/neuron-wallet/src/controllers/hardware.ts
index f6268d4614..c1e9c4e9a1 100644
--- a/packages/neuron-wallet/src/controllers/hardware.ts
+++ b/packages/neuron-wallet/src/controllers/hardware.ts
@@ -1,7 +1,8 @@
-import { DeviceInfo, ExtendedPublicKey } from "services/hardware/common";
+import { DeviceInfo, ExtendedPublicKey, PublicKey } from "services/hardware/common";
import { ResponseCode } from "utils/const"
import HardwareWalletService from "services/hardware";
import { connectDeviceFailed } from "exceptions";
+import { AccountExtendedPublicKey } from "models/keys/key";
export default class HardwareController {
public async connectDevice (deviceInfo: DeviceInfo): Promise> {
@@ -45,7 +46,7 @@ export default class HardwareController {
}
}
- public async getPublicKey (): Promise> {
+ public async getExtendedPublicKey (): Promise> {
const device = HardwareWalletService.getInstance().getCurrent()!
const pubkey = await device.getExtendedPublicKey()
@@ -54,4 +55,15 @@ export default class HardwareController {
result: pubkey,
}
}
+
+ public async getPublicKey (): Promise> {
+ const device = HardwareWalletService.getInstance().getCurrent()!
+ const defaultPath = AccountExtendedPublicKey.ckbAccountPath
+ const pubkey = await device.getPublicKey(defaultPath)
+
+ return {
+ status: ResponseCode.Success,
+ result: pubkey,
+ }
+ }
}
diff --git a/packages/neuron-wallet/src/services/hardware/common.ts b/packages/neuron-wallet/src/services/hardware/common.ts
index c2a11a021a..57cf637f8f 100644
--- a/packages/neuron-wallet/src/services/hardware/common.ts
+++ b/packages/neuron-wallet/src/services/hardware/common.ts
@@ -22,3 +22,9 @@ export interface ExtendedPublicKey {
publicKey: string
chainCode: string
}
+
+export interface PublicKey {
+ publicKey: string
+ lockArg: string
+ address: string
+}
diff --git a/packages/neuron-wallet/src/services/hardware/hardware.ts b/packages/neuron-wallet/src/services/hardware/hardware.ts
index c16dc85a58..0623620cdc 100644
--- a/packages/neuron-wallet/src/services/hardware/hardware.ts
+++ b/packages/neuron-wallet/src/services/hardware/hardware.ts
@@ -6,7 +6,7 @@ import TransactionSender from 'services/transaction-sender'
import MultiSign from 'models/multi-sign'
import WalletService from 'services/wallets'
import DeviceSignIndexSubject from 'models/subjects/device-sign-index-subject'
-import type { DeviceInfo, ExtendedPublicKey } from './common'
+import type { DeviceInfo, ExtendedPublicKey, PublicKey } from './common'
import { AccountExtendedPublicKey } from 'models/keys/key'
export abstract class Hardware {
@@ -113,6 +113,7 @@ export abstract class Hardware {
return tx
}
+ public abstract getPublicKey(path: string): Promise
public abstract getExtendedPublicKey(): Promise
public abstract connect(hardwareInfo?: DeviceInfo): Promise
public abstract signMessage(path: string, messageHex: string): Promise
diff --git a/packages/neuron-wallet/src/services/hardware/ledger.ts b/packages/neuron-wallet/src/services/hardware/ledger.ts
index 25cc216960..e46183728f 100644
--- a/packages/neuron-wallet/src/services/hardware/ledger.ts
+++ b/packages/neuron-wallet/src/services/hardware/ledger.ts
@@ -12,6 +12,7 @@ import NodeService from 'services/node'
import Address, { AddressType } from 'models/keys/address'
import HexUtils from 'utils/hex'
import logger from 'utils/logger'
+import NetworksService from 'services/networks'
export default class Ledger extends Hardware {
private ledgerCKB: LedgerCKB | null = null
@@ -94,6 +95,13 @@ export default class Ledger extends Hardware {
return version
}
+ async getPublicKey (path: string) {
+ const networkService = NetworksService.getInstance()
+ const isTestnet = !networkService.isMainnet()
+ const result = await this.ledgerCKB!.getWalletPublicKey(path === Address.pathForReceiving(0) ? this.defaultPath : path, isTestnet)
+ return result
+ }
+
public static async findDevices () {
const devices = await Promise.all([
Ledger.searchDevices(HID.listen, false),
diff --git a/packages/neuron-wallet/tests/controllers/hardware.test.ts b/packages/neuron-wallet/tests/controllers/hardware.test.ts
index 4dc8962fe6..d87435d05a 100644
--- a/packages/neuron-wallet/tests/controllers/hardware.test.ts
+++ b/packages/neuron-wallet/tests/controllers/hardware.test.ts
@@ -39,6 +39,13 @@ describe('hardware controller', () => {
it('#getPublicKey', async () => {
const { result } = await hardwareControler.getPublicKey()
expect(result!.publicKey).toBe(LedgerCkbApp.publicKey)
+ expect(result!.lockArg).toBe(LedgerCkbApp.lockArg)
+ expect(result!.address).toBe(LedgerCkbApp.address)
+ })
+
+ it('#getExtendedPublicKey', async () => {
+ const { result } = await hardwareControler.getExtendedPublicKey()
+ expect(result!.publicKey).toBe(LedgerCkbApp.publicKey)
expect(result!.chainCode).toBe(LedgerCkbApp.chainCode)
})
diff --git a/packages/neuron-wallet/tests/mock/hardware.ts b/packages/neuron-wallet/tests/mock/hardware.ts
index dd64285ddc..166feaf459 100644
--- a/packages/neuron-wallet/tests/mock/hardware.ts
+++ b/packages/neuron-wallet/tests/mock/hardware.ts
@@ -84,6 +84,16 @@ export class LedgerCkbApp {
public static publicKey = 'publicKey'
public static chainCode = 'chain_code'
public static version = '0.4.0'
+ public static lockArg = 'args'
+ public static address = 'address'
+
+ async getWalletPublicKey () {
+ return {
+ publicKey: LedgerCkbApp.publicKey,
+ lockArg: LedgerCkbApp.lockArg,
+ address: LedgerCkbApp.address
+ }
+ }
async getWalletExtendedPublicKey () {
return {