lnbits migrations work

This commit is contained in:
callebtc 2022-10-12 23:16:39 +02:00
parent 7111639446
commit c36f7a48aa
8 changed files with 358 additions and 158 deletions

View file

@ -9,7 +9,26 @@ from lnbits.tasks import catch_everything_and_restart
db = Database("ext_cashu") db = Database("ext_cashu")
cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"]) import sys
sys.path.append("/Users/cc/git/cashu")
from cashu.mint.ledger import Ledger
from .crud import LedgerCrud
# db = Database("ext_cashu", LNBITS_DATA_FOLDER)
ledger = Ledger(
db=db,
# seed=MINT_PRIVATE_KEY,
seed="asd",
derivation_path="0/0/0/1",
crud=LedgerCrud,
)
cashu_ext: APIRouter = APIRouter(prefix="/api/v1/cashu", tags=["cashu"])
# from cashu.mint.router import router as cashu_router
# cashu_ext.include_router(router=cashu_router)
cashu_static_files = [ cashu_static_files = [
{ {
@ -24,11 +43,12 @@ def cashu_renderer():
return template_renderer(["lnbits/extensions/cashu/templates"]) return template_renderer(["lnbits/extensions/cashu/templates"])
from .tasks import wait_for_paid_invoices from .tasks import wait_for_paid_invoices, startup_cashu_mint
from .views import * # noqa from .views import * # noqa
from .views_api import * # noqa from .views_api import * # noqa
def cashu_start(): def cashu_start():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(startup_cashu_mint))
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View file

@ -1,7 +1,9 @@
{ {
"name": "Cashu Ecash", "name": "Cashu Ecash",
"short_description": "Ecash mints with LN peg in/out", "short_description": "Ecash mint and wallet",
"icon": "approval", "icon": "approval",
"contributors": ["arcbtc", "calle", "vlad"], "contributors": ["arcbtc", "calle", "vlad"],
"hidden": false "hidden": false,
"migration_module": "cashu.mint.migrations",
"db_name": "cashu"
} }

View file

@ -1,7 +1,8 @@
import os import os
import random import random
import time
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from typing import List, Optional, Union from typing import List, Optional, Union, Any
from embit import bip32, bip39, ec, script from embit import bip32, bip39, ec, script
from embit.networks import NETWORKS from embit.networks import NETWORKS
@ -13,6 +14,49 @@ from . import db
from .core.base import Invoice from .core.base import Invoice
from .models import Cashu, Pegs, Promises, Proof from .models import Cashu, Pegs, Promises, Proof
from cashu.core.base import MintKeyset
from lnbits.db import Database, Connection
class LedgerCrud:
"""
Database interface for Cashu mint.
This class needs to be overloaded by any app that imports the Cashu mint.
"""
async def get_keyset(*args, **kwags):
return await get_keyset(*args, **kwags)
async def get_lightning_invoice(*args, **kwags):
return await get_lightning_invoice(*args, **kwags)
async def get_proofs_used(*args, **kwags):
return await get_proofs_used(*args, **kwags)
async def invalidate_proof(*args, **kwags):
return await invalidate_proof(*args, **kwags)
async def store_keyset(*args, **kwags):
return await store_keyset(*args, **kwags)
async def store_lightning_invoice(*args, **kwags):
return await store_lightning_invoice(*args, **kwags)
async def store_promise(*args, **kwags):
return await store_promise(*args, **kwags)
async def update_lightning_invoice(*args, **kwags):
return await update_lightning_invoice(*args, **kwags)
async def create_cashu(wallet_id: str, data: Cashu) -> Cashu: async def create_cashu(wallet_id: str, data: Cashu) -> Cashu:
cashu_id = urlsafe_short_hash() cashu_id = urlsafe_short_hash()
@ -120,9 +164,15 @@ async def get_promises(cashu_id) -> Optional[Cashu]:
return Promises(**row) if row else None return Promises(**row) if row else None
async def get_proofs_used(cashu_id): async def get_proofs_used(
rows = await db.fetchall( db: Database,
"SELECT secret from cashu.proofs_used WHERE cashu_id = ?", (cashu_id,) conn: Optional[Connection] = None,
):
rows = await (conn or db).fetchall(
"""
SELECT secret from cashu.proofs_used
"""
) )
return [row[0] for row in rows] return [row[0] for row in rows]
@ -184,3 +234,62 @@ async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool):
hash, hash,
), ),
) )
##############################
######### KEYSETS ############
##############################
async def store_keyset(
keyset: MintKeyset,
db: Database = None,
conn: Optional[Connection] = None,
):
await (conn or db).execute( # type: ignore
"""
INSERT INTO cashu.keysets
(id, derivation_path, valid_from, valid_to, first_seen, active, version)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
keyset.id,
keyset.derivation_path,
keyset.valid_from or db.timestamp_now,
keyset.valid_to or db.timestamp_now,
keyset.first_seen or db.timestamp_now,
True,
keyset.version,
),
)
async def get_keyset(
id: str = None,
derivation_path: str = "",
db: Database = None,
conn: Optional[Connection] = None,
):
clauses = []
values: List[Any] = []
clauses.append("active = ?")
values.append(True)
if id:
clauses.append("id = ?")
values.append(id)
if derivation_path:
clauses.append("derivation_path = ?")
values.append(derivation_path)
where = ""
if clauses:
where = f"WHERE {' AND '.join(clauses)}"
rows = await (conn or db).fetchall( # type: ignore
f"""
SELECT * from cashu.keysets
{where}
""",
tuple(values),
)
return [MintKeyset.from_row(row) for row in rows]

View file

@ -1,79 +1 @@
async def m001_initial(db): # this extension will use the migration_module module cashu.mint.migrations (see config.json)
"""
Initial cashu table.
"""
await db.execute(
"""
CREATE TABLE cashu.cashu (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
tickershort TEXT DEFAULT 'sats',
fraction BOOL,
maxsats INT,
coins INT,
prvkey TEXT NOT NULL,
pubkey TEXT NOT NULL
);
"""
)
"""
Initial cashus table.
"""
await db.execute(
"""
CREATE TABLE cashu.pegs (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
inout BOOL NOT NULL,
amount INT
);
"""
)
"""
Initial cashus table.
"""
await db.execute(
"""
CREATE TABLE cashu.promises (
id TEXT PRIMARY KEY,
amount INT,
B_b TEXT NOT NULL,
C_b TEXT NOT NULL,
cashu_id TEXT NOT NULL,
UNIQUE (B_b)
);
"""
)
"""
Initial cashus table.
"""
await db.execute(
"""
CREATE TABLE cashu.proofs_used (
id TEXT PRIMARY KEY,
amount INT,
C TEXT NOT NULL,
secret TEXT NOT NULL,
cashu_id TEXT NOT NULL
);
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS cashu.invoices (
cashu_id TEXT NOT NULL,
amount INTEGER NOT NULL,
pr TEXT NOT NULL,
hash TEXT NOT NULL,
issued BOOL NOT NULL,
UNIQUE (hash)
);
"""
)

View file

@ -9,6 +9,21 @@ from lnbits.tasks import internal_invoice_queue, register_invoice_listener
from .crud import get_cashu from .crud import get_cashu
import sys
sys.path.append("/Users/cc/git/cashu")
# from cashu.mint import migrations
# from cashu.core.migrations import migrate_databases
from . import db, ledger
async def startup_cashu_mint():
# await migrate_databases(db, migrations)
await ledger.load_used_proofs()
await ledger.init_keysets()
print(ledger.get_keyset())
pass
async def wait_for_paid_invoices(): async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue() invoice_queue = asyncio.Queue()

View file

@ -120,7 +120,7 @@
emit-value emit-value
v-model="formDialog.data.wallet" v-model="formDialog.data.wallet"
:options="g.user.walletOptions" :options="g.user.walletOptions"
label="Wallet *" label="Cashu wallet *"
></q-select> ></q-select>
<q-toggle <q-toggle
v-model="toggleAdvanced" v-model="toggleAdvanced"
@ -156,7 +156,7 @@
dense dense
v-model.trim="formDialog.data.tickershort" v-model.trim="formDialog.data.tickershort"
label="Ticker shorthand" label="Ticker shorthand"
placeholder="CC" placeholder="sats"
# #
></q-input> ></q-input>
</div> </div>
@ -229,7 +229,7 @@
{ {
name: 'wallet', name: 'wallet',
align: 'left', align: 'left',
label: 'Wallet', label: 'Cashu wallet',
field: 'wallet' field: 'wallet'
}, },
{ {

View file

@ -1,5 +1,5 @@
{% extends "public.html" %} {% block toolbar_title %} {% raw %} {{name}} Wallet {% extends "public.html" %} {% block toolbar_title %} {% raw %} {{name}} Cashu
{% endraw %} {% endblock %} {% block footer %}{% endblock %} {% block wallet {% endraw %} {% endblock %} {% block footer %}{% endblock %} {% block
page_container %} page_container %}
<q-page-container> <q-page-container>
<q-page> <q-page>
@ -14,9 +14,8 @@ page_container %}
rounded rounded
color="secondary" color="secondary"
class="full-width" class="full-width"
@click="showBuyTokensDialog" @click="showInvoicesDialog"
>Buy tokens >Create invoice
<h5 class="text-caption q-ml-sm q-mb-none">(with sats)</h5>
</q-btn> </q-btn>
</div> </div>
<div class="col-6"> <div class="col-6">
@ -34,8 +33,7 @@ page_container %}
rounded rounded
color="secondary" color="secondary"
class="full-width" class="full-width"
>Sell tokens >Pay invoice
<h5 class="text-caption q-ml-sm q-mb-none">(for sats)</h5>
</q-btn> </q-btn>
</div> </div>
</div> </div>
@ -115,11 +113,11 @@ page_container %}
<q-table <q-table
dense dense
flat flat
:data="buyOrders" :data="invoicesCashu"
:columns="buysTable.columns" :columns="invoicesTable.columns"
:pagination.sync="buysTable.pagination" :pagination.sync="invoicesTable.pagination"
no-data-label="No buys made yet" no-data-label="No invoices made yet"
:filter="buysTable.filter" :filter="invoicesTable.filter"
> >
{% raw %} {% raw %}
<template v-slot:body="props"> <template v-slot:body="props">
@ -137,7 +135,7 @@ page_container %}
size="lg" size="lg"
color="secondary" color="secondary"
class="q-mr-md cursor-pointer" class="q-mr-md cursor-pointer"
@click="recheckBuyOrder(props.row.hash)" @click="recheckInvoice(props.row.hash)"
> >
Recheck Recheck
</q-badge> </q-badge>
@ -182,11 +180,15 @@ page_container %}
active-class="px-0" active-class="px-0"
indicator-color="transparent" indicator-color="transparent"
> >
<q-tab icon="arrow_right" label="Buy" @click="showBuyTokensDialog"> <q-tab
icon="arrow_right"
label="Create Invoice"
@click="showInvoicesDialog"
>
</q-tab> </q-tab>
<q-tab icon="arrow_downward" label="Receive"></q-tab> <q-tab icon="arrow_downward" label="Receive Tokens"></q-tab>
<q-tab icon="arrow_upward" label="Send"></q-tab> <q-tab icon="arrow_upward" label="Send Token"></q-tab>
<q-tab icon="arrow_right" label="Sell"> </q-tab> <q-tab icon="arrow_right" label="Pay Invoice"> </q-tab>
</q-tabs> </q-tabs>
<q-dialog v-model="disclaimerDialog.show"> <q-dialog v-model="disclaimerDialog.show">
@ -214,7 +216,7 @@ page_container %}
<q-dialog v-model="showInvoiceDetails" position="top"> <q-dialog v-model="showInvoiceDetails" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="!buyData.bolt11"> <div v-if="!invoiceData.bolt11">
<div class="row items-center no-wrap q-mb-sm"> <div class="row items-center no-wrap q-mb-sm">
<div class="col-12"> <div class="col-12">
<span class="text-subtitle1" <span class="text-subtitle1"
@ -225,7 +227,7 @@ page_container %}
<q-input <q-input
filled filled
dense dense
v-model.number="buyData.amount" v-model.number="invoiceData.amount"
label="Amount (sats) *" label="Amount (sats) *"
type="number" type="number"
class="q-mb-lg" class="q-mb-lg"
@ -234,15 +236,15 @@ page_container %}
<q-input <q-input
filled filled
dense dense
v-model.trim="buyData.memo" v-model.trim="invoiceData.memo"
label="Memo" label="Memo"
></q-input> ></q-input>
</div> </div>
<div v-else class="text-center q-mb-lg"> <div v-else class="text-center q-mb-lg">
<a :href="'lightning:' + buyData.bolt11"> <a :href="'lightning:' + invoiceData.bolt11">
<q-responsive :ratio="1" class="q-mx-xl"> <q-responsive :ratio="1" class="q-mx-xl">
<qrcode <qrcode
:value="buyData.bolt11" :value="invoiceData.bolt11"
:options="{width: 340}" :options="{width: 340}"
class="rounded-borders" class="rounded-borders"
> >
@ -252,8 +254,8 @@ page_container %}
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn
v-if="buyData.bolt11" v-if="invoiceData.bolt11"
@click="copyText(buyData.bolt11)" @click="copyText(invoiceData.bolt11)"
outline outline
color="grey" color="grey"
>Copy invoice</q-btn >Copy invoice</q-btn
@ -449,8 +451,8 @@ page_container %}
mintId: '', mintId: '',
mintName: '', mintName: '',
keys: '', keys: '',
buyOrders: [], invoicesCashu: [],
buyData: { invoiceData: {
amount: 0, amount: 0,
memo: '', memo: '',
bolt11: '', bolt11: '',
@ -507,7 +509,7 @@ page_container %}
} }
}, },
payments: [], payments: [],
buysTable: { invoicesTable: {
columns: [ columns: [
{ {
name: 'status', name: 'status',
@ -873,17 +875,17 @@ page_container %}
}, },
/////////////////////////////////// WALLET /////////////////////////////////// /////////////////////////////////// WALLET ///////////////////////////////////
showBuyTokensDialog: async function () { showInvoicesDialog: async function () {
console.log('##### showBuyTokensDialog') console.log('##### showInvoicesDialog')
this.buyData.amount = 0 this.invoiceData.amount = 0
this.buyData.bolt11 = '' this.invoiceData.bolt11 = ''
this.buyData.hash = '' this.invoiceData.hash = ''
this.buyData.memo = '' this.invoiceData.memo = ''
this.showInvoiceDetails = true this.showInvoiceDetails = true
}, },
showInvoiceDialog: function (data) { showInvoiceDialog: function (data) {
this.buyData = _.clone(data) this.invoiceData = _.clone(data)
this.showInvoiceDetails = true this.showInvoiceDetails = true
}, },
@ -911,20 +913,20 @@ page_container %}
try { try {
const {data} = await LNbits.api.request( const {data} = await LNbits.api.request(
'GET', 'GET',
`/cashu/api/v1/cashu/${this.mintId}/mint?amount=${this.buyData.amount}` `/cashu/api/v1/cashu/${this.mintId}/mint?amount=${this.invoiceData.amount}`
) )
console.log('### data', data) console.log('### data', data)
this.buyData.bolt11 = data.pr this.invoiceData.bolt11 = data.pr
this.buyData.hash = data.hash this.invoiceData.hash = data.hash
this.buyOrders.push({ this.invoicesCashu.push({
..._.clone(this.buyData), ..._.clone(this.invoiceData),
date: currentDateStr(), date: currentDateStr(),
status: 'pending' status: 'pending'
}) })
this.storeBuyOrders() this.storeinvoicesCashu()
const amounts = splitAmount(this.buyData.amount) const amounts = splitAmount(this.invoiceData.amount)
await this.requestTokens(amounts, this.buyData.hash) await this.requestTokens(amounts, this.invoiceData.hash)
this.tab = 'orders' this.tab = 'orders'
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -933,12 +935,12 @@ page_container %}
}, },
checkXXXXXX: async function () { checkXXXXXX: async function () {
for (const tokenBuy of this.buyOrders) { for (const invoice of this.invoicesCashu) {
if (tokenBuy.status === 'pending') { if (invoice.status === 'pending') {
try { try {
const {data} = await LNbits.api.request( const {data} = await LNbits.api.request(
'POST', 'POST',
`/cashu/api/v1/cashu/${this.mintId}/mint?payment_hash=${tokenBuy.hash}`, `/cashu/api/v1/cashu/${this.mintId}/mint?payment_hash=${invoice.hash}`,
'', '',
{ {
blinded_messages: [] blinded_messages: []
@ -953,10 +955,10 @@ page_container %}
} }
}, },
recheckBuyOrder: async function (hash) { recheckInvoice: async function (hash) {
console.log('### recheckBuyOrder.hash', hash) console.log('### recheckInvoice.hash', hash)
const tokens = this.tokens.find(bt => bt.hash === hash) const tokens = this.tokens.find(bt => bt.hash === hash)
console.log('### recheckBuyOrder.tokens', tokens) console.log('### recheckInvoice.tokens', tokens)
if (!tokens) { if (!tokens) {
console.error('####### no token for hash', hash) console.error('####### no token for hash', hash)
return return
@ -970,9 +972,9 @@ page_container %}
tokens.status = 'paid' tokens.status = 'paid'
this.storeTokens() this.storeTokens()
const buyOrder = this.buyOrders.find(bo => bo.hash === hash) const invoice = this.invoicesCashu.find(bo => bo.hash === hash)
buyOrder.status = 'paid' invoice.status = 'paid'
this.storeBuyOrders() this.storeinvoicesCashu()
} }
}, },
@ -1261,8 +1263,11 @@ page_container %}
localStorage.setItem('cashu.keys', JSON.stringify(data)) localStorage.setItem('cashu.keys', JSON.stringify(data))
}, },
storeBuyOrders: function () { storeinvoicesCashu: function () {
localStorage.setItem('cashu.buyOrders', JSON.stringify(this.buyOrders)) localStorage.setItem(
'cashu.invoicesCashu',
JSON.stringify(this.invoicesCashu)
)
}, },
storeTokens: function () { storeTokens: function () {
localStorage.setItem( localStorage.setItem(
@ -1285,8 +1290,8 @@ page_container %}
!params.get('tsh') && !params.get('tsh') &&
!this.$q.localStorage.getItem('cashu.tickershort') !this.$q.localStorage.getItem('cashu.tickershort')
) { ) {
this.$q.localStorage.set('cashu.tickershort', 'CE') this.$q.localStorage.set('cashu.tickershort', 'sats')
this.tickershort = 'CE' this.tickershort = 'sats'
} else if (params.get('tsh')) { } else if (params.get('tsh')) {
this.$q.localStorage.set('cashu.tickershort', params.get('tsh')) this.$q.localStorage.set('cashu.tickershort', params.get('tsh'))
this.tickershort = params.get('tsh') this.tickershort = params.get('tsh')
@ -1322,11 +1327,11 @@ page_container %}
this.keys = JSON.parse(keysJson) this.keys = JSON.parse(keysJson)
} }
this.buyOrders = JSON.parse( this.invoicesCashu = JSON.parse(
localStorage.getItem('cashu.buyOrders') || '[]' localStorage.getItem('cashu.invoicesCashu') || '[]'
) )
this.tokens = JSON.parse(localStorage.getItem('cashu.tokens') || '[]') this.tokens = JSON.parse(localStorage.getItem('cashu.tokens') || '[]')
console.log('### buyOrders', this.buyOrders) console.log('### invoicesCashu', this.invoicesCashu)
console.table('### tokens', this.tokens) console.table('### tokens', this.tokens)
console.log('#### this.mintId', this.mintId) console.log('#### this.mintId', this.mintId)
console.log('#### this.mintName', this.mintName) console.log('#### this.mintName', this.mintName)

View file

@ -28,7 +28,8 @@ from .crud import (
store_promise, store_promise,
update_lightning_invoice, update_lightning_invoice,
) )
from .ledger import mint, request_mint
# from .ledger import mint, request_mint
from .mint import generate_promises, get_pubkeys, melt, split from .mint import generate_promises, get_pubkeys, melt, split
from .models import ( from .models import (
Cashu, Cashu,
@ -207,15 +208,15 @@ async def api_cashu_check_invoice(cashu_id: str, payment_hash: str):
######################################## ########################################
@cashu_ext.get("/api/v1/cashu/{cashu_id}/keys", status_code=HTTPStatus.OK) # @cashu_ext.get("/api/v1/cashu/{cashu_id}/keys", status_code=HTTPStatus.OK)
async def keys(cashu_id: str = Query(False)): # async def keys(cashu_id: str = Query(False)):
"""Get the public keys of the mint""" # """Get the public keys of the mint"""
mint = await get_cashu(cashu_id) # mint = await get_cashu(cashu_id)
if mint is None: # if mint is None:
raise HTTPException( # raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." # status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
) # )
return get_pubkeys(mint.prvkey) # return get_pubkeys(mint.prvkey)
@cashu_ext.get("/api/v1/cashu/{cashu_id}/mint") @cashu_ext.get("/api/v1/cashu/{cashu_id}/mint")
@ -355,3 +356,129 @@ async def split_proofs(payload: SplitRequest, cashu_id: str = Query(None)):
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises) resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
print("### resp", json.dumps(resp, default=vars)) print("### resp", json.dumps(resp, default=vars))
return resp return resp
##################################################################
##################################################################
# CASHU LIB
##################################################################
from typing import Dict, List, Union
from fastapi import APIRouter
from secp256k1 import PublicKey
from cashu.core.base import (
BlindedSignature,
CheckFeesRequest,
CheckFeesResponse,
CheckRequest,
GetMeltResponse,
GetMintResponse,
MeltRequest,
MintRequest,
PostSplitResponse,
SplitRequest,
)
from cashu.core.errors import CashuError
from . import db, ledger
@cashu_ext.get("/keys")
async def keys() -> dict[int, str]:
"""Get the public keys of the mint"""
return ledger.get_keyset()
@cashu_ext.get("/keysets")
async def keysets() -> dict[str, list[str]]:
"""Get all active keysets of the mint"""
return {"keysets": await ledger.keysets.get_ids()}
@cashu_ext.get("/mint")
async def request_mint(amount: int = 0) -> GetMintResponse:
"""
Request minting of new tokens. The mint responds with a Lightning invoice.
This endpoint can be used for a Lightning invoice UX flow.
Call `POST /mint` after paying the invoice.
"""
payment_request, payment_hash = await ledger.request_mint(amount)
print(f"Lightning invoice: {payment_request}")
resp = GetMintResponse(pr=payment_request, hash=payment_hash)
return resp
@cashu_ext.post("/mint")
async def mint(
payloads: MintRequest,
payment_hash: Union[str, None] = None,
) -> Union[List[BlindedSignature], CashuError]:
"""
Requests the minting of tokens belonging to a paid payment request.
Call this endpoint after `GET /mint`.
"""
amounts = []
B_s = []
for payload in payloads.blinded_messages:
amounts.append(payload.amount)
B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True))
try:
promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash)
return promises
except Exception as exc:
return CashuError(error=str(exc))
@cashu_ext.post("/melt")
async def melt(payload: MeltRequest) -> GetMeltResponse:
"""
Requests tokens to be destroyed and sent out via Lightning.
"""
ok, preimage = await ledger.melt(payload.proofs, payload.invoice)
resp = GetMeltResponse(paid=ok, preimage=preimage)
return resp
@cashu_ext.post("/check")
async def check_spendable(payload: CheckRequest) -> Dict[int, bool]:
"""Check whether a secret has been spent already or not."""
return await ledger.check_spendable(payload.proofs)
@cashu_ext.post("/checkfees")
async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
"""
Responds with the fees necessary to pay a Lightning invoice.
Used by wallets for figuring out the fees they need to supply.
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
"""
fees_msat = await ledger.check_fees(payload.pr)
return CheckFeesResponse(fee=fees_msat / 1000)
@cashu_ext.post("/split")
async def split(
payload: SplitRequest,
) -> Union[CashuError, PostSplitResponse]:
"""
Requetst a set of tokens with amount "total" to be split into two
newly minted sets with amount "split" and "total-split".
"""
proofs = payload.proofs
amount = payload.amount
outputs = payload.outputs.blinded_messages if payload.outputs else None
# backwards compatibility with clients < v0.2.2
assert outputs, Exception("no outputs provided.")
try:
split_return = await ledger.split(proofs, amount, outputs)
except Exception as exc:
return CashuError(error=str(exc))
if not split_return:
return CashuError(error="there was an error with the split")
frst_promises, scnd_promises = split_return
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
return resp