From 6b2f3d05d51d7aabe8a00dac10f9dfc3d96134a9 Mon Sep 17 00:00:00 2001 From: benarc Date: Thu, 27 Jan 2022 12:24:38 +0000 Subject: [PATCH] initial --- lnbits/extensions/diagonalley/README.md | 9 + lnbits/extensions/diagonalley/__init__.py | 16 + lnbits/extensions/diagonalley/config.json | 6 + lnbits/extensions/diagonalley/crud.py | 395 +++++++++ lnbits/extensions/diagonalley/migrations.py | 69 ++ lnbits/extensions/diagonalley/models.py | 57 ++ .../extensions/diagonalley/static/js/index.js | 824 ++++++++++++++++++ lnbits/extensions/diagonalley/tasks.py | 29 + .../templates/diagonalley/_api_docs.html | 129 +++ .../templates/diagonalley/index.html | 634 ++++++++++++++ .../templates/diagonalley/stall.html | 9 + lnbits/extensions/diagonalley/views.py | 44 + lnbits/extensions/diagonalley/views_api.py | 348 ++++++++ 13 files changed, 2569 insertions(+) create mode 100644 lnbits/extensions/diagonalley/README.md create mode 100644 lnbits/extensions/diagonalley/__init__.py create mode 100644 lnbits/extensions/diagonalley/config.json create mode 100644 lnbits/extensions/diagonalley/crud.py create mode 100644 lnbits/extensions/diagonalley/migrations.py create mode 100644 lnbits/extensions/diagonalley/models.py create mode 100644 lnbits/extensions/diagonalley/static/js/index.js create mode 100644 lnbits/extensions/diagonalley/tasks.py create mode 100644 lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html create mode 100644 lnbits/extensions/diagonalley/templates/diagonalley/index.html create mode 100644 lnbits/extensions/diagonalley/templates/diagonalley/stall.html create mode 100644 lnbits/extensions/diagonalley/views.py create mode 100644 lnbits/extensions/diagonalley/views_api.py diff --git a/lnbits/extensions/diagonalley/README.md b/lnbits/extensions/diagonalley/README.md new file mode 100644 index 00000000..e8035b74 --- /dev/null +++ b/lnbits/extensions/diagonalley/README.md @@ -0,0 +1,9 @@ +

Diagon Alley

+

A movable market stand

+Make a list of products to sell, point the list to an relay (or many), stack sats. +Diagon Alley is a movable market stand, for anon transactions. You then give permission for an relay to list those products. Delivery addresses are sent through the Lightning Network. + + +

API endpoints

