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..278545120 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.', + type: 'info', + buttons: ['Cancel', 'Import from Clipboard and Restart'] + }) + 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..4b66c56f0 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,18 @@ class WalletBackend { } seed = ethers.Wallet.createRandom().mnemonic.phrase + await this.setSeedPhrase(seed) + return { seed, isNew: true } + } + + /** + * @param {string} seed + */ + async setSeedPhrase (seed) { + ethers.Wallet.fromMnemonic(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..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') @@ -256,6 +257,16 @@ async function getSeedPhrase () { return seed } +/** + * @param {string} seed + * @returns {Promise} + */ +async function setSeedPhrase (seed) { + await backend.setSeedPhrase(seed) + app.relaunch() + app.exit(0) +} + /** * @param {string | ethers.utils.Bytes} message * @returns {Promise} @@ -276,5 +287,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..3a2e381fc 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; };