Merge pull request #1237 from lnbits/fix/cashu/timeout
cashu: refactor wallet
This commit is contained in:
commit
13f0159c15
2 changed files with 469 additions and 305 deletions
|
|
@ -159,7 +159,7 @@ page_container %}
|
||||||
size="lg"
|
size="lg"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
class="q-mr-md cursor-pointer"
|
class="q-mr-md cursor-pointer"
|
||||||
@click="recheckInvoice(props.row.hash)"
|
@click="checkInvoice(props.row.hash)"
|
||||||
>
|
>
|
||||||
Check
|
Check
|
||||||
</q-badge>
|
</q-badge>
|
||||||
|
|
@ -1528,57 +1528,17 @@ page_container %}
|
||||||
return proofs.reduce((s, t) => (s += t.amount), 0)
|
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
|
||||||
|
},
|
||||||
|
|
||||||
//////////// API ///////////
|
//////////// API ///////////
|
||||||
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
|
// MINT
|
||||||
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.recheckInvoice(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.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Payment received',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log('not paid yet')
|
|
||||||
}
|
|
||||||
}, 3000)
|
|
||||||
},
|
|
||||||
|
|
||||||
requestMintButton: async function () {
|
requestMintButton: async function () {
|
||||||
await this.requestMint()
|
await this.requestMint()
|
||||||
|
|
@ -1586,8 +1546,12 @@ page_container %}
|
||||||
await this.invoiceCheckWorker()
|
await this.invoiceCheckWorker()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /mint
|
||||||
|
|
||||||
requestMint: async function () {
|
requestMint: async function () {
|
||||||
// gets an invoice from the mint to get new tokens
|
/*
|
||||||
|
gets an invoice from the mint to get new tokens
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
|
|
@ -1611,7 +1575,14 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /mint
|
||||||
|
|
||||||
mintApi: async function (amounts, payment_hash, verbose = true) {
|
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 (blindedMessages)
|
||||||
|
*/
|
||||||
console.log('### promises', payment_hash)
|
console.log('### promises', payment_hash)
|
||||||
try {
|
try {
|
||||||
let secrets = await this.generateSecrets(amounts)
|
let secrets = await this.generateSecrets(amounts)
|
||||||
|
|
@ -1647,7 +1618,19 @@ page_container %}
|
||||||
}
|
}
|
||||||
this.proofs = this.proofs.concat(proofs)
|
this.proofs = this.proofs.concat(proofs)
|
||||||
this.storeProofs()
|
this.storeProofs()
|
||||||
|
|
||||||
|
// update UI
|
||||||
await this.setInvoicePaid(payment_hash)
|
await this.setInvoicePaid(payment_hash)
|
||||||
|
tokensBase64 = btoa(JSON.stringify(proofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: amount,
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
|
||||||
return proofs
|
return proofs
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
@ -1657,62 +1640,20 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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) {
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Balance too low',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
throw Error('balance too low.')
|
|
||||||
}
|
|
||||||
let {fristProofs, scndProofs} = await this.split(
|
|
||||||
spendableProofs,
|
|
||||||
amount
|
|
||||||
)
|
|
||||||
|
|
||||||
// set scndProofs in this.proofs as reserved
|
// SPLIT
|
||||||
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) {
|
split: async function (proofs, amount) {
|
||||||
|
/*
|
||||||
|
supplies proofs and requests a split from the mint of these
|
||||||
|
proofs at a specific amount
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
if (proofs.length == 0) {
|
if (proofs.length == 0) {
|
||||||
throw new Error('no proofs provided.')
|
throw new Error('no proofs provided.')
|
||||||
}
|
}
|
||||||
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
||||||
// delete proofs from this.proofs
|
this.deleteProofs(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
|
// add new fristProofs, scndProofs to this.proofs
|
||||||
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
||||||
this.storeProofs()
|
this.storeProofs()
|
||||||
|
|
@ -1723,6 +1664,9 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /split
|
||||||
|
|
||||||
splitApi: async function (proofs, amount) {
|
splitApi: async function (proofs, amount) {
|
||||||
try {
|
try {
|
||||||
const total = this.sumProofs(proofs)
|
const total = this.sumProofs(proofs)
|
||||||
|
|
@ -1782,7 +1726,62 @@ page_container %}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Balance too low',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (invlalidate) {
|
||||||
|
// delete scndProofs from db
|
||||||
|
this.deleteProofs(scndProofs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {fristProofs, scndProofs}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
redeem: async function () {
|
redeem: async function () {
|
||||||
|
/*
|
||||||
|
uses split to receive new tokens.
|
||||||
|
*/
|
||||||
this.showReceiveTokens = false
|
this.showReceiveTokens = false
|
||||||
console.log('### receive tokens', this.receiveData.tokensBase64)
|
console.log('### receive tokens', this.receiveData.tokensBase64)
|
||||||
try {
|
try {
|
||||||
|
|
@ -1793,6 +1792,9 @@ page_container %}
|
||||||
const proofs = JSON.parse(tokenJson)
|
const proofs = JSON.parse(tokenJson)
|
||||||
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
||||||
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
||||||
|
|
||||||
|
// update UI
|
||||||
|
|
||||||
// HACK: we need to do this so the balance updates
|
// HACK: we need to do this so the balance updates
|
||||||
this.proofs = this.proofs.concat([])
|
this.proofs = this.proofs.concat([])
|
||||||
|
|
||||||
|
|
@ -1827,13 +1829,18 @@ page_container %}
|
||||||
},
|
},
|
||||||
|
|
||||||
sendTokens: async function () {
|
sendTokens: async function () {
|
||||||
|
/*
|
||||||
|
calls splitToSend, displays token and kicks off the spendableWorker
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
// keep firstProofs, send scndProofs
|
// keep firstProofs, send scndProofs and delete them (invalidate=true)
|
||||||
let {fristProofs, scndProofs} = await this.splitToSend(
|
let {fristProofs, scndProofs} = await this.splitToSend(
|
||||||
this.proofs,
|
this.proofs,
|
||||||
this.sendData.amount,
|
this.sendData.amount,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// update UI
|
||||||
this.sendData.tokens = scndProofs
|
this.sendData.tokens = scndProofs
|
||||||
console.log('### this.sendData.tokens', this.sendData.tokens)
|
console.log('### this.sendData.tokens', this.sendData.tokens)
|
||||||
this.sendData.tokensBase64 = btoa(
|
this.sendData.tokensBase64 = btoa(
|
||||||
|
|
@ -1846,33 +1853,19 @@ page_container %}
|
||||||
date: currentDateStr(),
|
date: currentDateStr(),
|
||||||
token: this.sendData.tokensBase64
|
token: this.sendData.tokensBase64
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// store "pending" outgoing tokens in history table
|
||||||
this.storehistoryTokens()
|
this.storehistoryTokens()
|
||||||
|
|
||||||
this.checkTokenSpendableWorker()
|
this.checkTokenSpendableWorker()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkFees: async function (payment_request) {
|
|
||||||
const payload = {
|
// /melt
|
||||||
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 () {
|
melt: async function () {
|
||||||
// todo: get fees from server and add to inputs
|
// todo: get fees from server and add to inputs
|
||||||
this.payInvoiceData.blocking = true
|
this.payInvoiceData.blocking = true
|
||||||
|
|
@ -1924,8 +1917,20 @@ page_container %}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
// delete spent tokens from db
|
// delete spent tokens from db
|
||||||
this.proofs = fristProofs
|
this.deleteProofs(scndProofs)
|
||||||
this.storeProofs()
|
|
||||||
|
// update UI
|
||||||
|
|
||||||
|
tokensBase64 = btoa(JSON.stringify(scndProofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: -amount,
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
amount: -amount,
|
amount: -amount,
|
||||||
bolt11: this.payInvoiceData.data.request,
|
bolt11: this.payInvoiceData.data.request,
|
||||||
|
|
@ -1953,13 +1958,93 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// /check
|
||||||
|
|
||||||
|
checkProofsSpendable: async function (proofs) {
|
||||||
|
/*
|
||||||
|
checks with the mint whether an array of proofs is still
|
||||||
|
spendable or already invalidated
|
||||||
|
*/
|
||||||
|
const payload = {
|
||||||
|
proofs: proofs.flat()
|
||||||
|
}
|
||||||
|
console.log('#### payload', JSON.stringify(payload))
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`/cashu/api/v1/${this.mintId}/check`,
|
||||||
|
'',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
|
||||||
|
// delete proofs from database if it is spent
|
||||||
|
let spentProofs = proofs.filter((p, pidx) => !data[pidx])
|
||||||
|
if (spentProofs.length) {
|
||||||
|
this.deleteProofs(spentProofs)
|
||||||
|
|
||||||
|
// update UI
|
||||||
|
tokensBase64 = btoa(JSON.stringify(spentProofs))
|
||||||
|
|
||||||
|
this.historyTokens.push({
|
||||||
|
status: 'paid',
|
||||||
|
amount: -this.sumProofs(spentProofs),
|
||||||
|
date: currentDateStr(),
|
||||||
|
token: tokensBase64
|
||||||
|
})
|
||||||
|
this.storehistoryTokens()
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// /checkfees
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// /keys
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
},
|
||||||
setInvoicePaid: async function (payment_hash) {
|
setInvoicePaid: async function (payment_hash) {
|
||||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||||
invoice.status = 'paid'
|
invoice.status = 'paid'
|
||||||
this.storeinvoicesCashu()
|
this.storeinvoicesCashu()
|
||||||
},
|
},
|
||||||
recheckInvoice: async function (payment_hash, verbose = true) {
|
checkInvoice: async function (payment_hash, verbose = true) {
|
||||||
console.log('### recheckInvoice.hash', payment_hash)
|
console.log('### checkInvoice.hash', payment_hash)
|
||||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||||
try {
|
try {
|
||||||
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
||||||
|
|
@ -1969,15 +2054,15 @@ page_container %}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
recheckPendingInvoices: async function () {
|
checkPendingInvoices: async function () {
|
||||||
for (const invoice of this.invoicesCashu) {
|
for (const invoice of this.invoicesCashu) {
|
||||||
if (invoice.status === 'pending' && invoice.sat > 0) {
|
if (invoice.status === 'pending' && invoice.amount > 0) {
|
||||||
this.recheckInvoice(invoice.hash, false)
|
this.checkInvoice(invoice.hash, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
recheckPendingTokens: async function () {
|
checkPendingTokens: async function () {
|
||||||
for (const token of this.historyTokens) {
|
for (const token of this.historyTokens) {
|
||||||
if (token.status === 'pending' && token.amount < 0) {
|
if (token.status === 'pending' && token.amount < 0) {
|
||||||
this.checkTokenSpendable(token.token, false)
|
this.checkTokenSpendable(token.token, false)
|
||||||
|
|
@ -1990,6 +2075,113 @@ page_container %}
|
||||||
this.storehistoryTokens()
|
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 = atob(token)
|
||||||
|
const proofs = JSON.parse(tokenJson)
|
||||||
|
let data = await this.checkProofsSpendable(proofs)
|
||||||
|
|
||||||
|
// iterate through response of form {0: true, 1: false, ...}
|
||||||
|
let paid = false
|
||||||
|
for (const [key, spendable] of Object.entries(data)) {
|
||||||
|
if (!spendable) {
|
||||||
|
this.setTokenPaid(token)
|
||||||
|
paid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paid) {
|
||||||
|
console.log('### token paid')
|
||||||
|
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Token paid',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('### token not paid yet')
|
||||||
|
if (verbose) {
|
||||||
|
this.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
color: 'grey',
|
||||||
|
message: 'Token still pending',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.sendData.tokens = token
|
||||||
|
}
|
||||||
|
return paid
|
||||||
|
},
|
||||||
|
|
||||||
|
////////////// 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.$q.notify({
|
||||||
|
timeout: 5000,
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Payment received',
|
||||||
|
position: 'top',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
handler: () => {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('not paid yet')
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
},
|
||||||
checkTokenSpendableWorker: async function () {
|
checkTokenSpendableWorker: async function () {
|
||||||
let nInterval = 0
|
let nInterval = 0
|
||||||
this.clearAllWorkers()
|
this.clearAllWorkers()
|
||||||
|
|
@ -2021,83 +2213,6 @@ page_container %}
|
||||||
}, 3000)
|
}, 3000)
|
||||||
},
|
},
|
||||||
|
|
||||||
checkTokenSpendable: async function (token, verbose = true) {
|
|
||||||
const tokenJson = atob(token)
|
|
||||||
const proofs = JSON.parse(tokenJson)
|
|
||||||
const payload = {
|
|
||||||
proofs: proofs.flat()
|
|
||||||
}
|
|
||||||
console.log('#### payload', JSON.stringify(payload))
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
`/cashu/api/v1/${this.mintId}/check`,
|
|
||||||
'',
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
// iterate through response of form {0: true, 1: false, ...}
|
|
||||||
let paid = false
|
|
||||||
for (const [key, spendable] of Object.entries(data)) {
|
|
||||||
if (!spendable) {
|
|
||||||
this.setTokenPaid(token)
|
|
||||||
paid = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (paid) {
|
|
||||||
console.log('### token paid')
|
|
||||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Token paid',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log('### token not paid yet')
|
|
||||||
if (verbose) {
|
|
||||||
this.$q.notify({
|
|
||||||
timeout: 5000,
|
|
||||||
color: 'grey',
|
|
||||||
message: 'Token still pending',
|
|
||||||
position: 'top',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
icon: 'close',
|
|
||||||
color: 'white',
|
|
||||||
handler: () => {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.sendData.tokens = token
|
|
||||||
}
|
|
||||||
return paid
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -2116,62 +2231,62 @@ page_container %}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
checkInvoice: function () {
|
// checkInvoice: function () {
|
||||||
console.log('#### checkInvoice')
|
// console.log('#### checkInvoice')
|
||||||
try {
|
// try {
|
||||||
const invoice = decode(this.payInvoiceData.data.request)
|
// const invoice = decode(this.payInvoiceData.data.request)
|
||||||
|
|
||||||
const cleanInvoice = {
|
// const cleanInvoice = {
|
||||||
msat: invoice.human_readable_part.amount,
|
// msat: invoice.human_readable_part.amount,
|
||||||
sat: invoice.human_readable_part.amount / 1000,
|
// sat: invoice.human_readable_part.amount / 1000,
|
||||||
fsat: LNbits.utils.formatSat(
|
// fsat: LNbits.utils.formatSat(
|
||||||
invoice.human_readable_part.amount / 1000
|
// invoice.human_readable_part.amount / 1000
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
_.each(invoice.data.tags, tag => {
|
// _.each(invoice.data.tags, tag => {
|
||||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
// if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||||
if (tag.description === 'payment_hash') {
|
// if (tag.description === 'payment_hash') {
|
||||||
cleanInvoice.hash = tag.value
|
// cleanInvoice.hash = tag.value
|
||||||
} else if (tag.description === 'description') {
|
// } else if (tag.description === 'description') {
|
||||||
cleanInvoice.description = tag.value
|
// cleanInvoice.description = tag.value
|
||||||
} else if (tag.description === 'expiry') {
|
// } else if (tag.description === 'expiry') {
|
||||||
var expireDate = new Date(
|
// var expireDate = new Date(
|
||||||
(invoice.data.time_stamp + tag.value) * 1000
|
// (invoice.data.time_stamp + tag.value) * 1000
|
||||||
)
|
// )
|
||||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
// cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||||
expireDate,
|
// expireDate,
|
||||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
// 'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||||
)
|
// )
|
||||||
cleanInvoice.expired = false // TODO
|
// cleanInvoice.expired = false // TODO
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.payInvoiceData.invoice = cleanInvoice
|
// this.payInvoiceData.invoice = cleanInvoice
|
||||||
})
|
// })
|
||||||
|
|
||||||
console.log(
|
// console.log(
|
||||||
'#### this.payInvoiceData.invoice',
|
// '#### this.payInvoiceData.invoice',
|
||||||
this.payInvoiceData.invoice
|
// this.payInvoiceData.invoice
|
||||||
)
|
// )
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
this.$q.notify({
|
// this.$q.notify({
|
||||||
timeout: 5000,
|
// timeout: 5000,
|
||||||
type: 'warning',
|
// type: 'warning',
|
||||||
message: 'Could not decode invoice',
|
// message: 'Could not decode invoice',
|
||||||
caption: error + '',
|
// caption: error + '',
|
||||||
position: 'top',
|
// position: 'top',
|
||||||
actions: [
|
// actions: [
|
||||||
{
|
// {
|
||||||
icon: 'close',
|
// icon: 'close',
|
||||||
color: 'white',
|
// color: 'white',
|
||||||
handler: () => {}
|
// handler: () => {}
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
})
|
// })
|
||||||
throw error
|
// throw error
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
|
|
||||||
////////////// STORAGE /////////////
|
////////////// STORAGE /////////////
|
||||||
|
|
||||||
|
|
@ -2335,8 +2450,9 @@ page_container %}
|
||||||
console.log('#### this.mintId', this.mintId)
|
console.log('#### this.mintId', this.mintId)
|
||||||
console.log('#### this.mintName', this.mintName)
|
console.log('#### this.mintName', this.mintName)
|
||||||
|
|
||||||
this.recheckPendingInvoices()
|
this.checkProofsSpendable(this.proofs)
|
||||||
this.recheckPendingTokens()
|
this.checkPendingInvoices()
|
||||||
|
this.checkPendingTokens()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,16 @@ from .models import Cashu
|
||||||
|
|
||||||
# --------- extension imports
|
# --------- extension imports
|
||||||
|
|
||||||
|
# WARNING: Do not set this to False in production! This will create
|
||||||
|
# tokens for free otherwise. This is for testing purposes only!
|
||||||
|
|
||||||
LIGHTNING = True
|
LIGHTNING = True
|
||||||
|
|
||||||
|
if not LIGHTNING:
|
||||||
|
logger.warning(
|
||||||
|
"Cashu: LIGHTNING is set False! That means that I will create ecash for free!"
|
||||||
|
)
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
############### LNBITS MINTS ###########
|
############### LNBITS MINTS ###########
|
||||||
########################################
|
########################################
|
||||||
|
|
@ -130,6 +137,28 @@ async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
|
||||||
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
||||||
|
|
||||||
|
|
||||||
|
@cashu_ext.get("/api/v1/{cashu_id}/keys/{idBase64Urlsafe}")
|
||||||
|
async def keyset_keys(
|
||||||
|
cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None)
|
||||||
|
) -> dict[int, str]:
|
||||||
|
"""
|
||||||
|
Get the public keys of the mint of a specificy keyset id.
|
||||||
|
The id is encoded in base64_urlsafe and needs to be converted back to
|
||||||
|
normal base64 before it can be processed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||||
|
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||||
|
keyset = ledger.get_keyset(keyset_id=id)
|
||||||
|
return keyset
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
||||||
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
||||||
"""Get the public keys of the mint"""
|
"""Get the public keys of the mint"""
|
||||||
|
|
@ -182,7 +211,7 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
||||||
async def mint_coins(
|
async def mint(
|
||||||
data: MintRequest,
|
data: MintRequest,
|
||||||
cashu_id: str = Query(None),
|
cashu_id: str = Query(None),
|
||||||
payment_hash: str = Query(None),
|
payment_hash: str = Query(None),
|
||||||
|
|
@ -197,6 +226,8 @@ async def mint_coins(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||||
|
|
||||||
if LIGHTNING:
|
if LIGHTNING:
|
||||||
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
||||||
db=ledger.db, hash=payment_hash
|
db=ledger.db, hash=payment_hash
|
||||||
|
|
@ -206,42 +237,55 @@ async def mint_coins(
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
detail="Mint does not know this invoice.",
|
detail="Mint does not know this invoice.",
|
||||||
)
|
)
|
||||||
if invoice.issued == True:
|
if invoice.issued:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||||
detail="Tokens already issued for this invoice.",
|
detail="Tokens already issued for this invoice.",
|
||||||
)
|
)
|
||||||
|
|
||||||
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
# set this invoice as issued
|
||||||
if total_requested > invoice.amount:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
|
||||||
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
|
||||||
)
|
|
||||||
|
|
||||||
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
|
||||||
|
|
||||||
if LIGHTNING and status.paid != True:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
|
||||||
|
|
||||||
promises = await ledger._generate_promises(
|
|
||||||
B_s=data.blinded_messages, keyset=keyset
|
|
||||||
)
|
|
||||||
assert len(promises), HTTPException(
|
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="No promises returned."
|
|
||||||
)
|
|
||||||
await ledger.crud.update_lightning_invoice(
|
await ledger.crud.update_lightning_invoice(
|
||||||
db=ledger.db, hash=payment_hash, issued=True
|
db=ledger.db, hash=payment_hash, issued=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
status: PaymentStatus = await check_transaction_status(
|
||||||
|
cashu.wallet, payment_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
||||||
|
if total_requested > invoice.amount:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||||
|
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not status.paid:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||||
|
)
|
||||||
|
|
||||||
|
promises = await ledger._generate_promises(
|
||||||
|
B_s=data.blinded_messages, keyset=keyset
|
||||||
|
)
|
||||||
|
return promises
|
||||||
|
except (Exception, HTTPException) as e:
|
||||||
|
logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}")
|
||||||
|
# unset issued flag because something went wrong
|
||||||
|
await ledger.crud.update_lightning_invoice(
|
||||||
|
db=ledger.db, hash=payment_hash, issued=False
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=getattr(e, "status_code")
|
||||||
|
or HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail=str(e) or getattr(e, "detail"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# only used for testing when LIGHTNING=false
|
||||||
|
promises = await ledger._generate_promises(
|
||||||
|
B_s=data.blinded_messages, keyset=keyset
|
||||||
|
)
|
||||||
return promises
|
return promises
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
||||||
|
|
@ -285,26 +329,30 @@ async def melt_coins(
|
||||||
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
||||||
)
|
)
|
||||||
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
||||||
await pay_invoice(
|
try:
|
||||||
wallet_id=cashu.wallet,
|
await pay_invoice(
|
||||||
payment_request=invoice,
|
wallet_id=cashu.wallet,
|
||||||
description=f"Pay cashu invoice",
|
payment_request=invoice,
|
||||||
extra={"tag": "cashu", "cashu_name": cashu.name},
|
description=f"Pay cashu invoice",
|
||||||
)
|
extra={"tag": "cashu", "cashu_name": cashu.name},
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
|
||||||
)
|
|
||||||
status: PaymentStatus = await check_transaction_status(
|
|
||||||
cashu.wallet, invoice_obj.payment_hash
|
|
||||||
)
|
|
||||||
if status.paid == True:
|
|
||||||
logger.debug(
|
|
||||||
f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}"
|
|
||||||
)
|
)
|
||||||
await ledger._invalidate_proofs(proofs)
|
except Exception as e:
|
||||||
else:
|
logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}")
|
||||||
logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}")
|
raise e
|
||||||
|
finally:
|
||||||
|
logger.debug(
|
||||||
|
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
||||||
|
)
|
||||||
|
status: PaymentStatus = await check_transaction_status(
|
||||||
|
cashu.wallet, invoice_obj.payment_hash
|
||||||
|
)
|
||||||
|
if status.paid == True:
|
||||||
|
logger.debug(
|
||||||
|
f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}"
|
||||||
|
)
|
||||||
|
await ledger._invalidate_proofs(proofs)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}")
|
logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue