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