From 1a458ee757038be53aa2da1e573a64a55e8328c4 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 10:24:27 +0200 Subject: [PATCH 1/4] refactor: clean migration statements --- migrations.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/migrations.py b/migrations.py index e69de29..02565f7 100644 --- a/migrations.py +++ b/migrations.py @@ -0,0 +1,152 @@ +async def m001_initial(db): + + """ + Initial merchants table. + """ + await db.execute( + """ + CREATE TABLE nostrmarket.merchants ( + user_id TEXT NOT NULL, + private_key TEXT NOT NULL, + public_key TEXT NOT NULL, + config TEXT NOT NULL + ); + """ + ) + + """ + 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)" + ) From b1d0c5c4754d76a951c48b805e5fd15ffcba7abf Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 10:39:11 +0200 Subject: [PATCH 2/4] feat: add landing page --- tasks.py | 2 +- templates/nostrmarket/index.html | 55 +++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) 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/index.html b/templates/nostrmarket/index.html index cf4ff44..410fbf4 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -3,7 +3,60 @@
- 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 + +
+
+
From 2832ee928c8f0f7d525f9a4f1eed9884807df6d6 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Tue, 28 Feb 2023 10:41:35 +0200 Subject: [PATCH 3/4] doc: update created by --- templates/nostrmarket/_api_docs.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 Date: Tue, 28 Feb 2023 11:46:40 +0200 Subject: [PATCH 4/4] feat: init merchant --- crud.py | 42 +++++++++++++++++++++++++++++ migrations.py | 3 ++- models.py | 25 ++++++++++++++++++ static/js/index.js | 43 +++++++++++++++++++++++++++--- templates/nostrmarket/index.html | 14 +++++++--- views_api.py | 45 ++++++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 crud.py create mode 100644 models.py 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 02565f7..0880b62 100644 --- a/migrations.py +++ b/migrations.py @@ -7,9 +7,10 @@ async def m001_initial(db): """ CREATE TABLE nostrmarket.merchants ( user_id TEXT NOT NULL, + id TEXT PRIMARY KEY, private_key TEXT NOT NULL, public_key TEXT NOT NULL, - config TEXT NOT NULL + meta TEXT NOT NULL DEFAULT '{}' ); """ ) 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/templates/nostrmarket/index.html b/templates/nostrmarket/index.html index 410fbf4..37710a5 100644 --- a/templates/nostrmarket/index.html +++ b/templates/nostrmarket/index.html @@ -2,7 +2,7 @@ %} {% block page %}
- + Wellcome to Nostr Market!
In Nostr Market, merchant and customer communicate via NOSTR relays, so @@ -41,15 +41,14 @@ disabled label="Import Key" color="primary" - @click="importKey" class="float-left" > Use an existing private key (hex or npub) A new key pair will be generated for you @@ -58,6 +57,11 @@
+
+ + Merchant Exists + +
@@ -75,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", + )