commit 2d60683beb4ca1b1e347d8997c5feeac37e63e3e Author: Tiago Vasconcelos Date: Mon Feb 27 17:45:08 2023 +0000 Add files via upload diff --git a/README.md b/README.md new file mode 100644 index 0000000..88e796f --- /dev/null +++ b/README.md @@ -0,0 +1,281 @@ +## Nostr Diagon Alley protocol (for resilient marketplaces) + +#### Original protocol https://github.com/lnbits/Diagon-Alley + +> The concepts around resilience in Diagon Alley helped influence the creation of the NOSTR protocol, now we get to build Diagon Alley on NOSTR! + +In Diagon Alley, `merchant` and `customer` communicate via NOSTR relays, so loss of money, product information, and reputation become far less likely if attacked. + +A `merchant` and `customer` both have a NOSTR key-pair that are used to sign notes and subscribe to events. + +#### For further information about NOSTR, see https://github.com/nostr-protocol/nostr + + +## 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` + +## Diagon Alley Clients + +### Merchant admin + +Where the `merchant` creates, updates and deletes `stalls` and `products`, as well as where they manage sales, payments and communication with `customers`. + +The `merchant` admin software can be purely clientside, but for `convenience` and uptime, implementations will likely have a server listening for NOSTR events. + +### Marketplace + +`Marketplace` software should be entirely clientside, either as a stand-alone app, or as a purely frontend webpage. A `customer` subscribes to different merchant NOSTR public keys, and those `merchants` `stalls` and `products` become listed and searchable. The marketplace client is like any other ecommerce site, with basket and checkout. `Marketplaces` may also wish to include a `customer` support area for direct message communication with `merchants`. + +## `Merchant` publishing/updating products (event) + +NIP-01 https://github.com/nostr-protocol/nips/blob/master/01.md uses the basic NOSTR event type. + +The `merchant` event that publishes and updates product lists + +The below json goes in `content` of NIP-01. + +Data from newer events should replace data from older events. + +`action` types (used to indicate changes): +* `update` element has changed +* `delete` element should be deleted +* `suspend` element is suspended +* `unsuspend` element is unsuspended + + +``` +{ + "name": , + "description": , + "currency": , + "action": , + "shipping": [ + { + "id": , + "zones": , + "price": , + }, + { + "id": , + "zones": , + "price": , + }, + { + "id": , + "zones": , + "price": , + } + ], + "stalls": [ + { + "id": , + "name": , + "description": , + "categories": , + "shipping": , + "action": , + "products": [ + { + "id": , + "name": , + "description": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + { + "id": , + "name": , + "description": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + }, + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + ] + }, + { + "id": , + "name": , + "description": , + "categories": , + "shipping": , + "action": , + "products": [ + { + "id": , + "name": , + "categories": , + "amount": , + "price": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + } + ] + } + ] +} + +``` + +As all elements are optional, an `update` `action` to a `product` `image`, may look as simple as: + +``` +{ + "stalls": [ + { + "id": , + "products": [ + { + "id": , + "images": [ + { + "id": , + "name": , + "link": + } + ], + "action": , + }, + ] + } + ] +} + +``` + + +## Checkout events + +NIP-04 https://github.com/nostr-protocol/nips/blob/master/04.md, all checkout events are encrypted + +The below json goes in `content` of NIP-04. + +### Step 1: `customer` order (event) + + +``` +{ + "id": , + "name": , + "description": , + "address": , + "message": , + "contact": [ + "nostr": , + "phone": , + "email": + ], + "items": [ + { + "id": , + "quantity": , + "message": + }, + { + "id": , + "quantity": , + "message": + }, + { + "id": , + "quantity": , + "message": + } + +} + +``` + +Merchant should verify the sum of product ids + timestamp. + +### Step 2: `merchant` request payment (event) + +Sent back from the merchant for payment. Any payment option is valid that the merchant can check. + +The below json goes in `content` of NIP-04. + +`payment_options`/`type` include: +* `url` URL to a payment page, stripe, paypal, btcpayserver, etc +* `btc` onchain bitcoin address +* `ln` bitcoin lightning invoice +* `lnurl` bitcoin lnurl-pay + +``` +{ + "id": , + "message": , + "payment_options": [ + { + "type": , + "link": + }, + { + "type": , + "link": + }, + { + "type": , + "link": + } +} + +``` + +### Step 3: `merchant` verify payment/shipped (event) + +Once payment has been received and processed. + +The below json goes in `content` of NIP-04. + +``` +{ + "id": , + "message": , + "paid": , + "shipped": , +} + +``` + +## Customer support events + +Customer support is handle over whatever communication method was specified. If communicationg via nostr, NIP-04 is used https://github.com/nostr-protocol/nips/blob/master/04.md. + +## Additional + +Standard data models can be found here here + + + diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..f7ce7d0 --- /dev/null +++ b/migrations.py @@ -0,0 +1,302 @@ +async def m001_initial(db): + """ + Initial Market settings table. + """ + await db.execute( + """ + CREATE TABLE market.settings ( + "user" TEXT PRIMARY KEY, + currency TEXT DEFAULT 'sat', + fiat_base_multiplier INTEGER DEFAULT 1 + ); + """ + ) + + """ + Initial stalls table. + """ + await db.execute( + """ + CREATE TABLE market.stalls ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + currency TEXT, + publickey TEXT, + relays TEXT, + shippingzones TEXT NOT NULL, + rating INTEGER DEFAULT 0 + ); + """ + ) + + """ + Initial products table. + """ + await db.execute( + f""" + CREATE TABLE market.products ( + id TEXT PRIMARY KEY, + stall TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE, + product TEXT NOT NULL, + categories TEXT, + description TEXT, + image TEXT, + price INTEGER NOT NULL, + quantity INTEGER NOT NULL, + rating INTEGER DEFAULT 0 + ); + """ + ) + + """ + Initial zones table. + """ + await db.execute( + """ + CREATE TABLE market.zones ( + id TEXT PRIMARY KEY, + "user" TEXT NOT NULL, + cost TEXT NOT NULL, + countries TEXT NOT NULL + ); + """ + ) + + """ + Initial orders table. + """ + await db.execute( + f""" + CREATE TABLE market.orders ( + id {db.serial_primary_key}, + wallet TEXT NOT NULL, + username TEXT, + pubkey TEXT, + shippingzone TEXT NOT NULL, + address TEXT NOT NULL, + email TEXT NOT NULL, + total INTEGER NOT NULL, + invoiceid 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 market.order_details ( + id TEXT PRIMARY KEY, + order_id INTEGER NOT NULL REFERENCES {db.references_schema}orders (id) ON DELETE CASCADE, + product_id TEXT NOT NULL REFERENCES {db.references_schema}products (id) ON DELETE CASCADE, + quantity INTEGER NOT NULL + ); + """ + ) + + """ + Initial market table. + """ + await db.execute( + """ + CREATE TABLE market.markets ( + id TEXT PRIMARY KEY, + usr TEXT NOT NULL, + name TEXT + ); + """ + ) + + """ + Initial market stalls table. + """ + await db.execute( + f""" + CREATE TABLE market.market_stalls ( + id TEXT PRIMARY KEY, + marketid TEXT NOT NULL REFERENCES {db.references_schema}markets (id) ON DELETE CASCADE, + stallid TEXT NOT NULL REFERENCES {db.references_schema}stalls (id) ON DELETE CASCADE + ); + """ + ) + + """ + Initial chat messages table. + """ + await db.execute( + f""" + CREATE TABLE market.messages ( + id {db.serial_primary_key}, + msg TEXT NOT NULL, + pubkey TEXT NOT NULL, + id_conversation 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 market.messages (timestamp DESC)" + ) + await db.execute( + "CREATE INDEX idx_messages_conversations ON market.messages (id_conversation)" + ) + + +async def m002_add_custom_relays(db): + """ + Add custom relays to stores + """ + await db.execute("ALTER TABLE market.stalls ADD COLUMN crelays TEXT;") + + +async def m003_fiat_base_multiplier(db): + """ + Store the multiplier for fiat prices. We store the price in cents and + remember to multiply by 100 when we use it to convert to Dollars. + """ + await db.execute( + "ALTER TABLE market.stalls ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" + ) + + await db.execute( + "UPDATE market.stalls SET fiat_base_multiplier = 100 WHERE NOT currency = 'sat';" + ) + + +async def m004_add_privkey_to_stalls(db): + await db.execute("ALTER TABLE market.stalls ADD COLUMN privatekey TEXT") + + +async def m005_add_currency_to_zones(db): + await db.execute("ALTER TABLE market.zones ADD COLUMN stall TEXT") + await db.execute("ALTER TABLE market.zones ADD COLUMN currency TEXT DEFAULT 'sat'") + + +async def m006_delete_market_settings(db): + await db.execute("DROP TABLE market.settings") + + +async def m007_order_id_to_UUID(db): + """ + Migrate ID column type to string for UUIDs and migrate existing data + """ + + await db.execute("ALTER TABLE market.orders RENAME TO orders_old") + await db.execute( + f""" + CREATE TABLE market.orders ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + username TEXT, + pubkey TEXT, + shippingzone TEXT NOT NULL, + address TEXT NOT NULL, + email TEXT NOT NULL, + total INTEGER NOT NULL, + invoiceid TEXT NOT NULL, + paid BOOLEAN NOT NULL, + shipped BOOLEAN NOT NULL, + time TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + for row in [ + list(row) for row in await db.fetchall("SELECT * FROM market.orders_old") + ]: + await db.execute( + """ + INSERT INTO market.orders ( + id, + wallet, + username, + pubkey, + shippingzone, + address, + email, + total, + invoiceid, + paid, + shipped, + time + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + str(row[0]), + row[1], + row[2], + row[3], + row[4], + row[5], + row[6], + row[7], + row[8], + row[9], + row[10], + int(row[11]), + ), + ) + + await db.execute("DROP TABLE market.orders_old") + + +async def m008_message_id_to_TEXT(db): + """ + Migrate ID column type to string for UUIDs and migrate existing data + """ + + await db.execute("ALTER TABLE market.messages RENAME TO messages_old") + await db.execute( + f""" + CREATE TABLE market.messages ( + id TEXT PRIMARY KEY, + msg TEXT NOT NULL, + pubkey TEXT NOT NULL, + id_conversation TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT """ + + db.timestamp_now + + """ + ); + """ + ) + + for row in [ + list(row) for row in await db.fetchall("SELECT * FROM market.messages_old") + ]: + await db.execute( + """ + INSERT INTO market.messages( + id, + msg, + pubkey, + id_conversation, + timestamp + ) VALUES (?, ?, ?, ?, ?) + """, + ( + str(row[0]), + row[1], + row[2], + row[3], + int(row[4]), + ), + ) + + await db.execute("DROP TABLE market.messages_old")