+ +curl -X GET http://YOUR-TOR-ADDRESS diff --git a/lnbits/extensions/diagonalley/__init__.py b/lnbits/extensions/diagonalley/__init__.py new file mode 100644 index 00000000..720c55c8 --- /dev/null +++ b/lnbits/extensions/diagonalley/__init__.py @@ -0,0 +1,16 @@ +from quart import Blueprint +from lnbits.db import Database + +db = Database("ext_diagonalley") + +diagonalley_ext: Blueprint = Blueprint( + "diagonalley", __name__, static_folder="static", template_folder="templates" +) + +from .views_api import * # noqa +from .views import * # noqa + +from .tasks import register_listeners +from lnbits.tasks import record_async + +diagonalley_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/diagonalley/config.json b/lnbits/extensions/diagonalley/config.json new file mode 100644 index 00000000..99e92e9b --- /dev/null +++ b/lnbits/extensions/diagonalley/config.json @@ -0,0 +1,6 @@ +{ + "name": "Diagon Alley", + "short_description": "Movable anonymous market stand", + "icon": "add_shopping_cart", + "contributors": ["benarc","DeanH"] +} diff --git a/lnbits/extensions/diagonalley/crud.py b/lnbits/extensions/diagonalley/crud.py new file mode 100644 index 00000000..c6ce8222 --- /dev/null +++ b/lnbits/extensions/diagonalley/crud.py @@ -0,0 +1,395 @@ +from base64 import urlsafe_b64encode +from uuid import uuid4 +from typing import List, Optional, Union + +from lnbits.settings import WALLET + +# from lnbits.db import open_ext_db +from lnbits.db import SQLITE +from . import db +from .models import Products, Orders, Stalls, Zones + +import httpx +from lnbits.helpers import urlsafe_short_hash +import re + +regex = re.compile( + r"^(?:http|ftp)s?://" # http:// or https:// + r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" + r"localhost|" + r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" + r"(?::\d+)?" + r"(?:/?|[/?]\S+)$", + re.IGNORECASE, +) + + +###Products + + +async def create_diagonalley_product( + *, + stall_id: str, + product: str, + categories: str, + description: str, + image: Optional[str] = None, + price: int, + quantity: int, + shippingzones: str, +) -> Products: + returning = "" if db.type == SQLITE else "RETURNING ID" + method = db.execute if db.type == SQLITE else db.fetchone + product_id = urlsafe_short_hash() + # with open_ext_db("diagonalley") as db: + result = await (method)( + f""" + INSERT INTO diagonalley.products (id, stall, product, categories, description, image, price, quantity, shippingzones) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + {returning} + """, + ( + product_id, + stall_id, + product, + categories, + description, + image, + price, + quantity, + ), + ) + product = await get_diagonalley_product(product_id) + assert product, "Newly created product couldn't be retrieved" + return product + + +async def update_diagonalley_product(product_id: str, **kwargs) -> Optional[Stalls]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + + # with open_ext_db("diagonalley") as db: + await db.execute( + f"UPDATE diagonalley.products SET {q} WHERE id = ?", + (*kwargs.values(), product_id), + ) + row = await db.fetchone( + "SELECT * FROM diagonalley.products WHERE id = ?", (product_id,) + ) + + return get_diagonalley_stall(product_id) + + +async def get_diagonalley_product(product_id: str) -> Optional[Products]: + row = await db.fetchone( + "SELECT * FROM diagonalley.products WHERE id = ?", (product_id,) + ) + return Products.from_row(row) if row else None + + +async def get_diagonalley_products(wallet_ids: Union[str, List[str]]) -> List[Products]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + # with open_ext_db("diagonalley") as db: + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f""" + SELECT * FROM diagonalley.products WHERE stall IN ({q}) + """, + (*wallet_ids,), + ) + return [Products.from_row(row) for row in rows] + + +async def delete_diagonalley_product(product_id: str) -> None: + await db.execute("DELETE FROM diagonalley.products WHERE id = ?", (product_id,)) + + +###zones + + +async def create_diagonalley_zone( + *, + wallet: Optional[str] = None, + cost: Optional[int] = 0, + countries: Optional[str] = None, +) -> Zones: + + returning = "" if db.type == SQLITE else "RETURNING ID" + method = db.execute if db.type == SQLITE else db.fetchone + + zone_id = urlsafe_short_hash() + result = await (method)( + f""" + INSERT INTO diagonalley.zones ( + id, + wallet, + cost, + countries + + ) + VALUES (?, ?, ?, ?) + {returning} + """, + (zone_id, wallet, cost, countries), + ) + + zone = await get_diagonalley_zone(zone_id) + assert zone, "Newly created zone couldn't be retrieved" + return zone + + +async def update_diagonalley_zone(zone_id: str, **kwargs) -> Optional[Zones]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE diagonalley.zones SET {q} WHERE id = ?", + (*kwargs.values(), zone_id), + ) + row = await db.fetchone("SELECT * FROM diagonalley.zones WHERE id = ?", (zone_id,)) + return Zones.from_row(row) if row else None + + +async def get_diagonalley_zone(zone_id: str) -> Optional[Zones]: + row = await db.fetchone("SELECT * FROM diagonalley.zones WHERE id = ?", (zone_id,)) + return Zones.from_row(row) if row else None + + +async def get_diagonalley_zones(wallet_ids: Union[str, List[str]]) -> List[Zones]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + print(wallet_ids) + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM diagonalley.zones WHERE wallet IN ({q})", (*wallet_ids,) + ) + + for r in rows: + try: + x = httpx.get(r["zoneaddress"] + "/" + r["ratingkey"]) + if x.status_code == 200: + await db.execute( + "UPDATE diagonalley.zones SET online = ? WHERE id = ?", + ( + True, + r["id"], + ), + ) + else: + await db.execute( + "UPDATE diagonalley.zones SET online = ? WHERE id = ?", + ( + False, + r["id"], + ), + ) + except: + print("An exception occurred") + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM diagonalley.zones WHERE wallet IN ({q})", (*wallet_ids,) + ) + return [Zones.from_row(row) for row in rows] + + +async def delete_diagonalley_zone(zone_id: str) -> None: + await db.execute("DELETE FROM diagonalley.zones WHERE id = ?", (zone_id,)) + + +###Stalls + + +async def create_diagonalley_stall( + *, + wallet: str, + name: str, + publickey: str, + privatekey: str, + relays: str, + shippingzones: str, +) -> Stalls: + + returning = "" if db.type == SQLITE else "RETURNING ID" + method = db.execute if db.type == SQLITE else db.fetchone + + stall_id = urlsafe_short_hash() + result = await (method)( + f""" + INSERT INTO diagonalley.stalls ( + id, + wallet, + name, + publickey, + privatekey, + relays, + shippingzones + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + {returning} + """, + (stall_id, wallet, name, publickey, privatekey, relays, shippingzones), + ) + + stall = await get_diagonalley_stall(stall_id) + assert stall, "Newly created stall couldn't be retrieved" + return stall + + +async def update_diagonalley_stall(stall_id: str, **kwargs) -> Optional[Stalls]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE diagonalley.stalls SET {q} WHERE id = ?", + (*kwargs.values(), stall_id), + ) + row = await db.fetchone( + "SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,) + ) + return Stalls.from_row(row) if row else None + + +async def get_diagonalley_stall(stall_id: str) -> Optional[Stalls]: + roww = await db.fetchone( + "SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,) + ) + + try: + x = httpx.get(roww["stalladdress"] + "/" + roww["ratingkey"]) + if x.status_code == 200: + await db.execute( + "UPDATE diagonalley.stalls SET online = ? WHERE id = ?", + ( + True, + stall_id, + ), + ) + else: + await db.execute( + "UPDATE diagonalley.stalls SET online = ? WHERE id = ?", + ( + False, + stall_id, + ), + ) + except: + print("An exception occurred") + + # with open_ext_db("diagonalley") as db: + row = await db.fetchone( + "SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,) + ) + return Stalls.from_row(row) if row else None + + +async def get_diagonalley_stalls(wallet_ids: Union[str, List[str]]) -> List[Stalls]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM diagonalley.stalls WHERE wallet IN ({q})", (*wallet_ids,) + ) + + for r in rows: + try: + x = httpx.get(r["stalladdress"] + "/" + r["ratingkey"]) + if x.status_code == 200: + await db.execute( + "UPDATE diagonalley.stalls SET online = ? WHERE id = ?", + ( + True, + r["id"], + ), + ) + else: + await db.execute( + "UPDATE diagonalley.stalls SET online = ? WHERE id = ?", + ( + False, + r["id"], + ), + ) + except: + print("An exception occurred") + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM diagonalley.stalls WHERE wallet IN ({q})", (*wallet_ids,) + ) + return [Stalls.from_row(row) for row in rows] + + +async def delete_diagonalley_stall(stall_id: str) -> None: + await db.execute("DELETE FROM diagonalley.stalls WHERE id = ?", (stall_id,)) + + +###Orders + + +async def create_diagonalley_order( + *, + productid: str, + wallet: str, + product: str, + quantity: int, + shippingzone: str, + address: str, + email: str, + invoiceid: str, + paid: bool, + shipped: bool, +) -> Orders: + returning = "" if db.type == SQLITE else "RETURNING ID" + method = db.execute if db.type == SQLITE else db.fetchone + + order_id = urlsafe_short_hash() + result = await (method)( + f""" + INSERT INTO diagonalley.orders (id, productid, wallet, product, + quantity, shippingzone, address, email, invoiceid, paid, shipped) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + {returning} + """, + ( + order_id, + productid, + wallet, + product, + quantity, + shippingzone, + address, + email, + invoiceid, + False, + False, + ), + ) + if db.type == SQLITE: + order_id = result._result_proxy.lastrowid + else: + order_id = result[0] + + link = await get_diagonalley_order(order_id) + assert link, "Newly created link couldn't be retrieved" + return link + + +async def get_diagonalley_order(order_id: str) -> Optional[Orders]: + row = await db.fetchone( + "SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,) + ) + return Orders.from_row(row) if row else None + + +async def get_diagonalley_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM diagonalley.orders WHERE wallet IN ({q})", (*wallet_ids,) + ) + # + return [Orders.from_row(row) for row in rows] + + +async def delete_diagonalley_order(order_id: str) -> None: + await db.execute("DELETE FROM diagonalley.orders WHERE id = ?", (order_id,)) diff --git a/lnbits/extensions/diagonalley/migrations.py b/lnbits/extensions/diagonalley/migrations.py new file mode 100644 index 00000000..1523f398 --- /dev/null +++ b/lnbits/extensions/diagonalley/migrations.py @@ -0,0 +1,69 @@ +async def m001_initial(db): + """ + Initial products table. + """ + await db.execute( + """ + CREATE TABLE diagonalley.products ( + id TEXT PRIMARY KEY, + stall TEXT NOT NULL, + product TEXT NOT NULL, + categories TEXT NOT NULL, + description TEXT NOT NULL, + image TEXT NOT NULL, + price INTEGER NOT NULL, + quantity INTEGER NOT NULL + ); + """ + ) + + """ + Initial stalls table. + """ + await db.execute( + """ + CREATE TABLE diagonalley.stalls ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + publickey TEXT NOT NULL, + privatekey TEXT NOT NULL, + relays TEXT NOT NULL + ); + """ + ) + + """ + Initial zones table. + """ + await db.execute( + """ + CREATE TABLE diagonalley.zones ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + cost TEXT NOT NULL, + countries TEXT NOT NULL + ); + """ + ) + + """ + Initial orders table. + """ + await db.execute( + """ + CREATE TABLE diagonalley.orders ( + id TEXT PRIMARY KEY, + productid TEXT NOT NULL, + wallet TEXT NOT NULL, + product TEXT NOT NULL, + quantity INTEGER NOT NULL, + shippingzone INTEGER NOT NULL, + address TEXT NOT NULL, + email TEXT NOT NULL, + invoiceid TEXT NOT NULL, + paid BOOLEAN NOT NULL, + shipped BOOLEAN NOT NULL + ); + """ + ) diff --git a/lnbits/extensions/diagonalley/models.py b/lnbits/extensions/diagonalley/models.py new file mode 100644 index 00000000..0f2a1d78 --- /dev/null +++ b/lnbits/extensions/diagonalley/models.py @@ -0,0 +1,57 @@ +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult +from starlette.requests import Request +from fastapi.param_functions import Query +from typing import Optional, Dict +from lnbits.lnurl import encode as lnurl_encode # type: ignore +from lnurl.types import LnurlPayMetadata # type: ignore +from pydantic import BaseModel +import json +from sqlite3 import Row + + +class Stalls(BaseModel): + id: str = Query(None) + wallet: str = Query(None) + name: str = Query(None) + publickey: str = Query(None) + privatekey: str = Query(None) + relays: str = Query(None) + +class createStalls(BaseModel): + wallet: str = Query(None) + name: str = Query(None) + publickey: str = Query(None) + privatekey: str = Query(None) + relays: str = Query(None) + shippingzones: str = Query(None) + +class Products(BaseModel): + id: str = Query(None) + stall: str = Query(None) + product: str = Query(None) + categories: str = Query(None) + description: str = Query(None) + image: str = Query(None) + price: int = Query(0) + quantity: int = Query(0) + + +class Zones(BaseModel): + id: str = Query(None) + wallet: str = Query(None) + cost: str = Query(None) + countries: str = Query(None) + + +class Orders(BaseModel): + id: str = Query(None) + productid: str = Query(None) + stall: str = Query(None) + product: str = Query(None) + quantity: int = Query(0) + shippingzone: int = Query(0) + address: str = Query(None) + email: str = Query(None) + invoiceid: str = Query(None) + paid: bool + shipped: bool diff --git a/lnbits/extensions/diagonalley/static/js/index.js b/lnbits/extensions/diagonalley/static/js/index.js new file mode 100644 index 00000000..1a25edaa --- /dev/null +++ b/lnbits/extensions/diagonalley/static/js/index.js @@ -0,0 +1,824 @@ +/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ + +Vue.component(VueQrcode.name, VueQrcode) + +const pica = window.pica() + +new Vue({ + el: '#vue', + mixins: [windowMixin], + data: function () { + return { + products: [], + orders: [], + stalls: [], + zones: [], + shippedModel: false, + shippingZoneOptions: [ + 'Australia', + 'Austria', + 'Belgium', + 'Brazil', + 'Canada', + 'Denmark', + 'Finland', + 'France*', + 'Germany', + 'Greece', + 'Hong Kong', + 'Hungary', + 'Ireland', + 'Indonesia', + 'Israel', + 'Italy', + 'Japan', + 'Kazakhstan', + 'Korea', + 'Luxembourg', + 'Malaysia', + 'Mexico', + 'Netherlands', + 'New Zealand', + 'Norway', + 'Poland', + 'Portugal', + 'Russia', + 'Saudi Arabia', + 'Singapore', + 'Spain', + 'Sweden', + 'Switzerland', + 'Thailand', + 'Turkey', + 'Ukraine', + 'United Kingdom**', + 'United States***', + 'Vietnam', + 'China' + ], + categories: [ + 'Fashion (clothing and accessories)', + 'Health (and beauty)', + 'Toys (and baby equipment)', + 'Media (Books and CDs)', + 'Groceries (Food and Drink)', + 'Technology (Phones and Computers)', + 'Home (furniture and accessories)', + 'Gifts (flowers, cards, etc)' + ], + relayOptions: [ + 'wss://nostr-relay.herokuapp.com/ws', + 'wss://nostr-relay.bigsun.xyz/ws', + 'wss://freedom-relay.herokuapp.com/ws' + ], + label: '', + ordersTable: { + columns: [ + { + name: 'product', + align: 'left', + label: 'Product', + field: 'product' + }, + { + name: 'quantity', + align: 'left', + label: 'Quantity', + field: 'quantity' + }, + { + name: 'address', + align: 'left', + label: 'Address', + field: 'address' + }, + { + name: 'invoiceid', + align: 'left', + label: 'InvoiceID', + field: 'invoiceid' + }, + {name: 'paid', align: 'left', label: 'Paid', field: 'paid'}, + {name: 'shipped', align: 'left', label: 'Shipped', field: 'shipped'} + ], + pagination: { + rowsPerPage: 10 + } + }, + productsTable: { + columns: [ + { + name: 'stall', + align: 'left', + label: 'Stall', + field: 'stall' + }, + { + name: 'product', + align: 'left', + label: 'Product', + field: 'product' + }, + { + name: 'description', + align: 'left', + label: 'Description', + field: 'description' + }, + { + name: 'categories', + align: 'left', + label: 'Categories', + field: 'categories' + }, + {name: 'price', align: 'left', label: 'Price', field: 'price'}, + { + name: 'quantity', + align: 'left', + label: 'Quantity', + field: 'quantity' + }, + {name: 'id', align: 'left', label: 'ID', field: 'id'} + ], + pagination: { + rowsPerPage: 10 + } + }, + stallTable: { + columns: [ + { + name: 'id', + align: 'left', + label: 'ID', + field: 'id' + }, + { + name: 'name', + align: 'left', + label: 'Name', + field: 'name' + }, + { + name: 'wallet', + align: 'left', + label: 'Wallet', + field: 'wallet' + }, + { + name: 'publickey', + align: 'left', + label: 'Public key', + field: 'publickey' + }, + { + name: 'privatekey', + align: 'left', + label: 'Private key', + field: 'privatekey' + } + ], + pagination: { + rowsPerPage: 10 + } + }, + zonesTable: { + columns: [ + { + name: 'id', + align: 'left', + label: 'ID', + field: 'id' + }, + { + name: 'countries', + align: 'left', + label: 'Countries', + field: 'countries' + }, + { + name: 'cost', + align: 'left', + label: 'Cost', + field: 'cost' + } + ], + pagination: { + rowsPerPage: 10 + } + }, + productDialog: { + show: false, + data: {} + }, + stallDialog: { + show: false, + data: {} + }, + zoneDialog: { + show: false, + data: {} + }, + shopDialog: { + show: false, + data: {activate: false} + }, + orderDialog: { + show: false, + data: {} + }, + relayDialog: { + show: false, + data: {} + } + } + }, + computed: { + categoryOther: function () { + cats = trim(this.productDialog.data.categories.split(',')) + for (let i = 0; i < cats.length; i++) { + if (cats[i] == 'Others') { + return true + } + } + return false + } + }, + methods: { + //////////////////////////////////////// + ////////////////STALLS////////////////// + //////////////////////////////////////// + getStalls: function () { + var self = this + LNbits.api + .request( + 'GET', + '/diagonalley/api/v1/stalls?all_wallets', + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.stalls = response.data.map(function (obj) { + console.log(obj) + return mapDiagonAlley(obj) + }) + }) + }, + openStallUpdateDialog: function (linkId) { + var self = this + var link = _.findWhere(self.stalls, {id: linkId}) + + this.stallDialog.data = _.clone(link._data) + this.stallDialog.show = true + }, + sendStallFormData: function () { + if (this.stallDialog.data.id) { + } else { + var data = { + name: this.stallDialog.data.name, + wallet: this.stallDialog.data.wallet, + publickey: this.stallDialog.data.publickey, + privatekey: this.stallDialog.data.privatekey, + relays: this.stallDialog.data.relays + } + } + + if (this.stallDialog.data.id) { + this.updateStall(this.stallDialog.data) + } else { + this.createStall(data) + } + }, + updateStall: function (data) { + var self = this + LNbits.api + .request( + 'PUT', + '/diagonalley/api/v1/stalls' + data.id, + _.findWhere(self.g.user.wallets, { + id: self.stallDialog.data.wallet + }).inkey, + _.pick(data, 'name', 'wallet', 'publickey', 'privatekey') + ) + .then(function (response) { + self.stalls = _.reject(self.stalls, function (obj) { + return obj.id == data.id + }) + self.stalls.push(mapDiagonAlley(response.data)) + self.stallDialog.show = false + self.stallDialog.data = {} + data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + createStall: function (data) { + var self = this + LNbits.api + .request( + 'POST', + '/diagonalley/api/v1/stalls', + _.findWhere(self.g.user.wallets, { + id: self.stallDialog.data.wallet + }).inkey, + data + ) + .then(function (response) { + self.stalls.push(mapDiagonAlley(response.data)) + self.stallDialog.show = false + self.stallDialog.data = {} + data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + deleteStall: function (stallId) { + var self = this + var stall = _.findWhere(self.stalls, {id: stallId}) + + LNbits.utils + .confirmDialog('Are you sure you want to delete this Stall link?') + .onOk(function () { + LNbits.api + .request( + 'DELETE', + '/diagonalley/api/v1/stalls/' + stallId, + _.findWhere(self.g.user.wallets, {id: stall.wallet}).inkey + ) + .then(function (response) { + self.stalls = _.reject(self.stalls, function (obj) { + return obj.id == stallId + }) + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }) + }, + exportStallsCSV: function () { + LNbits.utils.exportCSV(this.stallsTable.columns, this.stalls) + }, + //////////////////////////////////////// + ///////////////PRODUCTS///////////////// + //////////////////////////////////////// + getProducts: function () { + var self = this + + LNbits.api + .request( + 'GET', + '/diagonalley/api/v1/products?all_stalls', + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.products = response.data.map(function (obj) { + return mapDiagonAlley(obj) + }) + }) + }, + openProductUpdateDialog: function (linkId) { + var self = this + var link = _.findWhere(self.products, {id: linkId}) + + self.productDialog.data = _.clone(link._data) + self.productDialog.show = true + }, + sendProductFormData: function () { + if (this.productDialog.data.id) { + } else { + var data = { + product: this.productDialog.data.product, + categories: + this.productDialog.data.categories + + this.productDialog.categoriesextra, + description: this.productDialog.data.description, + image: this.productDialog.data.image, + price: this.productDialog.data.price, + quantity: this.productDialog.data.quantity + } + } + if (this.productDialog.data.id) { + this.updateProduct(this.productDialog.data) + } else { + this.createProduct(data) + } + }, + imageAdded(file) { + let blobURL = URL.createObjectURL(file) + let image = new Image() + image.src = blobURL + image.onload = async () => { + let canvas = document.createElement('canvas') + canvas.setAttribute('width', 100) + canvas.setAttribute('height', 100) + await pica.resize(image, canvas, { + quality: 0, + alpha: true, + unsharpAmount: 95, + unsharpRadius: 0.9, + unsharpThreshold: 70 + }) + this.productDialog.data.image = canvas.toDataURL() + this.productDialog = {...this.productDialog} + } + }, + imageCleared() { + this.productDialog.data.image = null + this.productDialog = {...this.productDialog} + }, + updateProduct: function (data) { + var self = this + LNbits.api + .request( + 'PUT', + '/diagonalley/api/v1/products' + data.id, + _.findWhere(self.g.user.wallets, { + id: self.productDialog.data.wallet + }).inkey, + _.pick( + data, + 'shopname', + 'relayaddress', + 'shippingzone1', + 'zone1cost', + 'shippingzone2', + 'zone2cost', + 'email' + ) + ) + .then(function (response) { + self.products = _.reject(self.products, function (obj) { + return obj.id == data.id + }) + self.products.push(mapDiagonAlley(response.data)) + self.productDialog.show = false + self.productDialog.data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + createProduct: function (data) { + var self = this + LNbits.api + .request( + 'POST', + '/diagonalley/api/v1/products', + _.findWhere(self.g.user.wallets, { + id: self.productDialog.data.wallet + }).inkey, + data + ) + .then(function (response) { + self.products.push(mapDiagonAlley(response.data)) + self.productDialog.show = false + self.productDialog.data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + deleteProduct: function (productId) { + var self = this + var product = _.findWhere(this.products, {id: productId}) + + LNbits.utils + .confirmDialog('Are you sure you want to delete this products link?') + .onOk(function () { + LNbits.api + .request( + 'DELETE', + '/diagonalley/api/v1/products/' + productId, + _.findWhere(self.g.user.wallets, {id: product.wallet}).inkey + ) + .then(function (response) { + self.products = _.reject(self.products, function (obj) { + return obj.id == productId + }) + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }) + }, + exportProductsCSV: function () { + LNbits.utils.exportCSV(this.productsTable.columns, this.products) + }, + //////////////////////////////////////// + //////////////////ZONE////////////////// + //////////////////////////////////////// + getZones: function () { + var self = this + + LNbits.api + .request( + 'GET', + '/diagonalley/api/v1/zones?all_wallets', + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.zones = response.data.map(function (obj) { + return mapDiagonAlley(obj) + }) + }) + }, + openZoneUpdateDialog: function (linkId) { + var self = this + var link = _.findWhere(self.zones, {id: linkId}) + + this.zoneDialog.data = _.clone(link._data) + this.zoneDialog.show = true + }, + sendZoneFormData: function () { + if (this.zoneDialog.data.id) { + } else { + var data = { + countries: toString(this.zoneDialog.data.countries), + cost: parseInt(this.zoneDialog.data.cost) + } + } + + if (this.zoneDialog.data.id) { + this.updateZone(this.zoneDialog.data) + } else { + this.createZone(data) + } + }, + updateZone: function (data) { + var self = this + LNbits.api + .request( + 'PUT', + '/diagonalley/api/v1/zones' + data.id, + _.findWhere(self.g.user.wallets, { + id: self.zoneDialog.data.wallet + }).inkey, + _.pick(data, 'countries', 'cost') + ) + .then(function (response) { + self.zones = _.reject(self.zones, function (obj) { + return obj.id == data.id + }) + self.zones.push(mapDiagonAlley(response.data)) + self.zoneDialog.show = false + self.zoneDialog.data = {} + data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + createZone: function (data) { + var self = this + console.log(self.g.user.wallets[0]) + console.log(data) + LNbits.api + .request( + 'POST', + '/diagonalley/api/v1/zones', + self.g.user.wallets[0].inkey, + data + ) + .then(function (response) { + self.zones.push(mapDiagonAlley(response.data)) + self.zoneDialog.show = false + self.zoneDialog.data = {} + data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + deleteZone: function (zoneId) { + var self = this + var zone = _.findWhere(self.zones, {id: zoneId}) + + LNbits.utils + .confirmDialog('Are you sure you want to delete this Zone link?') + .onOk(function () { + LNbits.api + .request( + 'DELETE', + '/diagonalley/api/v1/zones/' + zoneId, + _.findWhere(self.g.user.wallets, {id: zone.wallet}).inkey + ) + .then(function (response) { + self.zones = _.reject(self.zones, function (obj) { + return obj.id == zoneId + }) + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }) + }, + exportZonesCSV: function () { + LNbits.utils.exportCSV(this.zonesTable.columns, this.zones) + }, + //////////////////////////////////////// + //////////////////SHOP////////////////// + //////////////////////////////////////// + getShops: function () { + var self = this + + LNbits.api + .request( + 'GET', + '/diagonalley/api/v1/shops?all_wallets', + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.shops = response.data.map(function (obj) { + return mapDiagonAlley(obj) + }) + }) + }, + openShopUpdateDialog: function (linkId) { + var self = this + var link = _.findWhere(self.shops, {id: linkId}) + + this.shopDialog.data = _.clone(link._data) + this.shopDialog.show = true + }, + sendShopFormData: function () { + if (this.shopDialog.data.id) { + } else { + var data = { + countries: this.shopDialog.data.countries, + cost: this.shopDialog.data.cost + } + } + + if (this.shopDialog.data.id) { + this.updateZone(this.shopDialog.data) + } else { + this.createZone(data) + } + }, + updateShop: function (data) { + var self = this + LNbits.api + .request( + 'PUT', + '/diagonalley/api/v1/shops' + data.id, + _.findWhere(self.g.user.wallets, { + id: self.shopDialog.data.wallet + }).inkey, + _.pick(data, 'countries', 'cost') + ) + .then(function (response) { + self.shops = _.reject(self.shops, function (obj) { + return obj.id == data.id + }) + self.shops.push(mapDiagonAlley(response.data)) + self.shopDialog.show = false + self.shopDialog.data = {} + data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + createShop: function (data) { + var self = this + console.log('cuntywoo') + LNbits.api + .request( + 'POST', + '/diagonalley/api/v1/shops', + _.findWhere(self.g.user.wallets, { + id: self.shopDialog.data.wallet + }).inkey, + data + ) + .then(function (response) { + self.shops.push(mapDiagonAlley(response.data)) + self.shopDialog.show = false + self.shopDialog.data = {} + data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + deleteShop: function (shopId) { + var self = this + var shop = _.findWhere(self.shops, {id: shopId}) + + LNbits.utils + .confirmDialog('Are you sure you want to delete this Shop link?') + .onOk(function () { + LNbits.api + .request( + 'DELETE', + '/diagonalley/api/v1/shops/' + shopId, + _.findWhere(self.g.user.wallets, {id: shop.wallet}).inkey + ) + .then(function (response) { + self.shops = _.reject(self.shops, function (obj) { + return obj.id == shopId + }) + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }) + }, + exportShopsCSV: function () { + LNbits.utils.exportCSV(this.shopsTable.columns, this.shops) + }, + //////////////////////////////////////// + ////////////////ORDERS////////////////// + //////////////////////////////////////// + getOrders: function () { + var self = this + + LNbits.api + .request( + 'GET', + '/diagonalley/api/v1/orders?all_wallets', + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.orders = response.data.map(function (obj) { + return mapDiagonAlley(obj) + }) + }) + }, + createOrder: function () { + var data = { + address: this.orderDialog.data.address, + email: this.orderDialog.data.email, + quantity: this.orderDialog.data.quantity, + shippingzone: this.orderDialog.data.shippingzone + } + var self = this + + LNbits.api + .request( + 'POST', + '/diagonalley/api/v1/orders', + _.findWhere(self.g.user.wallets, {id: self.orderDialog.data.wallet}) + .inkey, + data + ) + .then(function (response) { + self.orders.push(mapDiagonAlley(response.data)) + self.orderDialog.show = false + self.orderDialog.data = {} + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + deleteOrder: function (orderId) { + var self = this + var order = _.findWhere(self.orders, {id: orderId}) + + LNbits.utils + .confirmDialog('Are you sure you want to delete this order link?') + .onOk(function () { + LNbits.api + .request( + 'DELETE', + '/diagonalley/api/v1/orders/' + orderId, + _.findWhere(self.g.user.wallets, {id: order.wallet}).inkey + ) + .then(function (response) { + self.orders = _.reject(self.orders, function (obj) { + return obj.id == orderId + }) + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }) + }, + shipOrder: function (order_id) { + var self = this + + LNbits.api + .request( + 'GET', + '/diagonalley/api/v1/orders/shipped/' + order_id, + this.g.user.wallets[0].inkey + ) + .then(function (response) { + self.orders = response.data.map(function (obj) { + return mapDiagonAlley(obj) + }) + }) + }, + exportOrdersCSV: function () { + LNbits.utils.exportCSV(this.ordersTable.columns, this.orders) + } + }, + created: function () { + if (this.g.user.wallets.length) { + this.getStalls() + this.getProducts() + this.getZones() + this.getOrders() + } + } +}) diff --git a/lnbits/extensions/diagonalley/tasks.py b/lnbits/extensions/diagonalley/tasks.py new file mode 100644 index 00000000..3fee63d9 --- /dev/null +++ b/lnbits/extensions/diagonalley/tasks.py @@ -0,0 +1,29 @@ +import asyncio + +from lnbits.core.models import Payment +from lnbits.tasks import register_invoice_listener + +from .crud import get_ticket, set_ticket_paid + + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + if "lnticket" != payment.extra.get("tag"): + # not a lnticket invoice + return + + ticket = await get_ticket(payment.checking_id) + if not ticket: + print("this should never happen", payment) + return + + await payment.set_pending(False) + await set_ticket_paid(payment.payment_hash) \ No newline at end of file diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html b/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html new file mode 100644 index 00000000..530c89e8 --- /dev/null +++ b/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html @@ -0,0 +1,129 @@ + + + +
+ Diagon Alley: Decentralised Market-Stalls +
+

