diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..de15042 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png index 007d62b..c0f50e8 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/src/components/balance.js b/src/components/balance.js index daf3f43..9077705 100644 --- a/src/components/balance.js +++ b/src/components/balance.js @@ -1,5 +1,5 @@ import { lit as html } from '../helpers/lit.js' -import { fixedDash, envoy, } from '../helpers/utils.js' +import { envoy, formatDash, } from '../helpers/utils.js' import { updateAllFunds, } from '../helpers/wallet.js' const initialState = { @@ -14,6 +14,7 @@ const initialState = { addr: null, maxlen: 10, fract: 8, + sigsplit: 3, render( renderState = {}, position = 'afterbegin', @@ -23,41 +24,19 @@ const initialState = {
${state.name} ${state.id}
`, content: state => { - let funds = 0 - let balance = `${funds}` - - if (state.walletFunds.balance) { - funds += state.walletFunds.balance - balance = fixedDash(funds, state.fract) - // TODO FIX: does not support large balances - - // console.log('balance fixedDash', balance, balance.length) - - let [fundsInt,fundsFract] = balance.split('.') - state.maxlen -= fundsInt.length - - let fundsFraction = fundsFract?.substring( - 0, Math.min(Math.max(0, state.maxlen), 3) - ) - - let fundsRemainder = fundsFract?.substring( - fundsFraction.length, - Math.max(0, state.maxlen) - ) - - balance = html`${ - fundsInt - }.${ - fundsFraction - }${ - fundsRemainder - }` - } + let balance = formatDash( + state.walletFunds.balance, + { + fract: state.fract, + maxlen: state.maxlen, + sigsplit: state.sigsplit, + } + ) return html` ${state.header(state)} -
+
diff --git a/src/components/dialog.js b/src/components/dialog.js index f232feb..b133a09 100644 --- a/src/components/dialog.js +++ b/src/components/dialog.js @@ -267,7 +267,7 @@ const initialState = { // ) state.elements.dialog.close('cancel') } - } + }, }, } diff --git a/src/helpers/utils.js b/src/helpers/utils.js index 6ed6abc..57ca83b 100644 --- a/src/helpers/utils.js +++ b/src/helpers/utils.js @@ -270,6 +270,50 @@ export function toDuff(dash) { return Math.round(parseFloat(dash) * DUFFS); } +export function formatDash( + unformattedBalance, + options = {}, +) { + let opts = { + maxlen: 10, + fract: 8, + sigsplit: 3, + ...options, + } + let funds = 0 + let balance = `${funds}` + + if (unformattedBalance) { + funds += unformattedBalance + balance = fixedDash(funds, opts.fract) + // TODO FIX: does not support large balances + + // console.log('balance fixedDash', balance, balance.length) + + let [fundsInt,fundsFract] = balance.split('.') + opts.maxlen -= fundsInt.length + + let fundsFraction = fundsFract?.substring( + 0, Math.min(Math.max(0, opts.maxlen), opts.sigsplit) + ) + + let fundsRemainder = fundsFract?.substring( + fundsFraction.length, + Math.max(0, opts.maxlen) + ) + + balance = `${ + fundsInt + }.${ + fundsFraction + }${ + fundsRemainder + }` + } + + return balance +} + export function formDataEntries(event) { let fd = new FormData( event.target, @@ -984,20 +1028,90 @@ export async function getAvatar(c) { return `${avStr}">${initials}
` } -export function readFile(file, callback) { +export function fileIsSubType(file, type) { + const fileType = file?.type?.split('/')?.[1] + + if (!fileType) { + return false + } + + return fileType === type +} + +// fileInTypes({type:'application/json'}, ['image/png']) +export function fileInMIMETypes(file, types = []) { + const fileType = file?.type + + if (!fileType) { + return false + } + + return types.includes(fileType) +} + +export function fileTypeInTypes(file, types = []) { + const fileType = file?.type?.split('/')?.[0] + + if (!fileType) { + return false + } + + return types.includes(fileType) +} + +export function fileTypeInSubtype(file, subtypes = []) { + const fileSubType = file?.type?.split('/')?.[1] + + if (!fileSubType) { + return false + } + + return subtypes.includes(fileSubType) +} + +export function readFile(file, options) { + let opts = { + expectedFileType: 'json', + denyFileTypes: ['audio','video','image','font','model'], + denyFileSubTypes: ['msword','xml'], + callback: () => {}, + errorCallback: () => {}, + ...options, + } let reader = new FileReader(); let result reader.addEventListener('load', () => { + if ( + fileTypeInTypes( + file, + opts.denyFileTypes, + ) || fileTypeInSubtype( + file, + opts.denyFileSubTypes, + ) + ) { + return opts.errorCallback?.({ + err: `Wrong file type: ${file.type}. Expected: ${opts.expectedFileType}.`, + file, + }) + } + try { // @ts-ignore result = JSON.parse(reader?.result || '{}'); - console.log('parse loaded json', result); - callback?.(result) + // console.log('parse loaded json', result); + + opts.callback?.(result, file) // state[key] = result } catch(err) { + opts.errorCallback?.({ + err, + file, + }) + throw new Error(`failed to parse JSON data from ${file.name}`) } }); @@ -1054,11 +1168,26 @@ export function getPartialHDPath(wallet) { ].join('/') } -export function getAddressIndexFromUsage(wallet, account, usage) { - let usageIndex = usage ?? wallet.usageIndex +export function getAddressIndexFromUsage(wallet, account, usageIdx) { + let usageIndex = usageIdx ?? wallet?.usageIndex ?? 0 + let addressIndex = account.usage?.[usageIndex] ?? account.addressIndex ?? 0 + let usage = account.usage ?? [ + account.addressIndex ?? 0, + 0 + ] + + // console.log( + // 'getAddressIndexFromUsage', + // usageIndex, + // addressIndex, + // account, + // usage, + // ) + return { ...account, + usage, usageIndex, - addressIndex: account.usage[usageIndex], + addressIndex, } } diff --git a/src/index.css b/src/index.css index 7439fb5..7e69e4d 100644 --- a/src/index.css +++ b/src/index.css @@ -72,6 +72,7 @@ main header figure figcaption + div svg { main header figure figcaption + div sub { align-self: self-end; font-size: 2.75rem; + font-size: 74%; } main footer { text-align: center; diff --git a/src/main.js b/src/main.js index b3f0a79..89b417d 100644 --- a/src/main.js +++ b/src/main.js @@ -377,6 +377,13 @@ async function getUserInfo() { ([k,v]) => userInfo[k] = v ) }) + .catch(err => { + showErrorDialog({ + title: 'Unable to decrypt seed phrase', + msg: err, + showActBtn: false, + }) + }) } } @@ -428,6 +435,89 @@ function getTarget(event, selector) { return target } +async function showNotification({ + type = '', + title = '', + msg = '', + sticky = false, +}) { + console.log('notification', {type, title, msg, sticky}) +} + +async function showErrorDialog(options) { + let opts = { + type: 'warn', + title: '⚠️ Error', + msg: '', + showCancelBtn: true, + showActBtn: true, + cancelCallback: () => {}, + // timeout: null, + ...options, + } + + opts.callback = opts.callback || (() => { + let firstLineFromError = '' + let { msg } = opts + + if (typeof msg !== 'string' && msg.toString) { + msg = msg.toString() + } + if (typeof msg === 'string') { + firstLineFromError = msg.match(/[^\r\n]+/g)?.[0] + } + + // console.log('firstLineFromError', firstLineFromError) + + window.open( + `https://github.com/dashhive/wallet-ui/issues?q=${firstLineFromError}`, + '_blank', + ) + }) + + if (opts.type === 'dang') { + console.error('showErrorDialog', opts.msg) + } else { + console.log('showErrorDialog', opts) + } + + await appDialogs.confirmAction?.render({ + name: opts.title, + actionTxt: 'Report Issue', + actionAlt: 'Report the error at GitHub', + action: 'lock', + cancelTxt: 'Close', + cancelAlt: `Close`, + // target: '', + // targetFallback: 'this wallet', + actionType: opts.type, + // action: 'disconnect', + // target: '', + // targetFallback: 'this wallet', + // actionType: 'dang', + showCancelBtn: opts.showCancelBtn, + showActBtn: opts.showActBtn, + submitIcon: state => `⚠️`, + alert: state => html``, + content: state => html` + ${state.header(state)} + +
+ + Looks like we encountered an error. + +
${opts.msg}
+
+ + ${state.footer(state)} + `, + cancelCallback: opts.cancelCallback, + callback: opts.callback, + }) + + return appDialogs.confirmAction?.showModal() +} + async function main() { appState.encryptionPassword = window.atob( sessionStorage.encryptionPassword || '' @@ -463,6 +553,11 @@ async function main() { } ) + appDialogs.confirmAction = await confirmActionRig({ + mainApp, setupDialog, + appDialogs, appState, appTools, + }) + appDialogs.walletEncrypt = await walletEncryptRig({ setupDialog, appDialogs, appState, mainApp, wallet, wallets, bodyNav, dashBalance, @@ -471,6 +566,7 @@ async function main() { appDialogs.walletDecrypt = await walletDecryptRig({ setupDialog, appDialogs, appState, mainApp, importFromJson, wallets, decryptKeystore, getUserInfo, store, deriveWalletData, + showErrorDialog, }) appDialogs.walletBackup = await walletBackupRig({ @@ -491,6 +587,7 @@ async function main() { appDialogs.phraseImport = await phraseImportRig({ setupDialog, appDialogs, appState, store, mainApp, wallet, wallets, deriveWalletData, + showErrorDialog, }) appDialogs.onboard = await onboardRig({ @@ -503,11 +600,6 @@ async function main() { mainApp, wallet, userInfo, contactsList, }) - appDialogs.confirmAction = await confirmActionRig({ - mainApp, setupDialog, - appDialogs, appState, appTools, - }) - appDialogs.confirmDelete = await confirmDeleteRig({ mainApp, setupDialog, appDialogs, appState, appTools, store, userInfo, contactsList, @@ -532,7 +624,7 @@ async function main() { mainApp, appDialogs, appState, appTools, store, wallet, account: appState.account, walletFunds, setupDialog, deriveWalletData, createTx, - getAddrsWithFunds, batchGenAcctAddrs, getUnusedChangeAddress, getAccountWallet, + getAddrsWithFunds, batchGenAcctAddrs, getUnusedChangeAddress, getAccountWallet, showErrorDialog, }) appDialogs.txInfo = await txInfoRig({ @@ -577,10 +669,15 @@ async function main() { ks, ) } catch(err) { - console.error( - '[fail] unable to decrypt seed phrase', - err - ) + // console.error( + // '[fail] unable to decrypt seed phrase', + // err + // ) + await showErrorDialog({ + title: 'Unable to decrypt seed phrase', + msg: err, + showActBtn: false, + }) sessionStorage.removeItem('encryptionPassword') } } @@ -693,7 +790,7 @@ async function main() { await appDialogs.requestQr.render( { name: 'Share to receive funds', - submitTxt: `Select a Contact`, + submitTxt: `Edit Amount or Contact`, submitAlt: `Change the currently selected contact`, footer: state => html`
@@ -85,16 +90,22 @@ export let sendConfirmRig = (async function (globals) { if (!state.fee?.dash || !state.fullAmount) { return '' } + let dashFee = formatDash( + state.fee.dash, + ) + let totalAmount = formatDash( + Number(state.fullAmount) + Number(state.fee?.dash), + ) return html`
Dash Network Fee
-
+
- ${state.fee?.dash} + ${dashFee}
@@ -103,7 +114,7 @@ export let sendConfirmRig = (async function (globals) { - ${(Number(state.fullAmount) + Number(state.fee?.dash)).toFixed(8)} + ${totalAmount}
diff --git a/src/rigs/send-or-request.js b/src/rigs/send-or-request.js index ec876b5..dfc51ad 100644 --- a/src/rigs/send-or-request.js +++ b/src/rigs/send-or-request.js @@ -17,7 +17,7 @@ export let sendOrReceiveRig = (async function (globals) { let { mainApp, setupDialog, appDialogs, appState, appTools, store, createTx, deriveWalletData, getAddrsWithFunds, batchGenAcctAddrs, - wallet, wallets, accounts, walletFunds, getUnusedChangeAddress, getAccountWallet, + wallet, wallets, accounts, walletFunds, getUnusedChangeAddress, getAccountWallet, showErrorDialog, } = globals let sendOrReceive = await setupDialog( @@ -516,7 +516,15 @@ export let sendOrReceiveRig = (async function (globals) { address = to } - if (amount > 0 && walletFunds.balance < amount) { + let leftoverBalance = walletFunds.balance - amount + // let fullTransfer = leftoverBalance <= 0.0010_0200 + let fullTransfer = leftoverBalance <= 0.0001_0200 + + if ( + amount > 0 && + walletFunds.balance < amount && + !fullTransfer + ) { console.log( `INSUFFICIENT FUNDS IN WALLET`, [ @@ -591,18 +599,28 @@ export let sendOrReceiveRig = (async function (globals) { fundingAddrs = Object.values( fundingAddrs || {} ) - let leftoverBalance = walletFunds.balance - amount - // let fullTransfer = leftoverBalance <= 0.0010_0200 - let fullTransfer = leftoverBalance <= 0.0001_0200 - let { tx, changeAddr, fee } = await createTx( - state.wallet, - fundingAddrs, - [changeAddress], - address, - amount, - fullTransfer, - ) + let createdTx = {} + + try { + createdTx = await createTx( + state.wallet, + fundingAddrs, + [changeAddress], + address, + amount, + fullTransfer, + ) + } catch(err) { + await showErrorDialog({ + type: 'dang', + title: 'Failed to create transaction', + msg: err, + // showActBtn: false, + }) + } + + let { tx, changeAddr, fee } = createdTx let fullAmount = 0 @@ -743,7 +761,7 @@ export let sendOrReceiveRig = (async function (globals) { await appDialogs.requestQr.render( { name: 'Share to receive funds', - submitTxt: `Select a Contact`, + submitTxt: `Edit Amount or Contact`, submitAlt: `Change the currently selected contact`, // footer: state => html`