diff --git a/.ckb-version b/.ckb-version index 11c3166fe2..c7a0c41a13 100644 --- a/.ckb-version +++ b/.ckb-version @@ -1 +1 @@ -v0.120.0 +v0.121.0-rc1 diff --git a/.github/workflows/spam-comment-detection.yaml b/.github/workflows/spam-comment-detection.yaml index e44f106c5d..a6d46d855d 100644 --- a/.github/workflows/spam-comment-detection.yaml +++ b/.github/workflows/spam-comment-detection.yaml @@ -24,7 +24,7 @@ jobs: const issue_number = process.env.ISSUE_NUMBER const owner = process.env.REPO_OWNER const repo = process.env.REPO_NAME - const EXTERNAL_LINK_REGEXT = /https:\/\/(?!((\w+\.)?github\.com|github\.com|(\w+\.)?magickbase\.com|(\w+\.)?nervos\.org))/gi + const EXTERNAL_LINK_REGEXT = /https?:\/\/(?!((\w+\.)?github\.com|github\.com|(\w+\.)?magickbase\.com|(\w+\.)?nervos\.org))/gi if (spam_words.some(w => comment.includes(w))) { console.info(`Spam comment: ${comment}`) github.rest.issues.deleteComment({ owner, repo, comment_id }) @@ -46,4 +46,4 @@ jobs: ISSUE_NUMBER: ${{ github.event.issue.number }} REPO_OWNER: ${{github.repository_owner }} REPO_NAME: ${{ github.event.repository.name }} - SPAM_WORDS: ${{ github.secrets.SPAM_WORDS }} + SPAM_WORDS: ${{ secrets.SPAM_WORDS }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1181160063..56aef4adb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,49 @@ +# 0.121.0 (2025-01-16) + +### CKB Node & Light Client + +- [CKB@v0.121.0-rc1](https://github.com/nervosnetwork/ckb/releases/tag/v0.121.0-rc1) was released on Jan. 9th, 2025. This version of CKB node is now bundled and preconfigured in Neuron. +- [CKB Light Client@v0.4.1](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.4.1) was released on Nov. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron + +### Assumed valid target + +Block before `0x1381f9e4f70ce521256c4095fa536d11165488171a8a2cbac687f8cf53907afa`(at height `15,119,157`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3300) + +--- + +## New features + +- #3290: Support inspecting and exporting private key for specific addresses.(@devchenyan) +- #3293: Refine interaction of Sign and Verify Message.(@devchenyan) + + +**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.120.0...v0.121.0 + + +# 0.120.0 (2024-12-13) + +### CKB Node & Light Client + +- [CKB@v0.120.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.120.0) was released on Dec. 12th, 2024. This version of CKB node is now bundled and preconfigured in Neuron. +- [CKB Light Client@v0.4.1](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.4.1) was released on Nov. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron + +### Assumed valid target + +Block before `0xe1085c7ce8f4e8461ea75afe63ef21d2c1ce6a5d0bf0f0170042bebdd2fbde04`(at height `14,817,366`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3282) + +--- + +## New features + +- #3271: Support Arabic, FrCanadian/Belgian in User Interface.(@Natixe) + +## New Contributors + +- @Natixe made their first contribution in https://github.com/nervosnetwork/neuron/pull/3271 + +**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.119.0...v0.120.0 + + # 0.119.0 (2024-12-02) ### CKB Node & Light Client diff --git a/compatible.json b/compatible.json index 7e4b0d4c5b..50af59c90e 100644 --- a/compatible.json +++ b/compatible.json @@ -1,5 +1,6 @@ { "fullVersions": [ + "0.121", "0.120", "0.119", "0.118", @@ -27,6 +28,7 @@ "compatible": { "0.111": { "full": [ + "0.121", "0.120", "0.119", "0.118", @@ -48,6 +50,7 @@ }, "0.110": { "full": [ + "0.121", "0.120", "0.119", "0.118", @@ -85,6 +88,7 @@ }, "0.112": { "full": [ + "0.121", "0.120", "0.119", "0.118", @@ -106,6 +110,7 @@ }, "0.114": { "full": [ + "0.121", "0.120", "0.119", "0.118", @@ -127,6 +132,7 @@ }, "0.116": { "full": [ + "0.121", "0.120", "0.119", "0.118", @@ -148,6 +154,7 @@ }, "0.117": { "full": [ + "0.121", "0.120", "0.119", "0.118", @@ -169,6 +176,7 @@ }, "0.119": { "full": [ + "0.121", "0.120", "0.119", "0.118", @@ -188,8 +196,32 @@ "0.2" ] }, - "0.119": { + "0.120": { + "full": [ + "0.121", + "0.120", + "0.119", + "0.118", + "0.117", + "0.116", + "0.115", + "0.114", + "0.113", + "0.112", + "0.111", + "0.110", + "0.109" + ], + "light": [ + "0.4", + "0.3", + "0.2" + ] + }, + "0.121": { "full": [ + "0.121", + "0.120", "0.119", "0.118", "0.117", @@ -209,4 +241,4 @@ ] } } -} +} diff --git a/lerna.json b/lerna.json index 6124dd2790..e07f076837 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.119.0", + "version": "0.121.0", "npmClient": "yarn", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/package.json b/package.json index 65f963d9b5..cac8ae07d2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "neuron", "productName": "Neuron", "description": "CKB Neuron Wallet", - "version": "0.119.0", + "version": "0.121.0", "private": true, "author": { "name": "Nervos Core Dev", diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index aebcb7cd5d..5dee471d73 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -1,6 +1,6 @@ { "name": "neuron-ui", - "version": "0.119.0", + "version": "0.121.0", "private": true, "author": { "name": "Nervos Core Dev", diff --git a/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss b/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss index 118ab0d4b2..325e953d3e 100644 --- a/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss +++ b/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss @@ -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 { diff --git a/packages/neuron-ui/src/components/AddressBook/index.tsx b/packages/neuron-ui/src/components/AddressBook/index.tsx index 19c5e5d7c3..947c8675cb 100644 --- a/packages/neuron-ui/src/components/AddressBook/index.tsx +++ b/packages/neuron-ui/src/components/AddressBook/index.tsx @@ -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' @@ -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['columns']>( () => [ @@ -149,6 +151,21 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => { return 0 }, }, + { + title: '', + dataIndex: 'key', + align: 'left', + width: '40px', + render(_, __, { address }) { + return ( + + + + ) + }, + }, ], [t] ) @@ -179,6 +196,10 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => { } /> + + {!!viewPrivateKeyAddress && ( + setViewPrivateKeyAddress('')} /> + )} ) diff --git a/packages/neuron-ui/src/components/Receive/index.tsx b/packages/neuron-ui/src/components/Receive/index.tsx index 5387b873d0..a182a1ea2e 100644 --- a/packages/neuron-ui/src/components/Receive/index.tsx +++ b/packages/neuron-ui/src/components/Receive/index.tsx @@ -7,7 +7,8 @@ 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' @@ -29,6 +30,7 @@ export const AddressQrCodeWithCopyZone = ({ ) const [isCopySuccess, setIsCopySuccess] = useState(false) + const [showViewPrivateKey, setShowViewPrivateKey] = useState(false) const timer = useRef>() const { ref, onCopyQrCode, onDownloadQrCode, showCopySuccess } = useCopyAndDownloadQrCode() @@ -70,19 +72,27 @@ export const AddressQrCodeWithCopyZone = ({ {showAddress} - +
+ + +
+ + {showViewPrivateKey && setShowViewPrivateKey(false)} />} ) } diff --git a/packages/neuron-ui/src/components/Receive/receive.module.scss b/packages/neuron-ui/src/components/Receive/receive.module.scss index 68b6ab7a56..cf85d6b6aa 100644 --- a/packages/neuron-ui/src/components/Receive/receive.module.scss +++ b/packages/neuron-ui/src/components/Receive/receive.module.scss @@ -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); + } + } } } diff --git a/packages/neuron-ui/src/components/SignAndVerify/index.tsx b/packages/neuron-ui/src/components/SignAndVerify/index.tsx index b7fda6304b..610804eae4 100644 --- a/packages/neuron-ui/src/components/SignAndVerify/index.tsx +++ b/packages/neuron-ui/src/components/SignAndVerify/index.tsx @@ -1,9 +1,18 @@ -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback, useMemo } from 'react' import { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' import { showErrorMessage, signMessage, verifyMessage } from 'services/remote' import { ControllerResponse } from 'services/remote/remoteApiWrapper' -import { ErrorCode, isSuccessResponse, shannonToCKBFormatter, useExitOnWalletChange, useGoBack } from 'utils' +import { + ErrorCode, + isSuccessResponse, + shannonToCKBFormatter, + useExitOnWalletChange, + useGoBack, + validateAddress, + isMainnet as isMainnetUtil, +} from 'utils' +import { isErrorWithI18n } from 'exceptions' import { useState as useGlobalState } from 'states' import Button from 'widgets/Button' import Balance from 'widgets/Balance' @@ -130,8 +139,13 @@ const SignAndVerify = () => { const [message, setMessage] = useState('') const [signature, setSignature] = useState('') const [address, setAddress] = useState('') - const { wallet } = useGlobalState() + const { + chain: { networkID }, + settings: { networks }, + wallet, + } = useGlobalState() const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const isMainnet = isMainnetUtil(networks, networkID) useExitOnWalletChange() const handlePasswordDialogOpen = useCallback(() => { @@ -226,12 +240,29 @@ const SignAndVerify = () => { const onBack = useGoBack() + const addressError = useMemo(() => { + if (!address) { + return undefined + } + try { + validateAddress(address, isMainnet) + } catch (err) { + if (isErrorWithI18n(err)) { + return t(err.message, err.i18n) + } + } + if (wallet?.addresses && !wallet.addresses.find(item => item.address === address)) { + return t('sign-and-verify.address-not-found') + } + return undefined + }, [t, address, isMainnet, wallet.addresses]) + return (
{
} width="100%" + error={addressError} /> {isDropdownOpen && wallet?.addresses ? ( @@ -311,7 +343,7 @@ const SignAndVerify = () => { {wallet?.isWatchOnly || (
- diff --git a/packages/neuron-ui/src/components/ViewPrivateKey/index.tsx b/packages/neuron-ui/src/components/ViewPrivateKey/index.tsx new file mode 100644 index 0000000000..d1d63d5e15 --- /dev/null +++ b/packages/neuron-ui/src/components/ViewPrivateKey/index.tsx @@ -0,0 +1,143 @@ +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 }: { onClose?: () => void; address?: 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) => { + 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, + 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 ( + +
+
+ + {t('addresses.view-private-key-tip')} +
+ + {t('addresses.private-key')}} + value={privateKey} + field="password" + type="password" + disabled + suffix={ +
+ onCopy(privateKey)} /> +
+ } + /> + + {copied ? ( + + {t('common.copied')} + + ) : null} +
+
+ ) + } + return ( + +
+
+ + {t('addresses.view-private-key-tip')} +
+ + +
+
+ ) +} + +ViewPrivateKey.displayName = 'ViewPrivateKey' + +export default ViewPrivateKey diff --git a/packages/neuron-ui/src/components/ViewPrivateKey/viewPrivateKey.module.scss b/packages/neuron-ui/src/components/ViewPrivateKey/viewPrivateKey.module.scss new file mode 100644 index 0000000000..c5ecfa4b63 --- /dev/null +++ b/packages/neuron-ui/src/components/ViewPrivateKey/viewPrivateKey.module.scss @@ -0,0 +1,39 @@ +@import '../../styles/mixin.scss'; + +.passwordInput { + margin-top: 16px; +} + +.dialog { + width: 700px; +} + +.tip { + color: var(--warn-text-color); + background: var(--warn-background-color); + margin: -20px -16px 0; + display: flex; + align-items: center; + justify-content: center; + height: 32px; + font-size: 12px; + gap: 4px; + font-weight: 500; + border-bottom: 1px solid var(--warn-border-color); +} + +.label { + font-weight: 500; + color: var(--main-text-color); + font-size: 14px; +} + +.copy { + display: flex; + align-items: center; + margin-left: 6px; +} + +.notice { + @include dialog-copy-animation; +} diff --git a/packages/neuron-ui/src/locales/ar.json b/packages/neuron-ui/src/locales/ar.json index 3703ca6595..5ccbd9fce9 100644 --- a/packages/neuron-ui/src/locales/ar.json +++ b/packages/neuron-ui/src/locales/ar.json @@ -383,7 +383,10 @@ "request-payment": "طلب الدفع", "view-on-explorer": "عرض على المستكشف", "default-description": "لا شيء", - "all-address": "كل العناوين" + "all-address": "كل العناوين", + "view-private-key": "عرض المفتاح الخاص", + "private-key": "المفتاح الخاص", + "view-private-key-tip": "إذا فقدت مفتاحك الخاص، فستفقد أصولك. (عرضه في بيئة آمنة)" }, "settings": { "title": { @@ -822,7 +825,7 @@ "lock-info-dialog": { "address-info": "معلومات العنوان", "deprecated-address": "عنوان مهمل" - }, + }, "updates": { "check-updates": "تحقق من التحديثات", "checking-updates": "جارٍ التحقق...", @@ -1327,4 +1330,4 @@ "verify-wallet": "تحقق" } } -} \ No newline at end of file +} diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 4c5142990e..51e7787181 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -383,7 +383,10 @@ "request-payment": "Request Payment", "view-on-explorer": "View on Explorer", "default-description": "None", - "all-address": "All" + "all-address": "All", + "view-private-key": "View Private Key", + "private-key": "Private Key", + "view-private-key-tip": "If you lose your private key, your assets will be gone. (View in secure environment)" }, "settings": { "title": { diff --git a/packages/neuron-ui/src/locales/es.json b/packages/neuron-ui/src/locales/es.json index 2ba59528b9..5ac87dd864 100644 --- a/packages/neuron-ui/src/locales/es.json +++ b/packages/neuron-ui/src/locales/es.json @@ -375,7 +375,10 @@ "request-payment": "Solicitar Pago", "view-on-explorer": "Ver en el Explorador", "default-description": "Ninguna", - "all-address": "Todas" + "all-address": "Todas", + "view-private-key": "Ver clave privada", + "private-key": "Clave privada", + "view-private-key-tip": "Si pierdes tu clave privada, tus activos desaparecerán. (Ver en un entorno seguro)" }, "settings": { "title": { diff --git a/packages/neuron-ui/src/locales/fr.json b/packages/neuron-ui/src/locales/fr.json index 9973dc4e07..8db344661e 100644 --- a/packages/neuron-ui/src/locales/fr.json +++ b/packages/neuron-ui/src/locales/fr.json @@ -382,7 +382,10 @@ "request-payment": "Demander un paiement", "view-on-explorer": "Voir sur l'explorateur", "default-description": "Aucun", - "all-address": "Toutes" + "all-address": "Toutes", + "view-private-key": "Voir la clé privée", + "private-key": "Clé privée", + "view-private-key-tip": "Si vous perdez votre clé privée, vos actifs seront perdus. (Voir dans un environnement sécurisé)" }, "settings": { "title": { diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 6f69bd6bfd..72f4eb29b4 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -378,7 +378,10 @@ "request-payment": "請求支付", "view-on-explorer": "在瀏覽器中查看", "default-description": "無", - "all-address": "全部地址" + "all-address": "全部地址", + "view-private-key": "查看私鑰", + "private-key": "私鑰", + "view-private-key-tip": "如果您丟失了私鑰,您的資產將無法找回。(請在安全環境中查看)" }, "settings": { "title": { diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 5f2e16103e..1fe8278e5f 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -376,7 +376,10 @@ "request-payment": "请求支付", "view-on-explorer": "在浏览器中查看", "default-description": "无", - "all-address": "全部地址" + "all-address": "全部地址", + "view-private-key": "查看私钥", + "private-key": "私钥", + "view-private-key-tip": "如果您丢失了私钥,您的资产将无法找回。(请在安全环境中查看)" }, "settings": { "title": { diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index c8cb6f0722..ba8703f318 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -69,6 +69,7 @@ type Action = | 'backup-wallet' | 'update-wallet-start-block-number' | 'get-all-addresses' + | 'get-private-key-by-address' | 'update-address-description' | 'request-password' | 'send-tx' diff --git a/packages/neuron-ui/src/services/remote/wallets.ts b/packages/neuron-ui/src/services/remote/wallets.ts index 3f97e5b4ea..0610cb6521 100644 --- a/packages/neuron-ui/src/services/remote/wallets.ts +++ b/packages/neuron-ui/src/services/remote/wallets.ts @@ -14,6 +14,7 @@ export const updateWalletStartBlockNumber = remoteApi('get-all-addresses') +export const getPrivateKeyByAddress = remoteApi('get-private-key-by-address') export const updateAddressDescription = remoteApi('update-address-description') export const requestPassword = remoteApi('request-password') diff --git a/packages/neuron-ui/src/styles/theme.scss b/packages/neuron-ui/src/styles/theme.scss index 1421495687..4ef564a699 100644 --- a/packages/neuron-ui/src/styles/theme.scss +++ b/packages/neuron-ui/src/styles/theme.scss @@ -34,6 +34,7 @@ body { --main-pagination-text-color: #666666; --warn-text-color: #f68c2a; --warn-background-color: #fff6eb; + --warn-border-color: rgba(252, 136, 0, 0.2); --svg-fill-color: #ffffff; --lock-info-title-border: #e5e5e5; --script-tag-background-color: #eafbf6; @@ -89,6 +90,7 @@ body { --main-pagination-text-color: #999999; --warn-text-color: #f68c2a; --warn-background-color: rgba(255, 140, 0, 0.2); + --warn-border-color: rgba(252, 136, 0, 0.2); --svg-fill-color: #333333; --lock-info-title-border: #343e3c; --script-tag-background-color: #171b1a; diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index 6a1b6f22b2..3df4fe6782 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -184,6 +184,12 @@ declare namespace Controller { message: string } + interface GetPrivateKeyParams { + walletID: string + address?: string + password: string + } + interface VerifyMessageParams { address: string signature: string diff --git a/packages/neuron-ui/src/widgets/Icons/PrivateKey.svg b/packages/neuron-ui/src/widgets/Icons/PrivateKey.svg new file mode 100644 index 0000000000..c102dee0fd --- /dev/null +++ b/packages/neuron-ui/src/widgets/Icons/PrivateKey.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/neuron-ui/src/widgets/Icons/icon.tsx b/packages/neuron-ui/src/widgets/Icons/icon.tsx index 21eabcb23a..f9d0a6faf8 100644 --- a/packages/neuron-ui/src/widgets/Icons/icon.tsx +++ b/packages/neuron-ui/src/widgets/Icons/icon.tsx @@ -62,6 +62,7 @@ import { ReactComponent as QuestionSvg } from './Question.svg' import { ReactComponent as DetailsSvg } from './Details.svg' import { ReactComponent as ConfirmSvg } from './Confirm.svg' import { ReactComponent as UploadSvg } from './Upload.svg' +import { ReactComponent as PrivateKeySvg } from './PrivateKey.svg' import styles from './icon.module.scss' @@ -140,3 +141,4 @@ export const Question = WrapSvg(QuestionSvg) export const Details = WrapSvg(DetailsSvg) export const Confirm = WrapSvg(ConfirmSvg) export const Upload = WrapSvg(UploadSvg) +export const PrivateKey = WrapSvg(PrivateKeySvg) diff --git a/packages/neuron-ui/src/widgets/TextField/index.tsx b/packages/neuron-ui/src/widgets/TextField/index.tsx index 3f2208d302..d4f71acae0 100644 --- a/packages/neuron-ui/src/widgets/TextField/index.tsx +++ b/packages/neuron-ui/src/widgets/TextField/index.tsx @@ -132,8 +132,7 @@ const TextField = React.forwardRef( {...rest} /> )} - {suffix && (typeof suffix === 'string' ? {suffix} : suffix)} - {!suffix && type === 'password' && ( + {type === 'password' && ( : } )} + {suffix && (typeof suffix === 'string' ? {suffix} : suffix)}
{hint ? {hint} : null} {error ? ( diff --git a/packages/neuron-wallet/.env b/packages/neuron-wallet/.env index 3b6fcb4334..ebe286ad67 100644 --- a/packages/neuron-wallet/.env +++ b/packages/neuron-wallet/.env @@ -117,6 +117,6 @@ DAO_CODE_HASH=0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e MULTISIG_CODE_HASH=0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8 # CKB NODE OPTIONS -CKB_NODE_ASSUME_VALID_TARGET='0x7488acf2280ebf5b83c805a517f766eab77f45cd51f61476811d1ce96a60ea71' -CKB_NODE_ASSUME_VALID_TARGET_BLOCK_NUMBER=14687217 -CKB_NODE_DATA_SIZE=116 +CKB_NODE_ASSUME_VALID_TARGET='0x1381f9e4f70ce521256c4095fa536d11165488171a8a2cbac687f8cf53907afa' +CKB_NODE_ASSUME_VALID_TARGET_BLOCK_NUMBER=15119157 +CKB_NODE_DATA_SIZE=118 diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json index d1e4ebdfa6..0a58f5c41b 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.119.0", + "version": "0.121.0", "private": true, "author": { "name": "Nervos Core Dev", @@ -47,9 +47,9 @@ "@ckb-lumos/helpers": "0.23.0", "@ckb-lumos/lumos": "0.23.0", "@ckb-lumos/rpc": "0.23.0", - "@magickbase/hw-app-ckb": "0.2.0-alpha.0", "@iarna/toml": "2.2.5", "@ledgerhq/hw-transport-node-hid": "6.27.22", + "@magickbase/hw-app-ckb": "0.2.0-alpha.0", "@spore-sdk/core": "0.1.0", "archiver": "6.0.2", "async": "3.2.5", @@ -92,7 +92,7 @@ "electron-builder": "24.9.1", "electron-devtools-installer": "3.2.0", "jest-when": "3.6.0", - "neuron-ui": "0.119.0", + "neuron-ui": "0.121.0", "typescript": "5.3.3" } } diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index f7f75d5b9e..f8cf5e4e50 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -440,6 +440,14 @@ export default class ApiController { return this.#walletsController.getAllAddresses(id) }) + handle('get-private-key-by-address', async (_, { walletID, password, address }) => { + return this.#walletsController.getPrivateKeyByAddress({ + walletID, + password, + address, + }) + }) + handle( 'update-address-description', async (_, params: { walletID: string; address: string; description: string }) => { diff --git a/packages/neuron-wallet/src/controllers/wallets.ts b/packages/neuron-wallet/src/controllers/wallets.ts index a47f3c2015..2307c6be40 100644 --- a/packages/neuron-wallet/src/controllers/wallets.ts +++ b/packages/neuron-wallet/src/controllers/wallets.ts @@ -699,4 +699,24 @@ export default class WalletsController { } }) } + + public async getPrivateKeyByAddress({ + walletID, + password, + address, + }: { + walletID: string + password: string + address?: string + }) { + const privateKey = await AddressService.getPrivateKeyByAddress({ + walletID, + password, + address, + }) + return { + status: ResponseCode.Success, + result: privateKey, + } + } } diff --git a/packages/neuron-wallet/src/services/addresses.ts b/packages/neuron-wallet/src/services/addresses.ts index 8f16b045a1..85b8cb4ffd 100644 --- a/packages/neuron-wallet/src/services/addresses.ts +++ b/packages/neuron-wallet/src/services/addresses.ts @@ -1,5 +1,8 @@ import { hd } from '@ckb-lumos/lumos' -import { publicKeyToAddress, DefaultAddressNumber } from '../utils/scriptAndAddress' +import { bytes } from '@ckb-lumos/lumos/codec' +import WalletService from './wallets' +import { AddressNotFound } from '../exceptions' +import { publicKeyToAddress, DefaultAddressNumber, addressToScript } from '../utils/scriptAndAddress' import { Address as AddressInterface } from '../models/address' import AddressCreatedSubject from '../models/subjects/address-created-subject' import NetworksService from '../services/networks' @@ -445,4 +448,35 @@ export default class AddressService { public static async deleteByWalletId(walletId: string): Promise { await getConnection().createQueryBuilder().delete().from(HdPublicKeyInfo).where({ walletId }).execute() } + + public static async getPrivateKeyByAddress({ + walletID, + password, + address, + }: { + walletID: string + password: string + address?: string + }): Promise { + const wallet = WalletService.getInstance().get(walletID) + const addresses = await AddressService.getAddressesByWalletId(walletID) + let addr = address ? addresses.find(addr => addr.address === address) : addresses[0] + + if (!addr && address) { + const lock = addressToScript(address) + addr = addresses.find(a => a.blake160 === lock.args) + } + + if (!addr) { + throw new AddressNotFound() + } + + const masterPrivateKey = wallet.loadKeystore().extendedPrivateKey(password) + const masterKeychain = new hd.Keychain( + Buffer.from(bytes.bytify(masterPrivateKey.privateKey)), + Buffer.from(bytes.bytify(masterPrivateKey.chainCode)) + ) + + return `0x${masterKeychain.derivePath(addr.path).privateKey.toString('hex')}` + } } diff --git a/packages/neuron-wallet/tests/services/address.test.ts b/packages/neuron-wallet/tests/services/address.test.ts index 3083b5a588..00ebda11f7 100644 --- a/packages/neuron-wallet/tests/services/address.test.ts +++ b/packages/neuron-wallet/tests/services/address.test.ts @@ -1,6 +1,7 @@ import SystemScriptInfo from '../../src/models/system-script-info' import { OutputStatus } from '../../src/models/chain/output' import OutputEntity from '../../src/database/chain/entities/output' +import { bytes } from '@ckb-lumos/lumos/codec' import { hd } from '@ckb-lumos/lumos' import { Address } from '../../src/models/address' import Transaction from '../../src/database/chain/entities/transaction' @@ -9,6 +10,7 @@ import { when } from 'jest-when' import HdPublicKeyInfo from '../../src/database/chain/entities/hd-public-key-info' import { closeConnection, getConnection, initConnection } from '../setupAndTeardown' import { NetworkType } from '../../src/models/network' +import WalletService from '../../src/services/wallets' const { AddressType, AccountExtendedPublicKey } = hd @@ -707,5 +709,68 @@ describe('integration tests for AddressService', () => { expect(stubbedAddressDbChangedSubjectNext).toHaveBeenCalledTimes(1) }) }) + + describe('getPrivateKeyByAddress', () => { + const walletService = WalletService.getInstance() + const mnemonic = 'tank planet champion pottery together intact quick police asset flower sudden question' + const password = '1234abc~' + + const addressObj = { + walletId: '5af2473e-78f5-4799-a193-d2b1c2989838', + address: 'ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqvfewjgc69nj783sh03nuckxjacwr55vwgngf9dr', + path: "m/44'/309'/0'/0/0", + addressType: 0, + addressIndex: 0, + blake160: '0x89cba48c68b3978f185df19f31634bb870e94639', + } + + const privateKey = '0x848422863825f69e66dc7f48a3302459ec845395370c23578817456ad6b04b14' + + const AddressService = require('../../src/services/addresses').default + const getAddressesByWalletIdMock = jest.spyOn(AddressService, 'getAddressesByWalletId') + + let walletID = '' + + beforeAll(() => { + jest.setTimeout(15000) + + walletService.clearAll() + + const seed = hd.mnemonic.mnemonicToSeedSync(mnemonic) + const masterKeychain = hd.Keychain.fromSeed(seed) + const extendedKey = new hd.ExtendedPrivateKey( + bytes.hexify(masterKeychain.privateKey), + bytes.hexify(masterKeychain.chainCode) + ) + + const keystore = hd.Keystore.create(extendedKey, password) + + const accountKeychain = masterKeychain.derivePath(hd.AccountExtendedPublicKey.ckbAccountPath) + const accountExtendedPublicKey = new hd.AccountExtendedPublicKey( + bytes.hexify(accountKeychain.publicKey), + bytes.hexify(accountKeychain.chainCode) + ) + + const wallet = walletService.create({ + id: '', + name: 'Test Wallet', + extendedKey: accountExtendedPublicKey.serialize(), + keystore, + }) + walletID = wallet.id + }) + + it('getPrivateKeyByAddress', async () => { + getAddressesByWalletIdMock.mockReturnValueOnce([addressObj]) + + const pk = await AddressService.getPrivateKeyByAddress({ + walletID, + password, + address: addressObj.address, + }) + + expect(pk).toEqual(privateKey) + }) + }) }) })