From 21b8cbef5ae8c2478bd5cf0150d53eb713f74100 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Tue, 5 Nov 2024 16:39:40 -0800 Subject: [PATCH] Implement wallet import. Refactor account creation to use password unlock modal. --- nemo-wallet.html | 331 ++++++++++++++++++++++++++++------------------- 1 file changed, 197 insertions(+), 134 deletions(-) diff --git a/nemo-wallet.html b/nemo-wallet.html index cb549e9..f67eb4a 100644 --- a/nemo-wallet.html +++ b/nemo-wallet.html @@ -54,12 +54,12 @@ const themeCheckbox = document.getElementById('theme-checkbox') themeCheckbox.addEventListener('change', selectTheme) selectTheme() - const forms = document.querySelectorAll('.form') - for (const form of forms) { - const cancelButtons = form.querySelectorAll('button.cancel') + const dialogs = document.querySelectorAll('dialog, form, .form') + for (const dialog of dialogs) { + const cancelButtons = dialog.querySelectorAll('button.cancel') for (const cancelButton of cancelButtons) { cancelButton.addEventListener('click', (event) => { - cancelForm(form.id) + cancelDialog(dialog.id) }) } } @@ -72,13 +72,13 @@ const exportMnemonicButton = document.getElementById('export-mnemonic-button') exportMnemonicButton.addEventListener('click', async (event) => { if (event.detail === 1) { - requestPassword(exportMnemonic) + unlockThen(exportMnemonic) } }) const exportSeedButton = document.getElementById('export-seed-button') exportSeedButton.addEventListener('click', async (event) => { if (event.detail === 1) { - requestPassword(exportSeed) + unlockThen(exportSeed) } }) setTimeout(() => { @@ -184,7 +184,7 @@ } case '_new': { try { - document.querySelector(location.hash).classList.add('hide') + document.querySelectorAll('.page').forEach(n => n.classList.add('hide')) document.getElementById('wallet-new').classList.remove('hide') } catch (err) { walletSelect.value = '' @@ -196,7 +196,7 @@ } case '_import': { try { - document.querySelector(location.hash).classList.add('hide') + document.querySelectorAll('.page').forEach(n => n.classList.add('hide')) document.getElementById('wallet-import').classList.remove('hide') } catch (err) { walletSelect.value = '' @@ -297,11 +297,101 @@ option?.remove() notify.error(err.msg) } finally { - await cancelForm('wallet-new') + await cancelDialog('wallet-new') + return wallet + } + } + + async function importWallet () { + const walletSelect = document.getElementById('wallet') + const form = document.getElementById('wallet-import') + const name = document.getElementById('wallet-import-name-input').value + if (name.substring(0, 1) === '_') { + notify.error(`Wallet name cannot start with an underscore`) + return + } + const algorithm = document.getElementById('wallet-import-algorithm-select').value + const walletClass = getWalletClass(algorithm) + const type = document.getElementById(`wallet-import-type-select`).value + const secret = document.getElementById(`wallet-import-secret-input`).value + const salt = document.getElementById('wallet-import-salt-input')?.value + const password = document.getElementById('wallet-import-password-input')?.value + if (!password) { + notify.error('Password is required') + return + } + let wallet, option + try { + if (type === 'mnemonic') { + wallet = await walletClass.fromMnemonic(password, secret, salt) + } else if (type === 'seed') { + wallet = await walletClass.fromSeed(password, secret) + } else { + notify.error(`Error importing unknown wallet type`) + return + } + const walletData = { + id: wallet.id, + name: name || wallet.id, + algorithm + } + await addToStorage(`wallets`, JSON.stringify(walletData)) + + const select = document.getElementById('wallet') + option = new Option(walletData.name, walletData.id, false, true) + select.add(option) + await selectWallet() + } catch (err) { + option?.remove() + console.error(err) + notify.error(err.message) + } finally { + await cancelDialog('wallet-import') return wallet } } + async function unlockThen (callback) { + const getPassword = async (event) => { + if (event.detail === 1) { + try { + const passwordInput = document.getElementById('unlock-modal-password-input') + const password = passwordInput.value + passwordInput.value = '' + document.getElementById('unlock-modal').close() + unlockModalOkButton.removeEventListener('click', getPassword) + const wallet = await loadWallet() + await wallet.unlock(password) + await callback(wallet) + await wallet.lock(password) + } catch (err) { + notify.error(err) + } + } + } + const unlockModalOkButton = document.getElementById('unlock-modal-ok-button') + unlockModalOkButton.addEventListener('click', getPassword) + const unlockModal = document.getElementById('unlock-modal') + unlockModal.showModal() + } + + async function changePassword (newPassword) { + const getPassword = async (event) => { + if (event.detail === 1) { + const oldPassword = document.getElementById('unlock-modal-password-input').value + await cancelDialog('unlock-modal') + unlockModalOkButton.removeEventListener('click', getPassword) + const wallet = await loadWallet() + await wallet.unlock(oldPassword) + await wallet.lock(newPassword) + } + } + const unlockModalOkButton = document.getElementById('unlock-modal-ok-button') + unlockModalOkButton.addEventListener('click', getPassword) + const unlockModal = document.getElementById('unlock-modal') + unlockModal.showModal() + } + function updateWalletForm (id) { const form = document.getElementById(id) if (!form) { @@ -312,21 +402,21 @@ const algorithm = document.getElementById(`${id}-algorithm-select`) const typeContainer = document.getElementById(`${id}-type`) const type = document.getElementById(`${id}-type-select`) - const importValueContainer = document.getElementById(`${id}-value`) - const importValue = document.getElementById(`${id}-value-input`) - const importValueLabel = document.getElementById(`${id}-value-label`) + const importSecretContainer = document.getElementById(`${id}-secret`) + const importSecret = document.getElementById(`${id}-secret-input`) + const importSecretLabel = document.getElementById(`${id}-secret-label`) const saltContainer = document.getElementById(`${id}-salt`) const salt = document.getElementById(`${id}-salt-input`) - if (algorithm.value === 'bip44' && type?.value === 'mnemonic') { + if (algorithm.value === 'bip44' && type?.value !== 'seed') { saltContainer.classList.remove('hide') } else { saltContainer.classList.add('hide') } if (type?.value != null && type?.value !== '') { - importValueContainer.classList.remove('hide') + importSecretContainer.classList.remove('hide') } - if (importValueLabel) { - importValueLabel.textContent = type.value + if (importSecretLabel) { + importSecretLabel.textContent = type.value } } @@ -362,7 +452,8 @@ default: { try { const account = await loadAccount() - createQrCode(account.address) + await loadBalance(account) + await createQrCode(account.address) } catch (err) { accountSelect.value = '' console.error(err) @@ -462,32 +553,40 @@ return account } - async function createAccount () { - let account, option, wallet - const accountSelect = document.getElementById('account') + async function loadBalance (a) { + const balanceContainer = document.getElementById('account-balance') + const account = new libnemo.Account(a.address) try { - wallet = await loadWallet() + await account.refresh('https://node.somenano.com/proxy') } catch (err) { console.error(err) - notify.error(`Error creating account`) - return } + balanceContainer.textContent = account.balance ?? 0 + } + + async function createAccount () { + await unlockThen(async (wallet) => { + const indexInput = document.getElementById('account-new-index-input') + const index = indexInput?.value + const accounts = await wallet.accounts(+index) + await storeAccount(wallet.id, accounts[0]) + }) + } + + async function storeAccount (walletId, account) { + let option + const accountSelect = document.getElementById('account') const form = document.getElementById('account-new') - const name = document.getElementById('account-new-name-input')?.value + const nameInput = document.getElementById('account-new-name-input') + const name = nameInput?.value if (name?.substring(0, 1) === '_') { notify.error(`Account name cannot start with an underscore`) return } - const index = document.getElementById('account-new-index-input')?.value - const password = document.getElementById('account-new-password-input')?.value try { - await wallet.unlock(password) - const accounts = await wallet.accounts(+index) - await wallet.lock(password) - const account = accounts[0] const accountData = { - name: `${index}${name ? ': ' : ''}${name}`, - walletId: wallet.id, + name: `${account.index}${name ? ': ' : ''}${name}`, + walletId: walletId, index: await account.index, address: await account.address } @@ -502,19 +601,24 @@ console.error(err) notify.error(`Error creating account`) } finally { - await cancelForm('account-new') + await cancelDialog('account-new') return account } } - async function cancelForm (id) { - await resetFormInputs() - document.getElementById(id).classList.add('hide') - document.querySelector(location.hash).classList.remove('hide') + async function cancelDialog (id) { + const form = document.getElementById(id) + await resetInputs() + try { + form.close() + } catch (err) { + form.classList.add('hide') + document.querySelector(location.hash).classList.remove('hide') + } } - async function resetFormInputs () { - const fields = document.querySelectorAll('main form input, main form select') + async function resetInputs () { + const fields = document.querySelectorAll('main input, main select') for (const field of fields) { field.value = '' } @@ -596,70 +700,18 @@ qrCode.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABDgAAAQ4AQMAAADW3v7MAAAABlBMVEUAAAD///+l2Z/dAAAC0klEQVR42u3cPQ6CMBiAYYWFjStwE6+mN+Mq3oCVxKQO4k8N6ABNWvK8G1+HPuuXNBzDIYuOHBwcHBwcHBwcHBwcHMU6rt3cyanf/raxmZu2AwcHBwcHBwcHBwcHR3GOdoiPLudUjvoWj6bLOTg4ODg4ODg4ODg4ynO8d62Q2BGqj52Rg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4Oj986Rg4ODg4ODg4ODg4NjlWMm///g4ODg4ODg4ODg4OBYcuQQBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHB8e+Hdcu/g4cHBwcHBwcHBwcHBz5Otrhaag4ODg4ODg4ODg4ODjKdnzvWo9O/faOsTnUNw4ODg4ODg4ODg4Ojj04XrvW1OWcxPF3n+Pg4ODg4ODg4ODg4CjIMTbPWUjjiOLg4ODg4ODg4ODg4OBYFwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHB8feHVEcHBwcHBwcHBwcHBwcvx0zcXBwcHBwcHBwcHBwcCw5coiDg4ODg4ODg4ODg4ODY013wW3C00//4ZIAAAAASUVORK5CYII=' } - async function requestPassword (callback) { - const dialog = document.createElement('dialog') - dialog.classList.add('flex-y', 'center', 'mid') - document.body.appendChild(dialog) - - const label = document.createElement('label') - label.textContent = 'Unlock wallet to continue' - label.id = 'unlock-modal-password-label' - label.for = 'unlock-modal-password-input' - dialog.appendChild(label) - - const input = document.createElement('input') - input.type = 'password' - input.placeholder = 'Password' - input.id = 'unlock-modal-password-input' - dialog.appendChild(input) - - const buttons = document.createElement('div') - buttons.classList.add('flex-x', 'center') - dialog.appendChild(buttons) - - const cancel = document.createElement('button') - cancel.textContent = 'Cancel' - cancel.addEventListener('click', (event) => { - input.value = '' - dialog.close() - dialog.remove() - }) - buttons.appendChild(cancel) - - const ok = document.createElement('button') - ok.textContent = 'OK' - ok.addEventListener('click', (event) => { - if (event.detail === 1) { - const password = input.value - cancel.click() - callback(password) - } - }) - buttons.appendChild(ok) - - dialog.showModal() - } - - async function exportMnemonic (password) { + async function exportMnemonic (wallet) { try { - const wallet = await loadWallet() - await wallet.unlock(password) - const mnemonic = wallet.mnemonic - await wallet.lock(password) - notify.info(mnemonic) + notify.info(wallet.mnemonic) } catch (err) { console.error(err) notify.error(`Error exporting mnemonic`) } } - async function exportSeed (password) { + async function exportSeed (wallet) { try { - const wallet = await loadWallet() - await wallet.unlock(password) - const seed = wallet.seed - await wallet.lock(password) - notify.info(seed) + notify.info(wallet.seed) } catch (err) { console.error(err) notify.error(`Error exporting seed`) @@ -776,9 +828,6 @@ .page { height: 100%; } - .page.form { - z-index: 10; - } .hide { display: none; } @@ -900,36 +949,40 @@
-
- - - -
- - -
- + -
- - -
+
+ + +
+ + + +
+ + +
+ + + +
+ X0 +
-
@@ -1002,10 +1055,10 @@ -
-
@@ -1042,19 +1095,29 @@
-
- - -
- +
+ + + +
+ + +
+
+ + +
+
+
+
-- 2.34.1