window.PageWallet = { template: '#page-wallet', data() { return { parse: { show: false, invoice: null, lnurlpay: null, lnurlauth: null, data: { request: '', amount: 0, comment: '', internalMemo: null, unit: 'sat' }, paymentChecker: null, copy: { show: false }, camera: { show: false, camera: 'auto' } }, receive: { show: false, status: 'pending', paymentReq: null, paymentHash: null, amountMsat: null, minMax: [0, 2100000000000000], lnurl: null, units: [], unit: 'sat', fiatProvider: '', data: { amount: null, memo: '', internalMemo: null, payment_hash: null } }, update: { name: null, currency: null }, hasNfc: false, nfcReaderAbortController: null, formattedFiatAmount: 0, paymentFilter: { 'status[ne]': 'failed' }, chartConfig: Quasar.LocalStorage.getItem( 'lnbits.wallets.chartConfig' ) || { showPaymentInOutChart: true, showBalanceChart: true, showBalanceInOutChart: true } } }, computed: { canPay() { if (!this.parse.invoice) return false if (this.parse.invoice.expired) { Quasar.Notify.create({ message: 'Invoice has expired', color: 'negative' }) return false } return this.parse.invoice.sat <= this.g.wallet.sat }, formattedAmount() { if (this.receive.unit != 'sat' || !this.g.isSatsDenomination) { return LNbits.utils.formatCurrency( Number(this.receive.data.amount).toFixed(2), !this.g.isSatsDenomination ? this.g.denomination : this.receive.unit ) } else { return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat' } }, formattedSatAmount() { return LNbits.utils.formatMsat(this.receive.amountMsat) + ' sat' } }, methods: { handleSendLnurl(lnurl) { this.parse.data.request = lnurl this.parse.show = true this.lnurlScan() }, msatoshiFormat(value) { return LNbits.utils.formatSat(value / 1000) }, showReceiveDialog() { this.receive.show = true this.receive.status = 'pending' this.receive.paymentReq = null this.receive.paymentHash = null this.receive.data.amount = null this.receive.data.memo = null this.receive.data.internalMemo = null this.receive.data.payment_hash = null this.receive.units = [ 'sat', ...(this.g.allowedCurrencies.length > 0 ? this.g.allowedCurrencies : this.g.currencies) ] this.receive.unit = this.g.isFiatPriority ? this.g.wallet.currency || 'sat' : 'sat' this.receive.minMax = [0, 2100000000000000] this.receive.lnurl = null }, onReceiveDialogHide() { if (this.hasNfc) { this.nfcReaderAbortController.abort() } }, showParseDialog() { this.parse.show = true this.parse.invoice = null this.parse.lnurlpay = null this.parse.lnurlauth = null this.parse.copy.show = window.isSecureContext && navigator.clipboard?.readText !== undefined this.parse.data.request = '' this.parse.data.comment = '' this.parse.data.internalMemo = null this.parse.data.paymentChecker = null this.parse.camera.show = false }, closeParseDialog() { setTimeout(() => { clearInterval(this.parse.paymentChecker) }, 10000) }, handleBalanceUpdate(value) { this.g.wallet.sat = this.g.wallet.sat + value }, createInvoice() { this.receive.status = 'loading' if (!this.g.isSatsDenomination) { this.receive.data.amount = this.receive.data.amount * 100 } LNbits.api .createInvoice( this.g.wallet, this.receive.data.amount, this.receive.data.memo, this.receive.unit, this.receive.lnurlWithdraw, this.receive.fiatProvider, this.receive.data.internalMemo, this.receive.data.payment_hash ) .then(response => { this.g.updatePayments = !this.g.updatePayments this.receive.status = 'success' this.receive.paymentReq = response.data.bolt11 this.receive.fiatPaymentReq = response.data.extra?.fiat_payment_request this.receive.amountMsat = response.data.amount this.receive.paymentHash = response.data.payment_hash if (!this.receive.lnurl) { this.readNfcTag() } // WITHDRAW if ( this.receive.lnurl && response.data.extra?.lnurl_response !== null ) { if (response.data.extra.lnurl_response === false) { response.data.extra.lnurl_response = `Unable to connect` } const domain = this.receive.lnurl.callback.split('/')[2] if (typeof response.data.extra.lnurl_response === 'string') { // failure Quasar.Notify.create({ timeout: 5000, type: 'warning', message: `${domain} lnurl-withdraw call failed.`, caption: response.data.extra.lnurl_response }) return } else if (response.data.extra.lnurl_response === true) { // success Quasar.Notify.create({ timeout: 3000, message: `Invoice sent to ${domain}!`, spinner: true }) } } }) .catch(err => { LNbits.utils.notifyApiError(err) this.receive.status = 'pending' }) }, lnurlScan() { LNbits.api .request('POST', '/api/v1/lnurlscan', this.g.wallet.adminkey, { lnurl: this.parse.data.request }) .then(response => { const data = response.data if (data.status === 'ERROR') { Quasar.Notify.create({ timeout: 5000, type: 'warning', message: `lnurl scan failed.`, caption: data.reason }) return } if (data.tag === 'payRequest') { this.parse.lnurlpay = Object.freeze(data) this.parse.data.amount = data.minSendable / 1000 } else if (data.tag === 'login') { this.parse.lnurlauth = Object.freeze(data) } else if (data.tag === 'withdrawRequest') { this.parse.show = false this.receive.show = true this.receive.lnurlWithdraw = Object.freeze(data) this.receive.status = 'pending' this.receive.paymentReq = null this.receive.paymentHash = null this.receive.data.amount = data.maxWithdrawable / 1000 this.receive.data.memo = data.defaultDescription this.receive.minMax = [ data.minWithdrawable / 1000, data.maxWithdrawable / 1000 ] const domain = data.callback.split('/')[2] this.receive.lnurl = { domain: domain, callback: data.callback, fixed: data.fixed } } }) .catch(err => { LNbits.utils.notifyApiError(err) }) }, decodeQR(val) { this.parse.data.request = val this.decodeRequest() this.parse.camera.show = false }, isLnurl(req) { return ( req.toLowerCase().startsWith('lnurl1') || req.startsWith('lnurlp://') || req.startsWith('lnurlw://') || req.startsWith('lnurlauth://') || req.match(/[\w.+-~_]+@[\w.+-~_]/) ) }, decodeRequest() { this.parse.show = true this.parse.data.request = this.parse.data.request.trim() const req = this.parse.data.request.toLowerCase() if (req.startsWith('lightning:')) { this.parse.data.request = this.parse.data.request.slice(10) } else if (req.startsWith('lnurl:')) { this.parse.data.request = this.parse.data.request.slice(6) } else if (req.includes('lightning=lnurl1')) { this.parse.data.request = this.parse.data.request .split('lightning=')[1] .split('&')[0] } if (this.isLnurl(this.parse.data.request)) { this.lnurlScan() return } // BIP-21 support if (this.parse.data.request.toLowerCase().includes('lightning')) { this.parse.data.request = this.parse.data.request.split('lightning=')[1] // fail safe to check there's nothing after the lightning= part if (this.parse.data.request.includes('&')) { this.parse.data.request = this.parse.data.request.split('&')[0] } } let invoice try { invoice = decode(this.parse.data.request) } catch (error) { Quasar.Notify.create({ timeout: 3000, type: 'warning', message: error + '.', caption: '400 BAD REQUEST' }) this.parse.show = false return } let cleanInvoice = { msat: invoice.human_readable_part.amount, sat: invoice.human_readable_part.amount / 1000, fsat: LNbits.utils.formatSat(invoice.human_readable_part.amount / 1000), bolt11: this.parse.data.request } _.each(invoice.data.tags, tag => { if (_.isObject(tag) && _.has(tag, 'description')) { if (tag.description === 'payment_hash') { cleanInvoice.hash = tag.value } else if (tag.description === 'description') { cleanInvoice.description = tag.value } else if (tag.description === 'expiry') { const expireDate = new Date( (invoice.data.time_stamp + tag.value) * 1000 ) const createdDate = new Date(invoice.data.time_stamp * 1000) cleanInvoice.expireDate = Quasar.date.formatDate( expireDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ' ) cleanInvoice.createdDate = Quasar.date.formatDate( createdDate, 'YYYY-MM-DDTHH:mm:ss.SSSZ' ) cleanInvoice.expireDateFrom = moment .utc(expireDate) .local() .fromNow() cleanInvoice.createdDateFrom = moment .utc(createdDate) .local() .fromNow() cleanInvoice.expired = false // TODO } } }) if (this.g.wallet.currency) { cleanInvoice.fiatAmount = LNbits.utils.formatCurrency( ((cleanInvoice.sat / 1e8) * this.g.exchangeRate).toFixed(2), this.g.wallet.currency ) } this.parse.invoice = Object.freeze(cleanInvoice) }, payInvoice() { const dismissPaymentMsg = Quasar.Notify.create({ timeout: 0, message: this.$t('payment_processing') }) LNbits.api .payInvoice( this.g.wallet, this.parse.data.request, this.parse.data.internalMemo ) .then(response => { dismissPaymentMsg() this.g.updatePayments = !this.g.updatePayments this.parse.show = false if (response.data.status == 'success') { Quasar.Notify.create({ type: 'positive', message: this.$t('payment_successful') }) } if (response.data.status == 'pending') { Quasar.Notify.create({ type: 'info', message: this.$t('payment_pending') }) } }) .catch(err => { dismissPaymentMsg() LNbits.utils.notifyApiError(err) this.g.updatePayments = !this.g.updatePayments this.parse.show = false }) }, payLnurl() { LNbits.api .request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, { res: this.parse.lnurlpay, lnurl: this.parse.data.request, unit: this.parse.data.unit, amount: this.parse.data.amount * 1000, comment: this.parse.data.comment, internalMemo: this.parse.data.internalMemo }) .then(response => { this.parse.show = false if (response.data.extra.success_action) { const action = JSON.parse(response.data.extra.success_action) switch (action.tag) { case 'url': Quasar.Notify.create({ message: `${action.url}`, caption: action.description, html: true, type: 'positive', timeout: 0, closeBtn: true }) break case 'message': Quasar.Notify.create({ message: action.message, type: 'positive', timeout: 0, closeBtn: true }) break case 'aes': this.utils.decryptLnurlPayAES(action, response.data.preimage) Quasar.Notify.create({ message: value, caption: extra.success_action.description, html: true, type: 'positive', timeout: 0, closeBtn: true }) } } }) .catch(LNbits.utils.notifyApiError) }, authLnurl() { const dismissAuthMsg = Quasar.Notify.create({ timeout: 10, message: 'Performing authentication...' }) LNbits.api .request( 'post', '/api/v1/lnurlauth', wallet.adminkey, this.parse.lnurlauth ) .then(_ => { dismissAuthMsg() Quasar.Notify.create({ message: `Authentication successful.`, type: 'positive', timeout: 3500 }) this.parse.show = false }) .catch(err => { if (err.response.data.reason) { Quasar.Notify.create({ message: `Authentication failed. ${this.parse.lnurlauth.callback} says:`, caption: err.response.data.reason, type: 'warning', timeout: 5000 }) } else { LNbits.utils.notifyApiError(err) } }) }, updateWallet(data) { LNbits.api .request('PATCH', '/api/v1/wallet', this.g.wallet.adminkey, data) .then(response => { this.g.wallet = {...this.g.wallet, ...response.data} const walletIndex = this.g.user.wallets.findIndex( wallet => wallet.id === response.data.id ) if (walletIndex !== -1) { this.g.user.wallets[walletIndex] = { ...this.g.user.wallets[walletIndex], ...response.data } } Quasar.Notify.create({ message: 'Wallet updated.', type: 'positive', timeout: 3500 }) }) .catch(err => { LNbits.utils.notifyApiError(err) }) }, pasteToTextArea() { this.$refs.textArea.focus() navigator.clipboard.readText().then(text => { this.parse.data.request = text.trim() }) }, readNfcTag() { try { if (typeof NDEFReader == 'undefined') { console.debug('NFC not supported on this device or browser.') return } const ndef = new NDEFReader() this.nfcReaderAbortController = new AbortController() this.nfcReaderAbortController.signal.onabort = event => { console.debug('All NFC Read operations have been aborted.') } this.hasNfc = true const dismissNfcTapMsg = Quasar.Notify.create({ message: 'Tap your NFC tag to pay this invoice with LNURLw.' }) return ndef .scan({signal: this.nfcReaderAbortController.signal}) .then(() => { ndef.onreadingerror = () => { Quasar.Notify.create({ type: 'negative', message: 'There was an error reading this NFC tag.' }) } ndef.onreading = ({message}) => { //Decode NDEF data from tag const textDecoder = new TextDecoder('utf-8') const record = message.records.find(el => { const payload = textDecoder.decode(el.data) return payload.toUpperCase().indexOf('LNURLW') !== -1 }) if (record) { dismissNfcTapMsg() Quasar.Notify.create({ type: 'positive', message: 'NFC tag read successfully.' }) const lnurl = textDecoder.decode(record.data) this.payInvoiceWithNfc(lnurl) } else { Quasar.Notify.create({ type: 'warning', message: 'NFC tag does not have LNURLw record.' }) } } }) } catch (error) { Quasar.Notify.create({ type: 'negative', message: error ? error.toString() : 'An unexpected error has occurred.' }) } }, payInvoiceWithNfc(lnurl) { const dismissPaymentMsg = Quasar.Notify.create({ timeout: 0, spinner: true, message: this.$t('processing_payment') }) LNbits.api .request( 'POST', `/api/v1/payments/${this.receive.paymentReq}/pay-with-nfc`, this.g.wallet.adminkey, {lnurl_w: lnurl} ) .then(response => { dismissPaymentMsg() if (response.data.success) { Quasar.Notify.create({ type: 'positive', message: 'Payment successful' }) } else { Quasar.Notify.create({ type: 'negative', message: response.data.detail || 'Payment failed' }) } }) .catch(err => { dismissPaymentMsg() LNbits.utils.notifyApiError(err) }) } }, created() { const urlParams = new URLSearchParams(window.location.search) if (urlParams.has('lightning') || urlParams.has('lnurl')) { this.parse.data.request = urlParams.get('lightning') || urlParams.get('lnurl') this.decodeRequest() this.parse.show = true } const wallet = this.g.user.wallets.find(w => w.id === this.$route.params.id) if (wallet) { this.g.wallet = wallet this.g.lastActiveWallet = wallet.id this.$q.localStorage.setItem('lnbits.lastActiveWallet', wallet.id) this.$router.replace(`/wallet/${wallet.id}`) } else { this.g.errorCode = 404 this.g.errorMessage = 'Wallet not found.' this.$router.push('/error') } }, watch: { 'g.updatePaymentsHash'() { this.receive.show = false }, 'g.updatePayments'() { this.parse.show = false if ( this.g.wallet.currency && this.$q.localStorage.getItem( 'lnbits.exchangeRate.' + this.g.wallet.currency ) ) { this.g.exchangeRate = this.$q.localStorage.getItem( 'lnbits.exchangeRate.' + this.g.wallet.currency ) this.g.fiatBalance = (this.g.exchangeRate / 100000000) * this.g.wallet.sat } }, 'g.wallet'() { if (this.g.wallet.currency) { this.g.fiatTracking = true this.g.fiatBalance = (this.g.exchangeRate / 100000000) * this.g.wallet.sat } else { this.g.fiatBalance = 0 this.g.fiatTracking = false } }, 'g.isFiatPriority'() { this.receive.unit = this.g.isFiatPriority ? this.g.wallet.currency : 'sat' }, 'g.fiatBalance'() { this.formattedFiatAmount = LNbits.utils.formatCurrency( this.g.fiatBalance.toFixed(2), this.g.wallet.currency ) }, 'g.exchangeRate'() { if (this.g.fiatTracking && this.g.wallet.currency) { this.g.fiatBalance = (this.g.exchangeRate / 100000000) * this.g.wallet.sat } } } }