Files
lnbits/templates/cashu/wallet.html
T
2023-02-13 10:53:29 +01:00

2993 lines
99 KiB
HTML

{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu wallet {%
endraw %} {% endblock %} {% block footer %}{% endblock %} {% block
page_container %}
<q-page-container>
<q-page>
<div class="row q-col-gutter-y-md justify-center q-pt-sm q-pb-md">
<div class="col-12 col-sm-11 col-md-8 text-center q-gutter-y-md">
<q-card class="q-my-md q-py-sm">
<q-card-section class="q-mt-sm q-py-xs">
<div>
<div class="row">
<div class="col-12">
<h3 class="q-my-none q-py-none">
<center>
<strong>{% raw %} {{ getBalance() }} </strong>
{{tickerShort}} {% endraw %}
</center>
</h3>
</div>
</div>
<div class="row q-mt-xs q-mb-none" v-if="mints.length > 1">
<div class="col-12">
<a class="text-weight-light">
Balance on all mints: {% raw %}
<b>{{ getTotalBalance }} {{tickerShort}} </b>{% endraw %}</a
>
</div>
</div>
<div class="row q-mt-none q-mb-none" v-if="activeMintURL">
<div class="col-12">
<a class="text-weight-light">
Mint: {% raw %}{{ getActiveMintUrlShort()}} {% endraw %}</a
>
</div>
</div>
</div>
</q-card-section>
</q-card>
<q-card class="q-mt-xs">
<q-card-section class="q-pt-sm">
<div class="row items-center no-wrap q-mb-sm">
<div class="col-6 col-sm-5 col-md-4 q-pr-xs">
<q-btn
size="0.75rem"
rectangle
unelevated
dense
color="primary"
align="between"
icon="file_download"
icon-right="toll"
class="full-width"
@click="showReceiveTokensDialog"
>Get Ecash</q-btn
>
</div>
<div class="col-0 col-sm-2 col-md-4"></div>
<div class="col-6 col-sm-5 col-md-4 q-pl-xs">
<q-btn
size="0.75rem"
rectangle
unelevated
dense
align="between"
color="primary"
icon="file_upload"
icon-right="toll"
class="full-width"
@click="showSendTokensDialog"
>
Send Ecash</q-btn
>
</div>
</div>
<!-- ///////////////////////////////////////////
////////////////// TABLES /////////////////
/////////////////////////////////////////// -->
<q-tabs v-model="tab" no-caps class="bg-dark text-white shadow-2">
<q-tab name="invoices" label="Invoices"></q-tab>
<!-- <q-tab name="tokens" label="Tokens"></q-tab> -->
<q-tab name="history" label="History"></q-tab>
<q-tab name="settings" label="Settings"></q-tab>
</q-tabs>
<q-tab-panels v-model="tab">
<!-- ////////////////////// SETTINGS ////////////////// -->
<q-tab-panel name="settings" class="q-px-sm">
<div
class="q-py-md q-px-xs bg-grey-10 text-white text-left"
on-left
>
<q-list padding>
<q-item>
<q-item-section>
<q-item-label overline>Mints</q-item-label>
<q-item-label caption
>You can connect your wallet to multiple Cashu mints.
Enter a mint URL and select the mint your want to
use.</q-item-label
>
</q-item-section>
</q-item>
<!-- <q-item-label header>Your mints</q-item-label> -->
<div v-for="(mint, index) in mints" :key="mint.url">
{% raw %}
<q-item
:active="mint.url == activeMintURL"
active-class="text-weight-bold text-white"
>
<q-item-section avatar>
<q-icon
:color="mint.url == activeMintURL ? 'primary' : 'grey'"
:name="mint.url == activeMintURL ? 'check_circle' : 'radio_button_unchecked'"
@click="activateMint(mint.url, verbose=false)"
class="cursor-pointer"
/>
</q-item-section>
<q-item-section
class="q-mx-none q-pl-none"
style="max-width: 1.05em"
>
<q-icon
name="content_copy"
@click="copyText(mint.url)"
size="1em"
color="grey"
class="q-mr-xs cursor-pointer"
/>
</q-item-section>
<q-item-section>
<q-item-label
lines="1"
@click="activateMint(mint.url, verbose=false)"
>{{mint.url}}</q-item-label
>
<!-- <q-item-label caption v-if="mint.url == activeMintURL"
>This is your active mint.</q-item-label
> -->
</q-item-section>
<q-item-section side>
<q-badge
:color="mint.url == activeMintURL ? 'primary' : 'grey'"
:label="mint.balance + ' ' + tickerShort"
/>
</q-item-section>
<q-item-section side>
<q-icon
name="close"
@click="removeMint(mint.url)"
class="cursor-pointer"
/>
</q-item-section>
</q-item>
{% endraw %}
<q-separator spaced inset="item" />
</div>
</q-list>
</div>
<div class="q-gutter-md q-pt-md q-px-sm">
<div class="row-12">
<q-input
standout
bottom-slots
@keydown.enter.prevent="showAddMintDialog"
v-model="mintToAdd"
label="Add new mint URL"
>
<template v-slot:before>
<q-icon name="account_balance" />
</template>
<!-- <template v-slot:hint> Enter Mint URL</template> -->
<!-- "addMint(mintToAdd)" -->
<template v-slot:append>
<q-btn
round
dense
flat
color="primary"
icon="add"
click
@click="showAddMintDialog"
/>
</template>
</q-input>
</div>
</div>
<div
class="q-py-sm q-px-xs bg-grey-10 text-white text-left"
on-left
>
<q-list padding>
<q-item>
<q-item-section>
<q-item-label overline>Multimint Swaps</q-item-label>
<q-item-label caption
>Swap funds from one mint to another via Lightning.
Warning: this feature is still experimental and could
behave in unexpected ways!</q-item-label
>
</q-item-section>
</q-item>
<q-item>
<q-select
clearable
filled
dense
color="primary"
v-model="swapData.from_url"
:options="swapDataOptions()"
option-value="url"
option-label="shorturl"
label="Swap from"
style="min-width: 200px"
/>
</q-item>
<q-item>
<q-select
clearable
filled
dense
color="primary"
v-model="swapData.to_url"
:options="swapDataOptions()"
option-value="url"
option-label="shorturl"
label="Swap to"
style="min-width: 200px"
/>
</q-item>
<q-item>
<q-input
filled
dense
v-model.number="swapData.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}})"
style="min-width: 200px"
></q-input>
<q-btn
class="q-mx-md"
color="primary"
@click="mintSwap(swapData.from_url.url,swapData.to_url.url, swapData.amount)"
:disable="!swapData.from_url || !swapData.to_url || !(swapData.amount>0)"
>Swap</q-btn
>
</q-item>
</q-list>
</div>
</q-tab-panel>
<!-- ////////////////// TOKEN LIST ///////////////// -->
<q-tab-panel name="tokens">
<q-table
dense
flat
:data="getTokenList()"
:columns="tokensTable.columns"
:pagination.sync="tokensTable.pagination"
no-data-label="There are no tokens here yet"
:filter="tokensTable.filter"
>
{% raw %}
<template v-slot:body="props">
<q-tr :props="props">
<q-td
key="value"
:props="props"
:class="props.row.value > 0 ? 'text-green-13 text-weight-bold' : ''"
>
<div>{{props.row.value}}</div>
</q-td>
<q-td key="count" :props="props">
<div>{{props.row.count}}</div>
</q-td>
<q-td key="sum" :props="props">
<div>{{props.row.sum}}</div>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-tab-panel>
<!-- ////////////////// INVOICE LIST ///////////////// -->
<q-tab-panel name="invoices">
<q-table
dense
flat
:data="invoicesCashu"
:columns="invoicesTable.columns"
:pagination.sync="invoicesTable.pagination"
no-data-label="There are no invoices here 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="showInvoicInfoDialog(props.row)"
name="settings_ethernet"
color="grey"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
<q-icon
name="sync"
size="xs"
color="grey"
class="q-mr-xs cursor-pointer"
@click="checkInvoice(props.row.hash)"
>
<q-tooltip>Check status</q-tooltip>
</q-icon>
</div>
<div v-if="props.row.status === 'paid'">
<q-icon
v-if="props.row.amount>0"
name="call_received"
color="green"
><q-tooltip>Received</q-tooltip></q-icon
>
<q-icon
v-if="props.row.amount<0"
name="call_made"
color="red"
><q-tooltip>Paid</q-tooltip></q-icon
>
<!-- <q-icon name="props.row.amount < 0 ? 'call_made' : 'call_received'" color="green"></q-icon> -->
</div>
</q-td>
<q-td
key="amount"
:props="props"
:class="props.row.amount > 0 && props.row.status === 'paid' ? 'text-green-13 text-weight-bold' : ''"
>
<div>{{props.row.amount}}</div>
</q-td>
<q-td key="date" :props="props">
<div>{{props.row.date}}</div>
</q-td>
<!-- <q-td key="memo" :props="props">
<div>{{props.row.memo}}</div>
</q-td> -->
<q-td key="bolt11" :props="props">
<div @click="copyText(props.row.bolt11)">
{{shortenString(props.row.bolt11, 20, 10)}}
<q-tooltip>Click to copy</q-tooltip>
</div>
</q-td>
<q-td key="hash" :props="props">
<div @click="copyText(props.row.hash)">
{{props.row.hash}}
</div>
</q-td>
<q-td key="mint" :props="props">
<div>{{props.row.mint}}</div>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-tab-panel>
<!-- ////////////////// HISTORY LIST ///////////////// -->
<q-tab-panel name="history">
<q-table
dense
flat
:data="historyTokens"
:columns="historyTable.columns"
:pagination.sync="historyTable.pagination"
no-data-label="There are no tokens here yet"
:filter="historyTable.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="showTokenDialog(props.row.token)"
name="settings_ethernet"
color="grey"
>
<q-tooltip>Pending</q-tooltip>
</q-icon>
<q-icon
name="sync"
size="xs"
color="grey"
class="q-mr-xs cursor-pointer"
@click="checkTokenSpendable(props.row.token)"
>
<q-tooltip>Check status</q-tooltip>
</q-icon>
</div>
<div v-if="props.row.status === 'paid'">
<q-icon
v-if="props.row.amount>0"
name="call_received"
color="green"
><q-tooltip>Received</q-tooltip></q-icon
>
<q-icon
v-if="props.row.amount<0"
name="call_made"
color="red"
><q-tooltip>Paid</q-tooltip></q-icon
>
<!-- <q-icon name="props.row.amount < 0 ? 'call_made' : '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="date" :props="props">
<div>{{props.row.date}}</div>
</q-td>
<!-- <q-td key="memo" :props="props">
<div>{{props.row.memo}}</div>
</q-td> -->
<q-td key="token" :props="props">
<div @click="copyText(props.row.token)">
{{shortenString(props.row.token, 10, 40)}}
<q-tooltip>Click to copy</q-tooltip>
</div>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-tab-panel>
</q-tab-panels>
</q-card-section>
</q-card>
<div style="margin-bottom: 7rem">
<div class="row q-pt-sm">
<div class="col-4 q-pt-none">
<q-btn
class="full-width gt-sm"
size="1.0rem"
icon-right="bolt"
icon="file_download"
align="between"
rectangle
color="primary"
@click="showInvoiceCreateDialog"
><strong>Create invoice</strong>
</q-btn>
</div>
<!-- <div class="col-4"></div> -->
<div class="col-4 q-pt-xs">
<div align="center">
<q-btn
class="q-mx-xs q-px-none q-my-sm"
size="0.5rem"
rectangle
color="warning"
icon="warning"
outline
@click="showDisclaimerDialog"
><q-tooltip>Warning</q-tooltip></q-btn
>
<q-btn
class="q-mx-xs q-px-none q-my-sm"
size="0.5rem"
outline
rectangle
color="warning"
icon="download_for_offline"
@click="getLocalstorageToFile"
><q-tooltip>Download wallet backup</q-tooltip></q-btn
>
<q-btn
class="q-mx-xs q-px-none q-my-sm"
outline
size="0.5rem"
v-if="getPWADisplayMode()=='browser' && deferredPWAInstallPrompt != null"
color="primary"
@click="triggerPWAInstall()"
><b>Install</b><q-tooltip>Install Cashu</q-tooltip></q-btn
>
</div>
</div>
<div class="col-4 q-pt-none">
<q-btn
class="full-width gt-sm"
@click="showParseDialog"
size="1.0rem"
icon-right="bolt"
icon="file_upload"
align="between"
rectangle
color="primary"
class="full-width"
><strong>Pay invoice</strong>
</q-btn>
</div>
</div>
</div>
</div>
<!-- // BOTTOM BAR -->
<!-- disable bottom bar if dialogs are shown -->
<div
class="q-col-gutter"
v-if="!welcomeDialog.show && !showSendTokens && !showInvoiceDetails && !showReceiveTokens && !payInvoiceData.show && !disclaimerDialog.show && !camera.show"
>
<q-tabs
class="lt-md fixed-bottom q-px-none q-py-md left-0 right-0 bg-primary text-white shadow-2 z-top q-px-0"
indicator-color="transparent"
align="justify"
>
<q-tab
class="col-5"
label="Create Invoice"
icon="bolt"
@click="showInvoiceCreateDialog"
>
</q-tab>
<q-tab
class="col-2"
icon="photo_camera"
v-if="hasCamera"
@click="showCamera"
>
</q-tab>
<q-tab
class="col-5"
icon="bolt"
@click="showParseDialog"
label="Pay Invoice"
>
</q-tab>
</q-tabs>
</div>
<q-dialog
v-model="payInvoiceData.show"
@hide="closeParseDialog"
position="top"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="payInvoiceData.invoice">
{% raw %}
<h6 class="q-my-none">
Pay {{ payInvoiceData.invoice.fsat }}{% endraw %}
{{LNBITS_DENOMINATION}} {% raw %}
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{
payInvoiceData.invoice.description }}<br />
<strong>Expire date:</strong> {{ payInvoiceData.invoice.expireDate
}}<br />
<strong>Hash:</strong> {{ payInvoiceData.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disabled="payInvoiceData.blocking"
@click="melt"
:label="!payInvoiceData.blocking? 'Pay' : 'Paying...'"
><q-spinner-tail
v-if="payInvoiceData.blocking"
color="white"
size="1em"
/></q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</div>
<!-- <div v-else-if="payInvoiceData.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ payInvoiceData.lnurlauth.domain }}</b>?
</p>
<q-separator class="q-my-sm"></q-separator>
<p>
For every website and for every LNbits wallet, a new keypair
will be deterministically generated so your identity can't be
tied to your LNbits wallet or linked across websites. No other
data will be shared with {{ payInvoiceData.lnurlauth.domain }}.
</p>
<p>
Your public key for
<b>{{ payInvoiceData.lnurlauth.domain }}</b> is:
</p>
<p class="q-mx-xl">
<code class="text-wrap">
{{ payInvoiceData.lnurlauth.pubkey }}
</code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div> -->
<div v-else-if="payInvoiceData.lnurlpay">
{% raw %}
<q-form @submit="lnurlPaySecond" class="q-gutter-md">
<p
v-if="payInvoiceData.lnurlpay.maxSendable == payInvoiceData.lnurlpay.minSendable"
class="q-my-none text-h6 text-center"
>
<b>{{ payInvoiceData.domain }}</b> is requesting {{
payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} {%
endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<!-- <span v-if="payInvoiceData.lnurlpay.commentAllowed > 0">
<br />
and a {{payInvoiceData.lnurlpay.commentAllowed}}-char comment
</span> -->
</p>
<p v-else class="q-my-none text-h6 text-center">
<b
>{{ payInvoiceData.lnurlpay.targetUser ||
payInvoiceData.domain }}</b
>
is requesting <br />
between
<b
>{{ payInvoiceData.lnurlpay.minSendable | msatoshiFormat }}</b
>
and
<b
>{{ payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }}</b
>
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<!-- <span v-if="payInvoiceData.lnurlpay.commentAllowed > 0">
<br />
and a {{payInvoiceData.lnurlpay.commentAllowed}}-char comment
</span> -->
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row" v-if="payInvoiceData.lnurlpay.description">
<p class="col text-justify text-italic">
{{ payInvoiceData.lnurlpay.description }}
</p>
<p class="col-4 q-pl-md" v-if="payInvoiceData.lnurlpay.image">
<q-img :src="payInvoiceData.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
{% endraw %}
<q-input
filled
dense
autofocus
v-model.number="payInvoiceData.data.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:min="payInvoiceData.lnurlpay.minSendable / 1000"
:max="payInvoiceData.lnurlpay.maxSendable / 1000"
:readonly="payInvoiceData.lnurlpay.maxSendable == payInvoiceData.lnurlpay.minSendable"
></q-input>
</div>
<div
class="col-8 q-pl-md"
v-if="payInvoiceData.lnurlpay.commentAllowed > 0"
>
<q-input
filled
dense
v-model="payInvoiceData.data.comment"
_type="payInvoiceData.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="payInvoiceData.lnurlpay.commentAllowed"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Send</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</div>
<div v-else>
<q-form
v-if="!camera.show"
@submit="decodeRequest"
class="q-gutter-md"
>
<q-input
ref="pasteInput"
filled
dense
v-model.trim="payInvoiceData.data.request"
type="textarea"
label="Enter a Lightning invoice, an LNURL, or a Lightning address"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="payInvoiceData.data.request == ''"
type="submit"
>Enter</q-btn
>
<q-btn
unelevated
icon="photo_camera"
class="q-mx-0"
v-if="hasCamera"
@click="showCamera"
>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Close</q-btn
>
</div>
</q-form>
<div v-else>
<q-responsive :ratio="1">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
</q-btn>
</div>
</div>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="camera.show">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog
class="z-top"
persistent
v-model="welcomeDialog.show"
position="top"
>
<q-card class="q-pa-lg z-top">
<q-toolbar>
<q-avatar>
<img
src="https://raw.githubusercontent.com/cashubtc/cashu-ui/main/ui/icons/circle/128x128.png"
/>
</q-avatar>
<q-toolbar-title
><span class="text-weight-bold">Cashu</span>
wallet</q-toolbar-title
>
</q-toolbar>
<q-card-section>
<p>Please take a moment to read the following information.</p>
<p>
<strong>Open this wallet on your device's native browser</strong>
Cashu stores your ecash on your device locally. For the best
experience, use this wallet with your device's native web browser
(for example Safari for iOS, Chrome for Android).
</p>
<p>
<strong>Add to home screen.</strong>
Add Cashu to your home screen as a progressive web app (PWA). On
Android Chrome, click the hamburger menu at the upper right. On
iOS Safari, click the share button. Now press the Add to Home
screen button.
</p>
<p>
<strong>This software is in BETA!</strong> We hold no
responsibility for people losing access to funds. Use at your own
risk! Ecash is a bearer asset, meaning losing access to this
wallet will mean you will lose the funds. This wallet stores ecash
tokens in its database. If you lose the link or delete your your
data without backing up, you will lose your tokens. Press the
Backup button to download a copy of your tokens.
</p>
<div class="row q-mt-lg">
<q-btn
outline
class="q-mx-sm"
v-if="getPWADisplayMode()=='browser' && deferredPWAInstallPrompt != null"
color="primary"
@click="triggerPWAInstall()"
>Install Cashu</q-btn
>
<q-btn
outline
color="grey"
class="q-mx-sm"
@click="copyText(baseURL)"
>Copy URL</q-btn
>
<q-btn
v-close-popup
flat
color="primary"
class="q-ml-auto"
@click="setWelcomeDialogSeen()"
>Continue</q-btn
>
</div>
</q-card-section>
</q-card>
</q-dialog>
<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 and backup your tokens!</strong>
Ecash is a bearer asset, meaning losing access to this wallet will
mean you will lose the funds. This wallet stores ecash tokens in its
database. If you lose the link or delete your your data without
backing up, you will lose your tokens. Press the Backup button to
download a copy of your tokens.
</p>
<p>
<strong>Add to home screen.</strong>
You can add Cashu to your home screen as a progressive web app
(PWA). On Android Chrome, click the hamburger menu at the upper
right. On iOS Safari, click the share button. Now press the Add to
Home screen button.
</p>
<p>
<strong>This software is in BETA!</strong> We hold no responsibility
for people losing access to funds. Use at your own risk!
</p>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(disclaimerDialog.location.href)"
>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
type="number"
v-model.number="invoiceData.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
mask="#"
fill-mask="0"
reverse-fill-mask
autofocus
class="q-mb-lg"
@keyup.enter="requestMintButton"
></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 q-mt-none q-pt-none">
<a class="text-secondary" :href="'lightning:' + invoiceData.bolt11">
<q-responsive :ratio="1" class="q-mx-md q-mt-none q-pt-none">
<qrcode
:value="'lightning:' + invoiceData.bolt11.toUpperCase()"
: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="primary"
>Copy invoice</q-btn
>
<q-btn
v-else
color="primary"
@click="requestMintButton"
:disable="!invoiceData.amount > 0"
>Create 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
type="number"
v-model.number="sendData.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
mask="#"
fill-mask="0"
reverse-fill-mask
autofocus
class="q-mb-lg"
@keyup.enter="sendTokens"
></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">
<div class="text-center q-mb-lg" v-if="sendData.tokens.length < 2">
<!-- <a class="text-secondary" :href="'cashu:' + sendData.tokensBase64"> -->
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="baseURL + '?token=' + sendData.tokensBase64"
:options="{width: 340}"
class="rounded-borders"
>
</qrcode>
</q-responsive>
<!-- </a> -->
</div>
<q-input
outlined
dense
readonly
v-model="sendData.tokensBase64"
label="Token"
type="textarea"
class="q-mb-lg"
></q-input>
</div>
<div class="row q-mt-lg">
<q-btn
v-if="!sendData.tokens"
:disable="sendData.amount == null || sendData.amount <= 0"
@click="sendTokens"
color="primary"
type="submit"
>Send Tokens</q-btn
>
<!-- <q-btn v-else @click="burnTokens" outline color="grey"
>Burn Tokens</q-btn
> -->
<div v-else>
<q-btn color="primary" @click="copyText(sendData.tokensBase64)"
>Copy token</q-btn
>
<q-btn
color="primary"
outline
@click="copyText(baseURL + '?token=' + sendData.tokensBase64)"
>Copy link</q-btn
>
</div>
<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">Receive Cashu tokens</span>
</div>
</div>
<q-input
filled
dense
v-model="receiveData.tokensBase64"
label="Eneter Cashu token"
type="textarea"
autofocus
class="q-mb-lg"
></q-input>
</div>
<div class="row q-mt-lg">
<q-btn @click="redeem" color="primary">Receive</q-btn>
<q-btn
unelevated
icon="photo_camera"
class="q-mx-0"
v-if="hasCamera"
@click="showCamera"
></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="addMintDialog.show"
@keydown.enter.prevent="addMint(mintToAdd, verbose=true)"
>
<q-card class="q-pa-lg">
<h6 class="q-my-md text-primary">Do you trust this mint?</h6>
<p>
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.
</p>
<q-input
outlined
readonly
v-model="mintToAdd"
label="Mint URL"
type="textarea"
autogrow
class="q-mb-xs"
></q-input>
<div class="row q-mt-lg">
<q-btn
outline
v-close-popup
color="primary"
icon="check"
@click="addMint(mintToAdd, verbose=true)"
>Add mint</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</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:ss')
}
var mapMint = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm:ss'
)
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: 'sats',
ticketLong: 'Satoshis',
name: '',
mintId: '',
mintName: '',
mintToAdd: 'https://8333.space:3338',
activeMintURL: '',
mints: [],
keys: '',
proofs: [],
activeProofs: [],
deferredPWAInstallPrompt: null,
invoicesCashu: [],
historyTokens: [],
swapData: {
from_url: '',
to_url: '',
amount: 0
},
invoiceData: {
amount: 0,
memo: '',
bolt11: '',
hash: ''
},
camera: {
data: null,
show: false,
camera: 'auto'
},
invoiceCheckListener: () => {},
payInvoiceData: {
blocking: false,
bolt11: '',
show: false,
invoice: null,
lnurlpay: null,
lnurlauth: null,
data: {
request: '',
amount: 0,
comment: ''
},
paymentChecker: null,
camera: {
show: false,
camera: 'auto'
}
},
sendData: {
amount: 0,
memo: '',
tokens: '',
tokensBase64: ''
},
receiveData: {
tokensBase64: ''
},
showInvoiceDetails: false,
showPayInvoice: false,
showSendTokens: false,
showReceiveTokens: false,
promises: [],
tokens: [],
tab: 'invoices',
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: [],
tokensTable: {
columns: [
{
name: 'value',
align: 'left',
label: 'Value ({{LNBITS_DENOMINATION}})',
field: 'value',
sortable: true
},
{
name: 'count',
align: 'left',
label: 'Count',
field: 'count',
sortable: true
},
{
name: 'sum',
align: 'left',
label: 'Sum ({{LNBITS_DENOMINATION}})',
field: 'sum',
sortable: true
}
],
pagination: {
rowsPerPage: 5
},
filter: null
},
invoicesTable: {
columns: [
{
name: 'status',
align: 'left',
label: '',
field: 'status',
sortable: true
},
{
name: 'amount',
align: 'left',
label: 'Amount',
field: 'amount',
sortable: true
},
{
name: 'date',
align: 'left',
label: 'Date',
field: 'date',
sortable: true
},
// {
// name: 'memo',
// align: 'left',
// label: 'Memo',
// field: 'memo',
// sortable: true
// },
{
name: 'bolt11',
align: 'left',
label: 'Payment request',
field: 'bolt11',
sortable: false
},
{
name: 'hash',
align: 'left',
label: 'Hash',
field: 'hash',
sortable: false
},
{
name: 'mint',
align: 'left',
label: 'Mint',
field: 'mint',
sortable: true
}
],
pagination: {
sortBy: 'date',
descending: true,
rowsPerPage: 5
},
filter: null
},
historyTable: {
columns: [
{
name: 'status',
align: 'left',
label: '',
field: 'status',
sortable: true
},
{
name: 'amount',
align: 'left',
label: 'Amount',
field: 'amount',
sortable: true
},
{
name: 'date',
align: 'left',
label: 'Date',
field: 'date',
sortable: true
},
// {
// name: 'memo',
// align: 'left',
// label: 'Memo',
// field: 'memo',
// sortable: true
// },
{
name: 'token',
align: 'left',
label: 'Token',
field: 'token',
sortable: false
}
],
pagination: {
sortBy: 'date',
descending: true,
rowsPerPage: 5
},
filter: null
},
paymentsChart: {
show: false
},
welcomeDialog: {
show: false
},
disclaimerDialog: {
show: false,
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: {
canPay: function () {
if (!this.payInvoiceData.invoice) return false
return this.payInvoiceData.invoice.sat <= this.balance
},
pendingPaymentsExist: function () {
return this.payments.findIndex(payment => payment.pending) !== -1
},
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()
.reduce((sum, el) => (sum += el.amount), 0)
}
},
filters: {
msatoshiFormat: function (value) {
return LNbits.utils.formatSat(value / 1000)
}
},
methods: {
swapDataOptions: function () {
let options = []
for (const [i, m] of Object.entries(this.mints)) {
options.push({url: m.url, shorturl: this.getShortUrl(m.url)})
}
return options
},
addMint: async function (url, verbose = false) {
try {
// 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})
}
localStorage.setItem('cashu.mints', JSON.stringify(this.mints))
await this.activateMint(url, verbose)
} catch (error) {
throw error
} finally {
this.addMintDialog.show = false
}
},
activateMint: async function (url, verbose = false, stop_workers = true) {
if (url == this.activeMintURL) {
return
}
if (stop_workers) {
// we need to stop workers because they will reset the activeMint again
this.clearAllWorkers()
}
let presiouvURL = this.activeMintURL
try {
this.activeMintURL = url
console.log('### this.activeMintURL', this.activeMintURL)
keys = await this.fetchMintKeys()
// load proofs
this.activeProofs = this.proofs.filter(p =>
this.keysets.includes(p.id)
)
if (verbose) {
this.notifySuccess('Mint added.')
}
localStorage.setItem('cashu.activeMintURL', this.activeMintURL)
console.log('### activateMint: Mint activated: ', this.activeMintURL)
} catch (error) {
this.activeMintURL = presiouvURL
let err_msg = 'Could not connect to mint.'
if (error.message.length) {
err_msg = err_msg + ` ${error.message}.`
}
this.notifyError(err_msg)
throw error
}
},
removeMint: async function (url) {
this.mints = this.mints.filter(m => m.url != url)
localStorage.setItem('cashu.mints', JSON.stringify(this.mints))
// todo: we always reset to the first mint, improve this
await this.activateMint(this.mints[0].url)
this.notifySuccess('Mint removed.')
},
getBalance: function () {
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
},
getShortUrl: function (url) {
url = url.replace('https://', '')
const cut_param = 46
if (url.length > cut_param && url.indexOf('/') != -1) {
url =
url.substring(0, url.indexOf('/') + 1) +
'...' +
url.substring(url.length - cut_param / 2, url.length)
}
return url
},
getActiveMintUrlShort: function () {
return this.getShortUrl(this.activeMintURL)
},
shortenString: function (s, length = 20, lastchars = 5) {
if (s.length > length + lastchars) {
return (
s.substring(0, length) +
'...' +
s.substring(s.length - lastchars, s.length)
)
}
},
getTokenList: function () {
const amounts = this.activeProofs.map(t => t.amount)
const counts = {}
for (const num of amounts) {
counts[num] = counts[num] ? counts[num] + 1 : 1
}
return Object.keys(counts).map(k => ({
value: parseInt(k),
count: parseInt(counts[k]),
sum: k * counts[k]
}))
},
paymentTableRowKey: function (row) {
return row.payment_hash + row.amount
},
closeCamera: function () {
this.camera.show = false
},
showCamera: function () {
this.camera.show = true
},
hasCamera: function () {
navigator.permissions.query({name: 'camera'}).then(res => {
return res.state == 'granted'
})
},
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.payInvoiceData.show = true
this.payInvoiceData.invoice = null
this.payInvoiceData.lnurlpay = null
this.payInvoiceData.domain = ''
this.payInvoiceData.lnurlauth = null
this.payInvoiceData.data.request = ''
this.payInvoiceData.data.comment = ''
this.payInvoiceData.data.paymentChecker = null
this.camera.show = false
this.focusInput('pasteInput')
},
showWelcomeDialog: function () {
if (localStorage.getItem('cashu.welcomeDialogSeen') != 'seen') {
this.welcomeDialog.show = true
}
},
setWelcomeDialogSeen: function () {
localStorage.setItem('cashu.welcomeDialogSeen', 'seen')
// kick of notification that no mints are present
// we have to do this because we disabled this notification since it
// covers the dialog
this.showNoMintsWarning()
},
showDisclaimerDialog: function () {
this.disclaimerDialog.show = true
},
closeReceiveDialog: function () {
setTimeout(() => {
clearInterval(this.receive.paymentChecker)
}, 10000)
},
closeParseDialog: function () {
setTimeout(() => {
clearInterval(this.payInvoiceData.paymentChecker)
}, 10000)
},
decodeQR: function (res) {
this.camera.data = res
// this.payInvoiceData.data.request = res
this.decodeRequest()
this.camera.show = false
},
decodeRequest: function () {
let reqtype = null
let req = null
// get request
if (this.camera.data) {
// get request from camera
req = this.camera.data
} else if (this.payInvoiceData.data.request) {
// get request from pay invoice dialog
req = this.payInvoiceData.data.request
}
if (req.toLowerCase().startsWith('lnbc')) {
this.payInvoiceData.data.request = req
reqtype = 'bolt11'
} else if (req.toLowerCase().startsWith('lightning:')) {
this.payInvoiceData.data.request = req.slice(10)
reqtype = 'bolt11'
} else if (req.toLowerCase().startsWith('lnurl:')) {
this.payInvoiceData.data.request = req.slice(6)
reqtype = 'lnurl'
} else if (req.indexOf('lightning=lnurl1') !== -1) {
this.payInvoiceData.data.request = req
.split('lightning=')[1]
.split('&')[0]
reqtype = 'lnurl'
} else if (
req.toLowerCase().startsWith('lnurl1') ||
req.match(/[\w.+-~_]+@[\w.+-~_]/)
) {
this.payInvoiceData.data.request = req
reqtype = 'lnurl'
} else if (req.indexOf('eyJwcm')) {
// very dirty way of parsing cashu tokens from either a pasted token or a URL like https://host.com?token=eyJwcm
this.receiveData.tokensBase64 = req.slice(req.indexOf('eyJwcm'))
reqtype = 'cashu'
}
if (reqtype == 'bolt11') {
console.log('#### QR CODE: BOLT11')
this.payInvoiceData.show = true
let invoice
try {
invoice = decode(this.payInvoiceData.data.request)
} catch (error) {
this.notifyWarning('Failed to decode invoice', null, 3000)
this.payInvoiceData.show = false
throw error
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.payInvoiceData.invoice = Object.freeze(cleanInvoice)
} else if (reqtype == 'lnurl') {
console.log('#### QR CODE: LNURL')
this.lnurlPayFirst(this.payInvoiceData.data.request)
} else if (reqtype == 'cashu') {
console.log('#### QR CODE: CASHU TOKEN')
this.payInvoiceData.show = false
this.showReceiveTokens = true
}
},
lnurlPayFirst: async function (address) {
var host
if (address.split('@').length == 2) {
let [user, lnaddresshost] = address.split('@')
host = `https://${lnaddresshost}/.well-known/lnurlp/${user}`
} else if (address.toLowerCase().slice(0, 6) === 'lnurl1') {
// let host = Buffer.from(
// bech32.fromWords(bech32.decode(address, 20000).words)
// ).toString()
// var {data} = await axios.get(host)
const {data} = await LNbits.api.request(
'POST',
'/api/v1/payments/decode',
'',
{
data: address
}
)
host = data.domain
}
var {data} = await axios.get(host)
if (data.tag == 'payRequest') {
this.payInvoiceData.domain = host.split('https://')[1].split('/')[0]
this.payInvoiceData.lnurlpay = data
if (
this.payInvoiceData.lnurlpay.maxSendable ==
this.payInvoiceData.lnurlpay.minSendable
) {
this.payInvoiceData.data.amount =
this.payInvoiceData.lnurlpay.maxSendable / 1000
}
this.payInvoiceData.show = true
}
},
lnurlPaySecond: async function () {
let amount = this.payInvoiceData.data.amount
if (
this.payInvoiceData.lnurlpay.tag == 'payRequest' &&
this.payInvoiceData.lnurlpay.minSendable <=
amount * 1000 <=
this.payInvoiceData.lnurlpay.maxSendable
) {
var {data} = await axios.get(
`${this.payInvoiceData.lnurlpay.callback}?amount=${amount * 1000}`
)
console.log(data.pr)
this.payInvoiceData.data.request = data.pr
this.decodeRequest()
}
},
payInvoice: function () {
let dismissPaymentMsg = this.$q.notify({
timeout: 0,
message: 'Processing payment...',
position: 'top',
actions: [
{
icon: 'close',
color: 'white',
handler: () => {}
}
]
})
},
// payLnurl: function () {
// let dismissPaymentMsg = this.$q.notify({
// timeout: 0,
// message: 'Processing payment...',
// position: 'top',
// actions: [
// {
// icon: 'close',
// color: 'white',
// handler: () => {}
// }
// ]
// })
// this.lnurlPaySecond()
// },
// authLnurl: function () {
// let dismissAuthMsg = this.$q.notify({
// timeout: 10,
// message: 'Performing authentication...',
// position: 'top',
// actions: [
// {
// icon: 'close',
// color: 'white',
// handler: () => {}
// }
// ]
// })
// },
/////////////////////////////////// WALLET ///////////////////////////////////
showInvoiceCreateDialog: async function () {
console.log('##### showInvoiceCreateDialog')
this.invoiceData.amount = ''
this.invoiceData.bolt11 = ''
this.invoiceData.hash = ''
this.invoiceData.memo = ''
this.showInvoiceDetails = true
},
showInvoicInfoDialog: function (data) {
console.log('##### showInvoicInfoDialog')
this.invoiceData = _.clone(data)
this.showInvoiceDetails = true
// kick off invoice check worker
this.invoiceCheckWorker()
},
showTokenDialog: function (tokensBase64) {
console.log('##### showTokenDialog')
this.sendData.tokens = JSON.parse(atob(tokensBase64)).proofs
this.sendData.tokensBase64 = _.clone(tokensBase64)
this.showSendTokens = true
// kick off token check worker
this.checkTokenSpendableWorker()
},
showSendTokensDialog: function () {
console.log('##### showSendTokensDialog')
this.sendData.tokens = ''
this.sendData.tokensBase64 = ''
this.sendData.amount = ''
this.sendData.memo = ''
this.showSendTokens = true
},
showReceiveTokensDialog: function () {
this.receiveData.tokensBase64 = ''
this.showReceiveTokens = true
},
showAddMintDialog: function () {
this.addMintDialog.show = true
},
//////////////////////// MINT //////////////////////////////////////////
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 outputs = []
const rs = []
for (let i = 0; i < amounts.length; i++) {
const {B_, r} = await step1Alice(secrets[i])
outputs.push({amount: amounts[i], B_: B_})
rs.push(r)
}
return {
outputs,
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_,
nobleSecp256k1.utils.hexToBytes(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)
},
deleteProofs: function (proofs) {
// delete proofs from this.proofs
const usedSecrets = proofs.map(p => p.secret)
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
this.storeProofs()
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
requestMintButton: async function () {
await this.requestMint()
console.log('#### request mint', this.invoiceData)
await this.invoiceCheckWorker()
},
// /mint
requestMint: async function () {
/*
gets an invoice from the mint to get new tokens
*/
try {
const {data} = await axios.get(
`${this.activeMintURL}/mint?amount=${this.invoiceData.amount}`
)
this.assertMintError(data)
console.log('### data', data)
this.invoiceData.bolt11 = data.pr
this.invoiceData.hash = data.hash
this.invoicesCashu.push({
..._.clone(this.invoiceData),
date: currentDateStr(),
status: 'pending',
mint: this.activeMintURL
})
this.storeinvoicesCashu()
return data
} catch (error) {
console.error(error)
try {
LNbits.utils.notifyApiError(error)
} catch {}
throw error
}
},
// /mint
mintApi: async function (amounts, payment_hash, verbose = true) {
/*
asks the mint to check whether the invoice with payment_hash has been paid
and requests signing of the attached outputs.
*/
try {
let secrets = await this.generateSecrets(amounts)
let {outputs, rs} = await this.constructOutputs(amounts, secrets)
const promises = await axios.post(
`${this.activeMintURL}/mint?payment_hash=${payment_hash}`,
{
outputs
}
)
this.assertMintError(promises.data, false)
if (promises.data.promises == null) {
return {}
}
let proofs = await this.constructProofs(
promises.data.promises,
secrets,
rs
)
return proofs
} catch (error) {
console.error(error)
if (verbose) {
try {
LNbits.utils.notifyApiError(error)
} catch {}
}
throw error
}
},
mint: async function (amount, payment_hash, verbose = true) {
try {
const split = splitAmount(amount)
const proofs = await this.mintApi(split, payment_hash, verbose)
if (!proofs.length) {
throw 'could not mint'
}
this.proofs = this.proofs.concat(proofs)
// hack to update balance
this.activeProofs = this.activeProofs.concat([])
this.storeProofs()
// update UI
await this.setInvoicePaid(payment_hash)
this.historyTokens.push({
status: 'paid',
amount: amount,
date: currentDateStr(),
token: this.serializeProofs(proofs)
})
this.storehistoryTokens()
return proofs
} catch (error) {
console.error(error)
if (verbose) {
try {
LNbits.utils.notifyApiError(error)
} catch {}
}
throw error
}
},
// SPLIT
split: async function (proofs, amount) {
/*
supplies proofs and requests a split from the mint of these
proofs at a specific amount
*/
try {
if (proofs.length == 0) {
throw new Error('no proofs provided.')
}
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
this.deleteProofs(proofs)
// add new fristProofs, scndProofs to this.proofs
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
this.storeProofs()
return {fristProofs, scndProofs}
} catch (error) {
console.error(error)
try {
try {
LNbits.utils.notifyApiError(error)
} catch {}
} catch {}
throw error
}
},
// /split
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 {outputs, rs} = await this.constructOutputs(amounts, secrets)
const payload = {
amount,
proofs,
outputs
}
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)
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) {
this.payInvoiceData.blocking = false
console.error(error)
try {
LNbits.utils.notifyApiError(error)
} catch {}
throw error
}
},
splitToSend: async function (proofs, amount, invlalidate = false) {
/*
splits proofs so the user can keep firstProofs, send scndProofs.
then sets scndProofs as reserved.
if invalidate, scndProofs (the one to send) are invalidated
*/
try {
const spendableProofs = proofs.filter(p => !p.reserved)
if (this.sumProofs(spendableProofs) < amount) {
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.')
}
// call /split
let {fristProofs, scndProofs} = await this.split(
spendableProofs,
amount
)
// 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
}
}
// hack: to make Vue JS update
this.proofs = this.proofs.concat([])
if (invlalidate) {
// delete scndProofs from db
this.deleteProofs(scndProofs)
}
return {fristProofs, scndProofs}
} catch (error) {
console.error(error)
try {
LNbits.utils.notifyApiError(error)
} catch {}
throw error
}
},
redeem: async function () {
/*
uses split to receive new tokens.
*/
this.showReceiveTokens = false
console.log('### receive tokens', this.receiveData.tokensBase64)
try {
if (this.receiveData.tokensBase64.length == 0) {
throw new Error('no tokens provided.')
}
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)
) {
await 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) {
await 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',
amount: amount,
date: currentDateStr(),
token: this.receiveData.tokensBase64
})
this.storehistoryTokens()
if (window.navigator.vibrate) navigator.vibrate(200)
this.notifySuccess('Tokens received.')
} catch (error) {
console.error(error)
try {
LNbits.utils.notifyApiError(error)
} catch {}
throw error
}
// }
},
sendTokens: async function () {
/*
calls splitToSend, displays token and kicks off the spendableWorker
*/
try {
// keep firstProofs, send scndProofs and delete them (invalidate=true)
let {fristProofs, scndProofs} = await this.splitToSend(
this.activeProofs,
this.sendData.amount,
true
)
// update UI
this.sendData.tokens = scndProofs
console.log('### this.sendData.tokens', this.sendData.tokens)
this.sendData.tokensBase64 = this.serializeProofs(scndProofs)
this.historyTokens.push({
status: 'pending',
amount: -this.sendData.amount,
date: currentDateStr(),
token: this.sendData.tokensBase64
})
// store "pending" outgoing tokens in history table
this.storehistoryTokens()
this.checkTokenSpendableWorker()
} catch (error) {
console.error(error)
}
},
// /melt
melt: async function () {
// todo: get fees from server and add to inputs
this.payInvoiceData.blocking = true
console.log('#### pay lightning')
const amount_invoice = this.payInvoiceData.invoice.sat
const amount =
amount_invoice +
(await this.checkFees(this.payInvoiceData.data.request))
console.log(
'#### amount invoice',
amount_invoice,
'amount with fees',
amount
)
let {fristProofs, scndProofs} = await this.splitToSend(
this.activeProofs,
amount
)
const payload = {
proofs: scndProofs.flat(),
amount,
pr: this.payInvoiceData.data.request
}
try {
const {data} = await axios.post(`${this.activeMintURL}/melt`, payload)
this.assertMintError(data)
if (window.navigator.vibrate) navigator.vibrate(200)
this.notifySuccess('Token paid.')
console.log('#### pay lightning: token paid')
// delete spent tokens from db
this.deleteProofs(scndProofs)
// update UI
this.historyTokens.push({
status: 'paid',
amount: -amount,
date: currentDateStr(),
token: this.serializeProofs(scndProofs)
})
this.storehistoryTokens()
console.log({
amount: -amount,
bolt11: this.payInvoiceData.data.request,
hash: this.payInvoiceData.data.hash,
memo: this.payInvoiceData.data.memo
})
this.invoicesCashu.push({
amount: -amount,
bolt11: this.payInvoiceData.data.request,
hash: this.payInvoiceData.data.hash,
memo: this.payInvoiceData.data.memo,
date: currentDateStr(),
status: 'paid',
mint: this.activeMintURL
})
this.storeinvoicesCashu()
this.payInvoiceData.invoice = false
this.payInvoiceData.show = false
this.payInvoiceData.blocking = false
} catch (error) {
this.payInvoiceData.blocking = false
console.error(error)
try {
LNbits.utils.notifyApiError(error)
} catch {}
throw error
}
},
//
mintSwap: async function (from_url, to_url, amount) {
// get invoice
await this.activateMint(to_url)
this.invoiceData.amount = amount
let invoice = await this.requestMint()
// pay invoice
await this.activateMint(from_url)
this.payInvoiceData.data.request = invoice.pr
await this.decodeRequest()
let invoiceAmount = this.payInvoiceData.invoice.sat
await this.melt()
// settle invoice on other side
await this.activateMint(to_url)
await this.invoiceCheckWorker()
},
// /check
checkProofsSpendable: async function (proofs, update_history = false) {
/*
checks with the mint whether an array of proofs is still
spendable or already invalidated
*/
if (proofs.length == 0) {
return
}
const payload = {
proofs: proofs.map(p => {
return {secret: p.secret}
})
}
try {
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) {
this.deleteProofs(spentProofs)
// update UI
if (update_history) {
this.historyTokens.push({
status: 'paid',
amount: -this.sumProofs(spentProofs),
date: currentDateStr(),
token: this.serializeProofs(spentProofs)
})
this.storehistoryTokens()
}
}
return data.spendable
} catch (error) {
console.error(error)
try {
LNbits.utils.notifyApiError(error)
} catch {}
throw error
}
},
// /checkfees
checkFees: async function (payment_request) {
const payload = {
pr: payment_request
}
try {
const {data} = await axios.post(
`${this.activeMintURL}/checkfees`,
payload
)
this.assertMintError(data)
console.log('#### checkFees', payment_request, data.fee)
return data.fee
} catch (error) {
console.error(error)
try {
LNbits.utils.notifyApiError(error)
} catch {}
throw error
}
},
// /keys
fetchMintKeys: async function () {
try {
console.log('### GET', `${this.activeMintURL}/keys`)
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.mints', JSON.stringify(this.mints))
}
return keys
} catch (error) {
console.error(error)
try {
LNbits.utils.notifyApiError(error)
} catch {}
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)
try {
LNbits.utils.notifyApiError(error)
} catch {}
throw error
}
},
////////////// UI HELPERS //////////////
setInvoicePaid: async function (payment_hash) {
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
invoice.status = 'paid'
this.storeinvoicesCashu()
},
checkInvoice: async function (payment_hash, verbose = true) {
console.log('### checkInvoice.hash', payment_hash)
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
try {
if (invoice.mint != null) {
await this.activateMint(invoice.mint, false, false)
}
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
return proofs
} catch (error) {
console.log('Invoice still pending', invoice.hash)
throw error
}
},
checkPendingInvoices: async function () {
for (const invoice of this.invoicesCashu) {
if (invoice.status === 'pending' && invoice.amount > 0) {
try {
await this.checkInvoice(invoice.hash, false)
} catch (error) {
console.log(`${invoice.hash} still pending`)
throw error
}
}
}
},
checkPendingTokens: async function () {
for (const token of this.historyTokens) {
if (token.status === 'pending' && token.amount < 0) {
this.checkTokenSpendable(token.token, false)
}
}
},
setTokenPaid: async function (token) {
const invoice = this.historyTokens.find(i => i.token === token)
invoice.status = 'paid'
this.storehistoryTokens()
},
checkTokenSpendable: async function (token, verbose = true) {
/*
checks whether a base64-encoded token (from the history table) has been spent already.
if it is spent, the appropraite entry in the history table is set to paid.
*/
const tokenJson = JSON.parse(atob(token))
const proofs = tokenJson.proofs
if (tokenJson.mints != null && tokenJson.mints[0].url != null) {
// todo: we activate only the first mint in the token
await this.activateMint(tokenJson.mints[0].url)
}
const spendable = await this.checkProofsSpendable(proofs)
let paid = false
if (spendable.includes(false)) {
this.setTokenPaid(token)
paid = true
}
if (paid) {
if (window.navigator.vibrate) navigator.vibrate(200)
this.notifySuccess('Token paid.')
} else {
console.log('### token not paid yet')
if (verbose) {
this.notify('Token still pending', (color = 'grey'))
}
// this.sendData.tokens = token
}
return paid
},
findTokenForAmount: function (amount) {
// unused coin selection
for (const token of this.activeProofs) {
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]
}
}
}
},
////////////// WORKERS //////////////
clearAllWorkers: function () {
if (this.invoiceCheckListener) {
clearInterval(this.invoiceCheckListener)
}
if (this.tokensCheckSpendableListener) {
clearInterval(this.tokensCheckSpendableListener)
}
},
invoiceCheckWorker: async function () {
let nInterval = 0
this.clearAllWorkers()
this.invoiceCheckListener = setInterval(async () => {
try {
nInterval += 1
// exit loop after 2m
if (nInterval > 40) {
console.log('### stopping invoice check worker')
this.clearAllWorkers()
}
console.log('### invoiceCheckWorker setInterval', nInterval)
console.log(this.invoiceData)
// this will throw an error if the invoice is pending
await this.checkInvoice(this.invoiceData.hash, false)
// only without error (invoice paid) will we reach here
console.log('### stopping invoice check worker')
this.clearAllWorkers()
this.invoiceData.bolt11 = ''
this.showInvoiceDetails = false
if (window.navigator.vibrate) navigator.vibrate(200)
this.notifySuccess('Payment received', 'top')
} catch (error) {
console.log('not paid yet')
}
}, 3000)
},
checkTokenSpendableWorker: async function () {
let nInterval = 0
this.clearAllWorkers()
this.tokensCheckSpendableListener = setInterval(async () => {
try {
nInterval += 1
// exit loop after 2m
if (nInterval > 24) {
console.log('### stopping token check worker')
this.clearAllWorkers()
}
console.log('### checkTokenSpendableWorker setInterval', nInterval)
console.log(this.sendData)
// this will throw an error if the invoice is pending
paid = await this.checkTokenSpendable(
this.sendData.tokensBase64,
false
)
if (paid) {
console.log('### stopping token check worker')
this.clearAllWorkers()
this.sendData.tokens = ''
this.showSendTokens = false
}
} catch (error) {
console.log('not paid yet')
}
}, 3000)
},
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
assertMintError: function (response, verbose = true) {
if (response.error != null) {
if (verbose) {
this.notifyError(`Mint error: ${response.error}`)
}
throw new Error(`Mint error: ${response.error}`)
}
},
////////////// UI HELPERS /////////////
registerPWAEventHook: function () {
// register event listener for PWA install prompt
window.addEventListener('beforeinstallprompt', e => {
// Prevent the mini-infobar from appearing on mobile
// e.preventDefault()
// Stash the event so it can be triggered later.
this.deferredPWAInstallPrompt = e
console.log(
`'beforeinstallprompt' event was fired.`,
this.getPWADisplayMode()
)
})
},
getPWADisplayMode: function () {
const isStandalone = window.matchMedia(
'(display-mode: standalone)'
).matches
if (document.referrer.startsWith('android-app://')) {
return 'twa'
} else if (navigator.standalone || isStandalone) {
return 'standalone'
}
return 'browser'
},
triggerPWAInstall: function () {
// Show the install prompt
this.deferredPWAInstallPrompt.prompt()
// Wait for the user to respond to the prompt
this.deferredPWAInstallPrompt.userChoice.then(choiceResult => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt')
this.setWelcomeDialogSeen()
} else {
console.log('User dismissed the install prompt')
}
})
},
showNoMintsWarning: function () {
if (!this.activeMintURL) {
this.walletURL = this.baseURL
// we have to check whether the welcome dialog was pressed away because
// otherwise quasar falsely covers the welcome dialog with this notify
if (localStorage.getItem('cashu.welcomeDialogSeen') == 'seen') {
this.notifyWarning(
'You are not connected to any mints yet.',
'Add a new mint URL in the settings.',
30000
)
}
// switch to settings tab
this.tab = 'settings'
}
},
notifySuccess: async function (message, position = 'top') {
this.$q.notify({
timeout: 5000,
type: 'positive',
message: message,
position: position,
progress: true,
actions: [
{
icon: 'close',
color: 'white',
handler: () => {}
}
]
})
},
notifyError: async function (message, caption = null) {
this.$q.notify({
color: 'red',
message: message,
caption: caption,
position: 'top',
progress: true,
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',
progress: true,
actions: [
{
icon: 'close',
color: 'black',
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 () {
// https://stackoverflow.com/questions/24263682/save-restore-local-storage-to-a-local-file
const fileName = `cashu_backup_${currentDateStr()}.json`
var a = {}
for (var i = 0; i < localStorage.length; i++) {
var k = localStorage.key(i)
var v = localStorage.getItem(k)
a[k] = v
}
var textToSave = JSON.stringify(a)
var textToSaveAsBlob = new Blob([textToSave], {
type: 'text/plain'
})
var textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob)
var downloadLink = document.createElement('a')
downloadLink.download = fileName
downloadLink.innerHTML = 'Download File'
downloadLink.href = textToSaveAsURL
downloadLink.onclick = function () {
document.body.removeChild(event.target)
}
downloadLink.style.display = 'none'
document.body.appendChild(downloadLink)
downloadLink.click()
},
storeinvoicesCashu: function () {
localStorage.setItem(
'cashu.invoicesCashu',
JSON.stringify(this.invoicesCashu)
)
},
storehistoryTokens: function () {
localStorage.setItem(
'cashu.historyTokens',
JSON.stringify(this.historyTokens)
)
},
storeProofs: function () {
localStorage.setItem(
'cashu.proofs',
JSON.stringify(this.proofs, bigIntStringify)
)
},
migrationLocalstorage: async 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)
await 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
return 'cashu.' + mintId + '.' + key
}
},
watch: {
// payments: function () {
// this.getBalance()
// },
proofs: function () {
if (this.keysets) {
this.activeProofs = this.proofs.filter(p =>
this.keysets.includes(p.id)
)
}
}
},
mounted: function () {},
created: async function () {
let params = new URL(document.location).searchParams
// 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')
// works with only lnbits mints
activeMintURL =
location.protocol +
'//' +
location.host +
`/cashu/api/v1/${this.mintId}`
this.walletURL = this.baseURL + '?mint_id=' + this.mintId
await this.addMint(activeMintURL)
}
if (localStorage.getItem('cashu.activeMintURL')) {
if (!this.activeMintURL) {
this.walletURL = this.baseURL
}
activeMintURL = localStorage.getItem('cashu.activeMintURL')
await this.addMint(activeMintURL)
}
this.showNoMintsWarning()
// todo: remove:
if (!this.mintId.length) {
this.mintId = 'dummy'
}
console.log('Mint URL ' + this.activeMintURL)
console.log('Wallet URL ' + this.walletURL)
const startupMintUrl = this.activeMintURL
// get name
if (params.get('mint_name')) {
this.mintName = params.get('mint_name')
}
this.invoicesCashu = JSON.parse(
localStorage.getItem('cashu.invoicesCashu') || '[]'
)
this.historyTokens = JSON.parse(
localStorage.getItem('cashu.historyTokens') || '[]'
)
this.proofs = JSON.parse(localStorage.getItem('cashu.proofs') || '[]')
// run migrations
await this.migrationLocalstorage()
// get token to receive tokens from a link
if (params.get('token')) {
tokenBase64 = params.get('token')
// make sure to react only to tokens not in the users history
let seen = false
for (var i = 0; i < this.historyTokens.length; i++) {
var thisToken = this.historyTokens[i].token
if (thisToken == tokenBase64 && this.historyTokens[i].amount > 0) {
seen = true
}
}
if (!seen) {
// show receive token dialog
this.receiveData.tokensBase64 = params.get('token')
this.showReceiveTokens = true
}
}
// get lightning invoice from a link
if (params.get('lightning')) {
this.showParseDialog()
this.payInvoiceData.data.request = params.get('lightning')
}
// startup tasks
this.checkProofsSpendable(this.activeProofs, true).catch(err => {
return
})
this.checkPendingInvoices().catch(err => {
return
})
this.checkPendingTokens().catch(err => {
return
})
// reset to the mint from settings after workers have run
if (startupMintUrl.length > 0) {
await this.activateMint(startupMintUrl)
}
// PWA install hook
this.registerPWAEventHook()
// show welcome dialog
this.showWelcomeDialog()
}
})
</script>
{% endblock %}