<meta charset="utf-8" />
<meta lang="en" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="xel-theme" content="https://unpkg.com/xel@latest/themes/adwaita-dark.css" />
+ <meta name="xel-accent-color" content="blue" />
+ <meta name="xel-icons" content="https://unpkg.com/bootstrap-icons@latest/bootstrap-icons.svg" />
<script>
- let Account, Bip44Wallet, Blake2bWallet, LedgerWallet, SendBlock, ReceiveBlock, ChangeBlock, rpc, Rolodex, Xel
+ let Account, Bip44Wallet, Blake2bWallet, SendBlock, ReceiveBlock, ChangeBlock, rpc, Rolodex, Xel
+
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes") {
}
}
})
- function load () {
- loadUi().then(loadNemo).then(loadUx).then(() => { console.log('done') })
+
+ async function load () {
+ try {
+ document.querySelector(location.hash).classList.add('show')
+ } catch (err) {
+ location.hash = '#transact'
+ }
+ await loadUi()
+ await loadNemo()
+ await loadData()
+ console.log('done')
}
+
async function loadUi () {
console.log('loading xel')
Xel = await import('https://unpkg.com/xel@latest')
}, 250)
console.log('loaded xel')
}
+
async function loadNemo () {
console.log('loading libnemo')
- await ({ Account, Bip44Wallet, Blake2bWallet, LedgerWallet, SendBlock, ReceiveBlock, ChangeBlock, Node: rpc, Rolodex } = await import('https://unpkg.com/libnemo@latest'))
+ await ({ Account, Bip44Wallet, Blake2bWallet, SendBlock, ReceiveBlock, ChangeBlock, Node: rpc, Rolodex } = await import('https://unpkg.com/libnemo@latest'))
console.log('loaded libnemo')
}
- async function loadUx () {
+
+ async function loadData () {
console.log('loading data')
+ const walletSelect = document.querySelector('#wallet')
+ const accountSelect = document.querySelector('#account')
const wallets = await getFromStorage('wallets')
for (const wallet of wallets) {
- const { id, name } = JSON.parse(wallet)
- const option = document.createElement('option')
- option.value = id
- option.innerText = name
- wallet.appendChild(option)
+ const { id, name, accounts } = JSON.parse(wallet)
+ const walletOption = new Option(name, id)
+ walletSelect.add(walletOption)
+ for (const a of accounts ?? []) {
+ const { id, name } = JSON.parse(a)
+ const accountOption = new Option(name, id)
+ accountSelect.add(accountOption)
+ }
}
- const wallet = document.querySelector('#wallet')
- if (wallet.value === '_new' || wallet.value === '_import') {
- wallet.value = ''
+ if (walletSelect.value.substring(0, 1) === '_') {
+ walletSelect.value = ''
+ } else {
+ updateWalletSelect()
}
- const account = document.querySelector('#account')
- if (account.value === '_new') {
- account.value = ''
- }
- try {
- document.querySelector(location.hash).classList.add('show')
- } catch (err) {
- location.hash = '#transact'
+ if (accountSelect.value.substring(0, 1) === '_') {
+ accountSelect.setAttribute('disabled', '')
+ accountSelect.value = ''
}
console.log('loaded data')
}
document.querySelector(newHash).classList.add('show')
}
} catch (err) {
- notify.warn(`Page not found`)
+ notify.error(`Page not found`)
+ }
+ }
+
+ function addToStorage (key, value) {
+ if (typeof value !== 'string') {
+ throw new TypeError(`Cannot add ${typeof value} to storage`)
}
+ const item = JSON.parse(sessionStorage.getItem(key) ?? '[]')
+ if (!Array.isArray(item)) {
+ throw new TypeError(`Storage item is not an array`)
+ }
+ item.push(value)
+ return new Promise((resolve, reject) => {
+ try {
+ sessionStorage.setItem(key, JSON.stringify(item))
+ resolve()
+ } catch (err) {
+ console.error(err)
+ reject(err)
+ }
+ })
+ }
+
+ function getFromStorage (key) {
+ return new Promise((resolve, reject) => {
+ try {
+ const item = JSON.parse(sessionStorage.getItem(key) ?? '[]')
+ if (!Array.isArray(item)) {
+ throw new TypeError(`Storage item is not an array`)
+ }
+ resolve(item)
+ } catch (err) {
+ console.error(err)
+ reject(err)
+ }
+ })
}
function getWalletClass (algorithm) {
}
}
- async function updateWallet () {
- const wallet = document.querySelector('#wallet')
- const account = document.querySelector('#account')
- if (wallet.value === '_new') {
- return await document.querySelector('#wallet-new').showModal()
- }
- if (wallet.value === '_import') {
- return await document.querySelector('#wallet-import').showModal()
- }
- if (wallet.value === '_ledger') {
- try {
- await LedgerWallet.create()
- account.removeAttribute('disabled')
- } catch (err) {
- account.setAttribute('disabled', '')
- notify.error(`Error connecting to Ledger: ${err}`)
- } finally {
+ async function updateWalletSelect () {
+ const select = document.querySelector('#wallet')
+ switch (select.value) {
+ case '': {
+ await updateAccountSelect(null)
return
}
+ case '_new': {
+ try {
+ await document.querySelector('#wallet-new').showModal()
+ } catch (err) {
+ select.value = ''
+ notify.error(`Error creating wallet: ${err}`)
+ } finally {
+ return
+ }
+ }
+ case '_import': {
+ try {
+ await document.querySelector('#wallet-import').showModal()
+ } catch (err) {
+ select.value = ''
+ notify.error(`Error importing wallet: ${err}`)
+ } finally {
+ return
+ }
+ }
+ case '_ledger': {
+ try {
+ return await LedgerWallet.create()
+ } catch (err) {
+ select.value = ''
+ notify.error(`Error importing wallet: ${err}`)
+ return
+ }
+ }
+ default: {
+ const wallets = await getFromStorage('wallets')
+ const walletData = wallets.find(w => {
+ const { id, algorithm } = JSON.parse(w)
+ return id === select.value
+ })
+ const { id, algorithm } = JSON.parse(walletData)
+ const walletClass = getWalletClass(algorithm)
+ const wallet = await walletClass.restore(id)
+ await updateAccountSelect(wallet)
+ return wallet
+ }
}
- const form = document.querySelector('#wallet-form')
- const walletType = document.querySelector('#wallet-form-wallet-type')
- const importType = document.querySelector('#wallet-form-import-type')
- const importValue = document.querySelector('#wallet-form-import-value')
- const salt = document.querySelector('#wallet-form-salt')
- walletType.querySelector('x-radios').value = null
- importType.querySelector('x-select').value = null
- importValue.querySelector('x-input').value = null
- salt.querySelector('x-input').value = null
- if (wallet.value === '_new') {
- importType.classList.add('hide')
- importValue.classList.add('hide')
- salt.classList.remove('hide')
- }
- if (wallet.value === '_import') {
- importType.classList.remove('hide')
- importValue.classList.remove('hide')
- salt.classList.add('hide')
- }
- return await form.showModal()
}
- async function updateAccount () {
- const walletId = document.querySelector('#wallet').value
- const account = document.querySelector('#account')
- const wallets = await getFromStorage('wallets')
- const { id, algorithm } = wallets.find(wallet => {
- const { id } = JSON.parse(wallet)
- return id === walletId
- })
- try {
- const walletClass = getWalletClass(algorithm)
- const wallet = await walletClass.fromId(id)
- const accounts = await wallet.accounts()
- } catch (err) {
- notify.error(err)
+ async function updateAccountSelect (wallet) {
+ const accountSelect = document.querySelector('#account')
+ if (wallet == null || wallet === '') {
+ accountSelect.value = ''
+ accountSelect.setAttribute('disabled', '')
return
}
- const form = document.querySelector('#wallet-form')
- const walletType = document.querySelector('#wallet-form-wallet-type')
- const importType = document.querySelector('#wallet-form-import-type')
- const importValue = document.querySelector('#wallet-form-import-value')
- const salt = document.querySelector('#wallet-form-salt')
- walletType.querySelector('x-radios').value = null
- importType.querySelector('x-select').value = null
- importValue.querySelector('x-input').value = null
- salt.querySelector('x-input').value = null
- if (wallet.value === '_new') {
- importType.classList.add('hide')
- importValue.classList.add('hide')
- salt.classList.remove('hide')
- }
- if (wallet.value === '_import') {
- importType.classList.remove('hide')
- importValue.classList.remove('hide')
- salt.classList.add('hide')
+ switch (accountSelect.value) {
+ case '': {
+ accountSelect.removeAttribute('disabled')
+ return
+ }
+ case '_new': {
+ try {
+ return await document.querySelector('#account-new').showModal()
+ } catch (err) {
+ accountSelect.value = ''
+ notify.error(`Error creating account: ${err}`)
+ return
+ }
+ }
+ default: {
+ accountSelect.removeAttribute('disabled')
+ return await wallet.accounts(accountSelect.value)
+ }
}
- return await form.showModal()
}
async function createWallet () {
const walletSelect = document.querySelector('#wallet')
const form = document.querySelector('#wallet-new')
const name = form.querySelector('#wallet-new-name x-input').value
- if (name === '_ledger') {
- notify.warn(`Invalid wallet name "${name}".`)
+ if (name.substring(0, 1) === '_') {
+ notify.error(`Wallet name cannot start with an underscore`)
return
}
const algorithm = form.querySelector('#wallet-new-algorithm x-radios').value
await addToStorage(`wallets`, JSON.stringify(walletData))
const select = document.querySelector('#wallet')
- const option = document.createElement('option')
- option.value = walletData.id
- option.innerText = walletData.name
- select.appendChild(option)
- walletSelect.value = walletData.id
+ const option = new Option(walletData.name, walletData.id, false, true)
+ select.add(option)
+ await updateWalletSelect()
} catch (err) {
- label?.remove()
- menuitem?.remove()
+ option?.remove()
notify.error(err.msg)
} finally {
await form.close()
}
}
- function addToStorage (key, value) {
- if (typeof value !== 'string') {
- throw new TypeError(`Cannot add ${typeof value} to storage`)
- }
- const item = JSON.parse(sessionStorage.getItem(key) ?? '[]')
- if (!Array.isArray(item)) {
- throw new TypeError(`Storage item is not an array`)
+ async function createAccount () {
+ const wallet = await updateWalletSelect()
+ const accountSelect = document.querySelector('#account')
+ const form = document.querySelector('#account-new')
+ const name = form.querySelector('#account-new-name x-input').value
+ if (name.substring(0, 1) === '_') {
+ notify.error(`Account name cannot start with an underscore`)
+ return
}
- item.push(value)
- return new Promise((resolve, reject) => {
- try {
- sessionStorage.setItem(key, JSON.stringify(item))
- resolve()
- } catch (err) {
- console.error(err)
- reject(err)
- }
- })
- }
-
- function getFromStorage (key) {
- return new Promise((resolve, reject) => {
- try {
- const item = JSON.parse(sessionStorage.getItem(key) ?? '[]')
- if (!Array.isArray(item)) {
- throw new TypeError(`Storage item is not an array`)
- }
- resolve(item)
- } catch (err) {
- console.error(err)
- reject(err)
+ const index = form.querySelector('#account-new-index x-numberinput')?.value
+ let account
+ try {
+ account = await wallet.accounts(index)
+ const accountData = {
+ id: index,
+ name: name || index
}
- })
+ await addToStorage(`accounts`, JSON.stringify(accountData))
+ const select = document.querySelector('#account')
+ const option = new Option(accountData.name, accountData.id, false, true)
+ select.add(option)
+ await updateAccountSelect(wallet)
+ } catch (err) {
+ option?.remove()
+ notify.error(err.msg)
+ } finally {
+ await form.close()
+ return account
+ }
}
function updateWalletForm (id) {
}
}
- async function closeForm (id) {
+ async function formCancel (id) {
const form = document.querySelector(id)
- const inputs = form.querySelector('x-input')
+ const inputs = form.querySelectorAll('x-input')
for (const input of inputs) {
- input.value = null
+ input.value = ''
+ }
+ const wallet = document.querySelector('#wallet')
+ if (wallet.value.substring(0, 1) === '_') {
+ wallet.value = ''
}
await form.close()
}
card.replaceChildren(...walletElements)
}
- async function deriveAccounts () {
- const wallet = document.querySelector('#wallet')
- const account = document.querySelector('#account')
- if (!wallet.value) {
- account.value = null
- notify.error(`Select a wallet first`)
- return
- }
- const form = btn.querySelector('#account-form')
- await console.log('deriveAccounts')
- }
async function clearStorage () {
sessionStorage.clear()
+ const options = document.querySelectorAll('#wallet option, #account option')
+ for (const option of options) {
+ if (option.value !== '' && option.value.substring(0, 1) != '_') {
+ option.remove()
+ }
+ }
+ document.querySelector('#wallet').value = ''
+ document.querySelector('#account').value = ''
+ await updateWalletSelect()
notify.info(`Session storage cleared`)
}
+
const notify = {
_show: (msg, color) => {
const container = document.querySelector('#notifications')
},
info: (msg) => notify._show(`ℹ️ ${msg}`),
ok: (msg) => notify._show(`☑️ ${msg}`, 'Cyan'),
- warn: (msg) => notify._show(`⚠️ ${msg}`, 'Gold'),
- error: (msg) => notify._show(`🚫 ${msg}`, 'Tomato')
+ error: (msg) => notify._show(`⚠️ ${msg}`, 'Gold')
}
</script>
- <meta name="xel-theme" content="https://unpkg.com/xel@latest/themes/adwaita-dark.css" />
- <meta name="xel-accent-color" content="blue" />
- <meta name="xel-icons" content="https://unpkg.com/bootstrap-icons@latest/bootstrap-icons.svg" />
<style>
body {
background-color: slategrey;
color: #209CE9;
fill: #209CE9;
}
+ .flex {
+ display: flex;
+ }
+ .flex.v-start {
+ flex-direction: column;
+ justify-content: flex-start;
+ }
+ .flex.v-mid {
+ flex-direction: column;
+ justify-content: center;
+ }
+ .flex.v-end {
+ flex-direction: column;
+ justify-content: flex-end;
+ }
+ .flex.v-stretch {
+ flex-direction: column;
+ justify-content: stretch;
+ }
+ .flex.h-start {
+ align-items: flex-start;
+ flex-direction: row;
+ }
+ .flex.h-mid {
+ align-items: center;
+ flex-direction: row;
+ }
+ .flex.h-end {
+ align-items: flex-end;
+ flex-direction: row;
+ }
+ .flex.h-stretch {
+ align-items: stretch;
+ flex-direction: row;
+ }
#notifications {
display: flex;
flex-direction: column;
<x-box id="wallets">
<label for="wallet">Wallet</label>
- <select id="wallet" style="width:100%;" onchange="updateWallet()">
+ <select id="wallet" style="width:100%;" onchange="updateWalletSelect()">
<option value="" disabled>Choose...</option>
<hr />
<option value="_new">New</option>
<option value="_import">Import</option>
<hr />
- <option value="_ledger">Ledger</option>
+ <option disabled value="_ledger">Ledger</option>
<hr />
</select>
</x-box>
<!-- New Wallet Form -->
<dialog id="wallet-new">
- <x-box vertical>
- <x-box id="wallet-new-name">
+ <div class="flex v-stretch">
+ <div class="flex h-mid" id="wallet-new-name">
<x-label>Name</x-label>
<x-input type="text"></x-input>
- </x-box>
+ </div>
<x-box id="wallet-new-algorithm">
<x-label>Algorithm</x-label>
<x-radios required ontoggle="updateWalletForm('#wallet-new')">
</x-box>
<x-box>
<x-buttons>
- <x-button onclick="closeForm('#wallet-new')">
+ <x-button onclick="formCancel('#wallet-new')">
Cancel
</x-button>
<x-button onclick="createWallet().then(w => { console.log(w);showWallets() })">
</x-button>
</x-buttons>
</x-box>
- </x-box>
+ </div>
</dialog>
<!-- Import Wallet Form -->
<dialog id="wallet-import">
- <x-box vertical>
+ <div class="flex vertical">
<x-box id="wallet-import-name">
<x-label>Name</x-label>
<x-input type="text"></x-input>
</x-box>
<x-box>
<x-buttons>
- <x-button onclick="closeForm('#wallet-import')">
+ <x-button onclick="formCancel('#wallet-import')">
Cancel
</x-button>
<x-button onclick="importWallet().then(w => { console.log(w);showWallets() })">
</x-button>
</x-buttons>
</x-box>
- </x-box>
+ </div>
</dialog>
<!-- Account Select -->
<x-box id="accounts">
<label for="account">Account</label>
- <select id="account" style="width:100%;" onchange="updateAccount()">
+ <select id="account" style="width:100%;" onchange="updateWalletSelect()">
<option value="" disabled>Choose...</option>
<hr />
<option value="_new">New</option>
<hr />
</select>
- </x-box><!-- New Account Form -->
+ </x-box>
+
+ <!-- New Account Form -->
<dialog id="account-new">
<x-box vertical>
- <x-box>
+ <x-box id="account-new-name">
<x-label>Name</x-label>
<x-input type="text"></x-input>
</x-box>
- <x-box>
+ <x-box id="account-new-index">
<x-label>Index (optional)</x-label>
<x-numberinput min="0" max="2147483647"></x-numberinput>
</x-box>
+ <x-box id="account-new-password">
+ <x-label>Unlock</x-label>
+ <x-input type="password" required><x-label>Password</x-label></x-input>
+ </x-box>
<x-buttons>
<x-button onclick="document.querySelector('#account-new').close()">
Cancel
</x-button>
- <x-button onclick="deriveAccounts().then(w => { console.log(w);showWallets() })">
+ <x-button onclick="createAccount().then(w => { console.log(w);showWallets() })">
OK
</x-button>
</x-buttons>