From e8e4c7f9c3ee16c28b0a7e6cee65a958b0110db9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Oct 2022 09:26:32 +0200 Subject: [PATCH] works --- lnbits/extensions/cashu/migrations.py | 3 +- .../cashu/templates/cashu/_api_docs.html | 12 +- .../cashu/templates/cashu/index.html | 8 +- .../cashu/templates/cashu/wallet.html | 12 +- lnbits/extensions/cashu/views_api.py | 119 ++++++++++++------ poetry.lock | 75 ++++------- pyproject.toml | 2 +- 7 files changed, 125 insertions(+), 106 deletions(-) diff --git a/lnbits/extensions/cashu/migrations.py b/lnbits/extensions/cashu/migrations.py index ec5207658..b54c41087 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/templates/cashu/_api_docs.html b/lnbits/extensions/cashu/templates/cashu/_api_docs.html index 3476d41aa..27ce55f98 100644 --- a/lnbits/extensions/cashu/templates/cashu/_api_docs.html +++ b/lnbits/extensions/cashu/templates/cashu/_api_docs.html @@ -8,7 +8,7 @@ - GET /cashu/api/v1/cashus + GET /cashu/api/v1/mints
Headers
{"X-Api-Key": <invoice_key>}
Body (application/json)
@@ -18,7 +18,7 @@ [<cashu_object>, ...]
Curl example
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: <invoice_key>"
@@ -27,7 +27,7 @@ - POST /cashu/api/v1/cashus + POST /cashu/api/v1/mints
Headers
{"X-Api-Key": <invoice_key>}
Body (application/json)
@@ -43,7 +43,7 @@ >
Curl example
curl -X POST {{ request.base_url }}cashu/api/v1/cashus -d '{"name": + >curl -X POST {{ request.base_url }}cashu/api/v1/mints -d '{"name": <string>, "currency": <string>}' -H "Content-type: application/json" -H "X-Api-Key: <admin_key>" @@ -62,7 +62,7 @@ DELETE - /cashu/api/v1/cashus/<cashu_id>
Headers
{"X-Api-Key": <admin_key>}
@@ -71,7 +71,7 @@
Curl example
curl -X DELETE {{ request.base_url - }}cashu/api/v1/cashus/<cashu_id> -H "X-Api-Key: + }}cashu/api/v1/mints/<cashu_id> -H "X-Api-Key: <admin_key>"
diff --git a/lnbits/extensions/cashu/templates/cashu/index.html b/lnbits/extensions/cashu/templates/cashu/index.html index dcb2b4a74..20ed567ca 100644 --- a/lnbits/extensions/cashu/templates/cashu/index.html +++ b/lnbits/extensions/cashu/templates/cashu/index.html @@ -271,7 +271,7 @@ LNbits.api .request( 'GET', - '/cashu/api/v1/cashus?all_wallets=true', + '/cashu/api/v1/mints?all_wallets=true', this.g.user.wallets[0].inkey ) .then(function (response) { @@ -294,7 +294,7 @@ LNbits.api .request( 'POST', - '/cashu/api/v1/cashus', + '/cashu/api/v1/mints', _.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet}) .inkey, data @@ -314,13 +314,13 @@ LNbits.utils .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 () { LNbits.api .request( 'DELETE', - '/cashu/api/v1/cashus/' + cashuId, + '/cashu/api/v1/mints/' + cashuId, _.findWhere(self.g.user.wallets, {id: cashu.wallet}).adminkey ) .then(function (response) { diff --git a/lnbits/extensions/cashu/templates/cashu/wallet.html b/lnbits/extensions/cashu/templates/cashu/wallet.html index 4f12cd1c9..11ac044b3 100644 --- a/lnbits/extensions/cashu/templates/cashu/wallet.html +++ b/lnbits/extensions/cashu/templates/cashu/wallet.html @@ -913,7 +913,7 @@ page_container %} try { const {data} = await LNbits.api.request( 'GET', - `/cashu/api/v1/cashu/${this.mintId}/mint?amount=${this.invoiceData.amount}` + `/cashu/api/v1/${this.mintId}/mint?amount=${this.invoiceData.amount}` ) console.log('### data', data) @@ -940,7 +940,7 @@ page_container %} try { const {data} = await LNbits.api.request( 'POST', - `/cashu/api/v1/cashu/${this.mintId}/mint?payment_hash=${invoice.hash}`, + `/cashu/api/v1/${this.mintId}/mint?payment_hash=${invoice.hash}`, '', { blinded_messages: [] @@ -991,7 +991,7 @@ page_container %} try { const {data} = await LNbits.api.request( 'POST', - `/cashu/api/v1/cashu/${this.mintId}/mint?payment_hash=${hash}`, + `/cashu/api/v1/${this.mintId}/mint?payment_hash=${hash}`, '', { blinded_messages: blindedMessages @@ -1086,7 +1086,7 @@ page_container %} try { const {data} = await LNbits.api.request( 'POST', - `/cashu/api/v1/cashu/${this.mintId}/split`, + `/cashu/api/v1/${this.mintId}/split`, '', payload ) @@ -1221,7 +1221,7 @@ page_container %} try { const {data} = await LNbits.api.request( 'POST', - `/cashu/api/v1/cashu/${this.mintId}/melt`, + `/cashu/api/v1/${this.mintId}/melt`, '', payload ) @@ -1257,7 +1257,7 @@ page_container %} fetchMintKeys: async function () { const {data} = await LNbits.api.request( 'GET', - `/cashu/api/v1/cashu/${this.mintId}/keys` + `/cashu/api/v1/${this.mintId}/keys` ) this.keys = data localStorage.setItem('cashu.keys', JSON.stringify(data)) diff --git a/lnbits/extensions/cashu/views_api.py b/lnbits/extensions/cashu/views_api.py index 28857663f..d4654c55a 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: @@ -291,7 +351,8 @@ async def split( 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( status_code=HTTPStatus.BAD_REQUEST, @@ -318,24 +379,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 9c426798f..b87b102f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -124,55 +124,33 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cashu" version = "0.4.2" -description = "Ecash wallet and mint with Bitcoin Lightning support" +description = "Ecash wallet and mint." category = "main" optional = false -python-versions = ">=3.7" +python-versions = "^3.7" +develop = false [package.dependencies] -anyio = {version = "3.6.2", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -attrs = {version = "22.1.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -bech32 = {version = "1.2.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -bitstring = {version = "3.1.9", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -certifi = {version = "2022.9.24", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -cffi = {version = "1.15.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -charset-normalizer = {version = "2.0.12", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -click = {version = "8.0.4", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -colorama = {version = "0.4.5", markers = "python_version >= \"3.7\" and python_version < \"4.0\" and platform_system == \"Windows\" or python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform == \"win32\""} -ecdsa = {version = "0.18.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -environs = {version = "9.5.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -fastapi = {version = "0.83.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -h11 = {version = "0.12.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -idna = {version = "3.4", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -importlib-metadata = {version = "5.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.8\""} -iniconfig = {version = "1.1.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -loguru = {version = "0.6.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -marshmallow = {version = "3.18.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -outcome = {version = "1.2.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -packaging = {version = "21.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -pluggy = {version = "1.0.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -py = {version = "1.11.0", 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\""} -pyparsing = {version = "3.0.9", 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\""} -python-dotenv = {version = "0.21.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -represent = {version = "1.6.0.post0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -requests = {version = "2.27.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -secp256k1 = {version = "0.14.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -six = {version = "1.16.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -sniffio = {version = "1.3.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -sqlalchemy = {version = "1.3.24", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -sqlalchemy-aio = {version = "0.17.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -starlette = {version = "0.19.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -tomli = {version = "2.0.1", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -typing-extensions = {version = "4.4.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -urllib3 = {version = "1.26.12", markers = "python_version >= \"3.7\" and python_version < \"4\""} -uvicorn = {version = "0.18.3", markers = "python_version >= \"3.7\" and python_version < \"4.0\""} -win32-setctime = {version = "1.1.0", markers = "python_version >= \"3.7\" and python_version < \"4.0\" and sys_platform == \"win32\""} -zipp = {version = "3.9.0", markers = "python_version >= \"3.7\" and python_version < \"3.8\""} +bech32 = "^1.2.0" +bitstring = "^3.1.9" +click = "8.0.4" +ecdsa = "^0.18.0" +environs = "^9.5.0" +fastapi = "^0.83.0" +h11 = "0.12.0" +loguru = "^0.6.0" +pydantic = "^1.10.2" +pytest-asyncio = "0.19.0" +python-bitcoinlib = "^0.11.2" +requests = "2.27.1" +secp256k1 = "^0.14.0" +SQLAlchemy = "1.3.24" +sqlalchemy-aio = "^0.17.0" +uvicorn = "^0.18.3" + +[package.source] +type = "directory" +url = "../cashu" [[package]] name = "Cerberus" @@ -1143,7 +1121,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 = "ded9ab80b3bec75bf904c3f598afbdff5f1577aaedbec25045c3b42c332f8ccc" [metadata.files] aiofiles = [ @@ -1206,10 +1184,7 @@ black = [ {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, {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"}, -] +cashu = [] Cerberus = [ {file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"}, ] diff --git a/pyproject.toml b/pyproject.toml index 24c041d78..f1a523489 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 = {path = "../cashu"} [tool.poetry.dev-dependencies]