mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-13 19:37:42 +01:00
1712 lines
56 KiB
HTML
1712 lines
56 KiB
HTML
{% extends "public.html" %} {% block toolbar_title %} {% raw %} {{name}} Cashu {% endraw %} {% endblock %} {% block footer %}{% endblock %} {% block
|
|
page_container %}
|
|
<q-page-container>
|
|
<q-page>
|
|
<div class="row q-col-gutter-md justify-center q-pt-lg">
|
|
<div class="col-12 col-sm-8 col-md-9 col-lg-7 text-center q-gutter-y-md">
|
|
<q-card>
|
|
<q-card-section>
|
|
<div class="row">
|
|
<div class="col-3">
|
|
<q-btn
|
|
class="gt-sm"
|
|
size="16px"
|
|
icon="arrow_downward"
|
|
rectangle
|
|
color="secondary"
|
|
class="full-width"
|
|
@click="showInvoicesDialog"
|
|
>Receive invoice
|
|
</q-btn>
|
|
</div>
|
|
<div class="col-6">
|
|
<h3 class="q-my-none">
|
|
<center>
|
|
<strong>{% raw %} {{getBalance()}} </strong>
|
|
{{tickershort}}{% endraw %}
|
|
</center>
|
|
</h3>
|
|
</div>
|
|
<div class="col-3">
|
|
<q-btn
|
|
class="gt-sm"
|
|
@click="showParseDialog"
|
|
size="16px"
|
|
icon="arrow_upward"
|
|
rectangle
|
|
color="secondary"
|
|
class="full-width"
|
|
>Pay invoice
|
|
</q-btn>
|
|
</div>
|
|
</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
|
|
<q-card>
|
|
<q-card-section>
|
|
<div class="row items-center no-wrap q-mb-sm">
|
|
<div class="col-5 col-sm-5 col-md-4">
|
|
<q-btn
|
|
size="12px"
|
|
icon="arrow_downward"
|
|
rectangle
|
|
color="primary"
|
|
class="full-width"
|
|
@click="showReceiveTokensDialog"
|
|
>Receive Tokens</q-btn
|
|
>
|
|
</div>
|
|
<div class="col-2 col-sm-2 col-md-4"></div>
|
|
<div class="col-5 col-sm-5 col-md-4">
|
|
<q-btn
|
|
size="12px"
|
|
icon="arrow_upward"
|
|
rectangle
|
|
color="primary"
|
|
class="full-width"
|
|
@click="showSendTokensDialog"
|
|
>
|
|
Send Tokens</q-btn
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<q-tabs v-model="tab" no-caps class="bg-dark text-white shadow-2">
|
|
<q-tab name="tokens" label="Tokens"></q-tab>
|
|
<q-tab name="invoices" label="Invoices"></q-tab>
|
|
<q-tab name="history" label="History"></q-tab>
|
|
</q-tabs>
|
|
<q-tab-panels v-model="tab">
|
|
<q-tab-panel name="tokens">
|
|
<q-table
|
|
dense
|
|
flat
|
|
:data="getTokenList()"
|
|
:columns="tokensTable.columns"
|
|
:pagination.sync="tokensTable.pagination"
|
|
no-data-label="No tokens 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-td key="memo" :props="props">
|
|
<div>{{props.row.memo}}</div>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
{% endraw %}
|
|
</q-table>
|
|
</q-tab-panel>
|
|
<q-tab-panel name="invoices">
|
|
<q-table
|
|
dense
|
|
flat
|
|
:data="invoicesCashu"
|
|
:columns="invoicesTable.columns"
|
|
:pagination.sync="invoicesTable.pagination"
|
|
no-data-label="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="showInvoiceDialog(props.row)"
|
|
name="settings_ethernet"
|
|
color="grey"
|
|
>
|
|
<q-tooltip>Pending</q-tooltip>
|
|
</q-icon>
|
|
<q-badge
|
|
size="lg"
|
|
color="secondary"
|
|
class="q-mr-md cursor-pointer"
|
|
@click="recheckInvoice(props.row.hash)"
|
|
>
|
|
Check
|
|
</q-badge>
|
|
</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="memo" :props="props">
|
|
<div>{{props.row.memo}}</div>
|
|
</q-td>
|
|
<q-td key="date" :props="props">
|
|
<div>{{props.row.date}}</div>
|
|
</q-td>
|
|
<q-td key="hash" :props="props">
|
|
<div>{{props.row.hash}}</div>
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
{% endraw %}
|
|
</q-table>
|
|
</q-tab-panel>
|
|
|
|
<q-tab-panel name="history">
|
|
<span>History</span>
|
|
</q-tab-panel>
|
|
</q-tab-panels>
|
|
</q-card-section>
|
|
</q-card>
|
|
<div>
|
|
<q-btn
|
|
size="12px"
|
|
rectangle
|
|
color="warning"
|
|
outline
|
|
@click="showDisclaimerDialog"> Warning</q-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<q-tabs
|
|
class="lt-md fixed-bottom q-pa-none left-0 right-0 bg-primary text-white shadow-2 z-top"
|
|
active-class="px-0"
|
|
align="justify"
|
|
indicator-color="transparent"
|
|
>
|
|
<q-tab
|
|
icon="arrow_downward"
|
|
label="Receive Invoice"
|
|
@click="showInvoicesDialog"
|
|
>
|
|
</q-tab>
|
|
</q-tab>
|
|
<q-tab icon="arrow_upward" label="Pay Invoice" @click="showParseDialog">
|
|
</q-tab>
|
|
<!-- <q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab> -->
|
|
</q-tabs>
|
|
|
|
<q-dialog v-model="payInvoiceData.show" @hide="closeParseDialog">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<div v-if="payInvoiceData.invoice">
|
|
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
|
|
{% raw %} {{
|
|
parseFloat(String(payInvoiceData.invoice.fsat).replaceAll(",",
|
|
"")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
|
|
</h6>
|
|
<h6 v-else class="q-my-none">
|
|
{{ 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" @click="melt">Pay</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="payLnurl" class="q-gutter-md">
|
|
<p v-if="payInvoiceData.lnurlpay.fixed" class="q-my-none text-h6">
|
|
<b>{{ payInvoiceData.lnurlpay.domain }}</b> is requesting {{
|
|
payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }}
|
|
{{LNBITS_DENOMINATION}}
|
|
<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.lnurlpay.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">
|
|
<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
|
|
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.fixed"
|
|
></q-input>
|
|
{% raw %}
|
|
</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 {{LNBITS_DENOMINATION}}</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>
|
|
<q-form
|
|
v-if="!payInvoiceData.camera.show"
|
|
@submit="decodeRequest"
|
|
class="q-gutter-md"
|
|
>
|
|
<q-input
|
|
ref="pasteInput"
|
|
filled
|
|
dense
|
|
v-model.trim="payInvoiceData.data.request"
|
|
type="textarea"
|
|
label="Paste an invoice, payment request or lnurl code *"
|
|
>
|
|
</q-input>
|
|
<div class="row q-mt-lg">
|
|
<q-btn
|
|
unelevated
|
|
color="primary"
|
|
:disable="payInvoiceData.data.request == ''"
|
|
type="submit"
|
|
outline
|
|
>Continue</q-btn
|
|
>
|
|
<q-btn unelevated icon="photo_camera" label="Scan" class="q-ml-auto" @click="showCamera"> </q-btn>
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Cancel</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="payInvoiceData.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 v-model="disclaimerDialog.show">
|
|
<q-card class="q-pa-lg">
|
|
<h6 class="q-my-md text-primary">Warning</h6>
|
|
<p>
|
|
<strong>Bookmark this page!</strong>
|
|
</p>
|
|
<p>
|
|
Ecash is a bearer asset, meaning losing access to this wallet will mean you will
|
|
lose the funds. This wallet stores tokens in its database. If you lose the link or request
|
|
your data, you will lose your tokens. Bookmark this page or press the
|
|
hamburger icon and add this page to your home screen.
|
|
</p>
|
|
<p>
|
|
This service is in BETA, and 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
|
|
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">
|
|
<a :href="'lightning:' + invoiceData.bolt11">
|
|
<q-responsive :ratio="1" class="q-mx-xl">
|
|
<qrcode
|
|
:value="invoiceData.bolt11"
|
|
:options="{width: 340}"
|
|
class="rounded-borders"
|
|
>
|
|
</qrcode>
|
|
</q-responsive>
|
|
</a>
|
|
</div>
|
|
<div class="row q-mt-lg">
|
|
<q-btn
|
|
v-if="invoiceData.bolt11"
|
|
@click="copyText(invoiceData.bolt11)"
|
|
outline
|
|
color="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
|
|
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">
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model="sendData.tokensBase64"
|
|
label="Tokens"
|
|
type="textarea"
|
|
class="q-mb-lg"
|
|
></q-input>
|
|
</div>
|
|
<div class="row q-mt-lg">
|
|
<q-btn
|
|
v-if="!sendData.tokens"
|
|
: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
|
|
> -->
|
|
<q-btn
|
|
v-else
|
|
outline
|
|
color="primary"
|
|
@click="copyText(sendData.tokensBase64)"
|
|
>Copy token</q-btn
|
|
>
|
|
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Close</q-btn
|
|
>
|
|
</div>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-dialog v-model="showReceiveTokens" position="top">
|
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
|
<div>
|
|
<div class="row items-center no-wrap q-mb-sm">
|
|
<div class="col-12">
|
|
<span class="text-subtitle1">Receive Cashu tokens</span>
|
|
</div>
|
|
</div>
|
|
<q-input
|
|
filled
|
|
dense
|
|
v-model="receiveData.tokensBase64"
|
|
label="Paste encoded tokens *"
|
|
type="textarea"
|
|
autofocus
|
|
class="q-mb-lg"
|
|
></q-input>
|
|
</div>
|
|
|
|
<div class="row q-mt-lg">
|
|
<q-btn @click="redeem" outline color="grey"
|
|
>Receive Tokens</q-btn
|
|
>
|
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
|
>Close</q-btn
|
|
>
|
|
</div>
|
|
</q-card>
|
|
</q-dialog>
|
|
</div>
|
|
</q-page>
|
|
</q-page-container>
|
|
{% endblock %} {% block styles %}
|
|
<style>
|
|
* {
|
|
touch-action: manipulation;
|
|
}
|
|
|
|
.keypad {
|
|
display: grid;
|
|
grid-gap: 8px;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
grid-template-rows: repeat(4, 1fr);
|
|
}
|
|
|
|
.keypad .btn {
|
|
height: 100%;
|
|
}
|
|
|
|
.keypad .btn-confirm {
|
|
grid-area: 1 / 4 / 5 / 4;
|
|
}
|
|
</style>
|
|
{% endblock %} {% block scripts %}
|
|
<script src="{{ url_for('cashu_static', path='js/noble-secp256k1.js') }}"></script>
|
|
<script src="{{ url_for('cashu_static', path='js/utils.js') }}"></script>
|
|
<script src="{{ url_for('cashu_static', path='js/dhke.js') }}"></script>
|
|
<script src="{{ url_for('cashu_static', path='js/base64.js') }}"></script>
|
|
<script>
|
|
var currentDateStr = function () {
|
|
return Quasar.utils.date.formatDate(new Date(), 'YYYY-MM-DD HH:mm')
|
|
}
|
|
var mapMint = function (obj) {
|
|
obj.date = Quasar.utils.date.formatDate(
|
|
new Date(obj.time * 1000),
|
|
'YYYY-MM-DD HH:mm'
|
|
)
|
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
|
obj.cashu = ['/cashu/', obj.id].join('')
|
|
return obj
|
|
}
|
|
|
|
Vue.component(VueQrcode.name, VueQrcode)
|
|
|
|
new Vue({
|
|
el: '#vue',
|
|
mixins: [windowMixin],
|
|
data: function () {
|
|
return {
|
|
tickershort: '',
|
|
name: '',
|
|
|
|
mintId: '',
|
|
mintName: '',
|
|
keys: '',
|
|
invoicesCashu: [],
|
|
invoiceData: {
|
|
amount: 0,
|
|
memo: '',
|
|
bolt11: '',
|
|
hash: ''
|
|
},
|
|
invoiceCheckListener: () => {},
|
|
payInvoiceData: {
|
|
// invoice: '',
|
|
bolt11: '',
|
|
// camera: {
|
|
// show: false,
|
|
// camera: 'auto'
|
|
// }
|
|
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: 'tokens',
|
|
|
|
receive: {
|
|
show: false,
|
|
status: 'pending',
|
|
paymentReq: null,
|
|
paymentHash: null,
|
|
minMax: [0, 2100000000000000],
|
|
lnurl: null,
|
|
units: ['sat'],
|
|
unit: 'sat',
|
|
data: {
|
|
amount: null,
|
|
memo: ''
|
|
}
|
|
},
|
|
parse: {
|
|
show: false,
|
|
invoice: null,
|
|
lnurlpay: null,
|
|
lnurlauth: null,
|
|
data: {
|
|
request: '',
|
|
amount: 0,
|
|
comment: ''
|
|
},
|
|
paymentChecker: null,
|
|
camera: {
|
|
show: false,
|
|
camera: 'auto'
|
|
}
|
|
},
|
|
payments: [],
|
|
invoicesTable: {
|
|
columns: [
|
|
{
|
|
name: 'status',
|
|
align: 'left',
|
|
label: '',
|
|
field: 'status'
|
|
},
|
|
{
|
|
name: 'amount',
|
|
align: 'left',
|
|
label: 'Amount',
|
|
field: 'amount'
|
|
},
|
|
{
|
|
name: 'memo',
|
|
align: 'left',
|
|
label: 'Memo',
|
|
field: 'memo',
|
|
sortable: true
|
|
},
|
|
{
|
|
name: 'date',
|
|
align: 'left',
|
|
label: 'Date',
|
|
field: 'date',
|
|
sortable: true
|
|
},
|
|
{
|
|
name: 'hash',
|
|
align: 'right',
|
|
label: 'Hash',
|
|
field: 'hash',
|
|
sortable: true
|
|
}
|
|
],
|
|
pagination: {
|
|
sortBy: 'date',
|
|
descending: true,
|
|
rowsPerPage: 5
|
|
},
|
|
filter: null
|
|
},
|
|
|
|
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
|
|
}
|
|
// {
|
|
// name: 'memo',
|
|
// align: 'left',
|
|
// label: 'Memo',
|
|
// field: 'memo',
|
|
// sortable: true
|
|
// }
|
|
],
|
|
pagination: {
|
|
rowsPerPage: 5
|
|
},
|
|
filter: null
|
|
},
|
|
|
|
paymentsChart: {
|
|
show: false
|
|
},
|
|
disclaimerDialog: {
|
|
show: false,
|
|
location: window.location
|
|
},
|
|
|
|
credit: 0,
|
|
newName: ''
|
|
}
|
|
},
|
|
computed: {
|
|
formattedBalance: function () {
|
|
return this.balance / 100
|
|
},
|
|
|
|
canPay: function () {
|
|
if (!this.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.proofs
|
|
.map(t => t)
|
|
.flat()
|
|
.reduce((sum, el) => (sum += el.amount), 0)
|
|
}
|
|
},
|
|
filters: {
|
|
msatoshiFormat: function (value) {
|
|
return LNbits.utils.formatSat(value / 1000)
|
|
}
|
|
},
|
|
methods: {
|
|
getBalance: function () {
|
|
return this.proofs
|
|
.map(t => t)
|
|
.flat()
|
|
.reduce((sum, el) => (sum += el.amount), 0)
|
|
},
|
|
getTokenList: function () {
|
|
const x = this.proofs
|
|
.map(t => t.amount)
|
|
.reduce((acc, amount) => {
|
|
acc[amount] = acc[amount] + amount || 1
|
|
return acc
|
|
}, {})
|
|
return Object.keys(x).map(k => ({
|
|
value: k,
|
|
count: x[k],
|
|
sum: k * x[k]
|
|
}))
|
|
},
|
|
|
|
paymentTableRowKey: function (row) {
|
|
return row.payment_hash + row.amount
|
|
},
|
|
closeCamera: function () {
|
|
this.payInvoiceData.camera.show = false
|
|
},
|
|
showCamera: function () {
|
|
this.payInvoiceData.camera.show = true
|
|
},
|
|
showChart: function () {
|
|
this.paymentsChart.show = true
|
|
this.$nextTick(() => {
|
|
generateChart(this.$refs.canvas, this.payments)
|
|
})
|
|
},
|
|
focusInput(el) {
|
|
this.$nextTick(() => this.$refs[el].focus())
|
|
},
|
|
showReceiveDialog: function () {
|
|
this.receive.show = true
|
|
this.receive.status = 'pending'
|
|
this.receive.paymentReq = null
|
|
this.receive.paymentHash = null
|
|
this.receive.data.amount = null
|
|
this.receive.data.memo = null
|
|
this.receive.unit = 'sat'
|
|
this.receive.paymentChecker = null
|
|
this.receive.minMax = [0, 2100000000000000]
|
|
this.receive.lnurl = null
|
|
this.focusInput('setAmount')
|
|
},
|
|
showParseDialog: function () {
|
|
this.payInvoiceData.show = true
|
|
this.payInvoiceData.invoice = null
|
|
this.payInvoiceData.lnurlpay = null
|
|
this.payInvoiceData.lnurlauth = null
|
|
this.payInvoiceData.data.request = ''
|
|
this.payInvoiceData.data.comment = ''
|
|
this.payInvoiceData.data.paymentChecker = null
|
|
this.payInvoiceData.camera.show = false
|
|
this.focusInput('pasteInput')
|
|
},
|
|
showDisclaimerDialog: function() {
|
|
this.disclaimerDialog.show = true
|
|
},
|
|
|
|
closeReceiveDialog: function () {
|
|
setTimeout(() => {
|
|
clearInterval(this.receive.paymentChecker)
|
|
}, 10000)
|
|
},
|
|
closeParseDialog: function () {
|
|
setTimeout(() => {
|
|
clearInterval(this.payInvoiceData.paymentChecker)
|
|
}, 10000)
|
|
},
|
|
onPaymentReceived: function (paymentHash) {
|
|
this.fetchPayments()
|
|
this.fetchBalance()
|
|
|
|
if (this.receive.paymentHash === paymentHash) {
|
|
this.receive.show = false
|
|
this.receive.paymentHash = null
|
|
clearInterval(this.receive.paymentChecker)
|
|
}
|
|
},
|
|
createInvoice: function () {
|
|
this.receive.status = 'loading'
|
|
if (LNBITS_DENOMINATION != 'sats') {
|
|
this.receive.data.amount = this.receive.data.amount * 100
|
|
}
|
|
LNbits.api
|
|
.createInvoice(
|
|
this.receive.data.amount,
|
|
this.receive.data.memo,
|
|
this.receive.unit,
|
|
this.receive.lnurl && this.receive.lnurl.callback
|
|
)
|
|
.then(response => {
|
|
this.receive.status = 'success'
|
|
this.receive.paymentReq = response.data.payment_request
|
|
this.receive.paymentHash = response.data.payment_hash
|
|
|
|
if (response.data.lnurl_response !== null) {
|
|
if (response.data.lnurl_response === false) {
|
|
response.data.lnurl_response = `Unable to connect`
|
|
}
|
|
|
|
if (typeof response.data.lnurl_response === 'string') {
|
|
// failure
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
type: 'warning',
|
|
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
|
|
caption: response.data.lnurl_response
|
|
})
|
|
return
|
|
} else if (response.data.lnurl_response === true) {
|
|
// success
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
|
|
spinner: true
|
|
})
|
|
}
|
|
}
|
|
|
|
clearInterval(this.receive.paymentChecker)
|
|
setTimeout(() => {
|
|
clearInterval(this.receive.paymentChecker)
|
|
}, 40000)
|
|
})
|
|
.catch(err => {
|
|
LNbits.utils.notifyApiError(err)
|
|
this.receive.status = 'pending'
|
|
})
|
|
},
|
|
decodeQR: function (res) {
|
|
this.payInvoiceData.data.request = res
|
|
this.decodeRequest()
|
|
this.payInvoiceData.camera.show = false
|
|
},
|
|
decodeRequest: function () {
|
|
this.payInvoiceData.show = true
|
|
let req = this.payInvoiceData.data.request.toLowerCase()
|
|
if (
|
|
this.payInvoiceData.data.request
|
|
.toLowerCase()
|
|
.startsWith('lightning:')
|
|
) {
|
|
this.payInvoiceData.data.request = this.payInvoiceData.data.request.slice(
|
|
10
|
|
)
|
|
} else if (
|
|
this.payInvoiceData.data.request.toLowerCase().startsWith('lnurl:')
|
|
) {
|
|
this.payInvoiceData.data.request = this.payInvoiceData.data.request.slice(
|
|
6
|
|
)
|
|
} else if (req.indexOf('lightning=lnurl1') !== -1) {
|
|
this.payInvoiceData.data.request = this.payInvoiceData.data.request
|
|
.split('lightning=')[1]
|
|
.split('&')[0]
|
|
}
|
|
|
|
if (
|
|
this.payInvoiceData.data.request.toLowerCase().startsWith('lnurl1') ||
|
|
this.payInvoiceData.data.request.match(/[\w.+-~_]+@[\w.+-~_]/)
|
|
) {
|
|
return
|
|
}
|
|
|
|
let invoice
|
|
try {
|
|
invoice = decode(this.payInvoiceData.data.request)
|
|
} catch (error) {
|
|
this.$q.notify({
|
|
timeout: 3000,
|
|
type: 'warning',
|
|
message: error + '.',
|
|
caption: '400 BAD REQUEST'
|
|
})
|
|
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)
|
|
},
|
|
payInvoice: function () {
|
|
let dismissPaymentMsg = this.$q.notify({
|
|
timeout: 0,
|
|
message: 'Processing payment...'
|
|
})
|
|
},
|
|
payLnurl: function () {
|
|
let dismissPaymentMsg = this.$q.notify({
|
|
timeout: 0,
|
|
message: 'Processing payment...'
|
|
})
|
|
},
|
|
authLnurl: function () {
|
|
let dismissAuthMsg = this.$q.notify({
|
|
timeout: 10,
|
|
message: 'Performing authentication...'
|
|
})
|
|
},
|
|
|
|
deleteWallet: function (walletId, user) {
|
|
LNbits.utils
|
|
.confirmDialog('Are you sure you want to delete this wallet?')
|
|
.onOk(() => {
|
|
LNbits.href.deleteWallet(walletId, user)
|
|
})
|
|
},
|
|
fetchPayments: function () {
|
|
return
|
|
},
|
|
fetchBalance: function () {},
|
|
exportCSV: function () {
|
|
// status is important for export but it is not in paymentsTable
|
|
// because it is manually added with payment detail link and icons
|
|
// and would cause duplication in the list
|
|
let columns = this.paymentsTable.columns
|
|
columns.unshift({
|
|
name: 'pending',
|
|
align: 'left',
|
|
label: 'Pending',
|
|
field: 'pending'
|
|
})
|
|
LNbits.utils.exportCSV(columns, this.payments)
|
|
},
|
|
|
|
/////////////////////////////////// WALLET ///////////////////////////////////
|
|
showInvoicesDialog: async function () {
|
|
console.log('##### showInvoicesDialog')
|
|
this.invoiceData.amount = 0
|
|
this.invoiceData.bolt11 = ''
|
|
this.invoiceData.hash = ''
|
|
this.invoiceData.memo = ''
|
|
this.showInvoiceDetails = true
|
|
},
|
|
|
|
showInvoiceDialog: function (data) {
|
|
console.log('##### showInvoiceDialog')
|
|
this.invoiceData = _.clone(data)
|
|
this.showInvoiceDetails = true
|
|
},
|
|
|
|
showPayInvoiceDialog: function () {
|
|
console.log('### showPayInvoiceDialog')
|
|
this.payInvoiceData.invoice = ''
|
|
this.payInvoiceData.data.request = ''
|
|
this.showPayInvoice = true
|
|
this.payInvoiceData.camera.show = false
|
|
},
|
|
|
|
showSendTokensDialog: function () {
|
|
this.sendData.tokens = ''
|
|
this.sendData.tokensBase64 = ''
|
|
this.sendData.amount = 0
|
|
this.sendData.memo = ''
|
|
this.showSendTokens = true
|
|
},
|
|
|
|
showReceiveTokensDialog: function () {
|
|
this.receiveData.tokensBase64 = ''
|
|
this.showReceiveTokens = true
|
|
},
|
|
|
|
//////////////////////// MINT //////////////////////////////////////////
|
|
requestMintButton: async function () {
|
|
await this.requestMint()
|
|
console.log('this is your invoice BEFORE')
|
|
console.log(this.invoiceData)
|
|
this.invoiceCheckListener = setInterval(async () => {
|
|
try {
|
|
console.log('this is your invoice AFTER')
|
|
console.log(this.invoiceData)
|
|
await this.recheckInvoice(this.invoiceData.hash, false)
|
|
clearInterval(this.invoiceCheckListener)
|
|
this.invoiceData.bolt11 = ''
|
|
this.showInvoiceDetails = false
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
type: 'positive',
|
|
message: 'Payment received'
|
|
})
|
|
} catch (error) {
|
|
console.log('not paid yet')
|
|
}
|
|
}, 3000)
|
|
},
|
|
|
|
requestMint: async function () {
|
|
// gets an invoice from the mint to get new tokens
|
|
try {
|
|
const {data} = await LNbits.api.request(
|
|
'GET',
|
|
`/cashu/api/v1/${this.mintId}/mint?amount=${this.invoiceData.amount}`
|
|
)
|
|
console.log('### data', data)
|
|
|
|
this.invoiceData.bolt11 = data.pr
|
|
this.invoiceData.hash = data.hash
|
|
this.invoicesCashu.push({
|
|
..._.clone(this.invoiceData),
|
|
date: currentDateStr(),
|
|
status: 'pending'
|
|
})
|
|
this.storeinvoicesCashu()
|
|
this.tab = 'invoices'
|
|
return data
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
throw error
|
|
}
|
|
},
|
|
mintApi: async function (amounts, payment_hash, verbose = true) {
|
|
console.log('### promises', payment_hash)
|
|
try {
|
|
let secrets = await this.generateSecrets(amounts)
|
|
let {blindedMessages, rs} = await this.constructOutputs(
|
|
amounts,
|
|
secrets
|
|
)
|
|
const promises = await LNbits.api.request(
|
|
'POST',
|
|
`/cashu/api/v1/${this.mintId}/mint?payment_hash=${payment_hash}`,
|
|
'',
|
|
{
|
|
blinded_messages: blindedMessages
|
|
}
|
|
)
|
|
console.log('### promises data', promises.data)
|
|
let proofs = await this.constructProofs(promises.data, secrets, rs)
|
|
return proofs
|
|
} catch (error) {
|
|
console.error(error)
|
|
if (verbose) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
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)
|
|
this.storeProofs()
|
|
await this.setInvoicePaid(payment_hash)
|
|
return proofs
|
|
} catch (error) {
|
|
console.error(error)
|
|
if (verbose) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
throw error
|
|
}
|
|
},
|
|
setInvoicePaid: async function (payment_hash) {
|
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
|
invoice.status = 'paid'
|
|
this.storeinvoicesCashu()
|
|
},
|
|
recheckInvoice: async function (payment_hash, verbose = true) {
|
|
console.log('### recheckInvoice.hash', payment_hash)
|
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
|
try {
|
|
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
|
return proofs
|
|
} catch (error) {
|
|
console.log('Invoice still pending')
|
|
throw error
|
|
}
|
|
},
|
|
|
|
generateSecrets: async function (amounts) {
|
|
const secrets = []
|
|
for (let i = 0; i < amounts.length; i++) {
|
|
const secret = nobleSecp256k1.utils.randomBytes(32)
|
|
secrets.push(secret)
|
|
}
|
|
return secrets
|
|
},
|
|
|
|
constructOutputs: async function (amounts, secrets) {
|
|
const blindedMessages = []
|
|
const rs = []
|
|
for (let i = 0; i < amounts.length; i++) {
|
|
const {B_, r} = await step1Alice(secrets[i])
|
|
blindedMessages.push({amount: amounts[i], B_: B_})
|
|
rs.push(r)
|
|
}
|
|
return {
|
|
blindedMessages,
|
|
rs
|
|
}
|
|
},
|
|
|
|
constructProofs: function (promises, secrets, rs) {
|
|
const proofs = []
|
|
for (let i = 0; i < promises.length; i++) {
|
|
const encodedSecret = uint8ToBase64.encode(secrets[i])
|
|
let {id, amount, C, secret} = this.promiseToProof(
|
|
promises[i].id,
|
|
promises[i].amount,
|
|
promises[i]['C_'],
|
|
encodedSecret,
|
|
rs[i]
|
|
)
|
|
proofs.push({id, amount, C, secret})
|
|
}
|
|
return proofs
|
|
},
|
|
|
|
promiseToProof: function (id, amount, C_hex, secret, r) {
|
|
const C_ = nobleSecp256k1.Point.fromHex(C_hex)
|
|
const A = this.keys[amount]
|
|
const C = step3Alice(
|
|
C_,
|
|
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)
|
|
},
|
|
splitToSend: async function (proofs, amount, invlalidate = false) {
|
|
try {
|
|
const spendableProofs = proofs.filter(p => !p.reserved)
|
|
if (this.sumProofs(spendableProofs) < amount) {
|
|
throw new Error('balance too low.')
|
|
}
|
|
let {fristProofs, scndProofs} = await this.split(
|
|
spendableProofs,
|
|
amount
|
|
)
|
|
// keep firstProofs, send scndProofs
|
|
|
|
// set scndProofs in this.proofs as reserved
|
|
const usedSecrets = proofs.map(p => p.secret)
|
|
for (let i = 0; i < this.proofs.length; i++) {
|
|
if (usedSecrets.includes(this.proofs[i].secret)) {
|
|
this.proofs[i].reserved = true
|
|
}
|
|
}
|
|
if (invlalidate) {
|
|
// delete tokens from db
|
|
this.proofs = fristProofs
|
|
// add new fristProofs, scndProofs to this.proofs
|
|
this.storeProofs()
|
|
}
|
|
|
|
return {fristProofs, scndProofs}
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
throw error
|
|
}
|
|
},
|
|
|
|
split: async function (proofs, amount) {
|
|
try {
|
|
if (proofs.length == 0) {
|
|
throw new Error('no proofs provided.')
|
|
}
|
|
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
|
// delete proofs from this.proofs
|
|
const usedSecrets = proofs.map(p => p.secret)
|
|
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
|
// add new fristProofs, scndProofs to this.proofs
|
|
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
|
this.storeProofs()
|
|
return {fristProofs, scndProofs}
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
throw error
|
|
}
|
|
},
|
|
splitApi: async function (proofs, amount) {
|
|
try {
|
|
const total = this.sumProofs(proofs)
|
|
const frst_amount = total - amount
|
|
const scnd_amount = amount
|
|
const frst_amounts = splitAmount(frst_amount)
|
|
const scnd_amounts = splitAmount(scnd_amount)
|
|
const amounts = _.clone(frst_amounts)
|
|
amounts.push(...scnd_amounts)
|
|
let secrets = await this.generateSecrets(amounts)
|
|
if (secrets.length != amounts.length) {
|
|
throw new Error(
|
|
'number of secrets does not match number of outputs.'
|
|
)
|
|
}
|
|
let {blindedMessages, rs} = await this.constructOutputs(
|
|
amounts,
|
|
secrets
|
|
)
|
|
const payload = {
|
|
amount,
|
|
proofs,
|
|
outputs: {
|
|
blinded_messages: blindedMessages
|
|
}
|
|
}
|
|
|
|
console.log('payload', JSON.stringify(payload))
|
|
|
|
const {data} = await LNbits.api.request(
|
|
'POST',
|
|
`/cashu/api/v1/${this.mintId}/split`,
|
|
'',
|
|
payload
|
|
)
|
|
const frst_rs = rs.slice(0, frst_amounts.length)
|
|
const frst_secrets = secrets.slice(0, frst_amounts.length)
|
|
const scnd_rs = rs.slice(frst_amounts.length)
|
|
const scnd_secrets = secrets.slice(frst_amounts.length)
|
|
const fristProofs = this.constructProofs(
|
|
data.fst,
|
|
frst_secrets,
|
|
frst_rs
|
|
)
|
|
const scndProofs = this.constructProofs(
|
|
data.snd,
|
|
scnd_secrets,
|
|
scnd_rs
|
|
)
|
|
|
|
return {fristProofs, scndProofs}
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
throw error
|
|
}
|
|
},
|
|
|
|
redeem: async function () {
|
|
this.showReceiveTokens = false
|
|
console.log('### receive tokens', this.receiveData.tokensBase64)
|
|
try {
|
|
if (this.receiveData.tokensBase64.length == 0) {
|
|
throw new Error('no tokens provided.')
|
|
}
|
|
const tokensJson = atob(this.receiveData.tokensBase64)
|
|
const proofs = JSON.parse(tokensJson)
|
|
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
|
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
throw error
|
|
}
|
|
// }
|
|
},
|
|
|
|
sendTokens: async function () {
|
|
// keep firstProofs, send scndProofs
|
|
let {fristProofs, scndProofs} = await this.splitToSend(
|
|
this.proofs,
|
|
this.sendData.amount,
|
|
true
|
|
)
|
|
this.sendData.tokens = ''
|
|
this.sendData.tokensBase64 = ''
|
|
this.sendData.tokens = scndProofs
|
|
console.log('### this.sendData.tokens', this.sendData.tokens)
|
|
this.sendData.tokensBase64 = btoa(JSON.stringify(this.sendData.tokens))
|
|
},
|
|
checkFees: async function (payment_request) {
|
|
const payload = {
|
|
pr: payment_request
|
|
}
|
|
console.log('#### payload', JSON.stringify(payload))
|
|
try {
|
|
const {data} = await LNbits.api.request(
|
|
'POST',
|
|
`/cashu/api/v1/${this.mintId}/checkfees`,
|
|
'',
|
|
payload
|
|
)
|
|
console.log('#### checkFees', payment_request, data.fee)
|
|
return data.fee
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
throw error
|
|
}
|
|
},
|
|
melt: async function () {
|
|
// todo: get fees from server and add to inputs
|
|
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
|
|
)
|
|
// if (amount > balance()) {
|
|
// LNbits.utils.notifyApiError('Balance too low')
|
|
// return
|
|
// }
|
|
let {fristProofs, scndProofs} = await this.splitToSend(
|
|
this.proofs,
|
|
amount
|
|
)
|
|
const payload = {
|
|
proofs: scndProofs.flat(),
|
|
amount,
|
|
invoice: this.payInvoiceData.data.request
|
|
}
|
|
console.log('#### payload', JSON.stringify(payload))
|
|
try {
|
|
const {data} = await LNbits.api.request(
|
|
'POST',
|
|
`/cashu/api/v1/${this.mintId}/melt`,
|
|
'',
|
|
payload
|
|
)
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
type: 'positive',
|
|
message: 'Invoice paid'
|
|
})
|
|
// delete tokens from db
|
|
this.proofs = fristProofs
|
|
// add new fristProofs, scndProofs to this.proofs
|
|
this.storeProofs()
|
|
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'
|
|
})
|
|
this.storeinvoicesCashu()
|
|
this.tab = 'invoices'
|
|
|
|
this.payInvoiceData.invoice = false
|
|
this.payInvoiceData.show = false
|
|
|
|
} catch (error) {
|
|
console.error(error)
|
|
LNbits.utils.notifyApiError(error)
|
|
throw error
|
|
}
|
|
},
|
|
|
|
recheckPendingInvoices: async function () {
|
|
for (const invoice of this.invoicesCashu) {
|
|
if (invoice.status === 'pending' && invoice.sat > 0) {
|
|
this.recheckInvoice(invoice.hash, false)
|
|
}
|
|
}
|
|
},
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
findTokenForAmount: function (amount) {
|
|
for (const token of this.proofs) {
|
|
const index = token.promises?.findIndex(p => p.amount === amount)
|
|
if (index >= 0) {
|
|
return {
|
|
promise: token.promises[index],
|
|
secret: token.secrets[index],
|
|
r: token.rs[index]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
checkInvoice: function () {
|
|
console.log('#### checkInvoice')
|
|
try {
|
|
const invoice = decode(this.payInvoiceData.data.request)
|
|
|
|
const cleanInvoice = {
|
|
msat: invoice.human_readable_part.amount,
|
|
sat: invoice.human_readable_part.amount / 1000,
|
|
fsat: LNbits.utils.formatSat(
|
|
invoice.human_readable_part.amount / 1000
|
|
)
|
|
}
|
|
|
|
_.each(invoice.data.tags, tag => {
|
|
if (_.isObject(tag) && _.has(tag, 'description')) {
|
|
if (tag.description === 'payment_hash') {
|
|
cleanInvoice.hash = tag.value
|
|
} else if (tag.description === 'description') {
|
|
cleanInvoice.description = tag.value
|
|
} else if (tag.description === 'expiry') {
|
|
var expireDate = new Date(
|
|
(invoice.data.time_stamp + tag.value) * 1000
|
|
)
|
|
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
|
expireDate,
|
|
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
|
)
|
|
cleanInvoice.expired = false // TODO
|
|
}
|
|
}
|
|
|
|
this.payInvoiceData.invoice = cleanInvoice
|
|
})
|
|
|
|
console.log(
|
|
'#### this.payInvoiceData.invoice',
|
|
this.payInvoiceData.invoice
|
|
)
|
|
} catch (error) {
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
type: 'warning',
|
|
message: 'Cannot decode invoice',
|
|
caption: error + ''
|
|
})
|
|
throw error
|
|
}
|
|
},
|
|
|
|
fetchMintKeys: async function () {
|
|
const {data} = await LNbits.api.request(
|
|
'GET',
|
|
`/cashu/api/v1/${this.mintId}/keys`
|
|
)
|
|
this.keys = data
|
|
localStorage.setItem('cashu.keys', JSON.stringify(data))
|
|
},
|
|
|
|
storeinvoicesCashu: function () {
|
|
localStorage.setItem(
|
|
'cashu.invoicesCashu',
|
|
JSON.stringify(this.invoicesCashu)
|
|
)
|
|
},
|
|
storeProofs: function () {
|
|
localStorage.setItem(
|
|
'cashu.proofs',
|
|
JSON.stringify(this.proofs, bigIntStringify)
|
|
)
|
|
}
|
|
},
|
|
watch: {
|
|
payments: function () {
|
|
this.balance()
|
|
}
|
|
},
|
|
|
|
created: function () {
|
|
let params = new URL(document.location).searchParams
|
|
|
|
// get ticker
|
|
if (
|
|
!params.get('tsh') &&
|
|
!this.$q.localStorage.getItem('cashu.tickershort')
|
|
) {
|
|
this.$q.localStorage.set('cashu.tickershort', 'sats')
|
|
this.tickershort = 'sats'
|
|
} else if (params.get('tsh')) {
|
|
this.$q.localStorage.set('cashu.tickershort', params.get('tsh'))
|
|
this.tickershort = params.get('tsh')
|
|
} else if (this.$q.localStorage.getItem('cashu.tickershort')) {
|
|
this.tickershort = this.$q.localStorage.getItem('cashu.tickershort')
|
|
}
|
|
|
|
// get mint
|
|
if (params.get('mint_id')) {
|
|
this.mintId = params.get('mint_id')
|
|
this.$q.localStorage.set('cashu.mint', params.get('mint_id'))
|
|
} else if (this.$q.localStorage.getItem('cashu.mint')) {
|
|
this.mintId = this.$q.localStorage.getItem('cashu.mint')
|
|
} else {
|
|
this.$q.notify({
|
|
color: 'red',
|
|
message: 'No mint set!'
|
|
})
|
|
}
|
|
|
|
// get name
|
|
if (params.get('mint_name')) {
|
|
this.mintName = params.get('mint_name')
|
|
this.$q.localStorage.set('cashu.mintName', params.get('mint_name'))
|
|
} else if (this.$q.localStorage.getItem('cashu.name')) {
|
|
this.mintName = this.$q.localStorage.getItem('cashu.name')
|
|
}
|
|
|
|
const keysJson = localStorage.getItem('cashu.keys')
|
|
if (!keysJson) {
|
|
this.fetchMintKeys()
|
|
} else {
|
|
this.keys = JSON.parse(keysJson)
|
|
}
|
|
|
|
this.invoicesCashu = JSON.parse(
|
|
localStorage.getItem('cashu.invoicesCashu') || '[]'
|
|
)
|
|
this.proofs = JSON.parse(localStorage.getItem('cashu.proofs') || '[]')
|
|
console.log('### invoicesCashu', this.invoicesCashu)
|
|
console.table('### tokens', this.proofs)
|
|
console.log('#### this.mintId', this.mintId)
|
|
console.log('#### this.mintName', this.mintName)
|
|
|
|
this.recheckPendingInvoices()
|
|
}
|
|
})
|
|
</script>
|
|
{% endblock %}
|