Skip to content

Commit

Permalink
Merge pull request #1362 from RoboSats/use-nostr-as-cache-system
Browse files Browse the repository at this point in the history
Use nostr as cache system
  • Loading branch information
KoalaSat authored Sep 6, 2024
2 parents 4f434aa + e9620e3 commit ce8799d
Show file tree
Hide file tree
Showing 19 changed files with 435 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,6 @@ SLASHED_BOND_REWARD_SPLIT = 0.5

# Username for HTLCs escrows
ESCROW_USERNAME = 'admin'

#Social
NOSTR_NSEC = 'nsec1vxhs2zc4kqe0dhz4z2gfrdyjsrwf8pg3neeqx6w4nl8djfzdp0dqwd6rxh'
49 changes: 39 additions & 10 deletions api/logics.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from api.lightning.node import LNNode
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
from api.tasks import send_devfund_donation, send_notification
from api.tasks import send_devfund_donation, send_notification, nostr_send_order_event
from api.utils import get_minning_fee, validate_onchain_address, location_country
from chat.models import Message

Expand Down Expand Up @@ -704,19 +704,19 @@ def payout_amount(cls, order, user):

if context["invoice_amount"] < MIN_SWAP_AMOUNT:
context["swap_allowed"] = False
context[
"swap_failure_reason"
] = f"Order amount is smaller than the minimum swap available of {MIN_SWAP_AMOUNT} Sats"
context["swap_failure_reason"] = (
f"Order amount is smaller than the minimum swap available of {MIN_SWAP_AMOUNT} Sats"
)
order.log(
f"Onchain payment option was not offered: amount is smaller than the minimum swap available of {MIN_SWAP_AMOUNT} Sats",
level="WARN",
)
return True, context
elif context["invoice_amount"] > MAX_SWAP_AMOUNT:
context["swap_allowed"] = False
context[
"swap_failure_reason"
] = f"Order amount is bigger than the maximum swap available of {MAX_SWAP_AMOUNT} Sats"
context["swap_failure_reason"] = (
f"Order amount is bigger than the maximum swap available of {MAX_SWAP_AMOUNT} Sats"
)
order.log(
f"Onchain payment option was not offered: amount is bigger than the maximum swap available of {MAX_SWAP_AMOUNT} Sats",
level="WARN",
Expand All @@ -741,9 +741,9 @@ def payout_amount(cls, order, user):
)
if not valid:
context["swap_allowed"] = False
context[
"swap_failure_reason"
] = "Not enough onchain liquidity available to offer a swap"
context["swap_failure_reason"] = (
"Not enough onchain liquidity available to offer a swap"
)
order.log(
"Onchain payment option was not offered: onchain liquidity available to offer a swap",
level="WARN",
Expand Down Expand Up @@ -1019,6 +1019,8 @@ def cancel_order(cls, order, user, state=None):
order.log("Order expired while waiting for maker bond")
order.log("Maker bond was cancelled")

nostr_send_order_event.delay(order_id=order.id)

return True, None

# 2.a) When maker cancels after bond
Expand All @@ -1039,6 +1041,8 @@ def cancel_order(cls, order, user, state=None):
order.log("Order cancelled by maker while public or paused")
order.log("Maker bond was <b>unlocked</b>")

nostr_send_order_event.delay(order_id=order.id)

return True, None

# 2.b) When maker cancels after bond and before taker bond is locked
Expand All @@ -1058,6 +1062,8 @@ def cancel_order(cls, order, user, state=None):
order.log("Maker bond was <b>unlocked</b>")
order.log("Taker bond was <b>cancelled</b>")

nostr_send_order_event.delay(order_id=order.id)

return True, None

# 3) When taker cancels before bond
Expand All @@ -1070,6 +1076,8 @@ def cancel_order(cls, order, user, state=None):

order.log("Taker cancelled before locking the bond")

nostr_send_order_event.delay(order_id=order.id)

return True, None

# 4) When taker or maker cancel after bond (before escrow)
Expand Down Expand Up @@ -1099,6 +1107,8 @@ def cancel_order(cls, order, user, state=None):
order.log("Maker bond was <b>settled</b>")
order.log("Taker bond was <b>unlocked</b>")

nostr_send_order_event.delay(order_id=order.id)

return True, None

# 4.b) When taker cancel after bond (before escrow)
Expand All @@ -1121,6 +1131,8 @@ def cancel_order(cls, order, user, state=None):
order.log("Taker bond was <b>settled</b>")
order.log("Maker bond was <b>unlocked</b>")

nostr_send_order_event.delay(order_id=order.id)

return True, None

# 5) When trade collateral has been posted (after escrow)
Expand All @@ -1136,6 +1148,9 @@ def cancel_order(cls, order, user, state=None):
order.log(
f"Taker Robot({user.robot.id},{user.username}) accepted the collaborative cancellation"
)

nostr_send_order_event.delay(order_id=order.id)

return True, None

# if the taker had asked, and now the maker does: cancel order, return everything
Expand All @@ -1144,6 +1159,9 @@ def cancel_order(cls, order, user, state=None):
order.log(
f"Maker Robot({user.robot.id},{user.username}) accepted the collaborative cancellation"
)

nostr_send_order_event.delay(order_id=order.id)

return True, None

# Otherwise just make true the asked for cancel flags
Expand Down Expand Up @@ -1181,6 +1199,8 @@ def collaborative_cancel(cls, order):
order.update_status(Order.Status.CCA)
send_notification.delay(order_id=order.id, message="collaborative_cancelled")

nostr_send_order_event.delay(order_id=order.id)

order.log("Order was collaboratively cancelled")
order.log("Maker bond was <b>unlocked</b>")
order.log("Taker bond was <b>unlocked</b>")
Expand Down Expand Up @@ -1208,6 +1228,8 @@ def publish_order(cls, order):

order.save() # update all fields

nostr_send_order_event.delay(order_id=order.id)

order.log(f"Order({order.id},{str(order)}) is public in the order book")
return

Expand Down Expand Up @@ -1350,6 +1372,9 @@ def finalize_contract(cls, order):
except Exception:
pass
send_notification.delay(order_id=order.id, message="order_taken_confirmed")

nostr_send_order_event.delay(order_id=order.id)

order.log(
f"<b>Contract formalized.</b> Maker: Robot({order.maker.robot.id},{order.maker}). Taker: Robot({order.taker.robot.id},{order.taker}). API median price {order.currency.exchange_rate} {dict(Currency.currency_choices)[order.currency.currency]}/BTC. Premium is {order.premium}%. Contract size {order.last_satoshis} Sats"
)
Expand Down Expand Up @@ -1741,11 +1766,15 @@ def pause_unpause_public_order(order, user):
order.log(
f"Robot({user.robot.id},{user.username}) paused the public order"
)

nostr_send_order_event.delay(order_id=order.id)
elif order.status == Order.Status.PAU:
order.update_status(Order.Status.PUB)
order.log(
f"Robot({user.robot.id},{user.username}) made public the paused order"
)

nostr_send_order_event.delay(order_id=order.id)
else:
order.log(
f"Robot({user.robot.id},{user.username}) tried to pause/unpause an order that was not public or paused",
Expand Down
98 changes: 98 additions & 0 deletions api/nostr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import pygeohash
import hashlib
import uuid

from asgiref.sync import sync_to_async
from nostr_sdk import Keys, Client, EventBuilder, NostrSigner, Kind, Tag
from api.models import Order
from decouple import config


class Nostr:
"""Simple nostr events manager to be used as a cache system for clients"""

async def send_order_event(self, order):
"""Creates the event and sends it to the coordinator relay"""

if config("NOSTR_NSEC", cast=str, default="") == "":
return

print("Sending nostr event")

# Initialize with coordinator Keys
keys = Keys.parse(config("NOSTR_NSEC", cast=str))
signer = NostrSigner.keys(keys)
client = Client(signer)

# Add relays and connect
await client.add_relays(["ws://localhost:7777"])
await client.connect()

robot_name = await self.get_robot_name(order)
currency = await self.get_robot_currency(order)

event = EventBuilder(
Kind(38383), "", self.generate_tags(order, robot_name, currency)
).to_event(keys)
await client.send_event(event)
print(f"Nostr event sent: {event.as_json()}")

@sync_to_async
def get_robot_name(self, order):
return order.maker.username

@sync_to_async
def get_robot_currency(self, order):
return str(order.currency)

def generate_tags(self, order, robot_name, currency):
hashed_id = hashlib.md5(
f"{config("COORDINATOR_ALIAS", cast=str)}{order.id}".encode("utf-8")
).hexdigest()

tags = [
Tag.parse(["d", str(uuid.UUID(hashed_id))]),
Tag.parse(["name", robot_name]),
Tag.parse(["k", "sell" if order.type == Order.Types.SELL else "buy"]),
Tag.parse(["f", currency]),
Tag.parse(["s", self.get_status_tag(order)]),
Tag.parse(["amt", "0"]),
Tag.parse(
["fa"] + [str(order.amount)]
if not order.has_range
else [str(order.min_amount), str(order.max_amount)]
),
Tag.parse(["pm"] + order.payment_method.split(" ")),
Tag.parse(["premium", str(order.premium)]),
Tag.parse(
[
"source",
f"http://{config("HOST_NAME")}/order/{config("COORDINATOR_ALIAS", cast=str).lower()}/{order.id}",
]
),
Tag.parse(["expiration", str(int(order.expires_at.timestamp()))]),
Tag.parse(["y", "robosats", config("COORDINATOR_ALIAS", cast=str).lower()]),
Tag.parse(["n", str(config("NETWORK"))]),
Tag.parse(["layer"] + self.get_layer_tag(order)),
Tag.parse(["bond", str(order.bond_size)]),
Tag.parse(["z", "order"]),
]

if order.latitude and order.longitude:
tags.extend(
[Tag.parse(["g", pygeohash.encode(order.latitude, order.longitude)])]
)

return tags

def get_status_tag(self, order):
if order.status == Order.Status.PUB:
return "pending"
else:
return "success"

def get_layer_tag(self, order):
if order.type == Order.Types.SELL:
return ["onchain", "lightning"]
else:
return ["lightning"]
15 changes: 15 additions & 0 deletions api/tasks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from asgiref.sync import async_to_sync
from celery import shared_task
from celery.exceptions import SoftTimeLimitExceeded

Expand Down Expand Up @@ -251,6 +252,20 @@ def cache_market():
return


@shared_task(name="", ignore_result=True, time_limit=120)
def nostr_send_order_event(order_id=None):
if order_id:
from api.models import Order
from api.nostr import Nostr

order = Order.objects.get(id=order_id)

nostr = Nostr()
async_to_sync(nostr.send_order_event)(order)

return


@shared_task(name="send_notification", ignore_result=True, time_limit=120)
def send_notification(order_id=None, chat_message_id=None, message=None):
if order_id:
Expand Down
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ services:
volumes:
- ./node/db:/var/lib/postgresql/data

strfry:
build: ./docker/strfry
container_name: strfry-dev
restart: unless-stopped
volumes:
- ./docker/strfry/strfry.conf:/etc/strfry.conf:ro
- ./docker/strfry/onion_urls.txt:/app/onion_urls.txt:ro
- ./node/strfry/db:/app/strfry-db:rw
network_mode: service:tor

# # Postgresql for CLN
# postgres-cln:
# image: postgres:14.2-alpine
Expand Down
3 changes: 2 additions & 1 deletion docker-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ services:
- "9998:9998"
- "5432:5432"
- "6379:6379"
- "7777:7777"
volumes:
- bitcoin:/bitcoin/.bitcoin/
- ./tests/bitcoind/entrypoint.sh:/entrypoint.sh
Expand Down Expand Up @@ -182,7 +183,7 @@ services:
# celery-worker:
# image: backend-image
# pull_policy: never
# container_name: celery-worker
# container_name: test-celery-worker
# restart: always
# environment:
# DEVELOPMENT: True
Expand Down
41 changes: 41 additions & 0 deletions docker/strfry/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
FROM ubuntu:jammy
ENV TZ=Europe/London

RUN apt update && apt install -y --no-install-recommends \
git g++ make pkg-config libtool ca-certificates \
libssl-dev zlib1g-dev liblmdb-dev libflatbuffers-dev \
libsecp256k1-dev libzstd-dev

# setup app
RUN git clone https://github.com/KoalaSat/strfry /app

WORKDIR /app

RUN git submodule update --init
RUN make setup-golpe
RUN make clean
RUN make -j4

RUN apt update && apt install -y --no-install-recommends \
liblmdb0 libflatbuffers1 libsecp256k1-0 libb2-1 libzstd1 torsocks cron\
&& rm -rf /var/lib/apt/lists/*

RUN echo "TorAddress 127.0.0.1" >> /etc/tor/torsocks.conf
RUN echo "TorPort 9050" >> /etc/tor/torsocks.conf

# Setting up crontab
COPY crontab /etc/cron.d/crontab
RUN chmod 0644 /etc/cron.d/crontab
RUN crontab /etc/cron.d/crontab

# Setting up entrypoints
COPY sync.sh /etc/strfry/sync.sh
COPY entrypoint.sh /etc/strfry/entrypoint.sh

RUN chmod +x /etc/strfry/entrypoint.sh
RUN chmod +x /etc/strfry/sync.sh

#Setting up logs
RUN touch /var/log/cron.log && chmod 0644 /var/log/cron.log

ENTRYPOINT ["/etc/strfry/entrypoint.sh"]
24 changes: 24 additions & 0 deletions docker/strfry/crontab
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').
#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
*/1 * * * * torsocks /etc/strfry/sync.sh >> /var/log/cron.log 2>&1
Loading

0 comments on commit ce8799d

Please sign in to comment.