From 1ad4164c54e4571220f8ef3424b91c46aebc3886 Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Tue, 10 Dec 2024 15:23:20 +0800 Subject: [PATCH] feat: Support import hardware wallet 24-bit seed --- .../src/components/Receive/index.tsx | 5 +- .../src/components/WalletWizard/hooks.ts | 9 ++- .../src/components/WalletWizard/index.tsx | 18 ++++- .../src/components/withWizard/index.tsx | 12 ++- .../neuron-ui/src/types/Controller/index.d.ts | 1 + .../src/widgets/MnemonicInput/index.tsx | 8 +- packages/neuron-wallet/src/controllers/api.ts | 9 ++- .../neuron-wallet/src/controllers/app/menu.ts | 7 ++ .../neuron-wallet/src/controllers/wallets.ts | 7 +- packages/neuron-wallet/src/locales/en.ts | 1 + packages/neuron-wallet/src/locales/es.ts | 1 + packages/neuron-wallet/src/locales/fr.ts | 1 + packages/neuron-wallet/src/locales/zh-tw.ts | 1 + packages/neuron-wallet/src/locales/zh.ts | 1 + .../src/models/keys/hd-public-key-info.ts | 4 + .../neuron-wallet/src/services/wallets.ts | 74 ++++++++++++++++--- 16 files changed, 131 insertions(+), 28 deletions(-) diff --git a/packages/neuron-ui/src/components/Receive/index.tsx b/packages/neuron-ui/src/components/Receive/index.tsx index 5387b873d0..eaabcda0ef 100644 --- a/packages/neuron-ui/src/components/Receive/index.tsx +++ b/packages/neuron-ui/src/components/Receive/index.tsx @@ -90,8 +90,9 @@ export const AddressQrCodeWithCopyZone = ({ const Receive = ({ onClose, address }: { onClose?: () => void; address?: string }) => { const [t] = useTranslation() const { wallet } = useGlobalState() - const { addresses } = wallet + const { addresses, isHD } = wallet const isSingleAddress = addresses.length === 1 + const isHardwareWallet = !isHD && isSingleAddress const accountAddress = useMemo(() => { if (isSingleAddress) { @@ -128,7 +129,7 @@ const Receive = ({ onClose, address }: { onClose?: () => void; address?: string onClick={() => setIsInShortFormat(is => !is)} /> - {isSingleAddress && } + {isHardwareWallet && } ) diff --git a/packages/neuron-ui/src/components/WalletWizard/hooks.ts b/packages/neuron-ui/src/components/WalletWizard/hooks.ts index c073cc8661..3907f4974c 100644 --- a/packages/neuron-ui/src/components/WalletWizard/hooks.ts +++ b/packages/neuron-ui/src/components/WalletWizard/hooks.ts @@ -1,9 +1,9 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect } from 'react' const MNEMONIC_SENTENCE_WORDS = 12 -export const useInputWords = () => { - const [inputsWords, setInputsWords] = useState(new Array(MNEMONIC_SENTENCE_WORDS).fill('')) +export const useInputWords = (wordsCount?: number) => { + const [inputsWords, setInputsWords] = useState(new Array(wordsCount ?? MNEMONIC_SENTENCE_WORDS).fill('')) const onChangeInput = useCallback( ( e: @@ -37,6 +37,9 @@ export const useInputWords = () => { }, [setInputsWords] ) + useEffect(() => { + setInputsWords(new Array(wordsCount ?? MNEMONIC_SENTENCE_WORDS).fill('')) + }, [wordsCount]) return { inputsWords, onChangeInput, diff --git a/packages/neuron-ui/src/components/WalletWizard/index.tsx b/packages/neuron-ui/src/components/WalletWizard/index.tsx index b4c7ab61b0..2847909c55 100644 --- a/packages/neuron-ui/src/components/WalletWizard/index.tsx +++ b/packages/neuron-ui/src/components/WalletWizard/index.tsx @@ -76,6 +76,7 @@ const initState: WithWizardState = { password: '', confirmPassword: '', name: '', + isHardware: false, } const submissionInputs = [ @@ -182,6 +183,8 @@ const Welcome = ({ rootPath = '/wizard/', wallets = [], dispatch }: WizardElemen Welcome.displayName = 'Welcome' +const LEDGER_WORDS_COUNT = 24 + const Mnemonic = ({ state = initState, rootPath = '/wizard/', dispatch }: WizardElementProps) => { const { generated, imported } = state const navigate = useNavigate() @@ -193,8 +196,9 @@ const Mnemonic = ({ state = initState, rootPath = '/wizard/', dispatch }: Wizard [MnemonicAction.Verify]: 'wizard.replenish-your-seed', [MnemonicAction.Import]: 'wizard.input-your-seed', }[type] - const { inputsWords, onChangeInput, setInputsWords } = useInputWords() const [searchParams] = useSearchParams() + const isHardware = searchParams.get('isHardware') === 'true' + const { inputsWords, onChangeInput, setInputsWords } = useInputWords(isHardware ? LEDGER_WORDS_COUNT : undefined) const disableNext = (type === MnemonicAction.Import && inputsWords.some(v => !v)) || (type === MnemonicAction.Verify && generated !== inputsWords.join(' ')) @@ -228,6 +232,14 @@ const Mnemonic = ({ state = initState, rootPath = '/wizard/', dispatch }: Wizard }) } }, [dispatch, type, navigate, setBlankIndexes]) + useEffect(() => { + if (isHardware) { + dispatch({ + type: 'isHardware', + payload: true, + }) + } + }, [dispatch, isHardware]) const globalDispatch = useDispatch() @@ -317,6 +329,7 @@ const Mnemonic = ({ state = initState, rootPath = '/wizard/', dispatch }: Wizard inputsWords={inputsWords} onChangeInputWord={onChangeInput} blankIndexes={MnemonicAction.Import ? undefined : blankIndexes} + wordsCount={isHardware ? LEDGER_WORDS_COUNT : undefined} /> {type === MnemonicAction.Import &&
{t('wizard.input-seed-first-empty-space')}
}
@@ -337,7 +350,7 @@ export const getAlertStatus = (fieldInit: boolean, success: boolean) => { } const Submission = ({ state = initState, wallets = [], dispatch }: WizardElementProps) => { - const { name, password, confirmPassword, imported } = state + const { name, password, confirmPassword, imported, isHardware } = state const navigate = useNavigate() const { type = MnemonicAction.Create } = useParams<{ type: MnemonicAction }>() const [t] = useTranslation() @@ -396,6 +409,7 @@ const Submission = ({ state = initState, wallets = [], dispatch }: WizardElement name, password, mnemonic: imported, + isHardware, } openDialog() setTimeout(() => { diff --git a/packages/neuron-ui/src/components/withWizard/index.tsx b/packages/neuron-ui/src/components/withWizard/index.tsx index fa69695cd5..aac353913d 100644 --- a/packages/neuron-ui/src/components/withWizard/index.tsx +++ b/packages/neuron-ui/src/components/withWizard/index.tsx @@ -7,8 +7,14 @@ export interface Element { comp: React.FC } -export interface WithWizardState { - [key: string]: string +export type WithWizardState = { + generated: string + imported: string + password: string + confirmPassword: string + name: string + isHardware: boolean + [propName: string]: any } export interface WizardProps { @@ -27,7 +33,7 @@ export interface WizardElementProps { } const reducer = ( - state: { [key: string]: string }, + state: WithWizardState, { type, payload, diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index 380129f808..6ae6e86a0b 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -27,6 +27,7 @@ declare namespace Controller { name: string mnemonic: string password: string + isHardware?: boolean } interface ImportKeystoreParams { diff --git a/packages/neuron-ui/src/widgets/MnemonicInput/index.tsx b/packages/neuron-ui/src/widgets/MnemonicInput/index.tsx index 3830b686de..a2d2e39adb 100644 --- a/packages/neuron-ui/src/widgets/MnemonicInput/index.tsx +++ b/packages/neuron-ui/src/widgets/MnemonicInput/index.tsx @@ -9,6 +9,7 @@ const MnemonicInput = ({ inputsWords, onChangeInputWord, blankIndexes, + wordsCount, }: { disabled?: boolean words: string @@ -23,10 +24,13 @@ const MnemonicInput = ({ } } ) => void - + wordsCount?: number blankIndexes?: number[] }) => { - const wordList = useMemo(() => Object.assign(new Array(12).fill(''), words?.split(' ')), [words]) + const wordList = useMemo( + () => Object.assign(new Array(wordsCount ?? 12).fill(''), words?.split(' ')), + [words, wordsCount] + ) const [focusIndex, setFocusIndex] = useState(-1) const mounted = useRef(true) const root = useRef(null) diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index f7f75d5b9e..e9ae8c5399 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -368,9 +368,12 @@ export default class ApiController { return this.#walletsController.activate(id) }) - handle('import-mnemonic', async (_, params: { name: string; password: string; mnemonic: string }) => { - return this.#walletsController.importMnemonic(params) - }) + handle( + 'import-mnemonic', + async (_, params: { name: string; password: string; mnemonic: string; isHardware?: boolean }) => { + return this.#walletsController.importMnemonic(params) + } + ) handle('import-keystore', async (_, params: { name: string; password: string; keystorePath: string }) => { return this.#walletsController.importKeystore(params) diff --git a/packages/neuron-wallet/src/controllers/app/menu.ts b/packages/neuron-wallet/src/controllers/app/menu.ts index 1e1b728330..233e09440d 100644 --- a/packages/neuron-wallet/src/controllers/app/menu.ts +++ b/packages/neuron-wallet/src/controllers/app/menu.ts @@ -231,6 +231,13 @@ const updateApplicationMenu = (mainWindow: BrowserWindow | null) => { importHardware(URL.ImportHardware) }, }, + { + id: 'import-hardware-seed', + label: t('application-menu.wallet.import-hardware-mnemonic'), + click: () => { + importHardware(`${URL.ImportMnemonic}?isHardware=true`) + }, + }, ], }, separator, diff --git a/packages/neuron-wallet/src/controllers/wallets.ts b/packages/neuron-wallet/src/controllers/wallets.ts index a47f3c2015..3f0f874f1a 100644 --- a/packages/neuron-wallet/src/controllers/wallets.ts +++ b/packages/neuron-wallet/src/controllers/wallets.ts @@ -70,12 +70,14 @@ export default class WalletsController { name, password, mnemonic, + isHardware, }: { name: string password: string mnemonic: string + isHardware?: boolean }): Promise>> { - return await this.createByMnemonic({ name, password, mnemonic, isImporting: true }) + return await this.createByMnemonic({ name, password, mnemonic, isImporting: true, isHardware }) } public async create({ @@ -95,11 +97,13 @@ export default class WalletsController { password, mnemonic, isImporting, + isHardware, }: { name: string password: string mnemonic: string isImporting: boolean + isHardware?: boolean }): Promise>> { if (!validateMnemonic(mnemonic)) { throw new InvalidMnemonic() @@ -139,6 +143,7 @@ export default class WalletsController { extendedKey: accountExtendedPublicKey.serialize(), keystore, startBlockNumber: startBlockNumber, + hardwareFromSeed: isHardware, }) wallet.checkAndGenerateAddresses(isImporting) diff --git a/packages/neuron-wallet/src/locales/en.ts b/packages/neuron-wallet/src/locales/en.ts index fec1bc382c..b88ae9771b 100644 --- a/packages/neuron-wallet/src/locales/en.ts +++ b/packages/neuron-wallet/src/locales/en.ts @@ -25,6 +25,7 @@ export default { 'import-keystore': 'Import from Keystore', 'import-xpubkey': 'Import Extended Public Key', 'import-hardware': 'Import Hardware Wallet', + 'import-hardware-mnemonic': 'Import Hardware Wallet Seed', }, edit: { label: 'Edit', diff --git a/packages/neuron-wallet/src/locales/es.ts b/packages/neuron-wallet/src/locales/es.ts index 9d70452efd..efc5e8987e 100644 --- a/packages/neuron-wallet/src/locales/es.ts +++ b/packages/neuron-wallet/src/locales/es.ts @@ -25,6 +25,7 @@ export default { 'import-keystore': 'Importar desde Keystore', 'import-xpubkey': 'Importar Clave Pública Extendida', 'import-hardware': 'Importar Billetera de Hardware', + 'import-hardware-mnemonic': 'Importar semilla de billetera de hardware', }, edit: { label: 'Editar', diff --git a/packages/neuron-wallet/src/locales/fr.ts b/packages/neuron-wallet/src/locales/fr.ts index 58e11e3d3a..eb5f33454d 100644 --- a/packages/neuron-wallet/src/locales/fr.ts +++ b/packages/neuron-wallet/src/locales/fr.ts @@ -25,6 +25,7 @@ export default { 'import-keystore': 'Importer depuis le fichier Keystore', 'import-xpubkey': 'Importer la clé publique étendue', 'import-hardware': 'Importer un Wallet matériel', + 'import-hardware-mnemonic': 'Importer des semences de portefeuille matériel', }, edit: { label: 'Édition', diff --git a/packages/neuron-wallet/src/locales/zh-tw.ts b/packages/neuron-wallet/src/locales/zh-tw.ts index cc75c175f4..33a4887216 100644 --- a/packages/neuron-wallet/src/locales/zh-tw.ts +++ b/packages/neuron-wallet/src/locales/zh-tw.ts @@ -25,6 +25,7 @@ export default { 'import-keystore': '導入 Keystore 檔案', 'import-xpubkey': '導入 Extended Public Key', 'import-hardware': '導入硬體錢包', + 'import-hardware-mnemonic': '導入硬體錢包助記詞', }, edit: { label: '編輯', diff --git a/packages/neuron-wallet/src/locales/zh.ts b/packages/neuron-wallet/src/locales/zh.ts index 5200e52497..1f47efa9f8 100644 --- a/packages/neuron-wallet/src/locales/zh.ts +++ b/packages/neuron-wallet/src/locales/zh.ts @@ -25,6 +25,7 @@ export default { 'import-keystore': '导入 Keystore 文件', 'import-xpubkey': '导入 Extended Public Key', 'import-hardware': '导入硬件钱包', + 'import-hardware-mnemonic': '导入硬件钱包助记词', }, edit: { label: '编辑', diff --git a/packages/neuron-wallet/src/models/keys/hd-public-key-info.ts b/packages/neuron-wallet/src/models/keys/hd-public-key-info.ts index b8981a636f..d23afffcc5 100644 --- a/packages/neuron-wallet/src/models/keys/hd-public-key-info.ts +++ b/packages/neuron-wallet/src/models/keys/hd-public-key-info.ts @@ -3,6 +3,7 @@ import { scriptToAddress } from '../../utils/scriptAndAddress' import SystemScriptInfo from '../../models/system-script-info' import NetworksService from '../../services/networks' +export const ROOT_ADDRESS_INDEX = -1 export default class HdPublicKeyInfoModel { public walletId: string public addressType: hd.AddressType @@ -22,6 +23,9 @@ export default class HdPublicKeyInfoModel { } public get path(): string { + if (this.addressIndex === ROOT_ADDRESS_INDEX) { + return hd.AccountExtendedPublicKey.ckbAccountPath + } return hd.AccountExtendedPublicKey.pathFor(this.addressType, this.addressIndex) } diff --git a/packages/neuron-wallet/src/services/wallets.ts b/packages/neuron-wallet/src/services/wallets.ts index 77ac8df4a5..0bdd3d6638 100644 --- a/packages/neuron-wallet/src/services/wallets.ts +++ b/packages/neuron-wallet/src/services/wallets.ts @@ -18,6 +18,7 @@ import { NetworkType } from '../models/network' import { resetSyncTaskQueue } from '../block-sync-renderer' import SyncProgressService from './sync-progress' import { prefixWith0x } from '../utils/scriptAndAddress' +import { ROOT_ADDRESS_INDEX } from '../models/keys/hd-public-key-info' const fileService = FileService.getInstance() @@ -31,6 +32,7 @@ export interface WalletProperties { device?: DeviceInfo keystore?: hd.Keystore startBlockNumber?: string + hardwareFromSeed?: boolean } export abstract class Wallet { @@ -40,9 +42,10 @@ export abstract class Wallet { protected extendedKey: string = '' protected isHD: boolean protected startBlockNumber?: string + protected hardwareFromSeed?: boolean constructor(props: WalletProperties) { - const { id, name, extendedKey, device, isHDWallet, startBlockNumber } = props + const { id, name, extendedKey, device, isHDWallet, startBlockNumber, hardwareFromSeed } = props if (id === undefined) { throw new IsRequired('ID') @@ -61,6 +64,7 @@ export abstract class Wallet { this.device = device this.isHD = isHDWallet ?? true this.startBlockNumber = startBlockNumber + this.hardwareFromSeed = hardwareFromSeed } public toJSON = () => ({ @@ -70,6 +74,7 @@ export abstract class Wallet { device: this.device, isHD: this.isHD, startBlockNumber: this.startBlockNumber, + hardwareFromSeed: this.hardwareFromSeed, }) public fromJSON = () => { @@ -153,17 +158,6 @@ export class FileKeystoreWallet extends Wallet { return hd.AccountExtendedPublicKey.parse(this.extendedKey) } - public toJSON = () => { - return { - id: this.id, - name: this.name, - extendedKey: this.extendedKey, - device: this.device, - isHD: this.isHD, - startBlockNumber: this.startBlockNumber, - } - } - public loadKeystore = () => { const data = fileService.readFileSync(MODULE_NAME, this.keystoreFileName()) return hd.Keystore.fromJson(data) @@ -221,6 +215,59 @@ export class FileKeystoreWallet extends Wallet { } } +export class HardwareFromSeedWallet extends FileKeystoreWallet { + static fromJSON = (json: WalletProperties) => { + return new HardwareFromSeedWallet(json) + } + + public toJSON = () => { + return { + id: this.id, + name: this.name, + extendedKey: this.extendedKey, + device: this.device, + isHD: this.isHD, + startBlockNumber: this.startBlockNumber, + hardwareFromSeed: true, + } + } + + public checkAndGenerateAddresses = async (): Promise => { + const { publicKey } = hd.AccountExtendedPublicKey.parse(this.extendedKey) + const address = await AddressService.generateAndSaveForPublicKeyQueue.asyncPush({ + walletId: this.id, + publicKey, + addressType: hd.AddressType.Receiving, + addressIndex: ROOT_ADDRESS_INDEX, + }) + + if (address) { + return [address] + } + } + + public getNextAddress = async (): Promise => { + return AddressService.getFirstAddressByWalletId(this.id) + } + + public getNextChangeAddress = async (): Promise => { + return AddressService.getFirstAddressByWalletId(this.id) + } + + public getNextReceivingAddresses = async (): Promise => { + const address = await AddressService.getFirstAddressByWalletId(this.id) + if (address) { + return [address] + } + + return [] + } + + public getAllAddresses = async (): Promise => { + return AddressService.getAddressesByWalletId(this.id) + } +} + export class HardwareWallet extends Wallet { public isHardware = (): boolean => { return true @@ -329,6 +376,9 @@ export default class WalletService { if (json.device) { return HardwareWallet.fromJSON(json) } + if (json.hardwareFromSeed) { + return HardwareFromSeedWallet.fromJSON(json) + } return FileKeystoreWallet.fromJSON(json) }