Merge pull request #1237 from lnbits/fix/cashu/timeout

cashu: refactor wallet
This commit is contained in:
calle 2022-12-24 14:02:13 +01:00 committed by GitHub
commit 13f0159c15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 469 additions and 305 deletions

View file

@ -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>

View file

@ -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(