From 12dfdbfc2c08b888101790f4c3b4b7b84bfdcebe Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Wed, 31 Jul 2024 14:03:33 +0200 Subject: [PATCH 1/5] add import seed phrase --- main/index.js | 1 + main/ipc.js | 4 ++++ main/preload.js | 1 + main/settings.js | 13 +++++++++++++ main/typings.ts | 1 + main/wallet-backend.js | 15 +++++++++++---- main/wallet.js | 11 ++++++++++- renderer/src/assets/img/icons/import.svg | 1 + renderer/src/lib/station-config.tsx | 4 ++++ renderer/src/pages/settings/Settings.tsx | 18 +++++++++++++++++- renderer/src/test/settings.test.tsx | 12 +++++++++++- renderer/src/typings.ts | 1 + 12 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 renderer/src/assets/img/icons/import.svg diff --git a/main/index.js b/main/index.js index 32b5480a8..5fc0fb629 100644 --- a/main/index.js +++ b/main/index.js @@ -128,6 +128,7 @@ const ctx = { toggleOpenAtLogin: () => { throw new Error('never get here') }, isOpenAtLogin: () => { throw new Error('never get here') }, exportSeedPhrase: () => { throw new Error('never get here') }, + importSeedPhrase: () => { throw new Error('never get here') }, showUI: () => { throw new Error('never get here') }, isShowingUI: false, loadWebUIFromDist: serve({ diff --git a/main/ipc.js b/main/ipc.js index 320d36645..02a6e24fe 100644 --- a/main/ipc.js +++ b/main/ipc.js @@ -87,6 +87,10 @@ function setupIpcMain (/** @type {Context} */ ctx) { 'station:exportSeedPhrase', (_events) => ctx.exportSeedPhrase() ) + ipcMain.handle( + 'station:importSeedPhrase', + (_events) => ctx.importSeedPhrase() + ) ipcMain.handle( 'station:saveModuleLogsAs', (_events) => ctx.saveModuleLogsAs() diff --git a/main/preload.js b/main/preload.js index 44829dc91..1c8c91e14 100644 --- a/main/preload.js +++ b/main/preload.js @@ -47,6 +47,7 @@ contextBridge.exposeInMainWorld('electron', { ipcRenderer.invoke('station:toggleOpenAtLogin'), isOpenAtLogin: () => ipcRenderer.invoke('station:isOpenAtLogin'), exportSeedPhrase: () => ipcRenderer.invoke('station:exportSeedPhrase'), + importSeedPhrase: () => ipcRenderer.invoke('station:importSeedPhrase'), saveModuleLogsAs: () => ipcRenderer.invoke('station:saveModuleLogsAs'), checkForUpdates: () => ipcRenderer.invoke('station:checkForUpdates') }, diff --git a/main/settings.js b/main/settings.js index f6a03d093..db9b506f2 100644 --- a/main/settings.js +++ b/main/settings.js @@ -31,6 +31,19 @@ async function setup (ctx) { clipboard.writeText(await wallet.getSeedPhrase()) } } + + ctx.importSeedPhrase = async () => { + const button = showDialogSync({ + title: 'Import Seed Phrase', + // eslint-disable-next-line max-len + message: 'The seed phrase is used in order to back up your wallet, or move it to a different machine. Please copy it to your clipboard before proceeding. Please be cautious, as this will overwrite the seed phrase currently used, which will be permanently lost (unless backed up before).', + type: 'info', + buttons: ['Cancel', 'Import from Clipboard'] + }) + if (button === 1) { + await wallet.setSeedPhrase(clipboard.readText()) + } + } } module.exports = { diff --git a/main/typings.ts b/main/typings.ts index 140c63040..592a68bcf 100644 --- a/main/typings.ts +++ b/main/typings.ts @@ -54,6 +54,7 @@ export interface Context { toggleOpenAtLogin: () => void; isOpenAtLogin: () => boolean; exportSeedPhrase: () => void; + importSeedPhrase: () => void; } export interface WalletSeed { diff --git a/main/wallet-backend.js b/main/wallet-backend.js index 1215569bc..dab6af4ae 100644 --- a/main/wallet-backend.js +++ b/main/wallet-backend.js @@ -60,6 +60,7 @@ class WalletBackend { this.transactions = [] this.disableKeytar = disableKeytar this.onTransactionUpdate = onTransactionUpdate + this.keytarService = 'filecoin-station-wallet-0x' } /** @@ -104,12 +105,11 @@ class WalletBackend { * @returns {Promise} */ async getSeedPhrase () { - const service = 'filecoin-station-wallet-0x' let seed if (!this.disableKeytar) { log.info('Reading the seed phrase from the keychain...') try { - seed = await keytar.getPassword(service, 'seed') + seed = await keytar.getPassword(this.keytarService, 'seed') } catch (err) { throw new Error( 'Cannot read the seed phrase - did the user grant access?', @@ -123,10 +123,17 @@ class WalletBackend { } seed = ethers.Wallet.createRandom().mnemonic.phrase + await this.setSeedPhrase(seed) + return { seed, isNew: true } + } + + /** + * @param {string} seed + */ + async setSeedPhrase (seed) { if (!this.disableKeytar) { - await keytar.setPassword(service, 'seed', seed) + await keytar.setPassword(this.keytarService, 'seed', seed) } - return { seed, isNew: true } } async fetchBalance () { diff --git a/main/wallet.js b/main/wallet.js index 68c973210..30ad36854 100644 --- a/main/wallet.js +++ b/main/wallet.js @@ -256,6 +256,14 @@ async function getSeedPhrase () { return seed } +/** + * @param {string} seed + * @returns {Promise} + */ +async function setSeedPhrase (seed) { + await backend.setSeedPhrase(seed) +} + /** * @param {string | ethers.utils.Bytes} message * @returns {Promise} @@ -276,5 +284,6 @@ module.exports = { signMessage, transferAllFundsToDestinationWallet, getTransactionsForUI, - getSeedPhrase + getSeedPhrase, + setSeedPhrase } diff --git a/renderer/src/assets/img/icons/import.svg b/renderer/src/assets/img/icons/import.svg new file mode 100644 index 000000000..ca1833fb0 --- /dev/null +++ b/renderer/src/assets/img/icons/import.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/renderer/src/lib/station-config.tsx b/renderer/src/lib/station-config.tsx index ebb79084b..0691d3a1d 100644 --- a/renderer/src/lib/station-config.tsx +++ b/renderer/src/lib/station-config.tsx @@ -88,6 +88,10 @@ export function exportSeedPhrase () { return window.electron.stationConfig.exportSeedPhrase() } +export function importSeedPhrase () { + return window.electron.stationConfig.importSeedPhrase() +} + export function saveModuleLogsAs () { return window.electron.stationConfig.saveModuleLogsAs() } diff --git a/renderer/src/pages/settings/Settings.tsx b/renderer/src/pages/settings/Settings.tsx index c25835815..058956d7f 100644 --- a/renderer/src/pages/settings/Settings.tsx +++ b/renderer/src/pages/settings/Settings.tsx @@ -3,6 +3,7 @@ import Text from 'src/components/Text' import { checkForUpdates, exportSeedPhrase, + importSeedPhrase, isOpenAtLogin, saveModuleLogsAs, toggleOpenAtLogin @@ -13,6 +14,7 @@ import Button from 'src/components/Button' import UpdateIcon from 'src/assets/img/icons/update.svg?react' import SaveIcon from 'src/assets/img/icons/save.svg?react' import ExportIcon from 'src/assets/img/icons/export.svg?react' +import ImportIcon from 'src/assets/img/icons/import.svg?react' const Settings = () => { const [isOpenAtLoginChecked, setIsOpenAtLoginChecked] = useState() @@ -79,7 +81,7 @@ const Settings = () => { { } /> + } + onClick={importSeedPhrase} + > + Import seed phrase + + } + /> diff --git a/renderer/src/test/settings.test.tsx b/renderer/src/test/settings.test.tsx index 68c318be9..a0ac52e50 100644 --- a/renderer/src/test/settings.test.tsx +++ b/renderer/src/test/settings.test.tsx @@ -1,5 +1,11 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' -import { checkForUpdates, exportSeedPhrase, isOpenAtLogin, saveModuleLogsAs } from 'src/lib/station-config' +import { + checkForUpdates, + exportSeedPhrase, + importSeedPhrase, + isOpenAtLogin, + saveModuleLogsAs +} from 'src/lib/station-config' import Settings from 'src/pages/settings/Settings' import { describe, expect, test, vi } from 'vitest' @@ -9,6 +15,7 @@ const mocks = vi.hoisted(() => { return { checkForUpdates: vi.fn(), exportSeedPhrase: vi.fn(), + importSeedPhrase: vi.fn(), saveModuleLogsAs: vi.fn() } }) @@ -33,6 +40,7 @@ describe('Settings page', () => { beforeAll(() => { vi.mocked(checkForUpdates).mockImplementation(mocks.checkForUpdates) vi.mocked(exportSeedPhrase).mockImplementation(mocks.exportSeedPhrase) + vi.mocked(importSeedPhrase).mockImplementation(mocks.importSeedPhrase) vi.mocked(saveModuleLogsAs).mockImplementation(mocks.saveModuleLogsAs) render() @@ -43,12 +51,14 @@ describe('Settings page', () => { act(() => fireEvent.click(screen.getByText('Save module logs as...'))) act(() => fireEvent.click(screen.getByText('Check for updates'))) act(() => fireEvent.click(screen.getByText('Export seed phrase'))) + act(() => fireEvent.click(screen.getByText('Import seed phrase'))) }) await waitFor(() => { expect(mocks.saveModuleLogsAs).toHaveBeenCalledOnce() expect(mocks.checkForUpdates).toHaveBeenCalledOnce() expect(mocks.exportSeedPhrase).toHaveBeenCalledOnce() + expect(mocks.importSeedPhrase).toHaveBeenCalledOnce() }) }) }) diff --git a/renderer/src/typings.ts b/renderer/src/typings.ts index 0a6339884..4bba4f0ea 100644 --- a/renderer/src/typings.ts +++ b/renderer/src/typings.ts @@ -29,6 +29,7 @@ declare global { toggleOpenAtLogin: () => void; isOpenAtLogin: () => Promise; exportSeedPhrase: () => void; + importSeedPhrase: () => void; saveModuleLogsAs: () => void; checkForUpdates: () => void; }; From f1bc9a494a107ba134621db67aae9448ad3a4b0c Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Wed, 31 Jul 2024 14:06:30 +0200 Subject: [PATCH 2/5] add relaunch app after import --- main/settings.js | 4 ++-- main/wallet.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/main/settings.js b/main/settings.js index db9b506f2..278545120 100644 --- a/main/settings.js +++ b/main/settings.js @@ -36,9 +36,9 @@ async function setup (ctx) { const button = showDialogSync({ title: 'Import Seed Phrase', // eslint-disable-next-line max-len - message: 'The seed phrase is used in order to back up your wallet, or move it to a different machine. Please copy it to your clipboard before proceeding. Please be cautious, as this will overwrite the seed phrase currently used, which will be permanently lost (unless backed up before).', + message: 'The seed phrase is used in order to back up your wallet, or move it to a different machine. Please copy it to your clipboard before proceeding. Please be cautious, as this will overwrite the seed phrase currently used, which will be permanently lost.', type: 'info', - buttons: ['Cancel', 'Import from Clipboard'] + buttons: ['Cancel', 'Import from Clipboard and Restart'] }) if (button === 1) { await wallet.setSeedPhrase(clipboard.readText()) diff --git a/main/wallet.js b/main/wallet.js index 30ad36854..e80bf10c4 100644 --- a/main/wallet.js +++ b/main/wallet.js @@ -1,5 +1,6 @@ 'use strict' +const { app } = require('electron') const electronLog = require('electron-log') const assert = require('assert') const { getDestinationWalletAddress } = require('./station-config') @@ -262,6 +263,8 @@ async function getSeedPhrase () { */ async function setSeedPhrase (seed) { await backend.setSeedPhrase(seed) + app.relaunch() + app.exit(0) } /** From 76bfdb8dba4a94816c37ba28784c1de8e78ef163 Mon Sep 17 00:00:00 2001 From: Julian Gruber Date: Wed, 31 Jul 2024 15:28:26 +0200 Subject: [PATCH 3/5] fix test --- renderer/src/pages/settings/Settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renderer/src/pages/settings/Settings.tsx b/renderer/src/pages/settings/Settings.tsx index 058956d7f..04e6b246f 100644 --- a/renderer/src/pages/settings/Settings.tsx +++ b/renderer/src/pages/settings/Settings.tsx @@ -81,7 +81,7 @@ const Settings = () => { { } /> Date: Wed, 31 Jul 2024 15:28:42 +0200 Subject: [PATCH 4/5] improve text --- renderer/src/pages/settings/Settings.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renderer/src/pages/settings/Settings.tsx b/renderer/src/pages/settings/Settings.tsx index 04e6b246f..3a2e381fc 100644 --- a/renderer/src/pages/settings/Settings.tsx +++ b/renderer/src/pages/settings/Settings.tsx @@ -81,7 +81,7 @@ const Settings = () => { { } /> Date: Thu, 1 Aug 2024 13:12:43 +0200 Subject: [PATCH 5/5] add validate seed phrase --- main/wallet-backend.js | 1 + 1 file changed, 1 insertion(+) diff --git a/main/wallet-backend.js b/main/wallet-backend.js index dab6af4ae..4b66c56f0 100644 --- a/main/wallet-backend.js +++ b/main/wallet-backend.js @@ -131,6 +131,7 @@ class WalletBackend { * @param {string} seed */ async setSeedPhrase (seed) { + ethers.Wallet.fromMnemonic(seed) if (!this.disableKeytar) { await keytar.setPassword(this.keytarService, 'seed', seed) }