diff --git a/.env.example b/.env.example index 987c6ca6..7b787117 100644 --- a/.env.example +++ b/.env.example @@ -97,3 +97,8 @@ ECLAIR_PASS=eclairpw # Enter /api in LightningTipBot to get your key LNTIPS_API_KEY=LNTIPS_ADMIN_KEY 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" diff --git a/lnbits/extensions/cashu/__init__.py b/lnbits/extensions/cashu/__init__.py index 7944e658..e6507bba 100644 --- a/lnbits/extensions/cashu/__init__.py +++ b/lnbits/extensions/cashu/__init__.py @@ -1,5 +1,6 @@ import asyncio +from environs import Env # type: ignore from fastapi import APIRouter from fastapi.staticfiles import StaticFiles @@ -20,10 +21,12 @@ cashu_static_files = [ ] from cashu.mint.ledger import Ledger +env = Env() +env.read_env() + ledger = Ledger( db=db, - # seed=MINT_PRIVATE_KEY, - seed="asd", + seed=env.str("CASHU_PRIVATE_KEY", default="SuperSecretPrivateKey"), derivation_path="0/0/0/1", ) diff --git a/lnbits/extensions/cashu/config.json b/lnbits/extensions/cashu/config.json index 99838eb4..af202d43 100644 --- a/lnbits/extensions/cashu/config.json +++ b/lnbits/extensions/cashu/config.json @@ -1,7 +1,7 @@ { - "name": "Cashu Ecash", + "name": "Cashu", "short_description": "Ecash mint and wallet", - "icon": "approval", - "contributors": ["arcbtc", "calle", "vlad"], + "icon": "account_balance", + "contributors": ["calle", "vlad", "arcbtc"], "hidden": false } diff --git a/lnbits/extensions/cashu/config.json.example b/lnbits/extensions/cashu/config.json.example index e798e2ef..0cb4043d 100644 --- a/lnbits/extensions/cashu/config.json.example +++ b/lnbits/extensions/cashu/config.json.example @@ -1,7 +1,7 @@ { - "name": "Cashu Ecash", + "name": "Cashu", "short_description": "Ecash mints with LN peg in/out", - "icon": "approval", - "contributors": ["arcbtc", "calle"], + "icon": "account_balance", + "contributors": ["calle", "vlad", "arcbtc"], "hidden": true } diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py index ec520765..b54c4108 100644 --- a/lnbits/extensions/cashu/migrations.py +++ b/lnbits/extensions/cashu/migrations.py @@ -12,7 +12,8 @@ async def m001_initial(db): fraction BOOL, maxsats INT, coins INT, - keyset_id TEXT NOT NULL + keyset_id TEXT NOT NULL, + issued_sat INT ); """ ) diff --git a/lnbits/extensions/cashu/static/js/dhke.js b/lnbits/extensions/cashu/static/js/dhke.js index 7b3abd80..41c2fb46 100644 --- a/lnbits/extensions/cashu/static/js/dhke.js +++ b/lnbits/extensions/cashu/static/js/dhke.js @@ -19,20 +19,21 @@ async function hashToCurve(secretMessage) { return point } -async function step1Bob(secretMessage) { - secretMessage = nobleSecp256k1.utils.bytesToHex(secretMessage) - secretMessage = new TextEncoder().encode(secretMessage); +async function step1Alice(secretMessage) { + // todo: document & validate `secretMessage` format + secretMessage = uint8ToBase64.encode(secretMessage) + secretMessage = new TextEncoder().encode(secretMessage) const Y = await hashToCurve(secretMessage) - const randomBlindingFactor = bytesToNumber( - nobleSecp256k1.utils.randomPrivateKey() - ) - const P = nobleSecp256k1.Point.fromPrivateKey(randomBlindingFactor) + const rpk = nobleSecp256k1.utils.randomPrivateKey() + const r = bytesToNumber(rpk) + const P = nobleSecp256k1.Point.fromPrivateKey(r) 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) { - const rInt = BigInt(r) +function step3Alice(C_, r, A) { + // const rInt = BigInt(r) + const rInt = bytesToNumber(r) const C = C_.subtract(A.multiply(rInt)) return C } diff --git a/lnbits/extensions/cashu/tasks.py b/lnbits/extensions/cashu/tasks.py index 40b521f0..11eb2fd2 100644 --- a/lnbits/extensions/cashu/tasks.py +++ b/lnbits/extensions/cashu/tasks.py @@ -15,7 +15,6 @@ async def startup_cashu_mint(): await migrate_databases(db, migrations) await ledger.load_used_proofs() await ledger.init_keysets() - print(ledger.get_keyset()) pass diff --git a/lnbits/extensions/cashu/templates/cashu/_api_docs.html b/lnbits/extensions/cashu/templates/cashu/_api_docs.html index 3476d41a..f7bb19f6 100644 --- a/lnbits/extensions/cashu/templates/cashu/_api_docs.html +++ b/lnbits/extensions/cashu/templates/cashu/_api_docs.html @@ -5,10 +5,10 @@ :content-inset-level="0.5" > - + diff --git a/lnbits/extensions/cashu/templates/cashu/_cashu.html b/lnbits/extensions/cashu/templates/cashu/_cashu.html index 0c7c4338..952fe7e1 100644 --- a/lnbits/extensions/cashu/templates/cashu/_cashu.html +++ b/lnbits/extensions/cashu/templates/cashu/_cashu.html @@ -1,10 +1,7 @@ -

