Add files via upload

This commit is contained in:
Tiago Vasconcelos 2023-02-27 17:45:08 +00:00 committed by GitHub
commit 2d60683beb
2 changed files with 583 additions and 0 deletions

281
README.md Normal file
View file

@ -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": <String, name of merchant>,
"description": <String, description of merchant>,
"currency": <Str, currency used>,
"action": <String, optional action>,
"shipping": [
{
"id": <String, UUID derived from stall ID>,
"zones": <String, CSV of countries/zones>,
"price": <int, cost>,
},
{
"id": <String, UUID derived from stall ID>,
"zones": <String, CSV of countries/zones>,
"price": <int, cost>,
},
{
"id": <String, UUID derived from stall ID>,
"zones": <String, CSV of countries/zones>,
"price": <int, cost>,
}
],
"stalls": [
{
"id": <UUID derived from merchant public-key>,
"name": <String, stall name>,
"description": <String, stall description>,
"categories": <String, CSV of voluntary categories>,
"shipping": <String, CSV of shipping ids>,
"action": <String, optional action>,
"products": [
{
"id": <String, UUID derived from stall ID>,
"name": <String, name of product>,
"description": <String, product description>,
"categories": <String, CSV of voluntary categories>,
"amount": <Int, number of units>,
"price": <Int, cost per unit>,
"images": [
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
}
],
"action": <String, optional action>,
},
{
"id": <String, UUID derived from stall ID>,
"name": <String, name of product>,
"description": <String, product description>,
"categories": <String, CSV of voluntary categories>,
"amount": <Int, number of units>,
"price": <Int, cost per unit>,
"images": [
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
},
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
}
],
"action": <String, optional action>,
},
]
},
{
"id": <UUID derived from merchant public_key>,
"name": <String, stall name>,
"description": <String, stall description>,
"categories": <String, CSV of voluntary categories>,
"shipping": <String, CSV of shipping ids>,
"action": <String, optional action>,
"products": [
{
"id": <String, UUID derived from stall ID>,
"name": <String, name of product>,
"categories": <String, CSV of voluntary categories>,
"amount": <Int, number of units>,
"price": <Int, cost per unit>,
"images": [
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
}
],
"action": <String, optional action>,
}
]
}
]
}
```
As all elements are optional, an `update` `action` to a `product` `image`, may look as simple as:
```
{
"stalls": [
{
"id": <UUID derived from merchant public-key>,
"products": [
{
"id": <String, UUID derived from stall ID>,
"images": [
{
"id": <String, UUID derived from product ID>,
"name": <String, image name>,
"link": <String, URL or BASE64>
}
],
"action": <String, optional 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": <String, UUID derived from sum of product ids + timestamp>,
"name": <String, name of customer>,
"description": <String, description of customer>,
"address": <String, postal address>,
"message": <String, special request>,
"contact": [
"nostr": <String, NOSTR public key>,
"phone": <String, phone number>,
"email": <String, email address>
],
"items": [
{
"id": <String, product ID>,
"quantity": <String, stall name>,
"message": <String, special request>
},
{
"id": <String, product ID>,
"quantity": <String, stall name>,
"message": <String, special request>
},
{
"id": <String, product ID>,
"quantity": <String, stall name>,
"message": <String, special request>
}
}
```
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": <String, UUID derived from sum of product ids + timestamp>,
"message": <String, message to customer>,
"payment_options": [
{
"type": <String, option type>,
"link": <String, url, btc address, ln invoice, etc>
},
{
"type": <String, option type>,
"link": <String, url, btc address, ln invoice, etc>
},
{
"type": <String, option type>,
"link": <String, url, btc address, ln invoice, etc>
}
}
```
### Step 3: `merchant` verify payment/shipped (event)
Once payment has been received and processed.
The below json goes in `content` of NIP-04.
```
{
"id": <String, UUID derived from sum of product ids + timestamp>,
"message": <String, message to customer>,
"paid": <Bool, true/false has received payment>,
"shipped": <Bool, true/false has been 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 <a href="models.json">here</a>

302
migrations.py Normal file
View file

@ -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")