+ Each Stall has its own keys!
+

    +
  1. Create Shipping Zones you're willing to ship to
  2. +
  3. Create a Stall to list yiur products on
  4. +
  5. Create products to put on the Stall
  6. +
  7. List stalls on a simple frontend shop page, or point at Nostr shop client key
  8. +
+ Make a list of products to sell, point your list of products at a public + relay. Buyers browse your products on the relay, and pay you directly. + Ratings are managed by the relay. Your stall can be listed in multiple + relays, even over TOR, if you wish to be anonymous.
+ More information on the + Diagon Alley Protocol
+ + Created by, Ben Arc +

+
+
+
+ + + + + + GET + /diagonalley/api/v1/stall/products/<relay_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ Product JSON list +
Curl example
+ curl -X GET {{ request.url_root + }}api/v1/stall/products/<relay_id> +
+
+
+ + + + POST + /diagonalley/api/v1/stall/order/<relay_id> +
Body (application/json)
+ {"id": <string>, "address": <string>, "shippingzone": + <integer>, "email": <string>, "quantity": + <integer>} +
+ Returns 201 CREATED (application/json) +
+ {"checking_id": <string>,"payment_request": + <string>} +
Curl example
+ curl -X POST {{ request.url_root + }}api/v1/stall/order/<relay_id> -d '{"id": <product_id&>, + "email": <customer_email>, "address": <customer_address>, + "quantity": 2, "shippingzone": 1}' -H "Content-type: application/json" + +
+
+
+ + + + GET + /diagonalley/api/v1/stall/checkshipped/<checking_id> +
Headers
+
+ Returns 200 OK (application/json) +
+ {"shipped": <boolean>} +
Curl example
+ curl -X GET {{ request.url_root + }}api/v1/stall/checkshipped/<checking_id> -H "Content-type: + application/json" +
+
+
+
diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/index.html b/lnbits/extensions/diagonalley/templates/diagonalley/index.html new file mode 100644 index 00000000..98405f6d --- /dev/null +++ b/lnbits/extensions/diagonalley/templates/diagonalley/index.html @@ -0,0 +1,634 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+ + + + + + + + +
+
+ +
+
+ +
+
+ + + + + + + + +
+ Update Product + + Create Product + + Cancel +
+
+
+
+ + + + + + +
+ Update Shipping Zone + Create Shipping Zone + + Cancel +
+
+
+
+ + + + + + + +
+ Update Relay + Launch + + Cancel +
+
+
+
+ + + + + + + +
+
+ Generate keys +
+
+ Restore keys +
+
+ + + + + + + +
+ Update Stall + Create Stall + Cancel +
+
+
+
+ +
+ + + + Product List a product + + Shipping Zone Create a shipping zone + + Stall + Create a stall to list products on + Launch frontend shop (not Nostr) + Makes a simple frontend shop for your stalls + + + + + +
+
+
Orders
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Products
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Stalls
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Shipping Zones
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
+ LNbits Diagon Alley Extension, powered by Nostr +
+
+ + + {% include "diagonalley/_api_docs.html" %} + +
+ + +
Messages (example)
+
+ + + +
+
+ OrderID:87h87h
KJBIBYBUYBUF90898....
+ OrderID:NIUHB7
79867KJGJHGVFYFV....
+
+
+
+ + +
+ + +
+ + +
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/stall.html b/lnbits/extensions/diagonalley/templates/diagonalley/stall.html new file mode 100644 index 00000000..768bedfe --- /dev/null +++ b/lnbits/extensions/diagonalley/templates/diagonalley/stall.html @@ -0,0 +1,9 @@ +

