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")
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 = [
{
@ -24,11 +43,12 @@ def cashu_renderer():
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_api import * # noqa
def cashu_start():
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))

View file

@ -1,7 +1,9 @@
{
"name": "Cashu Ecash",
"short_description": "Ecash mints with LN peg in/out",
"short_description": "Ecash mint and wallet",
"icon": "approval",
"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 random
import time
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.networks import NETWORKS
@ -13,6 +14,49 @@ from . import db
from .core.base import Invoice
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:
cashu_id = urlsafe_short_hash()
@ -120,9 +164,15 @@ async def get_promises(cashu_id) -> Optional[Cashu]:
return Promises(**row) if row else None
async def get_proofs_used(cashu_id):
rows = await db.fetchall(
"SELECT secret from cashu.proofs_used WHERE cashu_id = ?", (cashu_id,)
async def get_proofs_used(
db: Database,
conn: Optional[Connection] = None,
):
rows = await (conn or db).fetchall(
"""
SELECT secret from cashu.proofs_used
"""
)
return [row[0] for row in rows]
@ -184,3 +234,62 @@ async def update_lightning_invoice(cashu_id: str, hash: str, issued: bool):
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):
"""
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)
);
"""
)
# this extension will use the migration_module module cashu.mint.migrations (see config.json)

View file

@ -9,6 +9,21 @@ from lnbits.tasks import internal_invoice_queue, register_invoice_listener
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():
invoice_queue = asyncio.Queue()

View file

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

View file

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

View file

@ -28,7 +28,8 @@ from .crud import (
store_promise,
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 .models import (
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)
async def keys(cashu_id: str = Query(False)):
"""Get the public keys of the mint"""
mint = await get_cashu(cashu_id)
if mint is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
)
return get_pubkeys(mint.prvkey)
# @cashu_ext.get("/api/v1/cashu/{cashu_id}/keys", status_code=HTTPStatus.OK)
# async def keys(cashu_id: str = Query(False)):
# """Get the public keys of the mint"""
# mint = await get_cashu(cashu_id)
# if mint is None:
# raise HTTPException(
# status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
# )
# return get_pubkeys(mint.prvkey)
@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)
print("### resp", json.dumps(resp, default=vars))
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