import json from http import HTTPStatus from typing import List, Optional from fastapi import Depends from fastapi.exceptions import HTTPException from loguru import logger from lnbits.core import create_invoice from lnbits.decorators import ( WalletTypeInfo, get_key_type, require_admin_key, require_invoice_key, ) from lnbits.utils.exchange_rates import currencies from . import nostrmarket_ext from .crud import ( create_merchant, create_order, create_product, create_stall, create_zone, delete_product, delete_stall, delete_zone, get_merchant_for_user, get_order, get_order_by_event_id, get_product, get_products, get_products_by_ids, get_stall, get_stalls, get_wallet_for_product, get_zone, get_zones, update_product, update_stall, update_zone, ) from .models import ( Merchant, Nostrable, Order, PartialMerchant, PartialOrder, PartialProduct, PartialStall, PartialZone, PaymentOption, PaymentRequest, Product, Stall, Zone, ) from .nostr.event import NostrEvent from .nostr.nostr_client import publish_nostr_event ######################################## MERCHANT ######################################## @nostrmarket_ext.post("/api/v1/merchant") async def api_create_merchant( data: PartialMerchant, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Merchant: try: merchant = await create_merchant(wallet.wallet.user, data) return merchant except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create merchant", ) @nostrmarket_ext.get("/api/v1/merchant") async def api_get_merchant( wallet: WalletTypeInfo = Depends(require_invoice_key), ) -> Optional[Merchant]: try: merchant = await get_merchant_for_user(wallet.wallet.user) return merchant except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create merchant", ) ######################################## ZONES ######################################## @nostrmarket_ext.get("/api/v1/zone") async def api_get_zones(wallet: WalletTypeInfo = Depends(get_key_type)) -> List[Zone]: try: return await get_zones(wallet.wallet.user) except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create merchant", ) @nostrmarket_ext.post("/api/v1/zone") async def api_create_zone( data: PartialZone, wallet: WalletTypeInfo = Depends(require_admin_key) ): try: zone = await create_zone(wallet.wallet.user, data) return zone except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create merchant", ) @nostrmarket_ext.patch("/api/v1/zone/{zone_id}") async def api_update_zone( data: Zone, zone_id: str, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Zone: try: zone = await get_zone(wallet.wallet.user, zone_id) if not zone: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Zone does not exist.", ) zone = await update_zone(wallet.wallet.user, data) assert zone, "Cannot find updated zone" return zone except HTTPException as ex: raise ex except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create merchant", ) @nostrmarket_ext.delete("/api/v1/zone/{zone_id}") async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admin_key)): try: zone = await get_zone(wallet.wallet.user, zone_id) if not zone: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Zone does not exist.", ) await delete_zone(zone_id) except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create merchant", ) ######################################## STALLS ######################################## @nostrmarket_ext.post("/api/v1/stall") async def api_create_stall( data: PartialStall, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Stall: try: data.validate_stall() stall = await create_stall(wallet.wallet.user, data=data) event = await sign_and_send_to_nostr(wallet.wallet.user, stall) stall.config.event_id = event.id await update_stall(wallet.wallet.user, stall) return stall except ValueError as ex: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(ex), ) except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create stall", ) @nostrmarket_ext.put("/api/v1/stall/{stall_id}") async def api_update_stall( data: Stall, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Stall: try: data.validate_stall() stall = await update_stall(wallet.wallet.user, data) assert stall, "Cannot update stall" event = await sign_and_send_to_nostr(wallet.wallet.user, stall) stall.config.event_id = event.id await update_stall(wallet.wallet.user, stall) return stall except HTTPException as ex: raise ex except ValueError as ex: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(ex), ) except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot update stall", ) @nostrmarket_ext.get("/api/v1/stall/{stall_id}") async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_type)): try: stall = await get_stall(wallet.wallet.user, stall_id) if not stall: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Stall does not exist.", ) return stall except HTTPException as ex: raise ex except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot get stall", ) @nostrmarket_ext.get("/api/v1/stall") async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)): try: stalls = await get_stalls(wallet.wallet.user) return stalls except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot get stalls", ) @nostrmarket_ext.get("/api/v1/stall/product/{stall_id}") async def api_get_stall_products( stall_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), ): try: products = await get_products(wallet.wallet.user, stall_id) return products except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot get stall products", ) @nostrmarket_ext.delete("/api/v1/stall/{stall_id}") async def api_delete_stall( stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) ): try: stall = await get_stall(wallet.wallet.user, stall_id) if not stall: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Stall does not exist.", ) await delete_stall(wallet.wallet.user, stall_id) event = await sign_and_send_to_nostr(wallet.wallet.user, stall, True) stall.config.event_id = event.id await update_stall(wallet.wallet.user, stall) except HTTPException as ex: raise ex except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot delete stall", ) ######################################## PRODUCTS ######################################## @nostrmarket_ext.post("/api/v1/product") async def api_create_product( data: PartialProduct, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Product: try: data.validate_product() stall = await get_stall(wallet.wallet.user, data.stall_id) assert stall, "Stall missing for product" data.config.currency = stall.currency product = await create_product(wallet.wallet.user, data=data) event = await sign_and_send_to_nostr(wallet.wallet.user, product) product.config.event_id = event.id await update_product(wallet.wallet.user, product) return product except ValueError as ex: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(ex), ) except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create product", ) @nostrmarket_ext.patch("/api/v1/product/{product_id}") async def api_update_product( product_id: str, product: Product, wallet: WalletTypeInfo = Depends(require_admin_key), ) -> Product: try: if product_id != product.id: raise ValueError("Bad product ID") product.validate_product() stall = await get_stall(wallet.wallet.user, product.stall_id) assert stall, "Stall missing for product" product.config.currency = stall.currency product = await update_product(wallet.wallet.user, product) event = await sign_and_send_to_nostr(wallet.wallet.user, product) product.config.event_id = event.id await update_product(wallet.wallet.user, product) return product except ValueError as ex: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=str(ex), ) except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot update product", ) @nostrmarket_ext.get("/api/v1/product/{product_id}") async def api_get_product( product_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key), ) -> Optional[Product]: try: products = await get_product(wallet.wallet.user, product_id) return products except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot get product", ) @nostrmarket_ext.delete("/api/v1/product/{product_id}") async def api_delete_product( product_id: str, wallet: WalletTypeInfo = Depends(require_admin_key), ): try: product = await get_product(wallet.wallet.user, product_id) if not product: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Product does not exist.", ) await delete_product(wallet.wallet.user, product_id) await sign_and_send_to_nostr(wallet.wallet.user, product, True) except HTTPException as ex: raise ex except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot delete product", ) ######################################## ORDERS ######################################## @nostrmarket_ext.post("/api/v1/order") async def api_create_order( data: PartialOrder, wallet: WalletTypeInfo = Depends(require_admin_key) ) -> Optional[PaymentRequest]: try: if await get_order(wallet.wallet.user, data.id): return None if data.event_id and await get_order_by_event_id( wallet.wallet.user, data.event_id ): return None products = await get_products_by_ids( wallet.wallet.user, [p.product_id for p in data.items] ) total_amount = await data.total_sats(products) wallet_id = await get_wallet_for_product(data.items[0].product_id) assert wallet_id, "Missing wallet for order `{data.id}`" payment_hash, invoice = await create_invoice( wallet_id=wallet_id, amount=round(total_amount), memo=f"Order '{data.id}' for pubkey '{data.pubkey}'", extra={ "tag": "nostrmarket", "order_id": data.id, }, ) order = Order(**data.dict(), invoice_id=payment_hash, total=total_amount) await create_order(wallet.wallet.user, order) return PaymentRequest( id=data.id, payment_options=[PaymentOption(type="ln", link=invoice)] ) except Exception as ex: logger.warning(ex) raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot create order", ) ######################################## OTHER ######################################## @nostrmarket_ext.get("/api/v1/currencies") async def api_list_currencies_available(): return list(currencies.keys()) ######################################## HELPERS ######################################## async def sign_and_send_to_nostr( user_id: str, n: Nostrable, delete=False ) -> NostrEvent: merchant = await get_merchant_for_user(user_id) assert merchant, "Cannot find merchant!" event = ( n.to_nostr_delete_event(merchant.public_key) if delete else n.to_nostr_event(merchant.public_key) ) event.sig = merchant.sign_hash(bytes.fromhex(event.id)) await publish_nostr_event(event) return event