+
+
diff --git a/lnbits/extensions/diagonalley/views.py b/lnbits/extensions/diagonalley/views.py
new file mode 100644
index 00000000..2deed72b
--- /dev/null
+++ b/lnbits/extensions/diagonalley/views.py
@@ -0,0 +1,44 @@
+
+from typing import List
+
+from fastapi import Request, WebSocket, WebSocketDisconnect
+from fastapi.params import Depends
+from fastapi.templating import Jinja2Templates
+from starlette.responses import HTMLResponse  # type: ignore
+
+from http import HTTPStatus
+import json
+from lnbits.decorators import check_user_exists, validate_uuids
+from lnbits.extensions.diagonalley import diagonalley_ext
+
+from .crud import (
+    create_diagonalley_product,
+    get_diagonalley_product,
+    get_diagonalley_products,
+    delete_diagonalley_product,
+    create_diagonalley_order,
+    get_diagonalley_order,
+    get_diagonalley_orders,
+    update_diagonalley_product,
+)
+
+
+@diagonalley_ext.get("/", response_class=HTMLResponse)
+@validate_uuids(["usr"], required=True)
+@check_user_exists(request: Request)
+async def index():
+    return await render_template("diagonalley/index.html", user=g.user)
+
+
+@diagonalley_ext.get("/", response_class=HTMLResponse)
+async def display(request: Request, stall_id):
+    product = await get_diagonalley_products(stall_id)
+    if not product:
+        abort(HTTPStatus.NOT_FOUND, "Stall does not exist.")
+
+    return await render_template(
+        "diagonalley/stall.html",
+        stall=json.dumps(
+            [product._asdict() for product in await get_diagonalley_products(stall_id)]
+        ),
+    )
diff --git a/lnbits/extensions/diagonalley/views_api.py b/lnbits/extensions/diagonalley/views_api.py
new file mode 100644
index 00000000..4de2799e
--- /dev/null
+++ b/lnbits/extensions/diagonalley/views_api.py
@@ -0,0 +1,348 @@
+from http import HTTPStatus
+
+from fastapi import Request
+from fastapi.param_functions import Query
+from fastapi.params import Depends
+from starlette.exceptions import HTTPException
+
+from lnbits.core.crud import get_user
+from lnbits.decorators import api_check_wallet_key, api_validate_post_request
+
+from . import diagonalley_ext
+from .crud import (
+    create_diagonalley_product,
+    get_diagonalley_product,
+    get_diagonalley_products,
+    delete_diagonalley_product,
+    create_diagonalley_zone,
+    update_diagonalley_zone,
+    get_diagonalley_zone,
+    get_diagonalley_zones,
+    delete_diagonalley_zone,
+    create_diagonalley_stall,
+    update_diagonalley_stall,
+    get_diagonalley_stall,
+    get_diagonalley_stalls,
+    delete_diagonalley_stall,
+    create_diagonalley_order,
+    get_diagonalley_order,
+    get_diagonalley_orders,
+    update_diagonalley_product,
+    delete_diagonalley_order,
+)
+from lnbits.core.services import create_invoice
+from base64 import urlsafe_b64encode
+from uuid import uuid4
+
+# from lnbits.db import open_ext_db
+
+from . import db
+from .models import Products, Orders, Stalls
+
+### Products
+
+@copilot_ext.get("/api/v1/copilot/{copilot_id}")
+async def api_copilot_retrieve(
+    req: Request,
+    copilot_id: str = Query(None),
+    wallet: WalletTypeInfo = Depends(get_key_type),
+):
+    copilot = await get_copilot(copilot_id)
+    if not copilot:
+        raise HTTPException(
+            status_code=HTTPStatus.NOT_FOUND, detail="Copilot not found"
+        )
+    if not copilot.lnurl_toggle:
+        return copilot.dict()
+    return {**copilot.dict(), **{"lnurl": copilot.lnurl(req)}}
+
+
+@diagonalley_ext.get("/api/v1/products")
+async def api_diagonalley_products(
+    req: Request,
+    wallet: WalletTypeInfo = Depends(get_key_type),
+):
+    wallet_ids = [wallet.wallet.id]
+
+    if "all_stalls" in request.args:
+        wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
+
+    return ([product._asdict() for product in await get_diagonalley_products(wallet_ids)])
+
+
+@diagonalley_ext.post("/api/v1/products")
+@diagonalley_ext.put("/api/v1/products/{product_id}")
+async def api_diagonalley_product_create(
+    data: Products, 
+    product_id=: str = Query(None), 
+    wallet: WalletTypeInfo = Depends(get_key_type)
+    ):
+
+    if product_id:
+        product = await get_diagonalley_product(product_id)
+
+        if not product:
+            return ({"message": "Withdraw product does not exist."}))
+
+        if product.wallet != wallet.wallet.id:
+            return ({"message": "Not your withdraw product."}))
+
+        product = await update_diagonalley_product(product_id, data)
+    else:
+        product = await create_diagonalley_product(wallet_id=wallet.wallet.id, data)
+
+    return ({**product._asdict()}))
+
+
+@diagonalley_ext.route("/api/v1/products/{product_id}")
+async def api_diagonalley_products_delete(product_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
+    product = await get_diagonalley_product(product_id)
+
+    if not product:
+        return ({"message": "Product does not exist."})
+
+    if product.wallet != wallet.wallet.id:
+        return ({"message": "Not your Diagon Alley."})
+
+    await delete_diagonalley_product(product_id)
+
+    raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+# # # Shippingzones
+
+
+@diagonalley_ext.get("/api/v1/zones")
+async def api_diagonalley_zones(wallet: WalletTypeInfo = Depends(get_key_type)):
+    wallet_ids = [wallet.wallet.id]
+
+    if "all_wallets" in request.args:
+        wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
+
+    return ([zone._asdict() for zone in await get_diagonalley_zones(wallet_ids)]))
+
+
+@diagonalley_ext.post("/api/v1/zones")
+@diagonalley_ext.put("/api/v1/zones/{zone_id}")
+async def api_diagonalley_zone_create(
+    data: Zones, 
+    zone_id: str = Query(None),  
+    wallet: WalletTypeInfo = Depends(get_key_type)
+    ):
+    if zone_id:
+        zone = await get_diagonalley_zone(zone_id)
+
+        if not zone:
+            return ({"message": "Zone does not exist."}))
+
+        if zone.wallet != walley.wallet.id:
+            return ({"message": "Not your record."}))
+
+        zone = await update_diagonalley_zone(zone_id, data)
+    else:
+        zone = await create_diagonalley_zone(wallet=wallet.wallet.id, data)
+
+    return ({**zone._asdict()}))
+
+
+@diagonalley_ext.delete("/api/v1/zones/{zone_id}")
+async def api_diagonalley_zone_delete(zone_id: str = Query(None),  wallet: WalletTypeInfo = Depends(require_admin_key)):
+    zone = await get_diagonalley_zone(zone_id)
+
+    if not zone:
+        return ({"message": "zone does not exist."})
+
+    if zone.wallet != wallet.wallet.id:
+        return ({"message": "Not your zone."})
+
+    await delete_diagonalley_zone(zone_id)
+
+    raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+# # # Stalls
+
+
+@diagonalley_ext.get("/api/v1/stalls")
+async def api_diagonalley_stalls(wallet: WalletTypeInfo = Depends(get_key_type)):
+    wallet_ids = [wallet.wallet.id]
+
+    if "all_wallets" in request.args:
+        wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
+
+    return ([stall._asdict() for stall in await get_diagonalley_stalls(wallet_ids)])
+
+
+@diagonalley_ext.post("/api/v1/stalls")
+@diagonalley_ext.put("/api/v1/stalls/{stall_id}")
+async def api_diagonalley_stall_create(data: createStalls, stall_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
+
+    if stall_id:
+        stall = await get_diagonalley_stall(stall_id)
+
+        if not stall:
+            return ({"message": "Withdraw stall does not exist."}))
+
+        if stall.wallet != wallet.wallet.id:
+            return ({"message": "Not your withdraw stall."}))
+
+        stall = await update_diagonalley_stall(stall_id, data)
+    else:
+        stall = await create_diagonalley_stall(wallet_id=wallet.wallet.id, data)
+
+    return ({**stall._asdict()}))
+
+
+@diagonalley_ext.delete("/api/v1/stalls/{stall_id}")
+async def api_diagonalley_stall_delete(stall_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)):
+    stall = await get_diagonalley_stall(stall_id)
+
+    if not stall:
+        return ({"message": "Stall does not exist."})
+
+    if stall.wallet != wallet.wallet.id:
+        return ({"message": "Not your Stall."})
+
+    await delete_diagonalley_stall(stall_id)
+
+    raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+###Orders
+
+
+@diagonalley_ext.get("/api/v1/orders")
+async def api_diagonalley_orders(wallet: WalletTypeInfo = Depends(get_key_type)):
+    wallet_ids = [wallet.wallet.id]
+
+    if "all_wallets" in request.args:
+        wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
+
+    try:
+        return ([order._asdict() for order in await get_diagonalley_orders(wallet_ids)])
+    except:
+        return ({"message": "We could not retrieve the orders."}))
+
+
+@diagonalley_ext.post("/api/v1/orders")
+
+async def api_diagonalley_order_create(data: createOrders, wallet: WalletTypeInfo = Depends(get_key_type)):
+    order = await create_diagonalley_order(wallet_id=wallet.wallet.id, data)
+    return ({**order._asdict()})
+
+
+@diagonalley_ext.delete("/api/v1/orders/{order_id}")
+async def api_diagonalley_order_delete(order_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
+    order = await get_diagonalley_order(order_id)
+
+    if not order:
+        return ({"message": "Order does not exist."})
+
+    if order.wallet != wallet.wallet.id:
+        return ({"message": "Not your Order."})
+
+    await delete_diagonalley_order(order_id)
+
+    raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
+
+
+@diagonalley_ext.get("/api/v1/orders/paid/{order_id}")
+async def api_diagonalley_order_paid(order_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)):
+    await db.execute(
+        "UPDATE diagonalley.orders SET paid = ? WHERE id = ?",
+        (
+            True,
+            order_id,
+        ),
+    )
+    return "", HTTPStatus.OK
+
+
+@diagonalley_ext.get("/api/v1/orders/shipped/{order_id}")
+async def api_diagonalley_order_shipped(order_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
+    await db.execute(
+        "UPDATE diagonalley.orders SET shipped = ? WHERE id = ?",
+        (
+            True,
+            order_id,
+        ),
+    )
+    order = await db.fetchone(
+        "SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,)
+    )
+
+    return ([order._asdict() for order in get_diagonalley_orders(order["wallet"])]))
+
+
+###List products based on stall id
+
+
+@diagonalley_ext.get("/api/v1/stall/products/{stall_id}")
+async def api_diagonalley_stall_products(stall_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
+
+    rows = await db.fetchone(
+        "SELECT * FROM diagonalley.stalls WHERE id = ?", (stall_id,)
+    )
+    print(rows[1])
+    if not rows:
+        return ({"message": "Stall does not exist."})
+
+    products = db.fetchone(
+        "SELECT * FROM diagonalley.products WHERE wallet = ?", (rows[1],)
+    )
+    if not products:
+        return ({"message": "No products"})
+
+    return ([products._asdict() for products in await get_diagonalley_products(rows[1])])
+
+
+###Check a product has been shipped
+
+
+@diagonalley_ext.get("/api/v1/stall/checkshipped/{checking_id}")
+async def api_diagonalley_stall_checkshipped(checking_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)):
+    rows = await db.fetchone(
+        "SELECT * FROM diagonalley.orders WHERE invoiceid = ?", (checking_id,)
+    )
+    return ({"shipped": rows["shipped"]})
+
+
+###Place order
+
+
+@diagonalley_ext.post("/api/v1/stall/order/{stall_id}")
+async def api_diagonalley_stall_order(data:createOrders, wallet: WalletTypeInfo = Depends(get_key_type)):
+    product = await get_diagonalley_product(data.id)
+    shipping = await get_diagonalley_stall(stall_id)
+
+    if data.shippingzone == 1:
+        shippingcost = shipping.zone1cost
+    else:
+        shippingcost = shipping.zone2cost
+
+    checking_id, payment_request = await create_invoice(
+        wallet_id=product.wallet,
+        amount=shippingcost + (data.quantity * product.price),
+        memo=data.id,
+    )
+    selling_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8")
+    await db.execute(
+        """
+            INSERT INTO diagonalley.orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped)
+            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+            """,
+        (
+            selling_id,
+            data.id,
+            product.wallet,
+            product.product,
+            data.quantity,
+            data.shippingzone,
+            data.address,
+            data.email,
+            checking_id,
+            False,
+            False,
+        ),
+    )
+    return ({"checking_id": checking_id, "payment_request": payment_request}))