Merge pull request #1103 from lnbits/cashu_rewrite_wallet

Cashu rewrite wallet
This commit is contained in:
calle 2022-11-23 13:48:28 +01:00 committed by GitHub
commit 9fdbaf92f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1941 additions and 703 deletions

View file

@ -97,3 +97,8 @@ ECLAIR_PASS=eclairpw
# Enter /api in LightningTipBot to get your key # Enter /api in LightningTipBot to get your key
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
LNTIPS_API_ENDPOINT=https://ln.tips LNTIPS_API_ENDPOINT=https://ln.tips
# Cashu Mint
# Use a long-enough random (!) private key.
# Once set, you cannot change this key as for now.
CASHU_PRIVATE_KEY="SuperSecretPrivateKey"

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
from environs import Env # type: ignore
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@ -20,10 +21,12 @@ cashu_static_files = [
] ]
from cashu.mint.ledger import Ledger from cashu.mint.ledger import Ledger
env = Env()
env.read_env()
ledger = Ledger( ledger = Ledger(
db=db, db=db,
# seed=MINT_PRIVATE_KEY, seed=env.str("CASHU_PRIVATE_KEY", default="SuperSecretPrivateKey"),
seed="asd",
derivation_path="0/0/0/1", derivation_path="0/0/0/1",
) )

View file

@ -1,7 +1,7 @@
{ {
"name": "Cashu Ecash", "name": "Cashu",
"short_description": "Ecash mint and wallet", "short_description": "Ecash mint and wallet",
"icon": "approval", "icon": "account_balance",
"contributors": ["arcbtc", "calle", "vlad"], "contributors": ["calle", "vlad", "arcbtc"],
"hidden": false "hidden": false
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "Cashu Ecash", "name": "Cashu",
"short_description": "Ecash mints with LN peg in/out", "short_description": "Ecash mints with LN peg in/out",
"icon": "approval", "icon": "account_balance",
"contributors": ["arcbtc", "calle"], "contributors": ["calle", "vlad", "arcbtc"],
"hidden": true "hidden": true
} }

View file

@ -12,7 +12,8 @@ async def m001_initial(db):
fraction BOOL, fraction BOOL,
maxsats INT, maxsats INT,
coins INT, coins INT,
keyset_id TEXT NOT NULL keyset_id TEXT NOT NULL,
issued_sat INT
); );
""" """
) )

View file

@ -19,20 +19,21 @@ async function hashToCurve(secretMessage) {
return point return point
} }
async function step1Bob(secretMessage) { async function step1Alice(secretMessage) {
secretMessage = nobleSecp256k1.utils.bytesToHex(secretMessage) // todo: document & validate `secretMessage` format
secretMessage = new TextEncoder().encode(secretMessage); secretMessage = uint8ToBase64.encode(secretMessage)
secretMessage = new TextEncoder().encode(secretMessage)
const Y = await hashToCurve(secretMessage) const Y = await hashToCurve(secretMessage)
const randomBlindingFactor = bytesToNumber( const rpk = nobleSecp256k1.utils.randomPrivateKey()
nobleSecp256k1.utils.randomPrivateKey() const r = bytesToNumber(rpk)
) const P = nobleSecp256k1.Point.fromPrivateKey(r)
const P = nobleSecp256k1.Point.fromPrivateKey(randomBlindingFactor)
const B_ = Y.add(P) const B_ = Y.add(P)
return {B_: B_.toHex(true), randomBlindingFactor} return {B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(rpk)}
} }
function step3Bob(C_, r, A) { function step3Alice(C_, r, A) {
const rInt = BigInt(r) // const rInt = BigInt(r)
const rInt = bytesToNumber(r)
const C = C_.subtract(A.multiply(rInt)) const C = C_.subtract(A.multiply(rInt))
return C return C
} }

View file

@ -15,7 +15,6 @@ async def startup_cashu_mint():
await migrate_databases(db, migrations) await migrate_databases(db, migrations)
await ledger.load_used_proofs() await ledger.load_used_proofs()
await ledger.init_keysets() await ledger.init_keysets()
print(ledger.get_keyset())
pass pass

