1505 lines
47 KiB
HTML
1505 lines
47 KiB
HTML
{% extends "public.html" %} {% block toolbar_title %} {% raw %} {{name}} Cashu
|
|
wallet {% endraw %} {% endblock %} {% block footer %}{% endblock %} {% block
|
|
page_container %}
|
|
<q-page-container>
|
|
<q-page>
|
|
<div class="row q-col-gutter-md justify-center q-pt-lg">
|
|
<div class="col-12 col-sm-8 col-md-9 col-lg-7 text-center q-gutter-y-md">
|
|
<q-card>
|
|
<q-card-section>
|
|
<div class="row">
|
|
<div class="col-3">
|
|
<q-btn
|
|
size="18px"
|
|
rounded
|
|
color="secondary"
|
|
class="full-width"
|
|
@click="showInvoicesDialog"
|
|
>Create invoice
|
|
</q-btn>
|
|
</div>
|
|
<div class="col-6">
|
|
<h3 class="q-my-none">
|
|
<center>
|
|
<strong>{% raw %} {{balance}} </strong>
|
|
{{tickershort}}{% endraw %}
|
|
</center>
|
|
</h3>
|
|
</div>
|
|
<div class="col-3">
|
|
<q-btn
|
|
@click="showPayInvoiceDialog"
|
|
size="18px"
|
|
rounded
|
|
color="secondary"
|
|
class="full-width"
|
|
>Pay invoice
|
|
</q-btn>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card>
|
|
<q-card-section>
|
|
<div class="row items-center no-wrap q-mb-sm">
|
|
<div class="col-sm-5 col-md-2">
|
|
<q-btn
|
|
size="18px"
|
|
icon="arrow_downward"
|
|
rounded
|
|
color="primary"
|
|
class="full-width"
|
|
@click="showReceiveTokensDialog"
|
|
>Receive</q-btn
|
|
>
|
|
</div>
|
|
<div class="col-sm-2 col-md-8"></div>
|
|
<div class="col-sm-5 col-md-2">
|
|
<q-btn
|
|
size="18px"
|
|
icon="arrow_upward"
|
|
rounded
|
|
color="primary"
|
|
class="full-width"
|
|
@click="showSendTokensDialog"
|
|
>
|
|
Send</q-btn
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<q-tabs v-model="tab" no-caps class="bg-dark text-white shadow-2">
|
|
<q-tab name="tokens" label="Tokens"></q-tab>
|
|
<q-tab name="invoices" label="Invoices"></q-tab>
|
|
<q-tab name="history" label="History"></q-tab>
|
|
</q-tabs>
|
|
<q-tab-panels v-model="tab">
|
|
<q-tab-panel name="tokens">
|
|
<q-table
|
|
dense
|
|
flat
|
|
:data="tokenList"
|
|
:columns="tokensTable.columns"
|
|
:pagination.sync="tokensTable.pagination"
|
|
no-data-label="No tokens made yet"
|
|
:filter="tokensTable.filter"
|
|
>
|
|
{% raw %}
|
|
<template v-slot:body="props">
|
|
<q-tr :props="props">
|
|
<q-td
|
|
key="denomination"
|
|
:props="props"
|
|
:class="props.row.denomination > 0 ? 'text-green-13 text-weight-bold' : ''"
|
|
>
|
|
<div>{{props.row.denomination}}</div>
|
|
</q-td>
|
|
<q-td key="count" :props="props">
|
|
<div>{{props.row.count}}</div>
|
|
</q-td>
|
|
<q-td key="value" :props="props">
|
|
<div>{{props.row.value}}</div>
|
|
</q-td>
|
|
<q-td key="memo" :props="props">
|
|
<div>{{props.row.memo}}</div>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
{% endraw %}
|
|
</q-table>
|
|
</q-tab-panel>
|
|
<q-tab-panel name="invoices">
|
|
<q-table
|
|
dense
|
|
flat
|
|
:data="invoicesCashu"
|
|
:columns="invoicesTable.columns"
|
|
:pagination.sync="invoicesTable.pagination"
|
|
no-data-label="No invoices made yet"
|
|
:filter="invoicesTable.filter"
|
|
>
|
|
{% raw %}
|
|
<template v-slot:body="props">
|
|
<q-tr :props="props">
|
|
<q-td key="status" :props="props">
|
|
<div v-if="props.row.status == 'pending'">
|
|
<q-icon
|
|
@click="showInvoiceDialog(props.row)"
|
|
name="settings_ethernet"
|
|
color="grey"
|
|
>
|
|
<q-tooltip>Pending</q-tooltip>
|
|
</q-icon>
|
|
<q-badge
|
|
size="lg"
|
|
color="secondary"
|
|
class="q-mr-md cursor-pointer"
|
|
@click="recheckInvoice(props.row.hash)"
|
|
>
|
|
Recheck
|
|
</q-badge>
|
|
</div>
|
|
<div v-if="props.row.status === 'paid'">
|
|
<q-icon name="call_received" color="green"></q-icon>
|
|
</div>
|
|
</q-td>
|
|
<q-td
|
|
key="amount"
|
|
:props="props"
|
|
:class="props.row.amount > 0 ? 'text-green-13 text-weight-bold' : ''"
|
|
>
|
|
<div>{{props.row.amount}}</div>
|
|
</q-td>
|
|
|
|
<q-td key="memo" :props="props">
|
|
<div>{{props.row.memo}}</div>
|
|
</q-td>
|
|
<q-td key="date" :props="props">
|
|
<div>{{props.row.date}}</div>
|
|
</q-td>
|
|
<q-td key="hash" :props="props">
|
|
<div>{{props.row.hash}}</div>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
{% endraw %}
|
|
</q-table>
|
|
</q-tab-panel>
|
|
|
|
<q-tab-panel name="history">
|
|
<span>History</span>
|
|
</q-tab-panel>
|
|
</q-tab-panels>
|
|
</q-card-section>
|
|
</q-card>
|
|
</div>
|
|
|
|
<q-tabs
|
|
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-top"
|
|
active-class="px-0"
|
|
indicator-color="transparent"
|
|
>
|
|
<q-tab
|
|
icon="arrow_right"
|
|
label="Create Invoice"
|
|
@click="showInvoicesDialog"
|
|
>
|
|
</q-tab>
|
|
<q-tab icon="arrow_downward" label="Receive Tokens"></q-tab>
|
|
<q-tab icon="arrow_upward" label="Send Token"></q-tab>
|
|
<q-tab icon="arrow_right" label="Pay Invoice"> </q-tab>
|
|
</q-tabs>
|
|
|
|
<q-dialog v-model="disclaimerDialog.show">
|
|
<q-card class="q-pa-lg">
|
|
<h6 class="q-my-md text-primary">Warning</h6>
|
|
<p>
|
|
<strong
|
|
>BOOKMARK THIS PAGE! If only mobile you can also click the 3 dots
|
|
and "Save to homescreen"/"Install app"</strong
|
|
>!
|
|
</p>
|
|
<p>
|
|
Ecash is a bearer asset, meaning you have the funds saved on this
|
|
page, losing the page without exporting the page will mean you will
|
|
lose the funds.
|
|
</p>
|
|
<div class="row q-mt-lg">
|
|
<q-btn outline color="grey">Copy wallet URL</q-btn>
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>I understand</q-btn
|
|
>
|
|
</div>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="showInvoiceDetails" position="top">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<div v-if="!invoiceData.bolt11">
|
|
<div class="row items-center no-wrap q-mb-sm">
|
|
<div class="col-12">
|
|
<span class="text-subtitle1">Create a Lightning invoice</span>
|
|
</div>
|
|
</div>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.number="invoiceData.amount"
|
|
label="Amount ({{LNBITS_DENOMINATION}}) *"
|
|
mask="#.##"
|
|
fill-mask="0"
|
|
reverse-fill-mask
|
|
type="number"
|
|
class="q-mb-lg"
|
|
></q-input>
|
|
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="invoiceData.memo"
|
|
label="Memo"
|
|
></q-input>
|
|
</div>
|
|
<div v-else class="text-center q-mb-lg">
|
|
<a :href="'lightning:' + invoiceData.bolt11">
|
|
<q-responsive :ratio="1" class="q-mx-xl">
|
|
<qrcode
|
|
:value="invoiceData.bolt11"
|
|
:options="{width: 340}"
|
|
class="rounded-borders"
|
|
>
|
|
</qrcode>
|
|
</q-responsive>
|
|
</a>
|
|
</div>
|
|
<div class="row q-mt-lg">
|
|
<q-btn
|
|
v-if="invoiceData.bolt11"
|
|
@click="copyText(invoiceData.bolt11)"
|
|
outline
|
|
color="grey"
|
|
>Copy invoice</q-btn
|
|
>
|
|
<q-btn v-else outline color="grey" @click="requestMint"
|
|
>Request Invoice</q-btn
|
|
>
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Close</q-btn
|
|
>
|
|
</div>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="showSendTokens" position="top">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<div v-if="!sendData.tokens">
|
|
<div class="row items-center no-wrap q-mb-sm">
|
|
<div class="col-12">
|
|
<span class="text-subtitle1"
|
|
>How much would you like to send?</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.number="sendData.amount"
|
|
label="Amount (tokens) *"
|
|
type="number"
|
|
class="q-mb-lg"
|
|
></q-input>
|
|
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model.trim="sendData.memo"
|
|
label="Memo"
|
|
></q-input>
|
|
</div>
|
|
<div v-else class="text-center q-mb-lg">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model="sendData.tokensBase64"
|
|
label="tokens"
|
|
type="textarea"
|
|
class="q-mb-lg"
|
|
></q-input>
|
|
</div>
|
|
<div class="row q-mt-lg">
|
|
<q-btn
|
|
v-if="!sendData.tokens"
|
|
@click="sendTokens"
|
|
outline
|
|
color="grey"
|
|
>Send Tokens</q-btn
|
|
>
|
|
<q-btn v-else @click="burnTokens" outline color="grey"
|
|
>Burn Tokens</q-btn
|
|
>
|
|
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Close</q-btn
|
|
>
|
|
</div>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="showReceiveTokens" position="top">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<div>
|
|
<div class="row items-center no-wrap q-mb-sm">
|
|
<div class="col-12">
|
|
<span class="text-subtitle1">Paste tokens please</span>
|
|
</div>
|
|
</div>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model="receiveData.tokensBase64"
|
|
label="tokens"
|
|
type="textarea"
|
|
class="q-mb-lg"
|
|
></q-input>
|
|
</div>
|
|
|
|
<div class="row q-mt-lg">
|
|
<q-btn @click="receiveTokens" outline color="grey"
|
|
>Receive Tokens</q-btn
|
|
>
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Close</q-btn
|
|
>
|
|
</div>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="showPayInvoice" position="top">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<div v-if="!payInvoiceData.invoice">
|
|
<div class="row items-center no-wrap q-mb-sm">
|
|
<div class="col-12">
|
|
<span class="text-subtitle1"
|
|
>Please paste a Lightning invoice</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model="payInvoiceData.bolt11"
|
|
label="Paste invoice"
|
|
type="textarea"
|
|
class="q-mb-lg"
|
|
></q-input>
|
|
</div>
|
|
<div v-else class="q-mb-lg">
|
|
{% raw %}
|
|
<strong>Amount:</strong> {{ payInvoiceData.invoice.sat }}
|
|
<strong>sats</strong><br />
|
|
<strong>Description:</strong> {{ payInvoiceData.invoice.description
|
|
}}<br />
|
|
<strong>Expire date:</strong> {{ payInvoiceData.invoice.expireDate
|
|
}}<br />
|
|
<strong>Expired:</strong> {{ payInvoiceData.invoice.expired }}<br />
|
|
<strong>Hash:</strong> {{ payInvoiceData.invoice.hash }} {% endraw
|
|
%}
|
|
</div>
|
|
<div class="row q-mt-lg">
|
|
<q-btn
|
|
v-if="!payInvoiceData.invoice"
|
|
@click="checkInvoice"
|
|
outline
|
|
color="grey"
|
|
>Check Invoice</q-btn
|
|
>
|
|
<q-btn v-else outline color="grey" @click="melt">Pay invoice</q-btn>
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Close</q-btn
|
|
>
|
|
</div>
|
|
</q-card>
|
|
</q-dialog>
|
|
</div>
|
|
</q-page>
|
|
</q-page-container>
|
|
{% endblock %} {% block styles %}
|
|
<style>
|
|
* {
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
.keypad {
|
|
display: grid;
|
|
grid-gap: 8px;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
grid-template-rows: repeat(4, 1fr);
|
|
}
|
|
|
|
.keypad .btn {
|
|
height: 100%;
|
|
}
|
|
|
|
.keypad .btn-confirm {
|
|
grid-area: 1 / 4 / 5 / 4;
|
|
}
|
|
</style>
|
|
{% endblock %} {% block scripts %}
|
|
<script src="{{ url_for('cashu_static', path='js/noble-secp256k1.js') }}"></script>
|
|
<script src="{{ url_for('cashu_static', path='js/utils.js') }}"></script>
|
|
<script src="{{ url_for('cashu_static', path='js/dhke.js') }}"></script>
|
|
<script src="{{ url_for('cashu_static', path='js/base64.js') }}"></script>
|
|
<script>
|
|
var currentDateStr = function () {
|
|
return Quasar.utils.date.formatDate(new Date(), 'YYYY-MM-DD HH:mm')
|
|
}
|
|
var mapMint = function (obj) {
|
|
obj.date = Quasar.utils.date.formatDate(
|
|
new Date(obj.time * 1000),
|
|
'YYYY-MM-DD HH:mm'
|
|
)
|
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
|
obj.cashu = ['/cashu/', obj.id].join('')
|
|
return obj
|
|
}
|
|
|
|
Vue.component(VueQrcode.name, VueQrcode)
|
|
|
|
new Vue({
|
|
el: '#vue',
|
|
mixins: [windowMixin],
|
|
data: function () {
|
|
return {
|
|
tickershort: '',
|
|
name: '',
|
|
|
|
mintId: '',
|
|
mintName: '',
|
|
keys: '',
|
|
invoicesCashu: [],
|
|
invoiceData: {
|
|
amount: 0,
|
|
memo: '',
|
|
bolt11: '',
|
|
hash: ''
|
|
},
|
|
payInvoiceData: {
|
|
invoice: '',
|
|
bolt11: ''
|
|
},
|
|
sendData: {
|
|
amount: 0,
|
|
memo: '',
|
|
tokens: '',
|
|
tokensBase64: ''
|
|
},
|
|
receiveData: {
|
|
tokensBase64: ''
|
|
},
|
|
showInvoiceDetails: false,
|
|
showPayInvoice: false,
|
|
showSendTokens: false,
|
|
showReceiveTokens: false,
|
|
promises: [],
|
|
tokens: [],
|
|
tab: 'tokens',
|
|
|
|
receive: {
|
|
show: false,
|
|
status: 'pending',
|
|
paymentReq: null,
|
|
paymentHash: null,
|
|
minMax: [0, 2100000000000000],
|
|
lnurl: null,
|
|
units: ['sat'],
|
|
unit: 'sat',
|
|
data: {
|
|
amount: null,
|
|
memo: ''
|
|
}
|
|
},
|
|
parse: {
|
|
show: false,
|
|
invoice: null,
|
|
lnurlpay: null,
|
|
lnurlauth: null,
|
|
data: {
|
|
request: '',
|
|
amount: 0,
|
|
comment: ''
|
|
},
|
|
paymentChecker: null,
|
|
camera: {
|
|
show: false,
|
|
camera: 'auto'
|
|
}
|
|
},
|
|
payments: [],
|
|
invoicesTable: {
|
|
columns: [
|
|
{
|
|
name: 'status',
|
|
align: 'left',
|
|
label: '',
|
|
field: 'status'
|
|
},
|
|
{
|
|
name: 'amount',
|
|
align: 'left',
|
|
label: 'Amount',
|
|
field: 'amount'
|
|
},
|
|
{
|
|
name: 'memo',
|
|
align: 'left',
|
|
label: 'Memo',
|
|
field: 'memo',
|
|
sortable: true
|
|
},
|
|
{
|
|
name: 'date',
|
|
align: 'left',
|
|
label: 'Date',
|
|
field: 'date',
|
|
sortable: true
|
|
},
|
|
{
|
|
name: 'hash',
|
|
align: 'right',
|
|
label: 'Hash',
|
|
field: 'hash',
|
|
sortable: true
|
|
}
|
|
],
|
|
pagination: {
|
|
rowsPerPage: 10
|
|
},
|
|
filter: null
|
|
},
|
|
|
|
tokensTable: {
|
|
columns: [
|
|
{
|
|
name: 'denomination',
|
|
align: 'left',
|
|
label: 'Denomination',
|
|
field: 'denomination',
|
|
sortable: true
|
|
},
|
|
{
|
|
name: 'count',
|
|
align: 'left',
|
|
label: 'Count',
|
|
field: 'count',
|
|
sortable: true
|
|
},
|
|
{
|
|
name: 'value',
|
|
align: 'left',
|
|
label: 'Value',
|
|
field: 'value',
|
|
sortable: true
|
|
},
|
|
{
|
|
name: 'memo',
|
|
align: 'left',
|
|
label: 'Memo',
|
|
field: 'memo',
|
|
sortable: true
|
|
}
|
|
],
|
|
pagination: {
|
|
rowsPerPage: 10
|
|
},
|
|
filter: null
|
|
},
|
|
|
|
paymentsChart: {
|
|
show: false
|
|
},
|
|
disclaimerDialog: {
|
|
show: false,
|
|
location: window.location
|
|
},
|
|
|
|
credit: 0,
|
|
newName: ''
|
|
}
|
|
},
|
|
computed: {
|
|
formattedBalance: function () {
|
|
return this.balance / 100
|
|
},
|
|
|
|
canPay: function () {
|
|
if (!this.parse.invoice) return false
|
|
return this.parse.invoice.sat <= this.balance
|
|
},
|
|
pendingPaymentsExist: function () {
|
|
return this.payments.findIndex(payment => payment.pending) !== -1
|
|
},
|
|
|
|
tokenList: function () {
|
|
const x = this.proofs
|
|
.filter(t => t.promises?.length)
|
|
.map(t => t.blindedMessages)
|
|
.flat()
|
|
.map(t => ({
|
|
blindingFactor: t.B_,
|
|
denomination: t.amount
|
|
}))
|
|
.reduce((y, t) => {
|
|
y[`_${t.denomination}`] = y[`_${t.denomination}`] || []
|
|
y[`_${t.denomination}`].push(t)
|
|
return y
|
|
}, {})
|
|
|
|
return Object.keys(x).map(k => ({
|
|
denomination: x[k][0].denomination,
|
|
count: x[k].length,
|
|
value: x[k][0].denomination * x[k].length
|
|
}))
|
|
},
|
|
|
|
balance: function () {
|
|
return this.proofs
|
|
.map(t => t)
|
|
.flat()
|
|
.reduce((sum, el) => (sum += el.amount), 0)
|
|
}
|
|
},
|
|
filters: {
|
|
msatoshiFormat: function (value) {
|
|
return LNbits.utils.formatSat(value / 1000)
|
|
}
|
|
},
|
|
methods: {
|
|
paymentTableRowKey: function (row) {
|
|
return row.payment_hash + row.amount
|
|
},
|
|
closeCamera: function () {
|
|
this.parse.camera.show = false
|
|
},
|
|
showCamera: function () {
|
|
this.parse.camera.show = true
|
|
},
|
|
showChart: function () {
|
|
this.paymentsChart.show = true
|
|
this.$nextTick(() => {
|
|
generateChart(this.$refs.canvas, this.payments)
|
|
})
|
|
},
|
|
focusInput(el) {
|
|
this.$nextTick(() => this.$refs[el].focus())
|
|
},
|
|
showReceiveDialog: function () {
|
|
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.unit = 'sat'
|
|
this.receive.paymentChecker = null
|
|
this.receive.minMax = [0, 2100000000000000]
|
|
this.receive.lnurl = null
|
|
this.focusInput('setAmount')
|
|
},
|
|
showParseDialog: function () {
|
|
this.parse.show = true
|
|
this.parse.invoice = null
|
|
this.parse.lnurlpay = null
|
|
this.parse.lnurlauth = null
|
|
this.parse.data.request = ''
|
|
this.parse.data.comment = ''
|
|
this.parse.data.paymentChecker = null
|
|
this.parse.camera.show = false
|
|
},
|
|
|
|
closeReceiveDialog: function () {
|
|
setTimeout(() => {
|
|
clearInterval(this.receive.paymentChecker)
|
|
}, 10000)
|
|
},
|
|
closeParseDialog: function () {
|
|
setTimeout(() => {
|
|
clearInterval(this.parse.paymentChecker)
|
|
}, 10000)
|
|
},
|
|
onPaymentReceived: function (paymentHash) {
|
|
this.fetchPayments()
|
|
this.fetchBalance()
|
|
|
|
if (this.receive.paymentHash === paymentHash) {
|
|
this.receive.show = false
|
|
this.receive.paymentHash = null
|
|
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
|
|
})
|
|
return
|
|
} else if (response.data.lnurl_response === true) {
|
|
// success
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
|
|
spinner: true
|
|
})
|
|
}
|
|
}
|
|
|
|
clearInterval(this.receive.paymentChecker)
|
|
setTimeout(() => {
|
|
clearInterval(this.receive.paymentChecker)
|
|
}, 40000)
|
|
})
|
|
.catch(err => {
|
|
LNbits.utils.notifyApiError(err)
|
|
this.receive.status = 'pending'
|
|
})
|
|
},
|
|
decodeQR: function (res) {
|
|
this.parse.data.request = res
|
|
this.decodeRequest()
|
|
this.parse.camera.show = false
|
|
},
|
|
decodeRequest: function () {
|
|
this.parse.show = true
|
|
let req = this.parse.data.request.toLowerCase()
|
|
if (this.parse.data.request.toLowerCase().startsWith('lightning:')) {
|
|
this.parse.data.request = this.parse.data.request.slice(10)
|
|
} else if (this.parse.data.request.toLowerCase().startsWith('lnurl:')) {
|
|
this.parse.data.request = this.parse.data.request.slice(6)
|
|
} else if (req.indexOf('lightning=lnurl1') !== -1) {
|
|
this.parse.data.request = this.parse.data.request
|
|
.split('lightning=')[1]
|
|
.split('&')[0]
|
|
}
|
|
|
|
if (
|
|
this.parse.data.request.toLowerCase().startsWith('lnurl1') ||
|
|
this.parse.data.request.match(/[\w.+-~_]+@[\w.+-~_]/)
|
|
) {
|
|
return
|
|
}
|
|
|
|
let invoice
|
|
try {
|
|
invoice = decode(this.parse.data.request)
|
|
} catch (error) {
|
|
this.$q.notify({
|
|
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
|
|
)
|
|
}
|
|
|
|
_.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') {
|
|
var expireDate = new Date(
|
|
(invoice.data.time_stamp + tag.value) * 1000
|
|
)
|
|
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
|
expireDate,
|
|
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
|
)
|
|
cleanInvoice.expired = false // TODO
|
|
}
|
|
}
|
|
})
|
|
|
|
this.parse.invoice = Object.freeze(cleanInvoice)
|
|
},
|
|
payInvoice: function () {
|
|
let dismissPaymentMsg = this.$q.notify({
|
|
timeout: 0,
|
|
message: 'Processing payment...'
|
|
})
|
|
},
|
|
payLnurl: function () {
|
|
let dismissPaymentMsg = this.$q.notify({
|
|
timeout: 0,
|
|
message: 'Processing payment...'
|
|
})
|
|
},
|
|
authLnurl: function () {
|
|
let dismissAuthMsg = this.$q.notify({
|
|
timeout: 10,
|
|
message: 'Performing authentication...'
|
|
})
|
|
},
|
|
|
|
deleteWallet: function (walletId, user) {
|
|
LNbits.utils
|
|
.confirmDialog('Are you sure you want to delete this wallet?')
|
|
.onOk(() => {
|
|
LNbits.href.deleteWallet(walletId, user)
|
|
})
|
|
},
|
|
fetchPayments: function () {
|
|
return
|
|
},
|
|
fetchBalance: function () {},
|
|
exportCSV: function () {
|
|
// status is important for export but it is not in paymentsTable
|
|
// because it is manually added with payment detail link and icons
|
|
// and would cause duplication in the list
|
|
let columns = this.paymentsTable.columns
|
|
columns.unshift({
|
|
name: 'pending',
|
|
align: 'left',
|
|
label: 'Pending',
|
|
field: 'pending'
|
|
})
|
|
LNbits.utils.exportCSV(columns, this.payments)
|
|
},
|
|
|
|
/////////////////////////////////// WALLET ///////////////////////////////////
|
|
showInvoicesDialog: async function () {
|
|
console.log('##### showInvoicesDialog')
|
|
this.invoiceData.amount = 0
|
|
this.invoiceData.bolt11 = ''
|
|
this.invoiceData.hash = ''
|
|
this.invoiceData.memo = ''
|
|
this.showInvoiceDetails = true
|
|
},
|
|
|
|
showInvoiceDialog: function (data) {
|
|
this.invoiceData = _.clone(data)
|
|
this.showInvoiceDetails = true
|
|
},
|
|
|
|
showPayInvoiceDialog: function () {
|
|
console.log('### showPayInvoiceDialog')
|
|
this.payInvoiceData.invoice = ''
|
|
this.payInvoiceData.bolt11 = ''
|
|
this.showPayInvoice = true
|
|
},
|
|
|
|
showSendTokensDialog: function () {
|
|
this.sendData.tokens = ''
|
|
this.sendData.tokensBase64 = ''
|
|
this.sendData.amount = 0
|
|
this.sendData.memo = ''
|
|
this.showSendTokens = true
|
|
},
|
|
|
|
showReceiveTokensDialog: function () {
|
|
this.receiveData.tokensBase64 = ''
|
|
this.showReceiveTokens = true
|
|
},
|
|
|
|
recheckPendingInvoices: async function () {
|
|
for (const invoice of this.invoicesCashu) {
|
|
if (invoice.status === 'pending') {
|
|
this.recheckInvoice(invoice.hash)
|
|
// try {
|
|
// const {data} = await LNbits.api.request(
|
|
// 'POST',
|
|
// `/cashu/api/v1/${this.mintId}/mint?payment_hash=${invoice.hash}`,
|
|
// '',
|
|
// {
|
|
// blinded_messages: []
|
|
// }
|
|
// )
|
|
// console.log('### data', data)
|
|
// } catch (error) {
|
|
// console.error(error)
|
|
// LNbits.utils.notifyApiError(error)
|
|
// }
|
|
}
|
|
}
|
|
},
|
|
|
|
//////////////////////// MINT //////////////////////////////////////////
|
|
|
|
requestMint: async function () {
|
|
// 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}`
|
|
)
|
|
console.log('### data', data)
|
|
|
|
this.invoiceData.bolt11 = data.pr
|
|
this.invoiceData.hash = data.hash
|
|
this.invoicesCashu.push({
|
|
..._.clone(this.invoiceData),
|
|
date: currentDateStr(),
|
|
status: 'pending'
|
|
})
|
|
this.storeinvoicesCashu()
|
|
this.tab = 'invoices'
|
|
return data
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
mintApi: async function (amounts, payment_hash) {
|
|
console.log('### promises', payment_hash)
|
|
try {
|
|
let secrets = await this.generateSecrets(amounts)
|
|
let {blindedMessages, rs} = await this.constructOutputs(
|
|
amounts,
|
|
secrets
|
|
)
|
|
const promises = await LNbits.api.request(
|
|
'POST',
|
|
`/cashu/api/v1/${this.mintId}/mint?payment_hash=${payment_hash}`,
|
|
'',
|
|
{
|
|
blinded_messages: blindedMessages
|
|
}
|
|
)
|
|
console.log('### promises data', promises.data)
|
|
let proofs = await this.constructProofs(promises.data, secrets, rs)
|
|
return proofs
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
mint: async function (amount, payment_hash) {
|
|
try {
|
|
const split = splitAmount(amount)
|
|
const proofs = await this.mintApi(split, payment_hash)
|
|
this.proofs.push(...proofs)
|
|
this.storeProofs()
|
|
await this.setInvoicePaid(payment_hash)
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
setInvoicePaid: async function (payment_hash) {
|
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
|
invoice.status = 'paid'
|
|
this.storeinvoicesCashu()
|
|
},
|
|
recheckInvoice: async function (payment_hash) {
|
|
console.log('### recheckInvoice.hash', payment_hash)
|
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
|
this.mint(invoice.amount, invoice.hash)
|
|
},
|
|
|
|
// requestTokens: async function (amounts, paymentHash) {
|
|
// const newTokens = await this.buildTokens(amounts, paymentHash)
|
|
// // this.proofs.push(newTokens)
|
|
// // this.storeProofs()
|
|
// // console.log('### this.proofs', this.proofs)
|
|
// // await this.mint(paymentHash, newTokens.newTokens)
|
|
// },
|
|
|
|
generateSecrets: async function (amounts) {
|
|
const secrets = []
|
|
for (let i = 0; i < amounts.length; i++) {
|
|
const secret = nobleSecp256k1.utils.randomBytes(32)
|
|
secrets.push(secret)
|
|
}
|
|
return secrets
|
|
},
|
|
|
|
constructOutputs: async function (amounts, secrets) {
|
|
const blindedMessages = []
|
|
const rs = []
|
|
for (let i = 0; i < amounts.length; i++) {
|
|
const {B_, r} = await step1Alice(secrets[i])
|
|
blindedMessages.push({amount: amounts[i], B_: B_})
|
|
rs.push(r)
|
|
}
|
|
return {
|
|
blindedMessages,
|
|
rs
|
|
}
|
|
},
|
|
|
|
constructProofs: function (promises, secrets, rs) {
|
|
const proofs = []
|
|
for (let i = 0; i < promises.length; i++) {
|
|
const encodedSecret = uint8ToBase64.encode(secrets[i])
|
|
let {id, amount, C, secret} = this.promiseToProof(
|
|
promises[i].id,
|
|
promises[i].amount,
|
|
promises[i]['C_'],
|
|
encodedSecret,
|
|
rs[i]
|
|
)
|
|
proofs.push({id, amount, C, secret})
|
|
}
|
|
return proofs
|
|
},
|
|
|
|
promiseToProof: function (id, amount, C_hex, secret, r) {
|
|
const C_ = nobleSecp256k1.Point.fromHex(C_hex)
|
|
const A = this.keys[amount]
|
|
const C = step3Alice(C_, r, nobleSecp256k1.Point.fromHex(A))
|
|
return {
|
|
id,
|
|
amount,
|
|
C: C.toHex(true),
|
|
secret
|
|
}
|
|
},
|
|
|
|
sumProofs: function (proofs) {
|
|
return proofs.reduce((s, t) => (s += t.amount), 0)
|
|
},
|
|
splitToSend: async function (proofs, amount) {
|
|
try {
|
|
const spendableProofs = proofs.filter(p => !p.reserved)
|
|
if (this.sumProofs(spendableProofs) < amount) {
|
|
throw new Error('balance too low.')
|
|
}
|
|
let {fristProofs, scndProofs} = await this.split(
|
|
spendableProofs,
|
|
amount
|
|
)
|
|
// keep firstProofs, send scndProofs
|
|
|
|
// set scndProofs in this.proofs as reserved
|
|
const usedSecrets = proofs.map(p => p.secret)
|
|
for (let i = 0; i < this.proofs.length; i++) {
|
|
if (usedSecrets.includes(this.proofs[i].secret)) {
|
|
this.proofs[i].reserved = true
|
|
}
|
|
}
|
|
this.storeProofs()
|
|
return {fristProofs, scndProofs}
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
|
|
split: async function (proofs, amount) {
|
|
try {
|
|
if (proofs.length == 0) {
|
|
throw new Error('no proofs provided.')
|
|
}
|
|
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
|
// delete proofs from this.proofs
|
|
const usedSecrets = proofs.map(p => p.secret)
|
|
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
|
// add new fristProofs, scndProofs to this.proofs
|
|
this.proofs.push(...fristProofs)
|
|
this.proofs.push(...scndProofs)
|
|
this.storeProofs()
|
|
return {fristProofs, scndProofs}
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
splitApi: async function (proofs, amount) {
|
|
try {
|
|
const total = this.sumProofs(proofs)
|
|
const frst_amount = total - amount
|
|
const scnd_amount = amount
|
|
const frst_amounts = splitAmount(frst_amount)
|
|
const scnd_amounts = splitAmount(scnd_amount)
|
|
const amounts = _.clone(frst_amounts)
|
|
amounts.push(...scnd_amounts)
|
|
let secrets = await this.generateSecrets(amounts)
|
|
if (secrets.length != amounts.length) {
|
|
throw new Error(
|
|
'number of secrets does not match number of outputs.'
|
|
)
|
|
}
|
|
let {blindedMessages, rs} = await this.constructOutputs(
|
|
amounts,
|
|
secrets
|
|
)
|
|
const payload = {
|
|
amount,
|
|
proofs,
|
|
outputs: {
|
|
blinded_messages: blindedMessages
|
|
}
|
|
}
|
|
|
|
console.log('payload', JSON.stringify(payload))
|
|
|
|
const {data} = await LNbits.api.request(
|
|
'POST',
|
|
`/cashu/api/v1/${this.mintId}/split`,
|
|
'',
|
|
payload
|
|
)
|
|
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)
|
|
const scnd_secrets = secrets.slice(frst_amounts.length)
|
|
const fristProofs = this.constructProofs(
|
|
data.fst,
|
|
frst_secrets,
|
|
frst_rs
|
|
)
|
|
const scndProofs = this.constructProofs(
|
|
data.snd,
|
|
scnd_secrets,
|
|
scnd_rs
|
|
)
|
|
|
|
return {fristProofs, scndProofs}
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
receiveTokens: async function () {
|
|
this.showReceiveTokens = false
|
|
console.log('### receive tokens', this.receiveData.tokensBase64)
|
|
if (this.receiveData.tokensBase64) {
|
|
const tokensJson = atob(this.receiveData.tokensBase64)
|
|
const proofs = JSON.parse(tokensJson)
|
|
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
|
const amounts = splitAmount(amount)
|
|
const newTokens = await this.buildTokens(amounts)
|
|
console.log('newTokens', newTokens)
|
|
|
|
const payload = {
|
|
amount,
|
|
proofs,
|
|
outputs: {
|
|
blinded_messages: newTokens.blindedMessages
|
|
}
|
|
}
|
|
|
|
console.log('payload', JSON.stringify(payload))
|
|
try {
|
|
const {data} = await LNbits.api.request(
|
|
'POST',
|
|
`/cashu/api/v1/${this.mintId}/split`,
|
|
'',
|
|
payload
|
|
)
|
|
|
|
newTokens.promises = data.snd
|
|
// console.log('split data', JSON.stringify(data.snd))
|
|
// for (let i =0 ;i < newTokens.length; i++) {
|
|
// Object.assign(newTokens[i], promises)
|
|
// }
|
|
console.log('newTokens 2', newTokens)
|
|
this.proofs.push(newTokens)
|
|
this.storeProofs()
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
}
|
|
},
|
|
|
|
buildTokens: async function (amounts, paymentHash) {
|
|
const blindedMessages = []
|
|
const secrets = []
|
|
const rs = []
|
|
for (let i = 0; i < amounts.length; i++) {
|
|
const secret = nobleSecp256k1.utils.randomBytes(32)
|
|
// const secret = nobleSecp256k1.utils.hexToBytes('0000000000000000000000000000000000000000000000000000000000000003')
|
|
// todo: base64Url
|
|
const encodedSecret = uint8ToBase64.encode(secret)
|
|
secrets.push(encodedSecret)
|
|
const {B_, r} = await step1Alice(secret)
|
|
rs.push(r)
|
|
blindedMessages.push({amount: amounts[i], B_: B_})
|
|
}
|
|
|
|
const newTokens = {
|
|
hash: paymentHash,
|
|
blindedMessages,
|
|
rs,
|
|
secrets,
|
|
status: 'pending'
|
|
}
|
|
return newTokens
|
|
},
|
|
|
|
sendTokens: async function () {
|
|
// keep firstProofs, send scndProofs
|
|
let {fristProofs, scndProofs} = await this.splitToSend(
|
|
this.proofs,
|
|
this.sendData.amount
|
|
)
|
|
|
|
// const amounts = splitAmount(this.sendData.amount)
|
|
// const sendTokens = []
|
|
// sendTokens.push(this.proofs)
|
|
// for (const amount of amounts) {
|
|
// const token = this.findTokenForAmount(amount)
|
|
// if (token) {
|
|
// sendTokens.push(token)
|
|
// } else {
|
|
// this.$q.notify({
|
|
// timeout: 5000,
|
|
// type: 'warning',
|
|
// message: `Cannot select amount for denomination ${amount}`
|
|
// })
|
|
// this.sendData.tokens = ''
|
|
// this.sendData.tokensBase64 = ''
|
|
// return
|
|
// }
|
|
// }
|
|
this.sendData.tokens = ''
|
|
this.sendData.tokensBase64 = ''
|
|
console.log('### sendTokens', sendTokens)
|
|
// this.sendData.tokens = sendTokens.map((token, tokenIndex) => {
|
|
// return this.promiseToProof(
|
|
// token.promises[tokenIndex].amount,
|
|
// token.promises[tokenIndex]['C_'],
|
|
// token.promises[tokenIndex].secret,
|
|
// token.promises[tokenIndex].r
|
|
// )
|
|
// })
|
|
this.sendData.tokens = scndProofs
|
|
console.log('### this.sendData.tokens', this.sendData.tokens)
|
|
this.sendData.tokensBase64 = btoa(JSON.stringify(this.sendData.tokens))
|
|
},
|
|
|
|
burnTokens: function () {
|
|
for (const sentToken of this.sendData.tokens) {
|
|
for (const token of this.proofs) {
|
|
if (token.status === 'paid') {
|
|
const secretIndex = token.secrets.findIndex(
|
|
s => s === sentToken.secret
|
|
)
|
|
console.log('### secretIndex', secretIndex)
|
|
if (secretIndex >= 0) {
|
|
token.blindedMessages?.splice(secretIndex, 1)
|
|
token.promises?.splice(secretIndex, 1)
|
|
token.rs?.splice(secretIndex, 1)
|
|
token.secrets?.splice(secretIndex, 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
message: 'Tokens burned'
|
|
})
|
|
this.storeProofs()
|
|
this.showSendTokens = false
|
|
console.log('### this.proofs', this.proofs)
|
|
},
|
|
|
|
findTokenForAmount: function (amount) {
|
|
for (const token of this.proofs) {
|
|
const index = token.promises?.findIndex(p => p.amount === amount)
|
|
if (index >= 0) {
|
|
return {
|
|
promise: token.promises[index],
|
|
secret: token.secrets[index],
|
|
r: token.rs[index]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
checkInvoice: function () {
|
|
console.log('#### checkInvoice')
|
|
try {
|
|
const invoice = decode(this.payInvoiceData.bolt11)
|
|
|
|
const cleanInvoice = {
|
|
msat: invoice.human_readable_part.amount,
|
|
sat: invoice.human_readable_part.amount / 1000,
|
|
fsat: LNbits.utils.formatSat(
|
|
invoice.human_readable_part.amount / 1000
|
|
)
|
|
}
|
|
|
|
_.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') {
|
|
var expireDate = new Date(
|
|
(invoice.data.time_stamp + tag.value) * 1000
|
|
)
|
|
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
|
expireDate,
|
|
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
|
)
|
|
cleanInvoice.expired = false // TODO
|
|
}
|
|
}
|
|
|
|
this.payInvoiceData.invoice = cleanInvoice
|
|
})
|
|
|
|
console.log(
|
|
'#### this.payInvoiceData.invoice',
|
|
this.payInvoiceData.invoice
|
|
)
|
|
} catch (error) {
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
type: 'warning',
|
|
message: 'Cannot decode invoice',
|
|
caption: error + ''
|
|
})
|
|
}
|
|
},
|
|
|
|
melt: async function () {
|
|
console.log('#### sell tokens')
|
|
const amount = this.payInvoiceData.invoice.sat
|
|
const paidTokens = this.proofs.filter(t => t.promises?.length)
|
|
console.log('### paidTokens', paidTokens)
|
|
const proofs = paidTokens.map(token => {
|
|
return token.promises.map((promise, promiseIndex) => {
|
|
console.log('### promise', promise)
|
|
|
|
const secret = token.secrets[promiseIndex]
|
|
const r = token.rs[promiseIndex]
|
|
|
|
return this.promiseToProof(promise.amount, promise['C_'], secret, r)
|
|
})
|
|
})
|
|
const payload = {
|
|
proofs: proofs.flat(),
|
|
amount,
|
|
invoice: this.payInvoiceData.bolt11
|
|
}
|
|
console.log('#### payload', JSON.stringify(payload))
|
|
try {
|
|
const {data} = await LNbits.api.request(
|
|
'POST',
|
|
`/cashu/api/v1/${this.mintId}/melt`,
|
|
'',
|
|
payload
|
|
)
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
message: 'Invoice paid'
|
|
})
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
|
|
// C_hex = promise['C_']
|
|
// amount = promise.amount
|
|
|
|
fetchMintKeys: async function () {
|
|
const {data} = await LNbits.api.request(
|
|
'GET',
|
|
`/cashu/api/v1/${this.mintId}/keys`
|
|
)
|
|
this.keys = data
|
|
localStorage.setItem('cashu.keys', JSON.stringify(data))
|
|
},
|
|
|
|
storeinvoicesCashu: function () {
|
|
localStorage.setItem(
|
|
'cashu.invoicesCashu',
|
|
JSON.stringify(this.invoicesCashu)
|
|
)
|
|
},
|
|
storeProofs: function () {
|
|
localStorage.setItem(
|
|
'cashu.proofs',
|
|
JSON.stringify(this.proofs, bigIntStringify)
|
|
)
|
|
}
|
|
},
|
|
watch: {
|
|
payments: function () {
|
|
this.fetchBalance()
|
|
}
|
|
},
|
|
|
|
created: function () {
|
|
let params = new URL(document.location).searchParams
|
|
|
|
// get ticker
|
|
if (
|
|
!params.get('tsh') &&
|
|
!this.$q.localStorage.getItem('cashu.tickershort')
|
|
) {
|
|
this.$q.localStorage.set('cashu.tickershort', 'sats')
|
|
this.tickershort = 'sats'
|
|
} else if (params.get('tsh')) {
|
|
this.$q.localStorage.set('cashu.tickershort', params.get('tsh'))
|
|
this.tickershort = params.get('tsh')
|
|
} else if (this.$q.localStorage.getItem('cashu.tickershort')) {
|
|
this.tickershort = this.$q.localStorage.getItem('cashu.tickershort')
|
|
}
|
|
|
|
// get mint
|
|
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!'
|
|
})
|
|
}
|
|
|
|
// get name
|
|
if (params.get('mint_name')) {
|
|
this.mintName = params.get('mint_name')
|
|
this.$q.localStorage.set('cashu.mintName', params.get('mint_name'))
|
|
} else if (this.$q.localStorage.getItem('cashu.name')) {
|
|
this.mintName = this.$q.localStorage.getItem('cashu.name')
|
|
}
|
|
|
|
const keysJson = localStorage.getItem('cashu.keys')
|
|
if (!keysJson) {
|
|
this.fetchMintKeys()
|
|
} else {
|
|
this.keys = JSON.parse(keysJson)
|
|
}
|
|
|
|
this.invoicesCashu = JSON.parse(
|
|
localStorage.getItem('cashu.invoicesCashu') || '[]'
|
|
)
|
|
this.proofs = JSON.parse(localStorage.getItem('cashu.proofs') || '[]')
|
|
console.log('### invoicesCashu', this.invoicesCashu)
|
|
console.table('### tokens', this.proofs)
|
|
console.log('#### this.mintId', this.mintId)
|
|
console.log('#### this.mintName', this.mintName)
|
|
|
|
this.recheckPendingInvoices()
|
|
}
|
|
})
|
|
</script>
|
|
{% endblock %}
|