2993 lines
99 KiB
HTML
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 %}
|