Add files via upload
This commit is contained in:
commit
2d60683beb
2 changed files with 583 additions and 0 deletions
281
README.md
Normal file
281
README.md
Normal 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
302
migrations.py
Normal 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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue