diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..8f41bff --- /dev/null +++ b/crud.py @@ -0,0 +1,42 @@ +import json +from typing import Optional + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import Merchant, PartialMerchant + + +async def create_merchant(user_id: str, m: PartialMerchant) -> Merchant: + merchant_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO nostrmarket.merchants (user_id, id, private_key, public_key, meta) + VALUES (?, ?, ?, ?, ?) + """, + (user_id, merchant_id, m.private_key, m.public_key, json.dumps(dict(m.config))), + ) + merchant = await get_merchant(user_id, merchant_id) + assert merchant, "Created merchant cannot be retrieved" + return merchant + + +async def get_merchant(user_id: str, merchant_id: str) -> Optional[Merchant]: + row = await db.fetchone( + """SELECT * FROM nostrmarket.merchants WHERE user_id = ? AND id = ?""", + ( + user_id, + merchant_id, + ), + ) + + return Merchant.from_row(row) if row else None + + +async def get_merchant_for_user(user_id: str) -> Optional[Merchant]: + row = await db.fetchone( + """SELECT * FROM nostrmarket.merchants WHERE user_id = ? """, + (user_id,), + ) + + return Merchant.from_row(row) if row else None diff --git a/migrations.py b/migrations.py index e69de29..0880b62 100644 --- a/migrations.py +++ b/migrations.py @@ -0,0 +1,153 @@ +async def m001_initial(db): + + """ + Initial merchants table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.merchants ( + user_id TEXT NOT NULL, + id TEXT PRIMARY KEY, + private_key TEXT NOT NULL, + public_key TEXT NOT NULL, + meta TEXT NOT NULL DEFAULT '{}' + ); + """ + ) + + """ + Initial stalls table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.stalls ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + currency TEXT, + shipping_zones TEXT NOT NULL, + rating REAL DEFAULT 0 + ); + """ + ) + + """ + Initial products table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.products ( + id TEXT PRIMARY KEY, + stall_id TEXT NOT NULL, + product TEXT NOT NULL, + categories TEXT, + description TEXT, + image TEXT, + price REAL NOT NULL, + quantity INTEGER NOT NULL, + rating REAL DEFAULT 0 + ); + """ + ) + + """ + Initial zones table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.zones ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + cost REAL NOT NULL, + countries TEXT NOT NULL + ); + """ + ) + + """ + Initial orders table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.orders ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + username TEXT, + pubkey TEXT, + shipping_zone TEXT NOT NULL, + address TEXT, + email TEXT, + total REAL NOT NULL, + invoice_id TEXT NOT NULL, + paid BOOLEAN NOT NULL, + shipped BOOLEAN NOT NULL, + time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + + """ + Initial order details table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.order_details ( + id TEXT PRIMARY KEY, + order_id TEXT NOT NULL, + product_id TEXT NOT NULL, + quantity INTEGER NOT NULL + ); + """ + ) + + """ + Initial market table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.markets ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT + ); + """ + ) + + """ + Initial market stalls table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.market_stalls ( + id TEXT PRIMARY KEY, + market_id TEXT NOT NULL, + stall_id TEXT NOT NULL + ); + """ + ) + + """ + Initial chat messages table. + """ + await db.execute( + f""" + CREATE TABLE nostrmarket.messages ( + id TEXT PRIMARY KEY, + msg TEXT NOT NULL, + pubkey TEXT NOT NULL, + conversation_id TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + + if db.type != "SQLITE": + """ + Create indexes for message fetching + """ + await db.execute( + "CREATE INDEX idx_messages_timestamp ON nostrmarket.messages (timestamp DESC)" + ) + await db.execute( + "CREATE INDEX idx_messages_conversations ON nostrmarket.messages (conversation_id)" + ) diff --git a/models.py b/models.py new file mode 100644 index 0000000..e6a4e5e --- /dev/null +++ b/models.py @@ -0,0 +1,25 @@ +import json +from sqlite3 import Row +from typing import Optional + +from pydantic import BaseModel + + +class MerchantConfig(BaseModel): + name: Optional[str] + + +class PartialMerchant(BaseModel): + private_key: str + public_key: str + config: MerchantConfig = MerchantConfig() + + +class Merchant(PartialMerchant): + id: str + + @classmethod + def from_row(cls, row: Row) -> "Merchant": + merchant = cls(**dict(row)) + merchant.config = MerchantConfig(**json.loads(row["meta"])) + return merchant diff --git a/static/js/index.js b/static/js/index.js index 71d65d5..a5adbd6 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,15 +1,52 @@ -const stalls = async () => { +const merchant = async () => { Vue.component(VueQrcode.name, VueQrcode) await stallDetails('static/components/stall-details/stall-details.html') + const nostr = window.NostrTools + new Vue({ el: '#vue', mixins: [windowMixin], data: function () { - return {} + return { + merchant: null + } + }, + methods: { + generateKeys: async function () { + const privkey = nostr.generatePrivateKey() + const pubkey = nostr.getPublicKey(privkey) + + const data = {private_key: privkey, public_key: pubkey, config: {}} + try { + const resp = await LNbits.api.request( + 'POST', + '/nostrmarket/api/v1/merchant', + this.g.user.wallets[0].adminkey, + data + ) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + getMerchant: async function () { + try { + const {data} = await LNbits.api.request( + 'get', + '/nostrmarket/api/v1/merchant', + this.g.user.wallets[0].adminkey + ) + this.merchant = data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: async function () { + await this.getMerchant() } }) } -stalls() +merchant() diff --git a/tasks.py b/tasks.py index cbf1b74..3254dcc 100644 --- a/tasks.py +++ b/tasks.py @@ -21,7 +21,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if payment.extra.get("tag") != "market": + if payment.extra.get("tag") != "nostrmarket": return print("### on_invoice_paid") diff --git a/templates/nostrmarket/_api_docs.html b/templates/nostrmarket/_api_docs.html index 9ed2f47..07e30eb 100644 --- a/templates/nostrmarket/_api_docs.html +++ b/templates/nostrmarket/_api_docs.html @@ -4,6 +4,20 @@ Nostr Market Created by, + Tal Vasconcelos + Ben Arc - - section + + + Wellcome to Nostr Market! + In Nostr Market, merchant and customer communicate via NOSTR relays, so + loss of money, product information, and reputation become far less + likely if attacked. + + + Terms + + + merchant - seller of products with + NOSTR key-pair + + + customer - buyer of products with + NOSTR key-pair + + + product - item for sale by the + merchant + + + stall - list of products controlled + by merchant (a merchant can have multiple stalls) + + + marketplace - clientside software for + searching stalls and purchasing products + + + + + + + + Use an existing private key (hex or npub) + + + A new key pair will be generated for you + + + + + + + Merchant Exists + + @@ -22,6 +79,8 @@ {% endblock%}{% block scripts %} {{ window_vars(user) }} + + diff --git a/views_api.py b/views_api.py index e69de29..ad4d66e 100644 --- a/views_api.py +++ b/views_api.py @@ -0,0 +1,45 @@ +from http import HTTPStatus +from typing import Optional + +from fastapi import Depends +from fastapi.exceptions import HTTPException +from loguru import logger + +from lnbits.decorators import WalletTypeInfo, require_admin_key, require_invoice_key + +from . import nostrmarket_ext +from .crud import create_merchant, get_merchant_for_user +from .models import Merchant, PartialMerchant + + +@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", + )