View file

@ -5,10 +5,10 @@
:content-inset-level="0.5" :content-inset-level="0.5"
> >
<q-btn flat label="Swagger API" type="a" href="../docs#/cashu"></q-btn> <q-btn flat label="Swagger API" type="a" href="../docs#/cashu"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List TPoS"> <!-- <q-expansion-item group="api" dense expand-separator label="List TPoS">
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-blue">GET</span> /cashu/api/v1/cashus</code> <code><span class="text-blue">GET</span> /cashu/api/v1/mints</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
@ -18,7 +18,7 @@
<code>[&lt;cashu_object&gt;, ...]</code> <code>[&lt;cashu_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X GET {{ request.base_url }}cashu/api/v1/cashus -H "X-Api-Key: >curl -X GET {{ request.base_url }}cashu/api/v1/mints -H "X-Api-Key:
&lt;invoice_key&gt;" &lt;invoice_key&gt;"
</code> </code>
</q-card-section> </q-card-section>
@ -27,7 +27,7 @@
<q-expansion-item group="api" dense expand-separator label="Create a TPoS"> <q-expansion-item group="api" dense expand-separator label="Create a TPoS">
<q-card> <q-card>
<q-card-section> <q-card-section>
<code><span class="text-green">POST</span> /cashu/api/v1/cashus</code> <code><span class="text-green">POST</span> /cashu/api/v1/mints</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5> <h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
@ -43,7 +43,7 @@
> >
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X POST {{ request.base_url }}cashu/api/v1/cashus -d '{"name": >curl -X POST {{ request.base_url }}cashu/api/v1/mints -d '{"name":
&lt;string&gt;, "currency": &lt;string&gt;}' -H "Content-type: &lt;string&gt;, "currency": &lt;string&gt;}' -H "Content-type:
application/json" -H "X-Api-Key: &lt;admin_key&gt;" application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code> </code>
@ -62,7 +62,7 @@
<q-card-section> <q-card-section>
<code <code
><span class="text-pink">DELETE</span> ><span class="text-pink">DELETE</span>
/cashu/api/v1/cashus/&lt;cashu_id&gt;</code /cashu/api/v1/mints/&lt;cashu_id&gt;</code
> >
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5> <h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br /> <code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
@ -71,10 +71,10 @@
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5> <h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code <code
>curl -X DELETE {{ request.base_url >curl -X DELETE {{ request.base_url
}}cashu/api/v1/cashus/&lt;cashu_id&gt; -H "X-Api-Key: }}cashu/api/v1/mints/&lt;cashu_id&gt; -H "X-Api-Key:
&lt;admin_key&gt;" &lt;admin_key&gt;"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item> -->
</q-expansion-item> </q-expansion-item>

View file

@ -1,10 +1,7 @@
<q-expansion-item group="extras" icon="info" label="About"> <q-expansion-item group="extras" icon="info" label="About">
<q-card> <q-card>
<q-card-section> <q-card-section>
<p> <p>Create Cashu ecash mints and wallets.</p>
Make Ecash mints with peg in/out to a wallet, that can create and manage
ecash.
</p>
<small <small
>Created by >Created by
<a href="https://github.com/arcbtc" target="_blank">arcbtc</a>, <a href="https://github.com/arcbtc" target="_blank">arcbtc</a>,

View file

@ -46,12 +46,12 @@
unelevated unelevated
dense dense
size="xs" size="xs"
icon="launch" icon="account_balance_wallet"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a" type="a"
:href="'wallet/?tsh=' + (props.row.tickershort || '') + '&mint_id=' + props.row.id + '&mint_name=' + props.row.name" :href="'wallet/?' + 'mint_id=' + props.row.id"
target="_blank" target="_blank"
><q-tooltip>Shareable wallet page</q-tooltip></q-btn ><q-tooltip>Shareable wallet</q-tooltip></q-btn
> >
<q-btn <q-btn
@ -218,18 +218,18 @@
toggleAdvanced: false, toggleAdvanced: false,
cashusTable: { cashusTable: {
columns: [ columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'}, {name: 'id', align: 'left', label: 'Mint ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'}, {name: 'name', align: 'left', label: 'Name', field: 'name'},
{ {
name: 'tickershort', name: 'tickershort',
align: 'left', align: 'left',
label: 'tickershort', label: 'Ticker',
field: 'tickershort' field: 'tickershort'
}, },
{ {
name: 'wallet', name: 'wallet',
align: 'left', align: 'left',
label: 'Cashu wallet', label: 'Mint wallet',
field: 'wallet' field: 'wallet'
}, },
{ {
@ -271,7 +271,7 @@
LNbits.api LNbits.api
.request( .request(
'GET', 'GET',
'/cashu/api/v1/cashus?all_wallets=true', '/cashu/api/v1/mints?all_wallets=true',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
) )
.then(function (response) { .then(function (response) {
@ -294,7 +294,7 @@
LNbits.api LNbits.api
.request( .request(
'POST', 'POST',
'/cashu/api/v1/cashus', '/cashu/api/v1/mints',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}) _.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey, .inkey,
data data
@ -314,13 +314,13 @@
LNbits.utils LNbits.utils
.confirmDialog( .confirmDialog(
'Are you sure you want to delete this Mint? It will suck for users.' "Are you sure you want to delete this Mint? This mint's users will not be able to redeem their tokens!"
) )
.onOk(function () { .onOk(function () {
LNbits.api LNbits.api
.request( .request(
'DELETE', 'DELETE',
'/cashu/api/v1/cashus/' + cashuId, '/cashu/api/v1/mints/' + cashuId,
_.findWhere(self.g.user.wallets, {id: cashu.wallet}).adminkey _.findWhere(self.g.user.wallets, {id: cashu.wallet}).adminkey
) )
.then(function (response) { .then(function (response) {

View file

@ -1,7 +1,7 @@
{% extends "public.html" %} {% block page %} {% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center"> <div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md"> <div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg"> <q-card class="q-pa-lg q-mb-xl">
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<center> <center>
<q-icon <q-icon
@ -9,13 +9,53 @@
class="text-grey" class="text-grey"
style="font-size: 10rem" style="font-size: 10rem"
></q-icon> ></q-icon>
<h3 class="q-my-none">{{ mint_name }}</h3> <h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
<br /> <a
class="q-my-xl text-white"
style="font-size: 1.5rem"
href="../wallet?mint_id={{ mint_id }}"
>Open wallet</a
>
</center> </center>
<h5 class="q-my-none"> </q-card-section>
Some data about mint here: <br />* whether its online <br />* Who to </q-card>
contact for support <br />* etc... <q-card class="q-pa-lg q-mb-xl">
</h5> <q-card-section class="q-pa-none">
<h5 class="q-my-md">Read the following carefully!</h5>
<p>
This is a
<a href="https://cashu.space/" style="color: white" target="”_blank”"
>Cashu</a
>
mint. Cashu is an ecash system for Bitcoin.
</p>
<p>
<strong>Open this page in your native browser</strong><br />
Before you continue to the wallet, make sure to open this page in your
device's native browser application (Safari for iOS, Chrome for
Android). Do not use Cashu in an embedded browser that opens when you
click a link in a messenger.
</p>
<p>
<strong>Add wallet to home screen</strong><br />
You can add Cashu to your home screen as a progressive web app (PWA).
After opening the wallet in your browser (click the link above), on
Android (Chrome), click the menu at the upper right. On iOS (Safari),
click the share button. Now press the Add to Home screen button.
</p>
<p>
<strong>Backup your wallet</strong><br />
Ecash is a bearer asset. That means losing access to your wallet will
make you lose your funds. The wallet stores ecash tokens on your
device's database. If you lose the link or delete your your data
without backing up, you will lose your tokens. Press the Backup button
in the wallet to download a copy of your tokens.
</p>
<p>
<strong>This service is in BETA</strong> <br />
We hold no responsibility for people losing access to funds. Use at
your own risk!
</p>
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>

File diff suppressed because it is too large Load diff

View file

@ -23,16 +23,28 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
) )
# @cashu_ext.get("/wallet")
# async def wallet(request: Request):
# return cashu_renderer().TemplateResponse("cashu/wallet.html", {"request": request})
@cashu_ext.get("/wallet") @cashu_ext.get("/wallet")
async def cashu(request: Request): async def wallet(request: Request, mint_id: str):
return cashu_renderer().TemplateResponse("cashu/wallet.html", {"request": request}) return cashu_renderer().TemplateResponse(
"cashu/wallet.html",
{
"request": request,
"web_manifest": f"/cashu/manifest/{mint_id}.webmanifest",
},
)
@cashu_ext.get("/mint/{mintID}") @cashu_ext.get("/mint/{mintID}")
async def cashu(request: Request, mintID): async def cashu(request: Request, mintID):
cashu = await get_cashu(mintID) cashu = await get_cashu(mintID)
return cashu_renderer().TemplateResponse( return cashu_renderer().TemplateResponse(
"cashu/mint.html", {"request": request, "mint_name": cashu.name} "cashu/mint.html",
{"request": request, "mint_name": cashu.name, "mint_id": mintID},
) )
@ -45,29 +57,167 @@ async def manifest(cashu_id: str):
) )
return { return {
"short_name": LNBITS_SITE_TITLE, "short_name": "Cashu",
"name": cashu.name + " - " + LNBITS_SITE_TITLE, "name": "Cashu" + " - " + cashu.name,
"icons": [ "icons": [
{ {
"src": LNBITS_CUSTOM_LOGO "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.png",
if LNBITS_CUSTOM_LOGO
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
"type": "image/png", "type": "image/png",
"sizes": "900x900", "sizes": "512x512",
} },
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-96-96.png",
"type": "image/png",
"sizes": "96x96",
},
], ],
"start_url": "/cashu/" + cashu_id, "id": "/cashu/wallet?mint_id=" + cashu_id,
"start_url": "/cashu/wallet?mint_id=" + cashu_id,
"background_color": "#1F2234", "background_color": "#1F2234",
"description": "Bitcoin Lightning tPOS", "description": "Cashu ecash wallet",
"display": "standalone", "display": "standalone",
"scope": "/cashu/" + cashu_id, "scope": "/cashu/",
"theme_color": "#1F2234", "theme_color": "#1F2234",
"protocol_handlers": [
{"protocol": "cashu", "url": "&recv_token=%s"},
{"protocol": "lightning", "url": "&lightning=%s"},
],
"shortcuts": [ "shortcuts": [
{ {
"name": cashu.name + " - " + LNBITS_SITE_TITLE, "name": "Cashu" + " - " + cashu.name,
"short_name": cashu.name, "short_name": "Cashu",
"description": cashu.name + " - " + LNBITS_SITE_TITLE, "description": "Cashu" + " - " + cashu.name,
"url": "/cashu/" + cashu_id, "url": "/cashu/wallet?mint_id=" + cashu_id,
"icons": [
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.png",
"sizes": "512x512",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-192-192.png",
"sizes": "192x192",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-144-144.png",
"sizes": "144x144",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-96-96.png",
"sizes": "96x96",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-72-72.png",
"sizes": "72x72",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-48-48.png",
"sizes": "48x48",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/16.png",
"sizes": "16x16",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/20.png",
"sizes": "20x20",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/29.png",
"sizes": "29x29",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/32.png",
"sizes": "32x32",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/40.png",
"sizes": "40x40",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/50.png",
"sizes": "50x50",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/57.png",
"sizes": "57x57",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/58.png",
"sizes": "58x58",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/60.png",
"sizes": "60x60",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/64.png",
"sizes": "64x64",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/72.png",
"sizes": "72x72",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/76.png",
"sizes": "76x76",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/80.png",
"sizes": "80x80",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/87.png",
"sizes": "87x87",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/100.png",
"sizes": "100x100",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/114.png",
"sizes": "114x114",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/120.png",
"sizes": "120x120",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/128.png",
"sizes": "128x128",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/144.png",
"sizes": "144x144",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/152.png",
"sizes": "152x152",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/167.png",
"sizes": "167x167",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/180.png",
"sizes": "180x180",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/192.png",
"sizes": "192x192",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/256.png",
"sizes": "256x256",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/512.png",
"sizes": "512x512",
},
{
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/1024.png",
"sizes": "1024x1024",
},
],
} }
], ],
} }

View file

@ -47,17 +47,20 @@ from .models import Cashu
# --------- extension imports # --------- extension imports
LIGHTNING = False LIGHTNING = True
######################################## ########################################
############### LNBITS MINTS ########### ############### LNBITS MINTS ###########
######################################## ########################################
# todo: use /mints
@cashu_ext.get("/api/v1/cashus", status_code=HTTPStatus.OK) @cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK)
async def api_cashus( async def api_cashus(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
): ):
"""
Get all mints of this wallet.
"""
wallet_ids = [wallet.wallet.id] wallet_ids = [wallet.wallet.id]
if all_wallets: if all_wallets:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
@ -65,8 +68,11 @@ async def api_cashus(
return [cashu.dict() for cashu in await get_cashus(wallet_ids)] return [cashu.dict() for cashu in await get_cashus(wallet_ids)]
@cashu_ext.post("/api/v1/cashus", status_code=HTTPStatus.CREATED) @cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED)
async def api_cashu_create(data: Cashu, wallet: WalletTypeInfo = Depends(get_key_type)): async def api_cashu_create(data: Cashu, wallet: WalletTypeInfo = Depends(get_key_type)):
"""
Create a new mint for this wallet.
"""
cashu_id = urlsafe_short_hash() cashu_id = urlsafe_short_hash()
# generate a new keyset in cashu # generate a new keyset in cashu
keyset = await ledger.load_keyset(cashu_id) keyset = await ledger.load_keyset(cashu_id)
@ -78,12 +84,35 @@ async def api_cashu_create(data: Cashu, wallet: WalletTypeInfo = Depends(get_key
return cashu.dict() return cashu.dict()
@cashu_ext.delete("/api/v1/mints/{cashu_id}")
async def api_cashu_delete(
cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
"""
Delete an existing cashu mint.
"""
cashu = await get_cashu(cashu_id)
if not cashu:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Cashu mint does not exist."
)
if cashu.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your Cashu mint."
)
await delete_cashu(cashu_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
####################################### #######################################
########### CASHU ENDPOINTS ########### ########### CASHU ENDPOINTS ###########
####################################### #######################################
@cashu_ext.get("/api/v1/cashu/{cashu_id}/keys", status_code=HTTPStatus.OK) @cashu_ext.get("/api/v1/{cashu_id}/keys", status_code=HTTPStatus.OK)
async def keys(cashu_id: str = Query(None)) -> dict[int, str]: async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
"""Get the public keys of the mint""" """Get the public keys of the mint"""
cashu: Union[Cashu, None] = await get_cashu(cashu_id) cashu: Union[Cashu, None] = await get_cashu(cashu_id)
@ -96,7 +125,20 @@ 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/{cashu_id}/mint") @cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
"""Get the public keys of the mint"""
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
if not cashu:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
)
return {"keysets": [cashu.keyset_id]}
@cashu_ext.get("/api/v1/{cashu_id}/mint")
async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintResponse: async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintResponse:
""" """
Request minting of new tokens. The mint responds with a Lightning invoice. Request minting of new tokens. The mint responds with a Lightning invoice.
@ -134,7 +176,7 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR
return resp return resp
@cashu_ext.post("/api/v1/cashu/{cashu_id}/mint") @cashu_ext.post("/api/v1/{cashu_id}/mint")
async def mint_coins( async def mint_coins(
data: MintRequest, data: MintRequest,
cashu_id: str = Query(None), cashu_id: str = Query(None),
@ -157,7 +199,7 @@ async def mint_coins(
if invoice is None: if invoice is None:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, status_code=HTTPStatus.NOT_FOUND,
detail="Mint does not have this invoice.", detail="Mint does not know this invoice.",
) )
if invoice.issued == True: if invoice.issued == True:
raise HTTPException( raise HTTPException(
@ -173,27 +215,31 @@ async def mint_coins(
) )
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash) status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
# todo: revert to: status.paid != True:
if status.paid != True: if status.paid != True:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
) )
try: try:
await ledger.crud.update_lightning_invoice(
db=ledger.db, hash=payment_hash, issued=True
)
keyset = ledger.keysets.keysets[cashu.keyset_id] keyset = ledger.keysets.keysets[cashu.keyset_id]
promises = await ledger._generate_promises( promises = await ledger._generate_promises(
B_s=data.blinded_messages, keyset=keyset 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(
db=ledger.db, hash=payment_hash, issued=True
)
return promises return promises
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
@cashu_ext.post("/api/v1/cashu/{cashu_id}/melt") @cashu_ext.post("/api/v1/{cashu_id}/melt")
async def melt_coins( async def melt_coins(
payload: MeltRequest, cashu_id: str = Query(None) payload: MeltRequest, cashu_id: str = Query(None)
) -> GetMeltResponse: ) -> GetMeltResponse:
@ -211,7 +257,7 @@ async def melt_coins(
# TOKENS # TOKENS
assert all([p.id == cashu.keyset_id for p in proofs]), HTTPException( assert all([p.id == cashu.keyset_id for p in proofs]), HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail="Proofs include tokens from other mint.", detail="Proofs include tokens from another mint.",
) )
assert all([ledger._verify_proof(p) for p in proofs]), HTTPException( assert all([ledger._verify_proof(p) for p in proofs]), HTTPException(
@ -248,19 +294,33 @@ async def melt_coins(
return GetMeltResponse(paid=status.paid, preimage=status.preimage) return GetMeltResponse(paid=status.paid, preimage=status.preimage)
@cashu_ext.post("/api/v1/check") @cashu_ext.post("/api/v1/{cashu_id}/check")
async def check_spendable(payload: CheckRequest) -> Dict[int, bool]: async def check_spendable(
payload: CheckRequest, cashu_id: str = Query(None)
) -> Dict[int, bool]:
"""Check whether a secret has been spent already or not.""" """Check whether a secret has been spent already or not."""
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
if cashu is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
)
return await ledger.check_spendable(payload.proofs) return await ledger.check_spendable(payload.proofs)
@cashu_ext.post("/api/v1/checkfees") @cashu_ext.post("/api/v1/{cashu_id}/checkfees")
async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse: async def check_fees(
payload: CheckFeesRequest, cashu_id: str = Query(None)
) -> CheckFeesResponse:
""" """
Responds with the fees necessary to pay a Lightning invoice. Responds with the fees necessary to pay a Lightning invoice.
Used by wallets for figuring out the fees they need to supply. 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). This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
""" """
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
if cashu is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
)
invoice_obj = bolt11.decode(payload.pr) invoice_obj = bolt11.decode(payload.pr)
internal_checking_id = await check_internal(invoice_obj.payment_hash) internal_checking_id = await check_internal(invoice_obj.payment_hash)
@ -271,7 +331,7 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
return CheckFeesResponse(fee=fees_msat / 1000) return CheckFeesResponse(fee=fees_msat / 1000)
@cashu_ext.post("/api/v1/cashu/{cashu_id}/split") @cashu_ext.post("/api/v1/{cashu_id}/split")
async def split( async def split(
payload: SplitRequest, cashu_id: str = Query(None) payload: SplitRequest, cashu_id: str = Query(None)
) -> PostSplitResponse: ) -> PostSplitResponse:
@ -285,15 +345,24 @@ async def split(
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
) )
proofs = payload.proofs proofs = payload.proofs
# !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID
# THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID
# TOKENS
assert all([p.id == cashu.keyset_id for p in proofs]), HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Proofs include tokens from another mint.",
)
amount = payload.amount amount = payload.amount
outputs = payload.outputs.blinded_messages outputs = payload.outputs.blinded_messages
# backwards compatibility with clients < v0.2.2
assert outputs, Exception("no outputs provided.") assert outputs, Exception("no outputs provided.")
split_return = None split_return = None
try: try:
split_return = await ledger.split(proofs, amount, outputs, cashu.keyset_id) keyset = ledger.keysets.keysets[cashu.keyset_id]
split_return = await ledger.split(proofs, amount, outputs, keyset)
except Exception as exc: except Exception as exc:
HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=str(exc), detail=str(exc),
) )
@ -318,24 +387,6 @@ async def split(
# return cashu.dict() # return cashu.dict()
# @cashu_ext.delete("/api/v1s/{cashu_id}")
# async def api_cashu_delete(
# cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
# ):
# cashu = await get_cashu(cashu_id)
# if not cashu:
# raise HTTPException(
# status_code=HTTPStatus.NOT_FOUND, detail="Cashu does not exist."
# )
# if cashu.wallet != wallet.wallet.id:
# raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your Cashu.")
# await delete_cashu(cashu_id)
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
# ######################################## # ########################################
# #################????################### # #################????###################
# ######################################## # ########################################

9
poetry.lock generated
View file

@ -123,7 +123,7 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "cashu" name = "cashu"
version = "0.4.2" version = "0.5.1"
description = "Ecash wallet and mint with Bitcoin Lightning support" description = "Ecash wallet and mint with Bitcoin Lightning support"
category = "main" category = "main"
optional = false optional = false
@ -155,6 +155,7 @@ py = {version = "1.11.0", markers = "python_version >= \"3.7\" and python_versio
pycparser = {version = "2.21", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} pycparser = {version = "2.21", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
pydantic = {version = "1.10.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} pydantic = {version = "1.10.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
pyparsing = {version = "3.0.9", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} pyparsing = {version = "3.0.9", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
pysocks = {version = "1.7.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
pytest = {version = "7.1.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} pytest = {version = "7.1.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
pytest-asyncio = {version = "0.19.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} pytest-asyncio = {version = "0.19.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
python-bitcoinlib = {version = "0.11.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} python-bitcoinlib = {version = "0.11.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""}
@ -1143,7 +1144,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7" python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
content-hash = "7de5e4d432bff49de536b1c90082a6a0821533b3d0fa9d92c22ccaa758d1a65f" content-hash = "c5d3b28864bf6b86385e38f63e3ba16d95804a812773e930b6ed818d4f09938a"
[metadata.files] [metadata.files]
aiofiles = [ aiofiles = [
@ -1207,8 +1208,8 @@ black = [
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
] ]
cashu = [ cashu = [
{file = "cashu-0.4.2-py3-none-any.whl", hash = "sha256:6d24f5e921c33dae1b6823f5e34feab0d6d5662b56a67c29095d48241163a887"}, {file = "cashu-0.5.1-py3-none-any.whl", hash = "sha256:893f6bc098331e73cb6a5d0108c929dc7f2299d3d5405ae3b29e0868d9cd78c9"},
{file = "cashu-0.4.2.tar.gz", hash = "sha256:97564481501cbe163e6be4d3cdd0d52d2841e15b830a0185c3c329657e4b8c36"}, {file = "cashu-0.5.1.tar.gz", hash = "sha256:c4533c72a09b0e1439836739653d3d79a7de00a1106e6676cb8f660f894006a7"},
] ]
Cerberus = [ Cerberus = [
{file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"}, {file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"},

View file

@ -64,7 +64,7 @@ protobuf = "^4.21.6"
Cerberus = "^1.3.4" Cerberus = "^1.3.4"
async-timeout = "^4.0.2" async-timeout = "^4.0.2"
pyln-client = "0.11.1" pyln-client = "0.11.1"
cashu = "0.4.2" cashu = "^0.5.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]