- Make Ecash mints with peg in/out to a wallet, that can create and manage - ecash. -

+

Create Cashu ecash mints and wallets.

Created by arcbtc, diff --git a/lnbits/extensions/cashu/templates/cashu/index.html b/lnbits/extensions/cashu/templates/cashu/index.html index dcb2b4a7..6c69da93 100644 --- a/lnbits/extensions/cashu/templates/cashu/index.html +++ b/lnbits/extensions/cashu/templates/cashu/index.html @@ -46,12 +46,12 @@ unelevated dense size="xs" - icon="launch" + icon="account_balance_wallet" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" 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" - >Shareable wallet pageShareable wallet
- +
-

{{ mint_name }}

-
+

{{ mint_name }}

+ Open wallet
-
- Some data about mint here:
* whether its online
* Who to - contact for support
* etc... -
+
+
+ + +
Read the following carefully!
+

+ This is a + Cashu + mint. Cashu is an ecash system for Bitcoin. +

+

+ Open this page in your native browser
+ 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. +

+

+ Add wallet to home screen
+ 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. +

+

+ Backup your wallet
+ 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. +

+

+ This service is in BETA
+ We hold no responsibility for people losing access to funds. Use at + your own risk! +

diff --git a/lnbits/extensions/cashu/templates/cashu/wallet.html b/lnbits/extensions/cashu/templates/cashu/wallet.html index 4f12cd1c..a8016ba7 100644 --- a/lnbits/extensions/cashu/templates/cashu/wallet.html +++ b/lnbits/extensions/cashu/templates/cashu/wallet.html @@ -1,122 +1,146 @@ {% extends "public.html" %} {% block toolbar_title %} {% raw %} {{name}} Cashu -wallet {% endraw %} {% endblock %} {% block footer %}{% endblock %} {% block +{% endraw %} {% endblock %} {% block footer %}{% endblock %} {% block page_container %} -
+
- - -
-
- Create invoice - + + +
+
+
+ Get invoice + +
+
+

+
+ {% raw %} {{getBalance()}} + {{tickershort}}{% endraw %} +
+

+
+
+ Pay invoice + +
-
-

-
- {% raw %} {{balance}} - {{tickershort}}{% endraw %} -
-

-
-
- Pay invoice - +
+
+
+
+

+
+ {% raw %} {{getBalance()}} + {{tickershort}}{% endraw %} +
+

+
- +
-
+
ReceiveGet Ecash
-
-
+
+
- Send
+ + - + + {% raw %} {% endraw %} - + + + + {% raw %} @@ -125,7 +149,7 @@ page_container %}
@@ -137,27 +161,42 @@ page_container %} class="q-mr-md cursor-pointer" @click="recheckInvoice(props.row.hash)" > - Recheck + Check
- + Received + Paid +
{{props.row.amount}}
- -
{{props.row.memo}}
-
{{props.row.date}}
+ + +
{{props.row.bolt11}}
+
{{props.row.hash}}
@@ -167,46 +206,380 @@ page_container %}
+ + - History + + {% raw %} + + {% endraw %} +
+ +
+ + Warning + BackupDownload wallet backup +
+ + + + - - - + + +
+
+ {% raw %} {{ + parseFloat(String(payInvoiceData.invoice.fsat).replaceAll(",", + "")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} +
+
+ {{ payInvoiceData.invoice.fsat }}{% endraw %} + {{LNBITS_DENOMINATION}} {% raw %} +
+ +

+ Description: {{ + payInvoiceData.invoice.description }}
+ Expire date: {{ payInvoiceData.invoice.expireDate + }}
+ Hash: {{ payInvoiceData.invoice.hash }} +

+ {% endraw %} +
+ Pay + Cancel +
+
+ Not enough funds! + Cancel +
+
+
+ {% raw %} + +

+ Authenticate with {{ payInvoiceData.lnurlauth.domain }}? +

+ +

+ For every website and for every LNbits wallet, a new keypair + will be deterministically generated so your identity can't be + tied to your LNbits wallet or linked across websites. No other + data will be shared with {{ payInvoiceData.lnurlauth.domain }}. +

+

+ Your public key for + {{ payInvoiceData.lnurlauth.domain }} is: +

+

+ + {{ payInvoiceData.lnurlauth.pubkey }} + +

+
+ Login + Cancel +
+
+ {% endraw %} +
+
+ {% raw %} + +

+ {{ payInvoiceData.lnurlpay.domain }} is requesting {{ + payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} + {{LNBITS_DENOMINATION}} + +
+ and a {{payInvoiceData.lnurlpay.commentAllowed}}-char comment +
+

+

+ {{ payInvoiceData.lnurlpay.targetUser || + payInvoiceData.lnurlpay.domain }} + is requesting
+ between + {{ payInvoiceData.lnurlpay.minSendable | msatoshiFormat }} + and + {{ payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} + {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} + +
+ and a {{payInvoiceData.lnurlpay.commentAllowed}}-char comment +
+

+ +
+

+ {{ payInvoiceData.lnurlpay.description }} +

+

+ +

+
+
+
+ {% endraw %} + + {% raw %} +
+
+ +
+
+
+ Send {{LNBITS_DENOMINATION}} + Cancel +
+
+ {% endraw %} +
+
+ + + +
+ Enter + + + Close +
+
+
+ + + +
+ + Cancel + +
+
+
+
+
+ + + +
+ +
+
+ Cancel +
+
+
+
Warning

- BOOKMARK THIS PAGE! If only mobile you can also click the 3 dots - and "Save to homescreen"/"Install app"! + Bookmark this page and backup your tokens! + Ecash is a bearer asset, meaning losing access to this wallet will + mean you will lose the funds. This wallet stores ecash tokens in its + database. If you lose the link or delete your your data without + backing up, you will lose your tokens. Press the Backup button to + download a copy of your tokens.

- Ecash is a bearer asset, meaning you have the funds saved on this - page, losing the page without exporting the page will mean you will - lose the funds. + Add to home screen. + You can add Cashu to your home screen as a progressive web app + (PWA). On Android Chrome, click the hamburger menu at the upper + right. On iOS Safari, click the share button. Now press the Add to + Home screen button. +

+

+ This service is in BETA! We hold no responsibility + for people losing access to funds. Use at your own risk!

- Copy wallet URL + Copy wallet URL I understand @@ -219,20 +592,22 @@ page_container %}
- How much would you like to buy? + Create a Lightning invoice
- Copy invoice - Request InvoiceCreate Invoice Close -
+
+ + + + + + +
@@ -309,14 +705,25 @@ page_container %}
Show TokensSend Tokens - Burn Tokens + > --> +
+ Copy token + Copy link +
Close
- Paste tokens please + Receive Cashu tokens
- Accept Tokens - Close -
- - - - - -
-
-
- Please paste a Lightning invoice -
-
- -
-
- {% raw %} - Amount: {{ sellData.invoice.sat }} - sats
- Description: {{ sellData.invoice.description }}
- Expire date: {{ sellData.invoice.expireDate }}
- Expired: {{ sellData.invoice.expired }}
- Hash: {{ sellData.invoice.hash }} {% endraw %} -
-
+ Receive Tokens Check Invoice - Sell Token + unelevated + icon="photo_camera" + class="q-mx-0" + v-if="hasCamera" + @click="showCamera" + > Close @@ -424,14 +791,18 @@ page_container %} } {% endblock %} {% block scripts %} + + + + - - - - {% endblock %} diff --git a/lnbits/extensions/cashu/views.py b/lnbits/extensions/cashu/views.py index 097fbd00..afbef23c 100644 --- a/lnbits/extensions/cashu/views.py +++ b/lnbits/extensions/cashu/views.py @@ -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") -async def cashu(request: Request): - return cashu_renderer().TemplateResponse("cashu/wallet.html", {"request": request}) +async def wallet(request: Request, mint_id: str): + return cashu_renderer().TemplateResponse( + "cashu/wallet.html", + { + "request": request, + "web_manifest": f"/cashu/manifest/{mint_id}.webmanifest", + }, + ) @cashu_ext.get("/mint/{mintID}") async def cashu(request: Request, mintID): cashu = await get_cashu(mintID) 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 { - "short_name": LNBITS_SITE_TITLE, - "name": cashu.name + " - " + LNBITS_SITE_TITLE, + "short_name": "Cashu", + "name": "Cashu" + " - " + cashu.name, "icons": [ { - "src": LNBITS_CUSTOM_LOGO - if LNBITS_CUSTOM_LOGO - else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png", + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.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", - "description": "Bitcoin Lightning tPOS", + "description": "Cashu ecash wallet", "display": "standalone", - "scope": "/cashu/" + cashu_id, + "scope": "/cashu/", "theme_color": "#1F2234", + "protocol_handlers": [ + {"protocol": "cashu", "url": "&recv_token=%s"}, + {"protocol": "lightning", "url": "&lightning=%s"}, + ], "shortcuts": [ { - "name": cashu.name + " - " + LNBITS_SITE_TITLE, - "short_name": cashu.name, - "description": cashu.name + " - " + LNBITS_SITE_TITLE, - "url": "/cashu/" + cashu_id, + "name": "Cashu" + " - " + cashu.name, + "short_name": "Cashu", + "description": "Cashu" + " - " + cashu.name, + "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", + }, + ], } ], } diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 28857663..5f7e9310 100644 --- a/lnbits/extensions/cashu/views_api.py +++ b/lnbits/extensions/cashu/views_api.py @@ -47,17 +47,20 @@ from .models import Cashu # --------- extension imports -LIGHTNING = False +LIGHTNING = True ######################################## ############### 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( all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) ): + """ + Get all mints of this wallet. + """ wallet_ids = [wallet.wallet.id] if all_wallets: 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)] -@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)): + """ + Create a new mint for this wallet. + """ cashu_id = urlsafe_short_hash() # generate a new keyset in cashu 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() +@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_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]: """Get the public keys of the mint""" 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) -@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: """ 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 -@cashu_ext.post("/api/v1/cashu/{cashu_id}/mint") +@cashu_ext.post("/api/v1/{cashu_id}/mint") async def mint_coins( data: MintRequest, cashu_id: str = Query(None), @@ -157,7 +199,7 @@ async def mint_coins( if invoice is None: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, - detail="Mint does not have this invoice.", + detail="Mint does not know this invoice.", ) if invoice.issued == True: raise HTTPException( @@ -173,27 +215,31 @@ async def mint_coins( ) status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash) - # todo: revert to: status.paid != True: + if status.paid != True: raise HTTPException( status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." ) try: - await ledger.crud.update_lightning_invoice( - db=ledger.db, hash=payment_hash, issued=True - ) 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( + db=ledger.db, hash=payment_hash, issued=True + ) + 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/{cashu_id}/melt") +@cashu_ext.post("/api/v1/{cashu_id}/melt") async def melt_coins( payload: MeltRequest, cashu_id: str = Query(None) ) -> GetMeltResponse: @@ -211,7 +257,7 @@ async def melt_coins( # TOKENS assert all([p.id == cashu.keyset_id for p in proofs]), HTTPException( 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( @@ -248,19 +294,33 @@ async def melt_coins( return GetMeltResponse(paid=status.paid, preimage=status.preimage) -@cashu_ext.post("/api/v1/check") -async def check_spendable(payload: CheckRequest) -> Dict[int, bool]: +@cashu_ext.post("/api/v1/{cashu_id}/check") +async def check_spendable( + payload: CheckRequest, cashu_id: str = Query(None) +) -> Dict[int, bool]: """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) -@cashu_ext.post("/api/v1/checkfees") -async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse: +@cashu_ext.post("/api/v1/{cashu_id}/checkfees") +async def check_fees( + payload: CheckFeesRequest, cashu_id: str = Query(None) +) -> 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). """ + 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) 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) -@cashu_ext.post("/api/v1/cashu/{cashu_id}/split") +@cashu_ext.post("/api/v1/{cashu_id}/split") async def split( payload: SplitRequest, cashu_id: str = Query(None) ) -> PostSplitResponse: @@ -285,15 +345,24 @@ async def split( status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." ) 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 outputs = payload.outputs.blinded_messages - # backwards compatibility with clients < v0.2.2 assert outputs, Exception("no outputs provided.") split_return = None 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: - HTTPException( + raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(exc), ) @@ -318,24 +387,6 @@ async def split( # 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) - - # ######################################## # #################????################### # ######################################## diff --git a/poetry.lock b/poetry.lock index 9c426798..28486a1a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -123,7 +123,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cashu" -version = "0.4.2" +version = "0.5.1" description = "Ecash wallet and mint with Bitcoin Lightning support" category = "main" 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\""} 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\""} +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-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\""} @@ -1143,7 +1144,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7" -content-hash = "7de5e4d432bff49de536b1c90082a6a0821533b3d0fa9d92c22ccaa758d1a65f" +content-hash = "c5d3b28864bf6b86385e38f63e3ba16d95804a812773e930b6ed818d4f09938a" [metadata.files] aiofiles = [ @@ -1207,8 +1208,8 @@ black = [ {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] cashu = [ - {file = "cashu-0.4.2-py3-none-any.whl", hash = "sha256:6d24f5e921c33dae1b6823f5e34feab0d6d5662b56a67c29095d48241163a887"}, - {file = "cashu-0.4.2.tar.gz", hash = "sha256:97564481501cbe163e6be4d3cdd0d52d2841e15b830a0185c3c329657e4b8c36"}, + {file = "cashu-0.5.1-py3-none-any.whl", hash = "sha256:893f6bc098331e73cb6a5d0108c929dc7f2299d3d5405ae3b29e0868d9cd78c9"}, + {file = "cashu-0.5.1.tar.gz", hash = "sha256:c4533c72a09b0e1439836739653d3d79a7de00a1106e6676cb8f660f894006a7"}, ] Cerberus = [ {file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"}, diff --git a/pyproject.toml b/pyproject.toml index 24c041d7..b493a87f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ protobuf = "^4.21.6" Cerberus = "^1.3.4" async-timeout = "^4.0.2" pyln-client = "0.11.1" -cashu = "0.4.2" +cashu = "^0.5.1" [tool.poetry.dev-dependencies]