mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-25 07:07:48 +01:00
1104 lines
32 KiB
JavaScript
1104 lines
32 KiB
JavaScript
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
|
|
navigator.vibrate(200)
|
|
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) {
|
|
// splits proofs so the user can keep firstProofs, send scndProofs
|
|
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
|
|
)
|
|
|
|
// 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)
|
|
// HACK: we need to do this so the balance updates
|
|
this.proofs = this.proofs.concat([])
|
|
navigator.vibrate(200)
|
|
this.$q.notify({
|
|
timeout: 5000,
|
|
type: 'positive',
|
|
message: 'Tokens received'
|
|
})
|
|
} 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))
|
|
navigator.vibrate(200)
|
|
},
|
|
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
|
|
)
|
|
navigator.vibrate(200)
|
|
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)
|
|
}
|
|
}
|
|
},
|
|
|
|
fetchMintKeys: async function () {
|
|
const {data} = await LNbits.api.request(
|
|
'GET',
|
|
`/cashu/api/v1/${this.mintId}/keys`
|
|
)
|
|
this.keys = data
|
|
localStorage.setItem(
|
|
this.mintKey(this.mintId, 'keys'),
|
|
JSON.stringify(data)
|
|
)
|
|
},
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
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: 'Could not decode invoice',
|
|
caption: error + ''
|
|
})
|
|
throw error
|
|
}
|
|
},
|
|
|
|
storeinvoicesCashu: function () {
|
|
localStorage.setItem(
|
|
this.mintKey(this.mintId, 'invoicesCashu'),
|
|
JSON.stringify(this.invoicesCashu)
|
|
)
|
|
},
|
|
storeProofs: function () {
|
|
localStorage.setItem(
|
|
this.mintKey(this.mintId, 'proofs'),
|
|
JSON.stringify(this.proofs, bigIntStringify)
|
|
)
|
|
},
|
|
|
|
mintKey: function (mintId, key) {
|
|
// returns a key for the local storage
|
|
// depending on the current mint
|
|
return 'cashu.' + mintId + '.' + key
|
|
}
|
|
},
|
|
watch: {
|
|
payments: function () {
|
|
this.balance()
|
|
}
|
|
},
|
|
|
|
created: function () {
|
|
let params = new URL(document.location).searchParams
|
|
|
|
// 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(
|
|
this.mintKey(this.mintId, 'mintName'),
|
|
this.mintName
|
|
)
|
|
} else if (this.$q.localStorage.getItem('cashu.name')) {
|
|
this.mintName = this.$q.localStorage.getItem('cashu.name')
|
|
}
|
|
|
|
// get ticker
|
|
if (
|
|
!params.get('tsh') &&
|
|
!this.$q.localStorage.getItem(this.mintKey(this.mintId, 'tickershort'))
|
|
) {
|
|
this.$q.localStorage.set(this.mintKey(this.mintId, 'tickershort'), 'sats')
|
|
this.tickershort = 'sats'
|
|
} else if (params.get('tsh')) {
|
|
this.$q.localStorage.set(
|
|
this.mintKey(this.mintId, 'tickershort'),
|
|
params.get('tsh')
|
|
)
|
|
this.tickershort = params.get('tsh')
|
|
} else if (
|
|
this.$q.localStorage.getItem(this.mintKey(this.mintId, 'tickershort'))
|
|
) {
|
|
this.tickershort = this.$q.localStorage.getItem(
|
|
this.mintKey(this.mintId, 'tickershort')
|
|
)
|
|
}
|
|
|
|
const keysJson = localStorage.getItem(this.mintKey(this.mintId, 'keys'))
|
|
if (!keysJson) {
|
|
this.fetchMintKeys()
|
|
} else {
|
|
this.keys = JSON.parse(keysJson)
|
|
}
|
|
|
|
this.invoicesCashu = JSON.parse(
|
|
localStorage.getItem(this.mintKey(this.mintId, 'invoicesCashu')) || '[]'
|
|
)
|
|
this.proofs = JSON.parse(
|
|
localStorage.getItem(this.mintKey(this.mintId, '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()
|
|
}
|
|
})
|