Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: PlebeianApp/plebeian-market
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.0.14
Choose a base ref
...
head repository: PlebeianApp/plebeian-market
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Loading
Showing with 1,160 additions and 1,751 deletions.
  1. +22 −0 .github/workflows/docker-publish.yml
  2. +75 −61 README.dev.md
  3. +30 −21 api/api.py
  4. +8 −0 api/config.py
  5. +19 −21 api/lnd_hub_client.py
  6. +50 −46 api/main.py
  7. +14 −6 api/models.py
  8. +16 −4 birdwatcher/main.py
  9. +10 −0 docker-compose.dev.yml
  10. +3 −0 docker-compose.prod.yml
  11. +8 −3 docker-compose.staging.yml
  12. +14 −1 docker-compose.yml
  13. +33 −6 install.sh
  14. +1 −1 scripts/prod.sh
  15. +1 −1 scripts/staging.sh
  16. +2 −1 services/nginx/Dockerfile
  17. +5 −1 services/nginx/nginx.conf
  18. +1 −0 services/postfix/Dockerfile
  19. +1 −3 web/Dockerfile
  20. +0 −1 web/backoffice/.env
  21. +0 −1 web/backoffice/.env.production
  22. +46 −46 web/backoffice/package-lock.json
  23. +4 −4 web/backoffice/package.json
  24. +1 −1 web/backoffice/src/lib/components/OrderRow.svelte
  25. +8 −1 web/backoffice/src/lib/components/StallView.svelte
  26. +1 −1 web/backoffice/src/lib/components/settings/App.svelte
  27. +1 −0 web/backoffice/src/lib/types/item.ts
  28. +1 −3 web/backoffice/src/lib/utils.ts
  29. +0 −2 web/backoffice/src/routes/+page.svelte
  30. +0 −1 web/frontoffice/.env
  31. +0 −1 web/frontoffice/.env.production
  32. +313 −507 web/frontoffice/package-lock.json
  33. +5 −5 web/frontoffice/package.json
  34. +7 −12 web/frontoffice/src/lib/components/nostr/Button.svelte
  35. +1 −1 web/frontoffice/src/lib/components/settings/NavbarSetup.svelte
  36. +3 −4 web/frontoffice/src/lib/components/stores/BidList.svelte
  37. +5 −7 web/frontoffice/src/lib/components/stores/BidWidget.svelte
  38. +1 −1 web/frontoffice/src/lib/components/stores/Product.svelte
  39. +14 −14 web/frontoffice/src/lib/components/stores/ProductCardBrowser.svelte
  40. +8 −8 web/frontoffice/src/lib/components/stores/StallsBrowser.svelte
  41. +2 −3 web/frontoffice/src/lib/shopping.ts
  42. +2 −4 web/frontoffice/src/routes/+layout.svelte
  43. +13 −13 web/frontoffice/src/routes/about/+page.svelte
  44. +11 −2 web/frontoffice/src/routes/marketsquare/+page.svelte
  45. +18 −3 web/frontoffice/src/routes/p/[pubkey]/+page.svelte
  46. +1 −1 web/frontoffice/src/routes/planet/+page.svelte
  47. +21 −1 web/frontoffice/src/routes/verification/+page.svelte
  48. +2 −1 web/frontoffice/static/config-example.json
  49. +2 −1 web/frontoffice/static/config-pm.json
  50. BIN web/frontoffice/static/team/gzuuus.webp
  51. BIN web/frontoffice/static/team/ibz.jpg
  52. +2 −2 web/frontoffice/tailwind.config.cjs
  53. +90 −762 web/shared/package-lock.json
  54. +6 −6 web/shared/package.json
  55. +41 −42 web/shared/src/lib/components/Navbar.svelte
  56. +15 −13 web/shared/src/lib/components/pagebuilder/AdminActions.svelte
  57. +32 −4 web/shared/src/lib/components/pagebuilder/BuilderSectionSetup.svelte
  58. +12 −3 web/shared/src/lib/components/pagebuilder/SectionsProducts.svelte
  59. +94 −80 web/shared/src/lib/components/pagebuilder/SectionsProductsSlider.svelte
  60. +39 −2 web/shared/src/lib/nostr/utils.ts
  61. +6 −0 web/shared/src/lib/pagebuilder.ts
  62. +8 −7 web/shared/src/lib/services/nostr.ts
  63. +9 −3 web/shared/src/lib/stores.ts
  64. +2 −1 web/shared/src/lib/utils.ts
