diff --git a/.gitignore b/.gitignore index 79ff36abf..c97313120 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,4 @@ docker fly.toml # Ignore extensions (post installable extension PR) -extensions/ upgrades/ \ No newline at end of file diff --git a/lnbits/extensions/cashu/templates/cashu/wallet.html b/lnbits/extensions/cashu/templates/cashu/wallet.html index d79c030ae..5c57ea838 100644 --- a/lnbits/extensions/cashu/templates/cashu/wallet.html +++ b/lnbits/extensions/cashu/templates/cashu/wallet.html @@ -1,64 +1,37 @@ -{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu {% endraw -%} - {{mint_name}} {% endblock %} {% block footer %}{% endblock %} {% block +{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu wallet {% +endraw %} {% endblock %} {% block footer %}{% endblock %} {% block page_container %}
-
+
- -
-
-
- Get invoice - -
-
-

-
- {% raw %} {{getBalance()}} - {{tickershort}}{% endraw %} -
-

-
-
- Pay invoice - -
-
-
-
+ +

- {% raw %} {{getBalance()}} - {{tickershort}}{% endraw %} + {% raw %} {{ getBalance() }} + {{tickerShort}} {% endraw %}

+
- +
Get EcashReceive Ecash
@@ -78,12 +53,14 @@ page_container %} size="12px" rectangle unelevated + align="between" color="primary" icon="file_upload" + icon-right="toll" class="full-width" @click="showSendTokensDialog" > - Pay Ecash
@@ -93,10 +70,91 @@ page_container %} /////////////////////////////////////////// --> - + + + + + +
+ + + + Cashu mints + You can use use multiple Cashu mints in this wallet. + Add the mint URL and select the mint you want to + use. + + + + +
+ {% raw %} + + + + + + {{mint.url}} + + + + + + + + + + + {% endraw %} + +
+
+
+
+
+ + + + + + + +
+
+
@@ -281,52 +339,92 @@ page_container %}
-
- - Warning - BackupDownload wallet backup +
+
+
+ Create invoice + +
+ +
+
+ + Download wallet backup +
+
+
+ Pay invoice + +
+
- - + - - - - - - + + + + + + + +
@@ -684,7 +782,7 @@ page_container %} @@ -720,7 +818,7 @@ page_container %} > Copy link
@@ -772,6 +870,36 @@ page_container %}
+ + +
Do you trust this mint?
+

+ A Cashu mint does not know about your financial activity but it + controls your funds. Make sure that you trust the operator of this + mint. +

+ + + +
+ Add mint + Cancel +
+
+
@@ -822,12 +950,17 @@ page_container %} mixins: [windowMixin], data: function () { return { - tickershort: '', + tickerShort: 'sats', + ticketLong: 'Satoshis', name: '', - mintId: '', mintName: '', + mintToAdd: 'https://8333.space:3338', + activeMintURL: '', + mints: [], keys: '', + proofs: [], + activeProofs: [], invoicesCashu: [], historyTokens: [], invoiceData: { @@ -844,12 +977,7 @@ page_container %} invoiceCheckListener: () => {}, payInvoiceData: { blocking: false, - // invoice: '', bolt11: '', - // camera: { - // show: false, - // camera: 'auto' - // } show: false, invoice: null, lnurlpay: null, @@ -880,8 +1008,7 @@ page_container %} showReceiveTokens: false, promises: [], tokens: [], - tab: 'invoices', - + tab: 'settings', receive: { show: false, status: 'pending', @@ -913,7 +1040,6 @@ page_container %} } }, payments: [], - tokensTable: { columns: [ { @@ -1050,16 +1176,16 @@ page_container %} location: window.location, base_url: location.protocol + '//' + location.host + location.pathname }, - + addMintDialog: { + show: false + }, + baseHost: location.protocol + '//' + location.host, + baseURL: location.protocol + '//' + location.host + location.pathname, credit: 0, newName: '' } }, computed: { - formattedBalance: function () { - return this.balance / 100 - }, - canPay: function () { if (!this.payInvoiceData.invoice) return false return this.payInvoiceData.invoice.sat <= this.balance @@ -1069,6 +1195,12 @@ page_container %} }, balance: function () { + return this.activeProofs + .map(t => t) + .flat() + .reduce((sum, el) => (sum += el.amount), 0) + }, + getTotalBalance: function () { return this.proofs .map(t => t) .flat() @@ -1081,14 +1213,58 @@ page_container %} } }, methods: { + addMint: function (url) { + var verbose = true + // we have no mints at all + if (this.mints.length == 0) { + this.mints = [{url: url, balance: 0}] + } else if (this.mints.filter(m => m.url == url).length == 0) { + // we don't have this mint yet + this.mints.push({url: url, balance: 0}) + } else { + verbose = false + } + localStorage.setItem('cashu.mints', JSON.stringify(this.mints)) + this.activateMint(url, verbose) + }, + removeMint: function (url) { + this.mints = this.mints.filter(m => m.url != url) + localStorage.setItem('cashu.mints', JSON.stringify(this.mints)) + this.activateMint(this.mints[0].url) + this.notifySuccess('Mint removed.') + }, getBalance: function () { - return this.proofs + var balance = this.activeProofs .map(t => t) .flat() .reduce((sum, el) => (sum += el.amount), 0) + if (this.mints.length > 0 && this.activeMintURL) { + this.mints.filter(m => m.url == this.activeMintURL)[0].balance = + balance + } + return balance + }, + activateMint: async function (url, verbose = false) { + let presiouvURL = this.activeMintURL + try { + this.activeMintURL = url + keys = await this.fetchMintKeys() + // load proofs + this.activeProofs = this.proofs.filter(p => + this.keysets.includes(p.id) + ) + + if (verbose) { + this.notifySuccess('Mint added.') + } + } catch (error) { + this.activeMintURL = presiouvURL + this.notifyError('Could not add mint.') + throw error + } }, getTokenList: function () { - const amounts = this.proofs.map(t => t.amount) + const amounts = this.activeProofs.map(t => t.amount) const counts = {} for (const num of amounts) { @@ -1162,6 +1338,7 @@ page_container %} clearInterval(this.payInvoiceData.paymentChecker) }, 10000) }, + onPaymentReceived: function (paymentHash) { this.fetchPayments() this.fetchBalance() @@ -1172,73 +1349,6 @@ page_container %} clearInterval(this.receive.paymentChecker) } }, - createInvoice: function () { - this.receive.status = 'loading' - if (LNBITS_DENOMINATION != 'sats') { - this.receive.data.amount = this.receive.data.amount * 100 - } - LNbits.api - .createInvoice( - this.receive.data.amount, - this.receive.data.memo, - this.receive.unit, - this.receive.lnurl && this.receive.lnurl.callback - ) - .then(response => { - this.receive.status = 'success' - this.receive.paymentReq = response.data.payment_request - this.receive.paymentHash = response.data.payment_hash - - if (response.data.lnurl_response !== null) { - if (response.data.lnurl_response === false) { - response.data.lnurl_response = `Unable to connect` - } - - if (typeof response.data.lnurl_response === 'string') { - // failure - this.$q.notify({ - timeout: 5000, - type: 'warning', - message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`, - caption: response.data.lnurl_response, - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) - return - } else if (response.data.lnurl_response === true) { - // success - this.$q.notify({ - timeout: 5000, - message: `Invoice sent to ${this.receive.lnurl.domain}!`, - spinner: true, - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) - } - } - - clearInterval(this.receive.paymentChecker) - setTimeout(() => { - clearInterval(this.receive.paymentChecker) - }, 40000) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - this.receive.status = 'pending' - }) - }, decodeQR: function (res) { this.camera.data = res // this.payInvoiceData.data.request = res @@ -1294,20 +1404,7 @@ page_container %} try { invoice = decode(this.payInvoiceData.data.request) } catch (error) { - this.$q.notify({ - timeout: 3000, - type: 'warning', - message: error + '.', - caption: 'Failed to decode invoice', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) + this.notifyWarning('Failed to decode invoice', null, 3000) this.payInvoiceData.show = false throw error return @@ -1435,14 +1532,6 @@ page_container %} this.invoiceCheckWorker() }, - // showPayInvoiceDialog: function () { - // console.log('### showPayInvoiceDialog') - // this.payInvoiceData.invoice = '' - // this.payInvoiceData.data.request = '' - // this.showPayInvoice = true - // this.camera.show = false - // }, - showTokenDialog: function (token) { console.log('##### showTokenDialog') // TODO: this must be decoded and desiarlized! @@ -1467,6 +1556,10 @@ page_container %} this.showReceiveTokens = true }, + showAddMintDialog: function () { + this.addMintDialog.show = true + }, + //////////////////////// MINT ////////////////////////////////////////// generateSecrets: async function (amounts) { @@ -1536,6 +1629,21 @@ page_container %} return this.proofs }, + serializeProofs: function (proofs) { + // unique keyset IDs of proofs + var uniqueIds = [...new Set(proofs.map(p => p.id))] + // mints that have any of the keyset IDs + var mints_keysets = this.mints.filter(m => + m.keysets.some(r => uniqueIds.indexOf(r) >= 0) + ) + // what we put into the JSON + var mints = mints_keysets.map(m => [{url: m.url, ids: m.keysets}][0]) + var token = { + proofs: proofs, + mints + } + return btoa(JSON.stringify(token)) + }, //////////// API /////////// // MINT @@ -1553,10 +1661,10 @@ page_container %} gets an invoice from the mint to get new tokens */ try { - const {data} = await LNbits.api.request( - 'GET', - `/cashu/api/v1/${this.mintId}/mint?amount=${this.invoiceData.amount}` + const {data} = await axios.get( + `${this.activeMintURL}/mint?amount=${this.invoiceData.amount}` ) + this.assertMintError(data) console.log('### data', data) this.invoiceData.bolt11 = data.pr @@ -1583,19 +1691,20 @@ page_container %} asks the mint to check whether the invoice with payment_hash has been paid and requests signing of the attached outputs. */ - console.log('### promises', payment_hash) + try { let secrets = await this.generateSecrets(amounts) let {outputs, rs} = await this.constructOutputs(amounts, secrets) - const promises = await LNbits.api.request( - 'POST', - `/cashu/api/v1/${this.mintId}/mint?payment_hash=${payment_hash}`, - '', + const promises = await axios.post( + `${this.activeMintURL}/mint?payment_hash=${payment_hash}`, { outputs } ) - console.log('### promises data', promises.data.promises) + this.assertMintError(promises.data) + if (promises.data.promises == null) { + return {} + } let proofs = await this.constructProofs( promises.data.promises, secrets, @@ -1618,6 +1727,8 @@ page_container %} throw 'could not mint' } this.proofs = this.proofs.concat(proofs) + // hack to update balance + this.activeProofs = this.activeProofs.concat([]) this.storeProofs() // update UI @@ -1690,12 +1801,11 @@ page_container %} outputs } - const {data} = await LNbits.api.request( - 'POST', - `/cashu/api/v1/${this.mintId}/split`, - '', + const {data} = await axios.post( + `${this.activeMintURL}/split`, payload ) + this.assertMintError(data) const frst_rs = rs.slice(0, frst_amounts.length) const frst_secrets = secrets.slice(0, frst_amounts.length) const scnd_rs = rs.slice(frst_amounts.length) @@ -1730,19 +1840,10 @@ page_container %} try { const spendableProofs = proofs.filter(p => !p.reserved) if (this.sumProofs(spendableProofs) < amount) { - this.$q.notify({ - timeout: 5000, - type: 'warning', - message: 'Balance too low', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) + this.notifyWarning( + 'Balance is too low.', + `Your balance is ${this.getBalance()} sat and you're tyring to pay ${amount} sats.` + ) throw Error('balance too low.') } @@ -1759,6 +1860,10 @@ page_container %} this.proofs[i].reserved = true } } + + // hack: to make Vue JS update + this.proofs = this.proofs.concat([]) + if (invlalidate) { // delete scndProofs from db this.deleteProofs(scndProofs) @@ -1782,15 +1887,40 @@ page_container %} if (this.receiveData.tokensBase64.length == 0) { throw new Error('no tokens provided.') } - const tokenJson = atob(this.receiveData.tokensBase64) - const proofs = JSON.parse(tokenJson) + const tokenJson = JSON.parse(atob(this.receiveData.tokensBase64)) + // v1 tokens: + var proofs = '' + if (tokenJson.proofs == null) { + proofs = tokenJson + } else { + proofs = tokenJson.proofs + // check if we have all mints + for (var i = 0; i < tokenJson.mints.length; i++) { + if ( + !this.mints.map(m => m.url).includes(tokenJson.mints[i].url) + ) { + this.addMint(tokenJson.mints[i].url) + } + } + + // TODO: We assume here that all proofs are from one mint! This will fail if + // that's not the case! + if (!tokenJson.mints[0].url == this.activeMintURL) { + this.activateMint(tokenJson.mints[0].url) + } + } + const amount = proofs.reduce((s, t) => (s += t.amount), 0) + + // redeem let {fristProofs, scndProofs} = await this.split(proofs, amount) // update UI // HACK: we need to do this so the balance updates this.proofs = this.proofs.concat([]) + this.activeProofs = this.activeProofs.concat([]) + this.getBalance() this.historyTokens.push({ status: 'paid', @@ -1801,19 +1931,20 @@ page_container %} this.storehistoryTokens() if (window.navigator.vibrate) navigator.vibrate(200) - this.$q.notify({ - timeout: 5000, - type: 'positive', - message: 'Tokens received', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) + this.notifySuccess('Tokens received.') + // this.$q.notify({ + // timeout: 5000, + // type: 'positive', + // message: 'Tokens received', + // position: 'top', + // actions: [ + // { + // icon: 'close', + // color: 'white', + // handler: () => {} + // } + // ] + // }) } catch (error) { console.error(error) LNbits.utils.notifyApiError(error) @@ -1829,7 +1960,7 @@ page_container %} try { // keep firstProofs, send scndProofs and delete them (invalidate=true) let {fristProofs, scndProofs} = await this.splitToSend( - this.proofs, + this.activeProofs, this.sendData.amount, true ) @@ -1837,9 +1968,7 @@ page_container %} // update UI this.sendData.tokens = scndProofs console.log('### this.sendData.tokens', this.sendData.tokens) - this.sendData.tokensBase64 = btoa( - JSON.stringify(this.sendData.tokens) - ) + this.sendData.tokensBase64 = this.serializeProofs(scndProofs) this.historyTokens.push({ status: 'pending', @@ -1854,7 +1983,6 @@ page_container %} this.checkTokenSpendableWorker() } catch (error) { console.error(error) - throw error } }, @@ -1877,7 +2005,7 @@ page_container %} ) let {fristProofs, scndProofs} = await this.splitToSend( - this.proofs, + this.activeProofs, amount ) const payload = { @@ -1885,28 +2013,24 @@ page_container %} amount, pr: this.payInvoiceData.data.request } - console.log('#### payload', JSON.stringify(payload)) try { - const {data} = await LNbits.api.request( - 'POST', - `/cashu/api/v1/${this.mintId}/melt`, - '', - payload - ) + const {data} = await axios.post(`${this.activeMintURL}/melt`, payload) + this.assertMintError(data) if (window.navigator.vibrate) navigator.vibrate(200) - this.$q.notify({ - timeout: 5000, - type: 'positive', - message: 'Invoice paid', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) + this.notifySuccess('Token paid.') + // this.$q.notify({ + // timeout: 5000, + // type: 'positive', + // message: 'Invoice paid', + // position: 'top', + // actions: [ + // { + // icon: 'close', + // color: 'white', + // handler: () => {} + // } + // ] + // }) // delete spent tokens from db this.deleteProofs(scndProofs) @@ -1965,15 +2089,12 @@ page_container %} return {secret: p.secret} }) } - console.log('#### payload', JSON.stringify(payload)) try { - const {data} = await LNbits.api.request( - 'POST', - `/cashu/api/v1/${this.mintId}/check`, - '', + const {data} = await axios.post( + `${this.activeMintURL}/check`, payload ) - + this.assertMintError(data) // delete proofs from database if it is spent let spentProofs = proofs.filter((p, pidx) => !data.spendable[pidx]) if (spentProofs.length) { @@ -2006,14 +2127,12 @@ page_container %} const payload = { pr: payment_request } - console.log('#### payload', JSON.stringify(payload)) try { - const {data} = await LNbits.api.request( - 'POST', - `/cashu/api/v1/${this.mintId}/checkfees`, - '', + const {data} = await axios.post( + `${this.activeMintURL}/checkfees`, payload ) + this.assertMintError(data) console.log('#### checkFees', payment_request, data.fee) return data.fee } catch (error) { @@ -2026,15 +2145,48 @@ page_container %} // /keys fetchMintKeys: async function () { - const {data} = await LNbits.api.request( - 'GET', - `/cashu/api/v1/${this.mintId}/keys` - ) - this.keys = data - localStorage.setItem( - this.mintKey(this.mintId, 'keys'), - JSON.stringify(data) - ) + try { + const {data} = await axios.get(`${this.activeMintURL}/keys`, { + timeout: 2000 + }) + var keys = data + this.assertMintError(keys) + this.keys = keys + localStorage.setItem('cashu.keys', JSON.stringify(keys)) + keysets = await this.fetchMintKeysets() + // save keys to mints in local storage + if (this.mints.filter(m => m.url == this.activeMintURL).length) { + this.mints.filter(m => m.url == this.activeMintURL)[0].keys = keys + this.mints.filter(m => m.url == this.activeMintURL)[0].keysets = + keysets + localStorage.setItem('cashu.activeMintURL', this.activeMintURL) + localStorage.setItem('cashu.mints', JSON.stringify(this.mints)) + } + + return keys + } catch (error) { + console.error(error) + LNbits.utils.notifyApiError(error) + throw error + } + }, + + // /keysets + + fetchMintKeysets: async function () { + try { + const {data} = await axios.get(`${this.activeMintURL}/keysets`, { + timeout: 2000 + }) + this.assertMintError(data) + this.keysets = data.keysets + localStorage.setItem('cashu.keysets', JSON.stringify(data.keysets)) + return data.keysets + } catch (error) { + console.error(error) + LNbits.utils.notifyApiError(error) + throw error + } }, ////////////// UI HELPERS ////////////// @@ -2058,7 +2210,12 @@ page_container %} checkPendingInvoices: async function () { for (const invoice of this.invoicesCashu) { if (invoice.status === 'pending' && invoice.amount > 0) { - this.checkInvoice(invoice.hash, false) + try { + await this.checkInvoice(invoice.hash, false) + } catch (error) { + console.log(`${invoice.hash} still pending`) + throw error + } } } }, @@ -2082,7 +2239,7 @@ page_container %} if it is spent, the appropraite entry in the history table is set to paid. */ const tokenJson = atob(token) - const proofs = JSON.parse(tokenJson) + const proofs = JSON.parse(tokenJson).proofs const spendable = await this.checkProofsSpendable(proofs) let paid = false if (spendable.includes(false)) { @@ -2090,37 +2247,38 @@ page_container %} paid = true } if (paid) { - console.log('### token paid') if (window.navigator.vibrate) navigator.vibrate(200) - this.$q.notify({ - timeout: 5000, - type: 'positive', - message: 'Token paid', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) + this.notifySuccess('Token paid.') + // this.$q.notify({ + // timeout: 5000, + // type: 'positive', + // message: 'Token paid', + // position: 'top', + // actions: [ + // { + // icon: 'close', + // color: 'white', + // handler: () => {} + // } + // ] + // }) } else { console.log('### token not paid yet') if (verbose) { - this.$q.notify({ - timeout: 5000, - color: 'grey', - message: 'Token still pending', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) + this.notify('Token still pending', (color = 'grey')) + // this.$q.notify({ + // timeout: 5000, + // color: 'grey', + // message: 'Token still pending', + // position: 'top', + // actions: [ + // { + // icon: 'close', + // color: 'white', + // handler: () => {} + // } + // ] + // }) } this.sendData.tokens = token } @@ -2129,7 +2287,7 @@ page_container %} findTokenForAmount: function (amount) { // unused coin selection - for (const token of this.proofs) { + for (const token of this.activeProofs) { const index = token.promises?.findIndex(p => p.amount === amount) if (index >= 0) { return { @@ -2175,19 +2333,7 @@ page_container %} this.invoiceData.bolt11 = '' this.showInvoiceDetails = false if (window.navigator.vibrate) navigator.vibrate(200) - this.$q.notify({ - timeout: 5000, - type: 'positive', - message: 'Payment received', - position: 'top', - actions: [ - { - icon: 'close', - color: 'white', - handler: () => {} - } - ] - }) + this.notifySuccess('Payment received', 'top') } catch (error) { console.log('not paid yet') } @@ -2229,6 +2375,84 @@ page_container %} //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////// UI HELPERS ///////////// + assertMintError: function (response) { + if (response.error != null) { + this.notifyError(`Mint error: ${response.error}`) + throw new Error(`Mint error: ${response.error}`) + } + }, + notifySuccess: async function (message, position = 'top') { + this.$q.notify({ + timeout: 5000, + type: 'positive', + message: message, + position: position, + actions: [ + { + icon: 'close', + color: 'white', + handler: () => {} + } + ] + }) + }, + notifyError: async function (message, caption = null) { + this.$q.notify({ + color: 'red', + message: message, + caption: caption, + position: 'top', + actions: [ + { + icon: 'close', + color: 'white', + handler: () => {} + } + ] + }) + }, + notifyWarning: async function (message, caption = null, timeout = 5000) { + this.$q.notify({ + timeout: timeout, + type: 'warning', + message: message, + caption: caption, + position: 'top', + actions: [ + { + icon: 'close', + color: 'white', + handler: () => {} + } + ] + }) + }, + notify: async function ( + message, + type = 'null', + position = 'top', + caption = null, + color = null + ) { + // failure + this.$q.notify({ + timeout: 5000, + type: type, + color: color, + message: message, + caption: caption, + position: position, + actions: [ + { + icon: 'close', + color: 'white', + handler: () => {} + } + ] + }) + }, + ////////////// STORAGE ///////////// getLocalstorageToFile: async function () { @@ -2260,23 +2484,43 @@ page_container %} storeinvoicesCashu: function () { localStorage.setItem( - this.mintKey(this.mintId, 'invoicesCashu'), + 'cashu.invoicesCashu', JSON.stringify(this.invoicesCashu) ) }, storehistoryTokens: function () { localStorage.setItem( - this.mintKey(this.mintId, 'historyTokens'), + 'cashu.historyTokens', JSON.stringify(this.historyTokens) ) }, storeProofs: function () { localStorage.setItem( - this.mintKey(this.mintId, 'proofs'), + 'cashu.proofs', JSON.stringify(this.proofs, bigIntStringify) ) }, - + migrationLocalstorage: function () { + // migration from old db to multimint + for (var key in localStorage) { + match = key.match('cashu.(.+).proofs') + if (match != null) { + console.log('Migrating mint', match[1]) + let mint_id = match[1] + const old_proofs = JSON.parse( + localStorage.getItem(`cashu.${mint_id}.proofs`) + ) + if (old_proofs) { + this.proofs = this.proofs.concat(old_proofs) + this.storeProofs() + let mint_url = this.baseHost + `/cashu/api/v1/${mint_id}` + console.log('Adding mint', mint_url) + this.addMint(mint_url) + localStorage.removeItem(`cashu.${mint_id}.proofs`) + } + } + } + }, mintKey: function (mintId, key) { // returns a key for the local storage // depending on the current mint @@ -2284,83 +2528,105 @@ page_container %} } }, watch: { - payments: function () { - this.balance() + // payments: function () { + // this.getBalance() + // }, + proofs: function () { + if (this.keysets) { + this.activeProofs = this.proofs.filter(p => + this.keysets.includes(p.id) + ) + } } }, mounted: function () {}, - created: function () { + created: async function () { let params = new URL(document.location).searchParams - // get mint + // load mints + if (localStorage.getItem('cashu.mints')) { + this.mints = JSON.parse(localStorage.getItem('cashu.mints')) + } + + // mint url if (params.get('mint_id')) { this.mintId = params.get('mint_id') - this.$q.localStorage.set('cashu.mint', params.get('mint_id')) - } else if (this.$q.localStorage.getItem('cashu.mint')) { - this.mintId = this.$q.localStorage.getItem('cashu.mint') - } else { - this.$q.notify({ - color: 'red', - message: 'No mint set!', - position: 'center' - }) + // works with only lnbits mints + activeMintURL = + location.protocol + + '//' + + location.host + + `/cashu/api/v1/${this.mintId}` + this.walletURL = this.baseURL + '?mint_id=' + this.mintId + this.addMint(activeMintURL) } + if (localStorage.getItem('cashu.activeMintURL')) { + if (!this.activeMintURL) { + this.walletURL = this.baseURL + } + activeMintURL = localStorage.getItem('cashu.activeMintURL') + this.addMint(activeMintURL) + } + if (!this.activeMintURL) { + this.walletURL = this.baseURL + this.notifyWarning( + 'You are not connected to any mints yet.', + 'Add a new mint URL in the settings.', + 20000 + ) + this.tab = 'settings' + } + + // todo: remove: + if (!this.mintId.length) { + this.mintId = 'dummy' + } + + console.log('Mint URL ' + this.activeMintURL) + console.log('Wallet URL ' + this.walletURL) // get name if (params.get('mint_name')) { this.mintName = params.get('mint_name') - this.$q.localStorage.set( - this.mintKey(this.mintId, 'mintName'), - this.mintName - ) - } else if (this.$q.localStorage.getItem('cashu.name')) { - this.mintName = this.$q.localStorage.getItem('cashu.name') } - // get ticker - if ( - !params.get('tsh') && - !this.$q.localStorage.getItem(this.mintKey(this.mintId, 'tickershort')) - ) { - this.$q.localStorage.set( - this.mintKey(this.mintId, 'tickershort'), - 'sats' - ) - this.tickershort = 'sats' - } else if (params.get('tsh')) { - this.$q.localStorage.set( - this.mintKey(this.mintId, 'tickershort'), - params.get('tsh') - ) - this.tickershort = params.get('tsh') - } else if ( - this.$q.localStorage.getItem(this.mintKey(this.mintId, 'tickershort')) - ) { - this.tickershort = this.$q.localStorage.getItem( - this.mintKey(this.mintId, 'tickershort') - ) + if (this.activeMintURL.length) { + await this.fetchMintKeys() } - const keysJson = localStorage.getItem(this.mintKey(this.mintId, 'keys')) - if (!keysJson) { - this.fetchMintKeys() - } else { - this.keys = JSON.parse(keysJson) - } + // const keysJson = localStorage.getItem(this.mintKey(this.mintId, 'keys')) + // if (!keysJson) { + // if (this.activeMintURL.length) { + // this.fetchMintKeys() + // } + // } else { + // this.keys = JSON.parse(keysJson) + // } + + // this.invoicesCashu = JSON.parse( + // localStorage.getItem(this.mintKey(this.mintId, 'invoicesCashu')) || '[]' + // ) + + // this.historyTokens = JSON.parse( + // localStorage.getItem(this.mintKey(this.mintId, 'historyTokens')) || '[]' + // ) + // this.proofs = JSON.parse( + // localStorage.getItem(this.mintKey(this.mintId, 'proofs')) || '[]' + // ) this.invoicesCashu = JSON.parse( - localStorage.getItem(this.mintKey(this.mintId, 'invoicesCashu')) || '[]' + localStorage.getItem('cashu.invoicesCashu') || '[]' ) this.historyTokens = JSON.parse( - localStorage.getItem(this.mintKey(this.mintId, 'historyTokens')) || '[]' + localStorage.getItem('cashu.historyTokens') || '[]' ) + this.proofs = JSON.parse(localStorage.getItem('cashu.proofs') || '[]') - this.proofs = JSON.parse( - localStorage.getItem(this.mintKey(this.mintId, 'proofs')) || '[]' - ) + // run migrations + this.migrationLocalstorage() // get recv_token to receive tokens from a link if (params.get('recv_token')) { @@ -2386,14 +2652,16 @@ page_container %} this.payInvoiceData.data.request = params.get('lightning') } - console.log('### invoicesCashu', this.invoicesCashu) - console.table('### tokens', this.proofs) - console.log('#### this.mintId', this.mintId) - console.log('#### this.mintName', this.mintName) - - this.checkProofsSpendable(this.proofs, true) - this.checkPendingInvoices() - this.checkPendingTokens() + // startup tasks + this.checkProofsSpendable(this.activeProofs, true).catch(err => { + return + }) + this.checkPendingInvoices().catch(err => { + return + }) + this.checkPendingTokens().catch(err => { + return + }) } }) diff --git a/lnbits/extensions/cashu/views.py b/lnbits/extensions/cashu/views.py index 85adf040a..d11329895 100644 --- a/lnbits/extensions/cashu/views.py +++ b/lnbits/extensions/cashu/views.py @@ -1,4 +1,5 @@ from http import HTTPStatus +from typing import Optional from fastapi import Depends, Request from fastapi.templating import Jinja2Templates @@ -25,18 +26,25 @@ async def index( @cashu_ext.get("/wallet") -async def wallet(request: Request, mint_id: str): - cashu = await get_cashu(mint_id) - if not cashu: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." - ) +async def wallet(request: Request, mint_id: Optional[str] = None): + if mint_id is not None: + cashu = await get_cashu(mint_id) + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + manifest_url = f"/cashu/manifest/{mint_id}.webmanifest" + mint_name = cashu.name + else: + manifest_url = "/cashu/cashu.webmanifest" + mint_name = "Cashu mint" + return cashu_renderer().TemplateResponse( "cashu/wallet.html", { "request": request, - "web_manifest": f"/cashu/manifest/{mint_id}.webmanifest", - "mint_name": cashu.name, + "web_manifest": manifest_url, + "mint_name": mint_name, }, ) @@ -50,177 +58,193 @@ async def cashu(request: Request, mintID): ) return cashu_renderer().TemplateResponse( "cashu/mint.html", - {"request": request, "mint_name": cashu.name, "mint_id": mintID}, + {"request": request, "mint_id": mintID}, ) @cashu_ext.get("/manifest/{cashu_id}.webmanifest") -async def manifest(cashu_id: str): +async def manifest_lnbits(cashu_id: str): cashu = await get_cashu(cashu_id) if not cashu: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." ) + return get_manifest(cashu_id, cashu.name) + + +@cashu_ext.get("/cashu.webmanifest") +async def manifest(): + return get_manifest() + + +def get_manifest(mint_id: Optional[str] = None, mint_name: Optional[str] = None): + manifest_name = "Cashu" + if mint_name: + manifest_name += " - " + mint_name + manifest_url = "/cashu/wallet" + if mint_id: + manifest_url += "?mint_id=" + mint_id + return { "short_name": "Cashu", - "name": "Cashu" + " - " + cashu.name, + "name": manifest_name, "icons": [ { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512-512.png", "type": "image/png", "sizes": "512x512", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-96-96.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/96-96.png", "type": "image/png", "sizes": "96x96", }, ], - "id": "/cashu/wallet?mint_id=" + cashu_id, - "start_url": "/cashu/wallet?mint_id=" + cashu_id, + "id": manifest_url, + "start_url": manifest_url, "background_color": "#1F2234", "description": "Cashu ecash wallet", "display": "standalone", "scope": "/cashu/", "theme_color": "#1F2234", "protocol_handlers": [ - {"protocol": "cashu", "url": "&recv_token=%s"}, - {"protocol": "lightning", "url": "&lightning=%s"}, + {"protocol": "web+cashu", "url": "&recv_token=%s"}, + {"protocol": "web+lightning", "url": "&lightning=%s"}, ], "shortcuts": [ { - "name": "Cashu" + " - " + cashu.name, + "name": manifest_name, "short_name": "Cashu", - "description": "Cashu" + " - " + cashu.name, - "url": "/cashu/wallet?mint_id=" + cashu_id, + "description": manifest_name, + "url": manifest_url, "icons": [ { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png", "sizes": "512x512", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-192-192.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/192x192.png", "sizes": "192x192", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-144-144.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/144x144.png", "sizes": "144x144", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-96-96.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/96x96.png", "sizes": "96x96", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-72-72.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/72x72.png", "sizes": "72x72", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-48-48.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/48x48.png", "sizes": "48x48", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/16.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/16x16.png", "sizes": "16x16", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/20.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/20x20.png", "sizes": "20x20", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/29.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/29x29.png", "sizes": "29x29", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/32.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/32x32.png", "sizes": "32x32", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/40.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/40x40.png", "sizes": "40x40", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/50.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/50x50.png", "sizes": "50x50", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/57.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/57x57.png", "sizes": "57x57", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/58.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/58x58.png", "sizes": "58x58", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/60.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/60x60.png", "sizes": "60x60", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/64.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/64x64.png", "sizes": "64x64", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/72.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/72x72.png", "sizes": "72x72", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/76.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/76x76.png", "sizes": "76x76", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/80.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/80x80.png", "sizes": "80x80", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/87.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/87x87.png", "sizes": "87x87", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/100.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/100x100.png", "sizes": "100x100", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/114.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/114x114.png", "sizes": "114x114", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/120.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/120x120.png", "sizes": "120x120", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/128.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/128x128.png", "sizes": "128x128", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/144.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/144x144.png", "sizes": "144x144", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/152.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/152x152.png", "sizes": "152x152", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/167.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/167x167.png", "sizes": "167x167", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/180.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/180x180.png", "sizes": "180x180", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/192.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/192x192.png", "sizes": "192x192", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/256.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/256x256.png", "sizes": "256x256", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/512.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png", "sizes": "512x512", }, { - "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/1024.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/1024x1024.png", "sizes": "1024x1024", }, ],