22 changes: 22 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -85,6 +85,28 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata (smtp)
id: meta-smtp
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/plebeiantech/plebeian-market-smtp

# Build and push Docker image with Buildx
# https://github.com/docker/build-push-action
- name: Build and push Docker image (smtp)
id: build-and-push-smtp
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: ./
file: ./services/postfix/Dockerfile
push: true
tags: ${{ steps.meta-smtp.outputs.tags }}
labels: ${{ steps.meta-smtp.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata (api)
136 changes: 75 additions & 61 deletions README.dev.md
Original file line number Diff line number Diff line change
@@ -1,98 +1,112 @@
## Advanced install options
# Development

### Install just the front office (from git master)
This documentation will guide you through setting up a local development environment to contribute to the Plebeian Market. If you have not read the repository README, please take a look, as it will help you understand the project's architecture.

* Clone the repository
`git clone git@github.com:PlebeianTech/plebeian-market.git`
It is important to note that the Plebeian stack consists of the front office, back office, and background services. If your intention is to do front-end development, the background services are not strictly necessary, but you will need to run them if you want to have the full functionality of the application and populate the front-end (front or back office) with some data.

* Change to the front office directory
`cd plebeianmarket/web/frontoffice`
## Running the Dev Environment Locally

* Build the site
`npm i ; npm run build`
### Prerequisites

* (optional) Copy the file config-example.json to config.json:
`cd web/frontoffice/build ; cp config-example.json config.json`
- You need to have the Docker Engine installed on your system. You can follow the official instructions: [Install Docker engine](https://docs.docker.com/engine/install/)
- If you are on Linux, you'll want to install Docker in a different user using `useradd -m -G docker YOUR_USER`, and also follow the post-installation steps: [Docker post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/)
- You need to have Node.js >= 16 and npm installed on your machine.
- Git clone this repository in the directory of the user you will use, using `git clone git@github.com:PlebeianTech/plebeian-market.git`

* (optional) Edit the `config.json` file and add your Nostr public key to the `admin_pubkeys` array:
```
"admin_pubkeys": [
"123456789012345678901234567890",
"another_admin_nostr_public_key"
]
```
### Running Dev Environment

* Copy the content of the `web/frontoffice/build` directory to your web server using your app of choice.
#### Background Services

### Install the entire marketplace
- Once you have cloned the repository, open a terminal in the `plebeian-market` project directory and run `./scripts/dev.sh` to start the development environment: database, API, and all background services
- There will be some background services that will exit with errors like smtp and birdwatcher, don't worry, they are not necessary for development. If you now run `docker ps` in your terminal, you will see the different containers running.

* install [`nginx-proxy-automation`](https://github.com/evertramos/nginx-proxy-automation)
* clone repo
* add `plebeian-market-secrets/secret_key` (random string)
* add `plebeian-market-secrets/db.json` (default username: *pleb* / default password: *plebpass*)
* add `plebeian-market-secrets/lndhub.json`
* add `plebeian-market-secrets/mail.json`
* add `plebeian-market-secrets/site-admin.json`
* edit `.env.prod`
* `API_BASE_URL`, `WWW_BASE_URL`, `DOMAIN_NAME`
* edit `docker-compose.prod.yml`
* `VIRTUAL_HOST`, `LETSENCRYPT_HOST`
* `flask db upgrade`
* `./scripts/prod.sh`

## Development
#### Back Office (Seller Part of the App)

### Front office (buyer part of the app)
- First, go to `web/shared` with `cd web/shared` and run `npm i`
- Then go to the `backoffice` directory with `cd web/backoffice` and run `npm i`, and then `npm run dev` to run the web app
- This will start the development server of the `backoffice`, and you will be able to access it from `localhost:5173` in your browser
- **Tip**: To complete the process of creating a stall, you will need the email verification code, as smtp does not work in the development environment. You will need to get it from the logs of the `api` container. To do this, you can do `docker logs -f plebeian-market-api-1` and this will show you the logs of the api. If you are going through the create stall process, the email verification code will appear in these logs.

#### Build the static files
#### Front Office (Buyer Part of the App)

```npm run build``` - Build static files
- If you have not previously installed `shared` node dependencies: go to `web/shared` with `cd web/shared` and run `npm i`
- Then go to the `frontoffice` directory with `cd web/frontoffice` and run `npm i`, and then `npm run dev` to run the web app
- This will start the development server of the `frontoffice` successfully, and you will be able to access it from `localhost:5173` or `localhost:5174` (if you are running backoffice at the same time) in your browser

All the generated static files will be inside the `build` folder. You can copy them to your server to use the frontoffice on your website.
#### Build/Preview the Static Files

#### Run the app locally for development
- If you want to build or preview, you can do so using these commands in the front or back office directories
- `npm run preview` - Build static files and serve them using a http server
- `npm run build` - Build static files

```npm i``` - Install npm dependencies
All the generated static files will be inside the `build` folder. You can copy them to your server to use the `frontoffice` on your website.

```npm run dev``` - Run the app

### Back office (seller part of the app)

### Running the dev environment locally

```./scripts/test.sh``` to run the automated tests for the API

```./scripts/dev.sh``` to start the development environment: database, API and all background services

```cd web/backoffice && npm run dev``` to run the web app

### Background services
### Background Services

```finalize-auctions``` - monitors running auctions and picks a winner for auctions that ended

```settle-btc-payments``` - monitors on-chain payments that are "pending" in our database and looks up the corresponding transactions in the mempool or on-chain

```settle-lightning-payments``` - monitors incoming Lightning Network payments from buyers, and make outgoing payments to sellers
```settle-lightning-payments``` - monitors incoming Lightning Network payments from buyers, and makes outgoing payments to sellers

## Nostr

### Used event kinds
### Used Event Kinds

* 30017 - market stall
* 30018 - fixed price product listing
* 30020 - auction
* 1021 - bid
* 1022 - bid confirmation

### Stall chat
### Stall Chat

In order to have Stall chats, we fake a Nostr channel creation (kind = 40) but don't send the event to the network.
In this way, we're able to have chat rooms without polluting the chat room list in the clients. So if you want to
use this chat room in your client, you'll have to copy the channel id manually instead of searching for it.
In order to have Stall chats, we fake a Nostr channel creation (kind = 40) but don't send the event to the network. In this way, we're able to have chat rooms without polluting the chat room list in the clients. So if you want to use this chat room in your client, you'll have to copy the channel id manually instead of searching for it.

Give a look to the `getChannelIdForStall` function to know how we're generating the Channel id.

## Keys used in the code
## Keys Used in the Code

* `VITE_NOSTR_MARKET_SQUARE_CHANNEL_ID` - Nostr channel id of the Market Square for each community
* `VITE_NOSTR_PM_STALL_PUBLIC_KEY` - Nostr public key of Plebeian Market's market stall

## Advanced Install Options

### Install Just the Front Office (from Git Master)

* Clone the repository
`git clone git@github.com:PlebeianTech/plebeian-market.git`

* Change to the front office directory
`cd plebeianmarket/web/frontoffice`

* Build the site
`npm i ; npm run build`

* (optional) Copy the file config-example.json to config.json:
`cd web/frontoffice/build ; cp config-example.json config.json`

* (optional) Edit the `config.json` file and add your Nostr public key to the `admin_pubkeys` array:
```
"admin\_pubkeys": [
"123456789012345678901234567890",
"another\_admin\_nostr\_public\_key"
]
```

* Copy the content of the `web/frontoffice/build` directory to your web server using your app of choice.

### Install the Entire Marketplace

* install [`nginx-proxy-automation`](https://github.com/evertramos/nginx-proxy-automation)
* clone repo
* add `plebeian-market-secrets/secret_key` (random string)
* add `plebeian-market-secrets/db.json` (default username: *pleb* / default password: *plebpass*)
* add `plebeian-market-secrets/lndhub.json`
* add `plebeian-market-secrets/mail.json`
* add `plebeian-market-secrets/site-admin.json`
* edit `.env.prod`
* `API_BASE_URL`, `WWW_BASE_URL`, `DOMAIN_NAME`
* edit `docker-compose.prod.yml`
* `VIRTUAL_HOST`, `LETSENCRYPT_HOST`
* `flask db upgrade`
* `./scripts/prod.sh`
51 changes: 30 additions & 21 deletions api/api.py
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@

from extensions import db
import models as m
from main import app, get_birdwatcher, get_lndhub_client, get_file_storage
from main import app, get_birdwatcher, get_lndhub_client, get_file_storage, get_mail
from main import get_token_from_request, get_user_from_token, user_required, nip98_auth_required
from main import MempoolSpaceError
from nostr_utils import EventValidationError, validate_event
@@ -258,6 +258,8 @@ def put_me(user: m.User):
if request.json.get('wallet'):
try:
k = parse_xpub(request.json['wallet'])
except ValueError as e:
return jsonify({'message': "Invalid xpub/zpub."}), 400
except UnknownKeyTypeError as e:
return jsonify({'message': "Invalid wallet."}), 400
try:
@@ -516,7 +518,7 @@ def put_order(user, uuid):

if request.json.get('paid'):
message = "The seller accepted your payment!"
order.set_paid()
order.set_paid("Payment accepted manually by the seller")

if request.json.get('shipped'):
message = "Your order was shipped!"
@@ -723,11 +725,9 @@ def post_media(key, cls, singular):
if reason:
return jsonify({'message': reason}), 403

last_index = max([media.index for media in entity.item.media], default=0)

index = int(request.form['index'])
f = list(request.files.values())[0]
media = m.Media(item_id=entity.item_id, index=(last_index + index + 1))
media = m.Media(item_id=entity.item_id, index=index)
if not media.store(get_file_storage(), f"{singular}_{entity.key}_media", f.filename, f.read()):
return jsonify({'message': "Error saving picture!"}), 400
db.session.add(media)
@@ -836,6 +836,14 @@ def iter_entities():

return jsonify({plural: [e.to_dict(for_user=for_user_id) for e in sorted_entities]})

@api_blueprint.route("/api/users/<nostr_pubkey>/stalls", methods=['GET'])
def get_user_stalls(nostr_pubkey):
user = m.User.query.filter_by(nostr_public_key=nostr_pubkey).first()
if not user:
return jsonify({'message': "User not found."}), 404

return jsonify({'stalls': [{'id': user.stall_id}]})

@api_blueprint.route("/api/relays", methods=['GET'])
def get_relays():
if app.config['ENV'] in ('test', 'dev'):
@@ -920,6 +928,8 @@ def post_merchant_message(pubkey):
app.logger.info("Missing message type. Ignoring.")
return jsonify({})

birdwatcher = get_birdwatcher()

message_type = None
try:
message_type = int(cleartext_content['type'])
@@ -933,12 +943,12 @@ def post_merchant_message(pubkey):

if 'id' not in cleartext_content:
message = "Invalid order: missing id."
get_birdwatcher().send_dm(merchant_private_key, request.json['pubkey'], message)
birdwatcher.send_dm(merchant_private_key, request.json['pubkey'], message)
return jsonify({'message': message}), 400

if 'shipping_id' not in cleartext_content:
message = "Invalid order: missing shipping zone."
get_birdwatcher().send_dm(merchant_private_key, request.json['pubkey'],
birdwatcher.send_dm(merchant_private_key, request.json['pubkey'],
json.dumps({'id': cleartext_content['id'], 'type': 2, 'paid': False, 'shipped': False, 'message': message}))
return jsonify({'message': message}), 400

@@ -965,7 +975,7 @@ def post_merchant_message(pubkey):
shipping_usd = merchant.shipping_domestic_usd
else:
message = "Invalid shipping zone!"
get_birdwatcher().send_dm(merchant_private_key, request.json['pubkey'],
birdwatcher.send_dm(merchant_private_key, request.json['pubkey'],
json.dumps({'id': cleartext_content['id'], 'type': 2, 'paid': False, 'shipped': False, 'message': message}))
return jsonify({'message': message}), 400

@@ -988,19 +998,19 @@ def post_merchant_message(pubkey):
if listing:
if not listing.nostr_event_id:
message = "Listing not active."
get_birdwatcher().send_dm(merchant_private_key, request.json['pubkey'],
birdwatcher.send_dm(merchant_private_key, request.json['pubkey'],
json.dumps({'id': cleartext_content['id'], 'type': 2, 'paid': False, 'shipped': False, 'message': message}))
return jsonify({'message': message}), 403
if listing.available_quantity is not None and listing.available_quantity < item['quantity']:
message = "Not enough items in stock!"
get_birdwatcher().send_dm(merchant_private_key, request.json['pubkey'],
birdwatcher.send_dm(merchant_private_key, request.json['pubkey'],
json.dumps({'id': cleartext_content['id'], 'type': 2, 'paid': False, 'shipped': False, 'message': message}))
return jsonify({'message': message}), 400
order_listings.append((listing, item['quantity']))

if len(order_listings) == 0:
message = "Empty order!"
get_birdwatcher().send_dm(merchant_private_key, request.json['pubkey'],
birdwatcher.send_dm(merchant_private_key, request.json['pubkey'],
json.dumps({'id': cleartext_content['id'], 'type': 2, 'paid': False, 'shipped': False, 'message': message}))
return jsonify({'message': message}), 400

@@ -1019,12 +1029,17 @@ def post_merchant_message(pubkey):
db.session.add(order)
db.session.commit()

if merchant.nostr_public_key:
birdwatcher.send_dm(birdwatcher.site_admin_private_key, merchant.nostr_public_key, f"New order was placed: {order.uuid}!")
if merchant.email and merchant.email_verified:
get_mail().send(merchant.email, "New order", f"New order was placed: {order.uuid}!", f"New order was placed: <a href=\"{app.config['WWW_BASE_URL']}/admin/account/orders/\">{order.uuid}</a>!")

for listing, quantity in order_listings:
if listing.available_quantity is not None:
# here we "lock" the quantity. it is given back if the order expires
listing.available_quantity -= quantity
# NB: we need to update the quantity in Nostr as well!
listing.nostr_event_id = get_birdwatcher().publish_product(listing)
listing.nostr_event_id = birdwatcher.publish_product(listing)

order_item = m.OrderItem(order_id=order.id, item_id=listing.item_id, listing_id=listing.id, quantity=quantity)
db.session.add(order_item)
@@ -1089,17 +1104,12 @@ def post_merchant_message(pubkey):

if order.on_chain_address:
payment_options.append({'type': 'btc', 'link': order.on_chain_address, 'amount_sats': order.total})
birdwatcher.send_dm(merchant_private_key, order.buyer_public_key, f"Please go to {app.config['WWW_BASE_URL']}/donations and make your contribution for the community!")

if order.lightning_address:
lndhub_client = get_lndhub_client()
invoice_information = lndhub_client.create_invoice(order.uuid, order.total)

if not invoice_information:
app.logger.info(f"Error while trying to create_invoice. Retrying...")
time.sleep(5)
lndhub_client.get_login_token()
invoice_information = lndhub_client.create_invoice(order.uuid, order.total)

if invoice_information and invoice_information['payment_request']:
lightning_invoice = m.LightningInvoice(
order_id=order.id,
@@ -1112,11 +1122,10 @@ def post_merchant_message(pubkey):
db.session.commit()

payment_options.append({'type': 'ln', 'link': invoice_information['payment_request'], 'amount_sats': order.total})

else:
return jsonify({'message': "Error sending the payment options back to the buyer (couldn't create a new LN invoice)"}), 500

if not get_birdwatcher().send_dm(merchant_private_key, order.buyer_public_key,
if not birdwatcher.send_dm(merchant_private_key, order.buyer_public_key,
json.dumps({
'id': order.uuid,
'type': 1,
@@ -1187,8 +1196,8 @@ def post_auction_bid(merchant_pubkey, auction_event_id):
if auction.verified_identities_required > 0:
buyer_metadata = birdwatcher.query_metadata(request.json['pubkey'])
if len(buyer_metadata['verified_identities']) < auction.verified_identities_required:
app.logger.info(f"{message} pubkey={request.json['pubkey']} verified_identities={buyer_metadata['verified_identities']}")
message = f"User needs at least {auction.verified_identities_required} verified external identities in order to bid!"
app.logger.info(f"{message} pubkey={request.json['pubkey']} verified_identities={buyer_metadata['verified_identities']}")
birdwatcher.publish_bid_status(auction, request.json['id'], 'rejected', message)
is_settled = False

8 changes: 8 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
@@ -76,6 +76,14 @@
CONTRIBUTION_PERCENT_DEFAULT = 5.0 # NB: must be in sync with the value in V4V.svelte

MOCK_NOSTR = bool(int(os.environ.get("MOCK_NOSTR", 0)))
if not MOCK_NOSTR:
with open("/secrets/nostr.json") as f:
from nostr.key import PrivateKey
NOSTR_SECRETS = json.load(f)
NOSTR_PRIVATE_KEY = PrivateKey.from_nsec(NOSTR_SECRETS['NSEC'])
else:
from nostr.key import PrivateKey
NOSTR_PRIVATE_KEY = PrivateKey()

MOCK_S3 = bool(int(os.environ.get('MOCK_S3', 0)))
USE_S3 = bool(int(os.environ.get('USE_S3', 0)))
Loading