+ )} },
{ field: 'payment_method', headerName: 'Payment Method', width: 180 },
{ field: 'price', headerName: 'Price', type: 'number', width: 140,
renderCell: (params) => {return (
diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js
index 1c44ce45..748be167 100644
--- a/frontend/src/components/OrderPage.js
+++ b/frontend/src/components/OrderPage.js
@@ -47,7 +47,7 @@ export default class OrderPage extends Component {
this.getCurrencyDict();
this.getOrderDetails();
- // Change refresh delay according to Order status
+ // Refresh delais according to Order status
this.statusToDelay = {
"0": 3000, //'Waiting for maker bond'
"1": 30000, //'Public'
@@ -207,9 +207,9 @@ export default class OrderPage extends Component {
}));
}
- // set delay to the one matching the order status. If no order status, set delay to 9999999
- setDelay(val){
- return val ? this.statusToDelay[val.toString()] : 99999999;
+ // set delay to the one matching the order status. If null order status, delay goes to 9999999.
+ setDelay = (status)=>{
+ return status >= 0 ? this.statusToDelay[status.toString()] : 99999999;
}
getCurrencyCode(val){
diff --git a/frontend/src/components/getFlags.js b/frontend/src/components/getFlags.js
new file mode 100644
index 00000000..bdfc98a0
--- /dev/null
+++ b/frontend/src/components/getFlags.js
@@ -0,0 +1,33 @@
+export default function getFlags(code){
+ if(code == 'AUD') return '🇦🇺';
+ if(code == 'ARS') return '🇦🇷';
+ if(code == 'BRL') return '🇧🇷';
+ if(code == 'CAD') return '🇨🇦';
+ if(code == 'CHF') return '🇨🇭';
+ if(code == 'CLP') return '🇨🇱';
+ if(code == 'CNY') return '🇨🇳';
+ if(code == 'EUR') return '🇪🇺';
+ if(code == 'HKR') return '🇨🇷';
+ if(code == 'CZK') return '🇨🇿';
+ if(code == 'DKK') return '🇩🇰';
+ if(code == 'GBP') return '🇬🇧';
+ if(code == 'HKD') return '🇭🇰';
+ if(code == 'HUF') return '🇭🇺';
+ if(code == 'INR') return '🇮🇳';
+ if(code == 'ISK') return '🇮🇸';
+ if(code == 'JPY') return '🇯🇵';
+ if(code == 'KRW') return '🇰🇷';
+ if(code == 'MXN') return '🇲🇽';
+ if(code == 'NOK') return '🇳🇴';
+ if(code == 'NZD') return '🇳🇿';
+ if(code == 'PLN') return '🇵🇱';
+ if(code == 'RON') return '🇷🇴';
+ if(code == 'RUB') return '🇷🇺';
+ if(code == 'SEK') return '🇸🇪';
+ if(code == 'SGD') return '🇸🇬';
+ if(code == 'VES') return '🇻🇪';
+ if(code == 'TRY') return '🇹🇷';
+ if(code == 'USD') return '🇺🇸';
+ if(code == 'ZAR') return '🇿🇦';
+ return '🏳';
+};
\ No newline at end of file
diff --git a/frontend/static/assets/currencies.json b/frontend/static/assets/currencies.json
index 84294a4f..3376cc5e 100644
--- a/frontend/static/assets/currencies.json
+++ b/frontend/static/assets/currencies.json
@@ -27,5 +27,7 @@
"26": "INR",
"27": "ISK",
"28": "PLN",
- "29": "RON"
+ "29": "RON",
+ "30": "ARS",
+ "31": "VES"
}
\ No newline at end of file
From a10ee979582ce524ab70951b595a50244406bed2 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Sat, 15 Jan 2022 07:45:44 -0800
Subject: [PATCH 15/37] Fix small typos and usergen bug
---
frontend/src/components/BottomBar.js | 18 ++++++++++++++----
frontend/src/components/InfoDialog.js | 13 ++++++-------
frontend/src/components/UserGenPage.js | 3 ++-
frontend/templates/frontend/index.html | 4 ++++
4 files changed, 26 insertions(+), 12 deletions(-)
diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js
index d36a1b1b..a0800075 100644
--- a/frontend/src/components/BottomBar.js
+++ b/frontend/src/components/BottomBar.js
@@ -13,6 +13,7 @@ import BoltIcon from '@mui/icons-material/Bolt';
import GitHubIcon from '@mui/icons-material/GitHub';
import EqualizerIcon from '@mui/icons-material/Equalizer';
import SendIcon from '@mui/icons-material/Send';
+import PublicIcon from '@mui/icons-material/Public';
export default class BottomBar extends Component {
constructor(props) {
@@ -66,6 +67,7 @@ export default class BottomBar extends Component {
+
@@ -75,12 +77,20 @@ export default class BottomBar extends Component {
+
+
+
+
+
+
+
+
)
@@ -105,10 +115,10 @@ export default class BottomBar extends Component {
Community
Support is only offered via public channels.
- For questions and hanging out with other robots
- join the Telegram Groups. If you find a bug
- or want to see new features, use the Github
- Issues page.
+ Writte us on our Telegram community if you have
+ questions or want to hang out with other cool robots.
+ If you find a bug or want to see new features, use
+ the Github Issues page.
diff --git a/frontend/src/components/InfoDialog.js b/frontend/src/components/InfoDialog.js
index ac867a1a..3815edcf 100644
--- a/frontend/src/components/InfoDialog.js
+++ b/frontend/src/components/InfoDialog.js
@@ -44,9 +44,9 @@ export default class InfoDialog extends Component {
Are there trade limits?
Maximum single trade size is 500,000 Satoshis to minimize lightning
- routing failures. This limit will be raised as the Lightning Network
- matures. There is no limits to the number of trades per day
- or number of simultaneous Robots you can use.
+ routing. There is no limits to the number of trades per day. A robot
+ can only have one order at a time. However, you can use multiple
+ Robots simultatenously in different browsers (remember to back up the tokens!).
Is RoboSats private?
@@ -99,7 +99,7 @@ export default class InfoDialog extends Component {
- It RoboSats legal in my country?
+ Is RoboSats legal in my country?
In many countries using RoboSats is no different than using Ebay
or Craiglist. Your regulation may vary. It is your responsibility
@@ -112,12 +112,11 @@ export default class InfoDialog extends Component {
This lightning application is provided as is. It is in active
development: trade with the utmost caution. There is no private
support. Support is only offered via public channels
- (Telegram). RoboSats will never contact you.
- RoboSats will definitely never ask for your user token.
+ (Telegram). RoboSats will never contact you.
+ RoboSats will definitely never ask for your robot token.
-
diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js
index 5ed55d18..796e756e 100644
--- a/frontend/src/components/UserGenPage.js
+++ b/frontend/src/components/UserGenPage.js
@@ -71,7 +71,8 @@ export default class UserGenPage extends Component {
this.delGeneratedUser()
this.setState({
token: this.genBase62Token(34),
- })
+ });
+ this.getGeneratedUser(this.state.token);
}
handleChangeToken=(e)=>{
diff --git a/frontend/templates/frontend/index.html b/frontend/templates/frontend/index.html
index a8fca818..34f11a4a 100644
--- a/frontend/templates/frontend/index.html
+++ b/frontend/templates/frontend/index.html
@@ -1,6 +1,10 @@
+
+ {% comment %} TODO Add a proper fav icon {% endcomment %}
+
+
RoboSats - Simple and Private Bitcoin Exchange
From 7ba2fcc921ed988c90c839aae3721a4d4abc5635 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Sun, 16 Jan 2022 04:31:25 -0800
Subject: [PATCH 16/37] Add celery background and scheduled tasks. Add user
cleansing task
---
.gitignore | 3 +
api/admin.py | 2 +-
api/logics.py | 6 +
api/models.py | 16 +-
api/nick_generator/dicts/en/nouns.py | 2 +-
api/tasks.py | 57 +++++++
api/views.py | 2 +-
chat/templates/chatroom.html | 82 ----------
.../static/assets/misc/unknown_avatar.png | Bin 5347 -> 0 bytes
requirements.txt | 140 ++++++++++++++++++
robosats/__init__.py | 7 +
robosats/celery/__init__.py | 37 +++++
robosats/celery/conf.py | 2 +
robosats/settings.py | 3 +
setup.md | 12 +-
15 files changed, 280 insertions(+), 91 deletions(-)
create mode 100644 api/tasks.py
delete mode 100644 chat/templates/chatroom.html
delete mode 100644 frontend/static/assets/misc/unknown_avatar.png
create mode 100644 requirements.txt
create mode 100644 robosats/celery/__init__.py
create mode 100644 robosats/celery/conf.py
diff --git a/.gitignore b/.gitignore
index 07c2094f..f3a0f6bc 100755
--- a/.gitignore
+++ b/.gitignore
@@ -639,6 +639,9 @@ FodyWeavers.xsd
*migrations*
frontend/static/frontend/main*
+# Celery
+django
+
# robosats
frontend/static/assets/avatars*
api/lightning/lightning*
diff --git a/api/admin.py b/api/admin.py
index 9d8ab64f..6cba65a7 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -38,7 +38,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes')
+ list_display = ('avatar_tag','id','user_link','total_contracts','total_ratings','avg_rating','num_disputes','lost_disputes')
list_display_links = ('avatar_tag','id')
change_links =['user']
readonly_fields = ['avatar_tag']
diff --git a/api/logics.py b/api/logics.py
index cee59d8a..1a4994e9 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -339,6 +339,12 @@ class Logics():
order.taker_bond.status = LNPayment.Status.LOCKED
order.taker_bond.save()
+ # Both users profile have one more contract done
+ order.maker.profile.total_contracts = order.maker.profile.total_contracts + 1
+ order.taker.profile.total_contracts = order.taker.profile.total_contracts + 1
+ order.maker.profile.save()
+ order.taker.profile.save()
+
# Log a market tick
MarketTick.log_a_tick(order)
diff --git a/api/models.py b/api/models.py
index 9ad4be95..1505b303 100644
--- a/api/models.py
+++ b/api/models.py
@@ -159,12 +159,12 @@ class Order(models.Model):
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}')
@receiver(pre_delete, sender=Order)
-def delete_HTLCs_at_order_deletion(sender, instance, **kwargs):
+def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow)
- for htlc in to_delete:
+ for lnpayment in to_delete:
try:
- htlc.delete()
+ lnpayment.delete()
except:
pass
@@ -172,6 +172,9 @@ class Profile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE)
+ # Total trades
+ total_contracts = models.PositiveIntegerField(null=False, default=0)
+
# Ratings stored as a comma separated integer list
total_ratings = models.PositiveIntegerField(null=False, default=0)
latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store latest ratings
@@ -198,8 +201,11 @@ class Profile(models.Model):
@receiver(pre_delete, sender=User)
def del_avatar_from_disk(sender, instance, **kwargs):
- avatar_file=Path('frontend/' + instance.profile.avatar.url)
- avatar_file.unlink() # FIX deleting user fails if avatar is not found
+ try:
+ avatar_file=Path('frontend/' + instance.profile.avatar.url)
+ avatar_file.unlink()
+ except:
+ pass
def __str__(self):
return self.user.username
diff --git a/api/nick_generator/dicts/en/nouns.py b/api/nick_generator/dicts/en/nouns.py
index e48a739d..ae3243e8 100755
--- a/api/nick_generator/dicts/en/nouns.py
+++ b/api/nick_generator/dicts/en/nouns.py
@@ -3633,7 +3633,7 @@ nouns = [
"Fever",
"Few",
"Fiance",
- "Fiancé",
+ "Fiance",
"Fiasco",
"Fiat",
"Fiber",
diff --git a/api/tasks.py b/api/tasks.py
new file mode 100644
index 00000000..8876b84d
--- /dev/null
+++ b/api/tasks.py
@@ -0,0 +1,57 @@
+from celery import shared_task
+
+from .lightning.node import LNNode
+from django.contrib.auth.models import User
+from .models import LNPayment, Order
+from .logics import Logics
+from django.db.models import Q
+
+from datetime import timedelta
+from django.utils import timezone
+
+from decouple import config
+
+@shared_task(name="users_cleansing")
+def users_cleansing():
+ '''
+ Deletes users never used 12 hours after creation
+ '''
+ # Users who's last login has not been in the last 12 hours
+ active_time_range = (timezone.now() - timedelta(hours=12), timezone.now())
+ queryset = User.objects.filter(~Q(last_login__range=active_time_range))
+
+ # And do not have an active trade or any pass finished trade.
+ deleted_users = []
+ for user in queryset:
+ if user.username == str(config('ESCROW_USERNAME')): # Do not delete admin user by mistake
+ continue
+ if not user.profile.total_contracts == 0:
+ continue
+ valid, _ = Logics.validate_already_maker_or_taker(user)
+ if valid:
+ deleted_users.append(str(user))
+ user.delete()
+
+ results = {
+ 'num_deleted': len(deleted_users),
+ 'deleted_users': deleted_users,
+ }
+
+ return results
+
+
+@shared_task
+def orders_expire():
+ pass
+
+@shared_task
+def follow_lnd_payment():
+ pass
+
+@shared_task
+def query_all_lnd_invoices():
+ pass
+
+@shared_task
+def cache_market():
+ pass
\ No newline at end of file
diff --git a/api/views.py b/api/views.py
index e779498f..d6a2ee0d 100644
--- a/api/views.py
+++ b/api/views.py
@@ -422,7 +422,7 @@ class InfoView(ListAPIView):
context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB))
# Number of active users (logged in in last 30 minutes)
- active_user_time_range = (timezone.now() - timedelta(minutes=30), timezone.now())
+ active_user_time_range = (timezone.now() - timedelta(minutes=120), timezone.now())
context['num_active_robotsats'] = len(User.objects.filter(last_login__range=active_user_time_range))
# Compute average premium and volume of today
diff --git a/chat/templates/chatroom.html b/chat/templates/chatroom.html
deleted file mode 100644
index f638de64..00000000
--- a/chat/templates/chatroom.html
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Hello, world!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ request.user.username|json_script:"user_username" }}
- {{ order_id|json_script:"order-id" }}
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/static/assets/misc/unknown_avatar.png b/frontend/static/assets/misc/unknown_avatar.png
deleted file mode 100644
index 9e19b6a9857296d12ca124efe8c1f61aa0fa5431..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 5347
zcmaKwXE+;P*v9R>E5xc*wRe?JwDzVX#2&GC#A>bDE2zCGs%lG7yRDH>TBAj&m?2b2
z5o%Q1xBvJ1`+hjrxv%G3_qopbJ?G20(=E*O8R@v`NJvN+4GnZGNk~Yg{u?wD|0
#FPTKc#ARy)?570mon;BN~Wl78|0=-iTE>i5d2dxuQ>
zzdJO~4nTDl-dU466(1enoL(!d@|}XvGqRmKOe;@VQybNPs@v&r&4Z&Je*cfVOGwRp
zr^DSv)(W4JR;Dhg9&WL%e&3c@hoEODBM`g7#)_`J$4M-+L+ML;XIS>$_bQYs9F_Z;
z4x#>w2@vo2st}#4$GZ*IEXM!T>YLuTq?EkT9@)VNvW^{r>A*NGTsA3Y9Lrxi-pE4!
zMR*Jujd-NoCTLP?n-sDJ1iC{TBd1;dBYp1W?}>yNk`9*Gr-pyBgK>wP~(q
zb(>#f&~o|vr{OZ};k&WbKtJVnj-g6>RP62^Pb1-`?z;46-U`LV$hu$Lu^+tx8sDgS
zwou0eS0J8t&Ka;rQ2dtuaGKRRa$WFI7ojgFmW7CMZ}|bQ@FvjAvtEyAVn%L<4SEwy
zQWg@{ue{M;jZ3#TEC9`|vZ_3>;wnq(ItVXh7}=ML_t1i`1KODV5&nOEIwRaat(wIX
zDYU4&QO`8^q8E^2bg^iBi@st?c2L3Z;v{^wkn{VdzKSj3Yn
zTc(ykpBO!(kFmqtt?d);9T{QZ@4h{g8v0cAwQL1m7&gRMdO?5|DNXD5g#IDrnmQ~&
zrCi{hTqy~KS1%Em{mpz^MOOkEIJW|Vg`Jo-uipiUhkk;7yeu_Gb~80({zEQV-hMF#
zrC+u@4WEXRVt4aTe*GI6V&Th?Q(mC@HQ=}evO@LjV%81
z4A5HXh~hp5@dN$jZ-qsu#F%eFi8J)_ncx0B0m&jt&3m6hMRzVq#=o@O#VCu}urQ>3
zVj4A5{!{v5VNXm|jh`Xk)~y$X@2J^3P|1+`xv5bXw2PIDpRH8-?j7sVfeN>v8XV~I
zq`Z9ICz=9%a&K;_wQ`e&ARmr?oN%V=+LPc)Eu%DV9i=h|Rgn6EgyiG^?c$tAm}=Yo
zfGqEJumSEVN_=NJ&dOg}e(Fs}%kI`c9p$yjDjXK4;{L8SsVFMLBl(0BLpf@6&!#}=
zYkxqi1I}r2nYlFoKqG^^Vj`ptc}8iM-lke$H~>7OFcK%YSb!IXp=Xza4;EUK_C(5j
z<7+r%x2<7FB}WX$xBPd?JANrkzM=(r^v+*_k`u-s_dbp|h$vTgPj=b?=gJ|;-|PF#
zB^hXssK{SjOM6y@p8F(|a-3Xb?b6rv0N&Hqgb&M24BXS$4NAWUc-GK;a@k!hYwjYX
zsH4kRV<|T|5Tz}3KWfgxnBBffN#ppSb(if$M#|M;7#!`or1VJkkjm7t|!0oQ$S59+qSb1RWj6Rd%hdW#H1O*P1h&jE7Ft^)a;r2SL8M>
zf;;EHDgHC7U`40k%?wv^%gIs|tF}yC$pk8}Vo@*x834BtbTb`)UGhFk=`dm`BUb|C
zwi8IxGuBIbQ8jvK^6g&hvNeZ2&Xte`Du_;ju>|~{!tro>+G)bMh4E?Ol5eas6W&uH
znUomPlbg(!AR1n%mb*#<*jk0!LaBN23-Ey5FUj2M9&qDe;xE5P0S6j)I5~fxJA2bz
zz4}Q7P5dLl(jBs1uIU}NeNCyl9G@2F7V|y$sT0O#dAO+Ty8VyV=U_q3c|mu`mPZ(P
zUdzuS3n?Fj7_pX2Hwa?kfcj5=`5qc*F}$n-F*N;CY|b?%;$Sl_T(a6Lcy*vwh^8$&
zU{>B@PFoVsU?`ebV7Tcf9sT?<$NMG^+oEI>hj0}>Ff^-ej{J%5lX4~C>JXnzs*q>0
z1TAOqs_RaWF_`~R;ew^@wQ$@$EExFlO24zco_j+!%!zhubQL)MVWKc3bi^k0QM40U
z_ujzlOse{%gvll#)u&l*6q`6_kn<-_`-TSIP5;`-mo1qtDprpw&8lXKc;!(v;P=yq`fKU7`x}f
z;{s4V(sS3>5bhv97*2-&Zj`vZt2XT1%t3xI8HAG}Xk?=lVJoR83e@Wxwv(>O52vXQ
zNbGT|z5MEhL}zbYgG3aY*~TD+L2tV7VM*W(MJ~tVH{_i~k}Ackwdz18_hsn|H!ZU}
zQ^&G+j;m4YL@E4neNr?=qU}D7Au8Fe3zv_r+DQ6s8=Cfq_>7C+04n&LA^$+iIn=+=
z-^36qX$*L~k&fB{pgGD<9=Dm4v@Ra&;W>1FgS3`Q3TCb1r3c5TtppDQ+KrEHi2P`}
zP5@e21IB!!L8O{98q*`CHNcF5i7jc`A3*!*r<>#mhyvBE>z^hfYnGxIUuo!vO`2G?
z@ev=cw_Dx*2~e^SCS%^-lZWW`h4(iY*68gf#(y#SpBf)a*bDxI+$lViFZIJ&5ZktI
zU-MPwa(q62InI`fHvu0#!oLHRN>|)}M8|_OAv^Q~6NHlmrmq63>
z+=4GBe2&D1sHVKTv0>Gvgtye-4oPWN{Q2{Uv`Z%VvLY_{T_93zze7GPLS>NAR&UAI
z`-!kaAT3j81%E+<(-)D-<@WF6W}W6B$s&
z*|w#h>lyN>{%cj%Ko^(NKy{E@(gB(17r)GJ2bgc%SQgbT7DVDM4@vhx@?gBxY|Xox
zFnSLAqua3!tEXu}k187MeM+={r&L4W;>NQ~v~T2F$h=cBLFQ9)8->@BuZ9NfD1$Y6
zd_X$hns2wm{NK-4rC%l*xe2pxg%M`M#|TEJjE|IfU$KT(IS!^GK-}Yu$S5d-k_Gt%
zgR+l$PhWz0UA%HYAu`iJR#8V*J395(a;oVT@rz$W2Owp$lA`C{__uz|p=E{{hc~$R
z86HnSM2O7oXKLjO-N`)-^_Fw&zb}+!UM?ibM#TNF_1^>8C+E$xm|w5{iCgS03WDF8
z>9kkmdjQzupQ7`SPj74zpVp=IL?mjjdLG+%P7|@@5)(V$tF9)fBaByiOM;H>R^$XW
zNplO=T?ud*S((n>9ExPD-Y&-0UT*v-yGR&clmfil;hvGqmbN&+hs2I7M>3rg?;_D+
z4#|@888|uGoK41Q9P`)Rr*pl9$v1ufNWKO$t)FZl3Y2VHD(bN_G1#dg?D%gf;@%Z_
z^~cZXZT+iLyLPG5xxam^f};#D_35*0l;v@+0iM@&JWWQcPw7BX+K
z@d4sc0X=VRlWC8%$Yx+5=j{{rw|ex40TA{Lz)jgZ7dgM=+562?yMAy$1tVU4^}baVxDZQ*|6Nj9_i_3J(_y%**pvd%)8
zQ2*u}S~^*N3w|ypd3h^rm8+HupP^H=qk#AM`uQE=h6a^o$%OT|o3!MS$*_)eyDBZL
z)l1g+7HkC)0C6OsSaB-W_2f65*(RUOKN>p{Ak~G@7^KDc?BF6b8I$eOikUt{(D;MeFqX({_@8g6oyMN*DCVq
z`m@4cHAjY^Y5Dfe9MJ*)<^=X>RzZdjuL@S;JGeMASTHCy*eX-fIHH@zbq&RIGb{?d
zzthNxNJ5>gCt`V76G$@MXUlT0KZO$qXPHjySceVT-1U{oU8#Psu_CvvPlFZN7e`@2
z3As{`8YdZUNqtowWI)yBP>5`dzE!Ls8*tM8+ZxW)U;3OjYiHeY@J3dCujMVd0@>jq
zubAa>%?yeZJe0rh;-|#KN1^x{j2*IWRdG=S`=1kVuw8{}E68-lgh=-f6vMJ|;eMD9
zVd{Rb7W?PxtI$B4OR>Mw{D|YHB7dm}*Ir|cgc*KF=m`ot{xDGK_WRHm?NCCJR^eph$t5FRSjMqI%mAjx?HMXuMLIVIZJv~P9iWLPsoRg3J?VcsJ#xkWD?xmqFo|b!aqpNk|NJ&2-wdToV2V*x4mB
literal 0
HcmV?d00001
diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py
index 797aa2c9..cba1b059 100644
--- a/robosats/celery/__init__.py
+++ b/robosats/celery/__init__.py
@@ -4,6 +4,8 @@ import os
from celery import Celery
from celery.schedules import crontab
+from datetime import timedelta
+
# You can use rabbitmq instead here.
BASE_REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379')
@@ -27,11 +29,18 @@ app.conf.broker_url = BASE_REDIS_URL
app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler'
-## Configure the periodic tasks
+# Configure the periodic tasks
app.conf.beat_schedule = {
- 'users-cleasing-every-hour': {
+ # User cleansing every 6 hours
+ 'users-cleansing': {
'task': 'users_cleansing',
- 'schedule': 60*60,
+ 'schedule': timedelta(hours=6),
+ },
+
+ 'cache-market-rates': {
+ 'task': 'cache_market',
+ 'schedule': timedelta(seconds=60), # Cache market prices every minutes for now.
},
}
+
app.conf.timezone = 'UTC'
\ No newline at end of file
From 2cbc82a535bb35a7e97546fe8215d895e45ba4ee Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Sun, 16 Jan 2022 08:06:53 -0800
Subject: [PATCH 18/37] Convert cached currency to relation children of order
---
api/admin.py | 10 +++++-----
api/logics.py | 8 ++++----
api/models.py | 43 +++++++++++++++++++++++++++++--------------
api/tasks.py | 11 ++++++-----
api/views.py | 4 ++--
5 files changed, 46 insertions(+), 30 deletions(-)
diff --git a/api/admin.py b/api/admin.py
index da92dcc5..2b485053 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -2,7 +2,7 @@ from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
from django.contrib.auth.models import Group, User
from django.contrib.auth.admin import UserAdmin
-from .models import Order, LNPayment, Profile, MarketTick, CachedExchangeRate
+from .models import Order, LNPayment, Profile, MarketTick, Currency
admin.site.unregister(Group)
admin.site.unregister(User)
@@ -24,9 +24,9 @@ class EUserAdmin(UserAdmin):
@admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
+ list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
list_display_links = ('id','type')
- change_links = ('maker','taker','buyer_invoice','maker_bond','taker_bond','trade_escrow')
+ change_links = ('maker','taker','currency','buyer_invoice','maker_bond','taker_bond','trade_escrow')
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
@admin.register(LNPayment)
@@ -43,8 +43,8 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
change_links =['user']
readonly_fields = ['avatar_tag']
-@admin.register(CachedExchangeRate)
-class CachedExchangeRateAdmin(admin.ModelAdmin):
+@admin.register(Currency)
+class CurrencieAdmin(admin.ModelAdmin):
list_display = ('currency','exchange_rate','timestamp')
readonly_fields = ('currency','exchange_rate','timestamp')
diff --git a/api/logics.py b/api/logics.py
index 034dfdff..1fd8105b 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -2,7 +2,7 @@ from datetime import time, timedelta
from django.utils import timezone
from .lightning.node import LNNode
-from .models import Order, LNPayment, MarketTick, User, CachedExchangeRate
+from .models import Order, LNPayment, MarketTick, User, Currency
from decouple import config
import math
@@ -72,7 +72,7 @@ class Logics():
if order.is_explicit:
satoshis_now = order.satoshis
else:
- exchange_rate = float(CachedExchangeRate.objects.get(currency=order.currency).exchange_rate)
+ exchange_rate = float(order.currency.exchange_rate)
premium_rate = exchange_rate * (1+float(order.premium)/100)
satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000
@@ -80,7 +80,7 @@ class Logics():
def price_and_premium_now(order):
''' computes order premium live '''
- exchange_rate = float(CachedExchangeRate.objects.get(currency=order.currency).exchange_rate)
+ exchange_rate = float(order.currency.exchange_rate)
if not order.is_explicit:
premium = order.premium
price = exchange_rate * (1+float(premium)/100)
@@ -373,7 +373,7 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
- description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Order.currency_dict[str(order.currency)]}"
+ description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + str(order.currency)}"# Order.currency_dict[str(order.currency)]}"
+ " - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel.")
# Gen hold Invoice
diff --git a/api/models.py b/api/models.py
index 8c2a561c..ee718031 100644
--- a/api/models.py
+++ b/api/models.py
@@ -15,6 +15,24 @@ MAX_TRADE = int(config('MAX_TRADE'))
FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE'))
+
+class Currency(models.Model):
+
+ currency_dict = json.load(open('./frontend/static/assets/currencies.json'))
+ currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
+
+ currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False, unique=True)
+ exchange_rate = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
+ timestamp = models.DateTimeField(auto_now_add=True)
+
+ def __str__(self):
+ # returns currency label ( 3 letters code)
+ return self.currency_dict[str(self.currency)]
+
+ class Meta:
+ verbose_name = 'Cached market currency'
+ verbose_name_plural = 'Currencies'
+
class LNPayment(models.Model):
class Types(models.IntegerChoices):
@@ -62,6 +80,10 @@ class LNPayment(models.Model):
def __str__(self):
return (f'LN-{str(self.id)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
+ class Meta:
+ verbose_name = 'Lightning payment'
+ verbose_name_plural = 'Lightning payments'
+
class Order(models.Model):
class Types(models.IntegerChoices):
@@ -87,9 +109,6 @@ class Order(models.Model):
FAI = 15, 'Failed lightning network routing'
MLD = 16, 'Maker lost dispute'
TLD = 17, 'Taker lost dispute'
-
- currency_dict = json.load(open('./frontend/static/assets/currencies.json'))
- currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
# order info
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
@@ -98,7 +117,7 @@ class Order(models.Model):
# order details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
- currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False)
+ currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)])
payment_method = models.CharField(max_length=35, null=False, default="not specified", blank=True)
@@ -155,7 +174,7 @@ class Order(models.Model):
def __str__(self):
# Make relational back to ORDER
- return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}')
+ return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
@receiver(pre_delete, sender=Order)
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
@@ -219,13 +238,6 @@ class Profile(models.Model):
def avatar_tag(self):
return mark_safe('' % self.get_avatar())
-class CachedExchangeRate(models.Model):
-
- currency = models.PositiveSmallIntegerField(choices=Order.currency_choices, null=False, unique=True)
- exchange_rate = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
- timestamp = models.DateTimeField(auto_now_add=True)
-
-
class MarketTick(models.Model):
'''
Records tick by tick Non-KYC Bitcoin price.
@@ -242,7 +254,7 @@ class MarketTick(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
- currency = models.PositiveSmallIntegerField(choices=Order.currency_choices, null=True)
+ currency = models.PositiveSmallIntegerField(choices=Currency.currency_choices, null=True)
timestamp = models.DateTimeField(auto_now_add=True)
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
@@ -259,7 +271,7 @@ class MarketTick(models.Model):
elif order.taker_bond.status == LNPayment.Status.LOCKED:
volume = order.last_satoshis / 100000000
price = float(order.amount) / volume # Amount Fiat / Amount BTC
- market_exchange_rate = float(CachedExchangeRate.objects.get(currency=order.currency).exchange_rate)
+ market_exchange_rate = float(order.currency.exchange_rate)
premium = 100 * (price / market_exchange_rate - 1)
tick = MarketTick.objects.create(
@@ -273,4 +285,7 @@ class MarketTick(models.Model):
def __str__(self):
return f'Tick: {str(self.id)[:8]}'
+ class Meta:
+ verbose_name = 'Market tick'
+ verbose_name_plural = 'Market ticks'
diff --git a/api/tasks.py b/api/tasks.py
index f41c2f03..fc6f65d2 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -2,7 +2,7 @@ from celery import shared_task
from .lightning.node import LNNode
from django.contrib.auth.models import User
-from .models import LNPayment, Order, CachedExchangeRate
+from .models import LNPayment, Order, Currency
from .logics import Logics
from .utils import get_exchange_rates
@@ -55,14 +55,15 @@ def query_all_lnd_invoices():
@shared_task(name="cache_market", ignore_result=True)
def cache_market():
- exchange_rates = get_exchange_rates(list(Order.currency_dict.values()))
+ exchange_rates = get_exchange_rates(list(Currency.currency_dict.values()))
results = {}
- for val in Order.currency_dict:
+ for val in Currency.currency_dict:
rate = exchange_rates[int(val)-1] # currecies are indexed starting at 1 (USD)
- results[val] = {Order.currency_dict[val], rate}
+ results[val] = {Currency.currency_dict[val], rate}
# Create / Update database cached prices
- CachedExchangeRate.objects.update_or_create(
+ Currency.objects.update_or_create(
+ id = int(val),
currency = int(val),
# if there is a Cached Exchange rate matching that value, it updates it with defaults below
defaults = {
diff --git a/api/views.py b/api/views.py
index d6a2ee0d..3ddbb600 100644
--- a/api/views.py
+++ b/api/views.py
@@ -9,7 +9,7 @@ from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
-from .models import LNPayment, MarketTick, Order
+from .models import LNPayment, MarketTick, Order, Currency
from .logics import Logics
from .utils import get_lnd_version, get_commit_robosats
@@ -54,7 +54,7 @@ class MakerView(CreateAPIView):
# Creates a new order
order = Order(
type=type,
- currency=currency,
+ currency=Currency.objects.get(id=currency),
amount=amount,
payment_method=payment_method,
premium=premium,
From 28bfaee93738b0b00412dfbad6295ee9ccf67654 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Sun, 16 Jan 2022 10:32:34 -0800
Subject: [PATCH 19/37] Add background task for expired orders removal
---
api/admin.py | 3 +-
api/logics.py | 20 +++++++++----
api/tasks.py | 44 ++++++++++++++++++++++++----
frontend/src/components/MakerPage.js | 6 +++-
frontend/src/components/OrderPage.js | 6 ++--
robosats/celery/__init__.py | 16 ++++++----
6 files changed, 75 insertions(+), 20 deletions(-)
diff --git a/api/admin.py b/api/admin.py
index 2b485053..8507a508 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -45,7 +45,8 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@admin.register(Currency)
class CurrencieAdmin(admin.ModelAdmin):
- list_display = ('currency','exchange_rate','timestamp')
+ list_display = ('id','currency','exchange_rate','timestamp')
+ list_display_links = ('id','currency')
readonly_fields = ('currency','exchange_rate','timestamp')
@admin.register(MarketTick)
diff --git a/api/logics.py b/api/logics.py
index 1fd8105b..e31937f5 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -95,13 +95,23 @@ class Logics():
return price, premium
- def order_expires(order):
+ @classmethod
+ def order_expires(cls, order):
''' General case when time runs out. Only
used when the maker does not lock a publishing bond'''
- order.status = Order.Status.EXP
- order.maker = None
- order.taker = None
- order.save()
+
+ if order.status == Order.Status.WFB:
+ order.status = Order.Status.EXP
+ order.maker = None
+ order.taker = None
+ order.save()
+
+ if order.status == Order.Status.PUB:
+ cls.return_bond(order.maker_bond)
+ order.status = Order.Status.EXP
+ order.maker = None
+ order.taker = None
+ order.save()
def kick_taker(order):
''' The taker did not lock the taker_bond. Now he has to go'''
diff --git a/api/tasks.py b/api/tasks.py
index fc6f65d2..2bd04398 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -11,6 +11,7 @@ from datetime import timedelta
from django.utils import timezone
from decouple import config
+import time
@shared_task(name="users_cleansing")
def users_cleansing():
@@ -42,18 +43,51 @@ def users_cleansing():
@shared_task(name="orders_expire")
-def orders_expire():
- pass
+def orders_expire(rest_secs):
+ '''
+ Continuously checks order expiration times for 1 hour.
+ If order is expires, it handles the actions.
+ '''
+ now = timezone.now()
+ end_time = now + timedelta(hours=1)
+ context = []
+
+ while now < end_time:
+ queryset = Order.objects.exclude(status=Order.Status.EXP).exclude(status=Order.Status.UCA).exclude(status= Order.Status.CCA)
+ queryset = queryset.filter(expires_at__lt=now) # expires at lower than now
+
+ for order in queryset:
+ context.append(str(order)+ " was "+ Order.Status(order.status).label)
+ Logics.order_expires(order)
+
+ # Allow for some thread rest.
+ time.sleep(rest_secs)
+
+ # Update 'now' for a new loop
+ now = timezone.now()
+
+ results = {
+ 'num_expired': len(context),
+ 'expired_orders_context': context,
+ 'rest_param': rest_secs,
+ }
+
+ return results
@shared_task
def follow_lnd_payment():
+ ''' Makes a payment and follows it.
+ Updates the LNpayment object, and retries
+ until payment is done'''
pass
@shared_task
-def query_all_lnd_invoices():
+def follow_lnd_hold_invoice():
+ ''' Follows and updates LNpayment object
+ until settled or canceled'''
pass
-@shared_task(name="cache_market", ignore_result=True)
+@shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market():
exchange_rates = get_exchange_rates(list(Currency.currency_dict.values()))
results = {}
@@ -65,7 +99,7 @@ def cache_market():
Currency.objects.update_or_create(
id = int(val),
currency = int(val),
- # if there is a Cached Exchange rate matching that value, it updates it with defaults below
+ # if there is a Cached market prices matching that id, it updates it with defaults below
defaults = {
'exchange_rate': rate,
'timestamp': timezone.now(),
diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js
index 7d2da795..61b967e9 100644
--- a/frontend/src/components/MakerPage.js
+++ b/frontend/src/components/MakerPage.js
@@ -2,6 +2,8 @@ import React, { Component } from 'react';
import { Paper, Alert, AlertTitle, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@mui/material"
import { Link } from 'react-router-dom'
+import getFlags from './getFlags'
+
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
@@ -190,7 +192,9 @@ export default class MakerPage extends Component {
>
{
Object.entries(this.state.currencies_dict)
- .map( ([key, value]) => )
+ .map( ([key, value]) => )
}
diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js
index 748be167..b5a26bf7 100644
--- a/frontend/src/components/OrderPage.js
+++ b/frontend/src/components/OrderPage.js
@@ -2,6 +2,7 @@ import React, { Component } from "react";
import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress} from "@mui/material"
import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown';
import TradeBox from "./TradeBox";
+import getFlags from './getFlags'
// icons
import AccessTimeIcon from '@mui/icons-material/AccessTime';
@@ -281,9 +282,10 @@ export default class OrderPage extends Component {
-
+ {getFlags(this.state.currencyCode)}
-
+
diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py
index cba1b059..df462750 100644
--- a/robosats/celery/__init__.py
+++ b/robosats/celery/__init__.py
@@ -32,14 +32,18 @@ app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler'
# Configure the periodic tasks
app.conf.beat_schedule = {
# User cleansing every 6 hours
- 'users-cleansing': {
+ 'users-cleansing': { # Cleans abandoned users every 6 hours
'task': 'users_cleansing',
- 'schedule': timedelta(hours=6),
+ 'schedule': timedelta(hours=6),
},
-
- 'cache-market-rates': {
- 'task': 'cache_market',
- 'schedule': timedelta(seconds=60), # Cache market prices every minutes for now.
+ 'cache-market-prices': { # Cache market prices every minutes for now.
+ 'task': 'cache_external_market_prices',
+ 'schedule': timedelta(seconds=60),
+ },
+ 'orders_expire': { # Continuous order expire removal (1 hour long process, every hour reports results)
+ 'task': 'orders_expire',
+ 'schedule': timedelta(hours=1),
+ 'args': [5], # Rest between checks (secs)
},
}
From 9009f3526987a18d1f103505d9b3f4f58ee4de0f Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Sun, 16 Jan 2022 11:16:11 -0800
Subject: [PATCH 20/37] Fix bug when caching np.nan prices
---
api/tasks.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/api/tasks.py b/api/tasks.py
index 2bd04398..6fe4f75c 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -10,7 +10,6 @@ from django.db.models import Q
from datetime import timedelta
from django.utils import timezone
-from decouple import config
import time
@shared_task(name="users_cleansing")
@@ -21,12 +20,11 @@ def users_cleansing():
# Users who's last login has not been in the last 12 hours
active_time_range = (timezone.now() - timedelta(hours=12), timezone.now())
queryset = User.objects.filter(~Q(last_login__range=active_time_range))
+ queryset = queryset(is_staff=False) # Do not delete staff users
# And do not have an active trade or any pass finished trade.
deleted_users = []
for user in queryset:
- if user.username == str(config('ESCROW_USERNAME')): # Do not delete admin user by mistake
- continue
if not user.profile.total_contracts == 0:
continue
valid, _ = Logics.validate_already_maker_or_taker(user)
@@ -94,6 +92,7 @@ def cache_market():
for val in Currency.currency_dict:
rate = exchange_rates[int(val)-1] # currecies are indexed starting at 1 (USD)
results[val] = {Currency.currency_dict[val], rate}
+ if str(rate) == 'nan': continue # Do not update if no new rate was found
# Create / Update database cached prices
Currency.objects.update_or_create(
@@ -101,7 +100,7 @@ def cache_market():
currency = int(val),
# if there is a Cached market prices matching that id, it updates it with defaults below
defaults = {
- 'exchange_rate': rate,
+ 'exchange_rate': float(rate),
'timestamp': timezone.now(),
})
From 9d883ccc4d3769f6dab61e7299e407d51772e5c4 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Sun, 16 Jan 2022 13:54:42 -0800
Subject: [PATCH 21/37] Add expiration logics. Add dispute statements.
---
api/logics.py | 163 ++++++++++++++++++++++++----
api/models.py | 32 ++++--
api/serializers.py | 3 +-
api/tasks.py | 10 +-
api/views.py | 13 ++-
chat/consumers.py | 5 +-
frontend/src/components/TradeBox.js | 79 +++++++++++++-
7 files changed, 253 insertions(+), 52 deletions(-)
diff --git a/api/logics.py b/api/logics.py
index e31937f5..dae70ab0 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -97,21 +97,104 @@ class Logics():
@classmethod
def order_expires(cls, order):
- ''' General case when time runs out. Only
- used when the maker does not lock a publishing bond'''
+ ''' General cases when time runs out.'''
- if order.status == Order.Status.WFB:
+ # Do not change order status if an order in any with
+ # any of these status is sent to expire here
+ do_nothing = [Order.Status.DEL, Order.Status.UCA,
+ Order.Status.EXP, Order.Status.FSE,
+ Order.Status.DIS, Order.Status.CCA,
+ Order.Status.PAY, Order.Status.SUC,
+ Order.Status.FAI, Order.Status.MLD,
+ Order.Status.TLD]
+
+ if order.status in do_nothing:
+ return False
+
+ elif order.status == Order.Status.WFB:
order.status = Order.Status.EXP
order.maker = None
order.taker = None
order.save()
+ return True
- if order.status == Order.Status.PUB:
+ elif order.status == Order.Status.PUB:
cls.return_bond(order.maker_bond)
order.status = Order.Status.EXP
order.maker = None
order.taker = None
order.save()
+ return True
+
+ elif order.status == Order.Status.TAK:
+ cls.kick_taker(order)
+ return True
+
+ elif order.status == Order.Status.WF2:
+ '''Weird case where an order expires and both participants
+ did not proceed with the contract. Likely the site was
+ down or there was a bug. Still bonds must be charged
+ to avoid service DDOS. '''
+
+ cls.settle_bond(order.maker_bond)
+ cls.settle_bond(order.taker_bond)
+ order.status = Order.Status.EXP
+ order.maker = None
+ order.taker = None
+ order.save()
+ return True
+
+ elif order.status == Order.Status.WFE:
+ maker_is_seller = cls.is_seller(order, order.maker)
+ # If maker is seller, settle the bond and order goes to expired
+ if maker_is_seller:
+ cls.settle_bond(order.maker_bond)
+ order.status = Order.Status.EXP
+ order.maker = None
+ order.taker = None
+ order.save()
+ return True
+
+ # If maker is buyer, settle the taker's bond order goes back to public
+ else:
+ cls.settle_bond(order.taker_bond)
+ order.status = Order.Status.PUB
+ order.taker = None
+ order.taker_bond = None
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
+ order.save()
+ return True
+
+ elif order.status == Order.Status.WFI:
+ # The trade could happen without a buyer invoice. However, this user
+ # is most likely AFK since he did not submit an invoice; will most
+ # likely desert the contract as well.
+ maker_is_buyer = cls.is_buyer(order, order.maker)
+ # If maker is buyer, settle the bond and order goes to expired
+ if maker_is_buyer:
+ cls.settle_bond(order.maker_bond)
+ order.status = Order.Status.EXP
+ order.maker = None
+ order.taker = None
+ order.save()
+ return True
+
+ # If maker is seller, settle the taker's bond order goes back to public
+ else:
+ cls.settle_bond(order.taker_bond)
+ order.status = Order.Status.PUB
+ order.taker = None
+ order.taker_bond = None
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
+ order.save()
+ return True
+
+ elif order.status == Order.Status.CHA:
+ # Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute
+ # was opened. A seller-scammer could persuade a buyer to not click "fiat sent"
+ # as of now, we assume this is a dispute case by default.
+ cls.open_dispute(order)
+ return True
def kick_taker(order):
''' The taker did not lock the taker_bond. Now he has to go'''
@@ -125,10 +208,48 @@ class Logics():
order.status = Order.Status.PUB
order.taker = None
order.taker_bond = None
- order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION) ## TO FIX. Restore the remaining order durantion, not all of it!
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
order.save()
return True
+ @classmethod
+ def open_dispute(cls, order, user=None):
+
+ # Always settle the escrow during a dispute (same as with 'Fiat Sent')
+ if not order.trade_escrow.status == LNPayment.Status.SETLED:
+ cls.settle_escrow(order)
+
+ order.is_disputed = True
+ order.status = Order.Status.DIS
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
+ order.save()
+
+ # User could be None if a dispute is open automatically due to weird expiration.
+ if not user == None:
+ profile = user.profile
+ profile.num_disputes = profile.num_disputes + 1
+ profile.orders_disputes_started = list(profile.orders_disputes_started).append(str(order.id))
+ profile.save()
+
+ return True, None
+ def dispute_statement(order, user, statement):
+ ''' Updates the dispute statements in DB'''
+
+ if len(statement) > 5000:
+ return False, {'bad_statement':'The statement is longer than 5000 characters'}
+ if order.maker == user:
+ order.maker_statement = statement
+ else:
+ order.taker_statement = statement
+
+ # If both statements are in, move to wait for dispute resolution
+ if order.maker_statement != None and order.taker_statement != None:
+ order.status = Order.Status.WFR
+ order.expires_at = timezone.now() + Order.t_to_expire[Order.Status.WFR]
+
+ order.save()
+ return True, None
+
@classmethod
def buyer_invoice_amount(cls, order, user):
''' Computes buyer invoice amount. Uses order.last_satoshis,
@@ -234,7 +355,7 @@ class Logics():
on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)'''
elif order.status == Order.Status.PUB and order.maker == user:
#Settle the maker bond (Maker loses the bond for cancelling public order)
- if cls.settle_maker_bond(order):
+ if cls.settle_bond(order.maker_bond):
order.maker = None
order.status = Order.Status.UCA
order.save()
@@ -257,7 +378,7 @@ class Logics():
'''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.PUB and order.status < Order.Status.CHA and order.maker == user:
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
- valid = cls.settle_maker_bond(order)
+ valid = cls.settle_bond(order.maker_bond)
if valid:
order.maker = None
order.status = Order.Status.UCA
@@ -268,7 +389,7 @@ class Logics():
'''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.TAK and order.status < Order.Status.CHA and order.taker == user:
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
- valid = cls.settle_taker_bond(order)
+ valid = cls.settle_bond(order.taker_bond)
if valid:
order.taker = None
order.status = Order.Status.PUB
@@ -277,7 +398,7 @@ class Logics():
return True, None
# 5) When trade collateral has been posted (after escrow)
- '''Always goes to cancelled status. Collaboration is needed.
+ '''Always goes to cancelled status. Collaboration is needed.
When a user asks for cancel, 'order.is_pending_cancel' goes True.
When the second user asks for cancel. Order is totally cancelled.
Has a small cost for both parties to prevent node DDOS.'''
@@ -383,7 +504,7 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
- description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + str(order.currency)}"# Order.currency_dict[str(order.currency)]}"
+ description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}"
+ " - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel.")
# Gen hold Invoice
@@ -472,22 +593,20 @@ class Logics():
order.trade_escrow.save()
return True
- def settle_maker_bond(order):
- ''' Settles the maker bond hold invoice'''
+ def settle_bond(bond):
+ ''' Settles the bond hold invoice'''
# TODO ERROR HANDLING
- if LNNode.settle_hold_invoice(order.maker_bond.preimage):
- order.maker_bond.status = LNPayment.Status.SETLED
- order.maker_bond.save()
+ if LNNode.settle_hold_invoice(bond.preimage):
+ bond.status = LNPayment.Status.SETLED
+ bond.save()
return True
- def settle_taker_bond(order):
- ''' Settles the taker bond hold invoice'''
- # TODO ERROR HANDLING
- if LNNode.settle_hold_invoice(order.taker_bond.preimage):
- order.taker_bond.status = LNPayment.Status.SETLED
- order.taker_bond.save()
+ def return_escrow(order):
+ '''returns the trade escrow'''
+ if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
+ order.trade_escrow.status = LNPayment.Status.RETNED
return True
-
+
def return_bond(bond):
'''returns a bond'''
if LNNode.cancel_return_hold_invoice(bond.payment_hash):
diff --git a/api/models.py b/api/models.py
index ee718031..a2d04adc 100644
--- a/api/models.py
+++ b/api/models.py
@@ -107,8 +107,9 @@ class Order(models.Model):
PAY = 13, 'Sending satoshis to buyer'
SUC = 14, 'Sucessful trade'
FAI = 15, 'Failed lightning network routing'
- MLD = 16, 'Maker lost dispute'
- TLD = 17, 'Taker lost dispute'
+ WFR = 16, 'Wait for dispute resolution'
+ MLD = 17, 'Maker lost dispute'
+ TLD = 18, 'Taker lost dispute'
# order info
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
@@ -135,10 +136,14 @@ class Order(models.Model):
maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order
taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order
is_pending_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
- is_disputed = models.BooleanField(default=False, null=False)
is_fiat_sent = models.BooleanField(default=False, null=False)
- # HTLCs
+ # in dispute
+ is_disputed = models.BooleanField(default=False, null=False)
+ maker_statement = models.TextField(max_length=5000, unique=True, null=True, default=None, blank=True)
+ taker_statement = models.TextField(max_length=5000, unique=True, null=True, default=None, blank=True)
+
+ # LNpayments
# Order collateral
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
@@ -147,11 +152,11 @@ class Order(models.Model):
# buyer payment LN invoice
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- # cancel LN invoice // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
- maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ # Unused so far. Cancel LN invoices // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
+ # maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ # taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- total_time_to_expire = {
+ t_to_expire = {
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
1 : 60*60*int(config('PUBLIC_ORDER_DURATION')), # 'Public'
2 : 0, # 'Deleted'
@@ -163,13 +168,14 @@ class Order(models.Model):
8 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for buyer invoice'
9 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Sending fiat - In chatroom'
10 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Fiat sent - In chatroom'
- 11 : 24*60*60, # 'In dispute'
+ 11 : 10*24*60*60, # 'In dispute'
12 : 0, # 'Collaboratively cancelled'
13 : 24*60*60, # 'Sending satoshis to buyer'
14 : 24*60*60, # 'Sucessful trade'
15 : 24*60*60, # 'Failed lightning network routing'
- 16 : 24*60*60, # 'Maker lost dispute'
- 17 : 24*60*60, # 'Taker lost dispute'
+ 16 : 24*60*60, # 'Wait for dispute resolution'
+ 17 : 24*60*60, # 'Maker lost dispute'
+ 18 : 24*60*60, # 'Taker lost dispute'
}
def __str__(self):
@@ -201,6 +207,8 @@ class Profile(models.Model):
# Disputes
num_disputes = models.PositiveIntegerField(null=False, default=0)
lost_disputes = models.PositiveIntegerField(null=False, default=0)
+ num_disputes_started = models.PositiveIntegerField(null=False, default=0)
+ orders_disputes_started = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store ID of orders
# RoboHash
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True)
@@ -254,7 +262,7 @@ class MarketTick(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
- currency = models.PositiveSmallIntegerField(choices=Currency.currency_choices, null=True)
+ currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
timestamp = models.DateTimeField(auto_now_add=True)
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
diff --git a/api/serializers.py b/api/serializers.py
index 6beff335..88997f3d 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -13,5 +13,6 @@ class MakeOrderSerializer(serializers.ModelSerializer):
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None)
- action = serializers.ChoiceField(choices=('take','update_invoice','dispute','cancel','confirm','rate'), allow_null=False)
+ statement = serializers.CharField(max_length=10000, allow_null=True, allow_blank=True, default=None)
+ action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate'), allow_null=False)
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)
\ No newline at end of file
diff --git a/api/tasks.py b/api/tasks.py
index 6fe4f75c..16219ac8 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -43,8 +43,8 @@ def users_cleansing():
@shared_task(name="orders_expire")
def orders_expire(rest_secs):
'''
- Continuously checks order expiration times for 1 hour.
- If order is expires, it handles the actions.
+ Continuously checks order expiration times for 1 hour. If order
+ has expires, it calls the logics module for expiration handling.
'''
now = timezone.now()
end_time = now + timedelta(hours=1)
@@ -55,8 +55,8 @@ def orders_expire(rest_secs):
queryset = queryset.filter(expires_at__lt=now) # expires at lower than now
for order in queryset:
- context.append(str(order)+ " was "+ Order.Status(order.status).label)
- Logics.order_expires(order)
+ if Logics.order_expires(order): # Order send to expire here
+ context.append(str(order)+ " was "+ Order.Status(order.status).label)
# Allow for some thread rest.
time.sleep(rest_secs)
@@ -77,12 +77,14 @@ def follow_lnd_payment():
''' Makes a payment and follows it.
Updates the LNpayment object, and retries
until payment is done'''
+
pass
@shared_task
def follow_lnd_hold_invoice():
''' Follows and updates LNpayment object
until settled or canceled'''
+
pass
@shared_task(name="cache_external_market_prices", ignore_result=True)
diff --git a/api/views.py b/api/views.py
index 3ddbb600..812b635b 100644
--- a/api/views.py
+++ b/api/views.py
@@ -106,7 +106,7 @@ class OrderView(viewsets.ViewSet):
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
data = ListOrderSerializer(order).data
- data['total_secs_exp'] = Order.total_time_to_expire[order.status]
+ data['total_secs_exp'] = Order.t_to_expire[order.status]
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
@@ -217,10 +217,13 @@ class OrderView(viewsets.ViewSet):
order = Order.objects.get(id=order_id)
- # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' 6)'rate' (counterparty)
+ # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
+ # 6)'submit_statement' (in dispute), 7)'rate' (counterparty)
action = serializer.data.get('action')
invoice = serializer.data.get('invoice')
+ statement = serializer.data.get('statement')
rating = serializer.data.get('rating')
+
# 1) If action is take, it is a taker request!
if action == 'take':
@@ -255,7 +258,11 @@ class OrderView(viewsets.ViewSet):
# 5) If action is dispute
elif action == 'dispute':
- valid, context = Logics.open_dispute(order,request.user, rating)
+ valid, context = Logics.open_dispute(order,request.user)
+ if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
+
+ elif action == 'submit_statement':
+ valid, context = Logics.dispute_statement(order,request.user, statement)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate
diff --git a/chat/consumers.py b/chat/consumers.py
index a65ed459..cb3da612 100644
--- a/chat/consumers.py
+++ b/chat/consumers.py
@@ -21,10 +21,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
# if not (Logics.is_buyer(order[0], self.user) or Logics.is_seller(order[0], self.user)):
# print ("Outta this chat")
# return False
-
- print(self.user_nick)
- print(self.order_id)
-
+
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index c71f2f08..fb359736 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -38,6 +38,7 @@ export default class TradeBox extends Component {
super(props);
this.state = {
badInvoice: false,
+ badStatement: false,
}
}
@@ -200,8 +201,6 @@ export default class TradeBox extends Component {
});
}
- // Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage.
-
handleClickSubmitInvoiceButton=()=>{
this.setState({badInvoice:false});
@@ -219,10 +218,34 @@ export default class TradeBox extends Component {
& console.log(data));
}
+ handleInputDisputeChanged=(e)=>{
+ this.setState({
+ statement: e.target.value,
+ badStatement: false,
+ });
+ }
+
+ handleClickSubmitStatementButton=()=>{
+ this.setState({badInvoice:false});
+
+ const requestOptions = {
+ method: 'POST',
+ headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
+ body: JSON.stringify({
+ 'action':'submit_statement',
+ 'statement': this.state.statement,
+ }),
+ };
+ fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
+ .then((response) => response.json())
+ .then((data) => this.setState({badStatement:data.bad_statement})
+ & console.log(data));
+}
+
showInputInvoice(){
return (
- // TODO Camera option to read QR
+ // TODO Option to upload files and images
@@ -252,7 +275,51 @@ export default class TradeBox extends Component {
/>
-
+
+
+
+ {this.showBondIsLocked()}
+
+ )
+ }
+
+ // Asks the user for a dispute statement.
+ showInDisputeStatement(){
+ return (
+
+ // TODO Option to upload files
+
+
+
+
+ A dispute has been opened
+
+
+
+
+ Please, submit your statement. Be clear and specific about what happened and provide the necessary
+ evidence. It is best to provide a burner email, XMPP or telegram username to follow up with the staff.
+ Disputes are solved at the discretion of real robots (aka humans), so be as helpful
+ as possible to ensure a fair outcome. Max 5000 chars.
+
+
+
+
+
+
+
+
{this.showBondIsLocked()}
@@ -463,8 +530,8 @@ handleRatingChange=(e)=>{
{/* Trade Finished - Payment Routing Failed */}
{this.props.data.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""}
- {/* Trade Finished - Payment Routing Failed - TODO Needs more planning */}
- {this.props.data.statusCode == 11 ? this.showInDispute() : ""}
+ {/* Trade Finished - TODO Needs more planning */}
+ {this.props.data.statusCode == 11 ? this.showInDisputeStatement() : ""}
{/* TODO */}
From eddd4674f6792d51249981fcd0dd496290f5ddce Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Mon, 17 Jan 2022 08:41:55 -0800
Subject: [PATCH 22/37] Add admin background task: follow all active hold
invoices
---
api/lightning/node.py | 4 +
api/logics.py | 32 ++++----
api/management/commands/follow_invoices.py | 84 +++++++++++++++++++++
api/models.py | 11 +--
api/tasks.py | 86 +++++++++++++++-------
api/utils.py | 2 +-
frontend/src/components/BottomBar.js | 6 +-
frontend/src/components/OrderPage.js | 2 +-
frontend/src/components/TradeBox.js | 2 +-
robosats/celery/__init__.py | 1 -
10 files changed, 178 insertions(+), 52 deletions(-)
create mode 100644 api/management/commands/follow_invoices.py
diff --git a/api/lightning/node.py b/api/lightning/node.py
index 76d6acb3..4f6cb348 100644
--- a/api/lightning/node.py
+++ b/api/lightning/node.py
@@ -28,6 +28,10 @@ class LNNode():
invoicesstub = invoicesstub.InvoicesStub(channel)
routerstub = routerstub.RouterStub(channel)
+ lnrpc = lnrpc
+ invoicesrpc = invoicesrpc
+ routerrpc = routerrpc
+
payment_failure_context = {
0: "Payment isn't failed (yet)",
1: "There are more routes to try, but the payment timeout was exceeded.",
diff --git a/api/logics.py b/api/logics.py
index dae70ab0..c0a0383a 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -53,7 +53,7 @@ class Logics():
else:
order.taker = user
order.status = Order.Status.TAK
- order.expires_at = timezone.now() + timedelta(minutes=EXP_TAKER_BOND_INVOICE)
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
order.save()
return True, None
@@ -234,18 +234,21 @@ class Logics():
return True, None
def dispute_statement(order, user, statement):
''' Updates the dispute statements in DB'''
+ if not order.status == Order.Status.DIS:
+ return False, {'bad_request':'Only orders in dispute accept a dispute statements'}
if len(statement) > 5000:
return False, {'bad_statement':'The statement is longer than 5000 characters'}
+
if order.maker == user:
order.maker_statement = statement
else:
order.taker_statement = statement
- # If both statements are in, move to wait for dispute resolution
+ # If both statements are in, move status to wait for dispute resolution
if order.maker_statement != None and order.taker_statement != None:
order.status = Order.Status.WFR
- order.expires_at = timezone.now() + Order.t_to_expire[Order.Status.WFR]
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WFR])
order.save()
return True, None
@@ -296,14 +299,14 @@ class Logics():
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
if order.status == Order.Status.WFI:
order.status = Order.Status.CHA
- order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION)
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2:
# If the escrow is lock move to Chat.
if order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA
- order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION)
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
else:
order.status = Order.Status.WFE
@@ -413,7 +416,7 @@ class Logics():
order.maker_bond.save()
order.status = Order.Status.PUB
# With the bond confirmation the order is extended 'public_order_duration' hours
- order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION)
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
order.save()
return True
return False
@@ -461,6 +464,9 @@ class Logics():
@classmethod
def is_taker_bond_locked(cls, order):
+ if order.taker_bond.status == LNPayment.Status.LOCKED:
+ return True
+
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
# (This is the last update to "last_satoshis", it becomes the escrow amount next!)
@@ -468,9 +474,9 @@ class Logics():
order.taker_bond.status = LNPayment.Status.LOCKED
order.taker_bond.save()
- # Both users profiles are added one more contract
- order.maker.profile.total_contracts = order.maker.profile.total_contracts + 1
- order.taker.profile.total_contracts = order.taker.profile.total_contracts + 1
+ # Both users profiles are added one more contract // Unsafe can add more than once.
+ order.maker.profile.total_contracts += 1
+ order.taker.profile.total_contracts += 1
order.maker.profile.save()
order.taker.profile.save()
@@ -478,7 +484,7 @@ class Logics():
MarketTick.log_a_tick(order)
# With the bond confirmation the order is extended 'public_order_duration' hours
- order.expires_at = timezone.now() + timedelta(minutes=INVOICE_AND_ESCROW_DURATION)
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WF2])
order.status = Order.Status.WF2
order.save()
return True
@@ -524,7 +530,7 @@ class Logics():
created_at = hold_payment['created_at'],
expires_at = hold_payment['expires_at'])
- order.expires_at = timezone.now() + timedelta(seconds=EXP_TAKER_BOND_INVOICE)
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
order.save()
return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
@@ -540,7 +546,7 @@ class Logics():
# If status is 'Waiting for invoice' move to Chat
elif order.status == Order.Status.WFE:
order.status = Order.Status.CHA
- order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION)
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
order.save()
return True
return False
@@ -649,7 +655,7 @@ class Logics():
if is_payed:
order.status = Order.Status.SUC
order.buyer_invoice.status = LNPayment.Status.SUCCED
- order.expires_at = timezone.now() + timedelta(days=1) # One day to rate / see this order.
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
order.save()
# RETURN THE BONDS
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
new file mode 100644
index 00000000..abee1eac
--- /dev/null
+++ b/api/management/commands/follow_invoices.py
@@ -0,0 +1,84 @@
+from distutils.log import debug
+from re import L
+from xmlrpc.client import boolean
+from django.core.management.base import BaseCommand, CommandError
+from api.lightning.node import LNNode
+from decouple import config
+from base64 import b64decode
+from api.models import LNPayment
+import time
+
+MACAROON = b64decode(config('LND_MACAROON_BASE64'))
+
+class Command(BaseCommand):
+ '''
+ Background: SubscribeInvoices stub iterator would be great to use here
+ however it only sends updates when the invoice is OPEN (new) or SETTLED.
+ We are very interested on the other two states (CANCELLED and ACCEPTED).
+ Therefore, this thread (follow_invoices) will iterate over all LNpayment
+ objects and do InvoiceLookupV2 to update their state 'live' '''
+
+ help = 'Follows all active hold invoices'
+
+ # def add_arguments(self, parser):
+ # parser.add_argument('debug', nargs='+', type=boolean)
+
+ def handle(self, *args, **options):
+ ''' Follows and updates LNpayment objects
+ until settled or canceled'''
+
+ lnd_state_to_lnpayment_status = {
+ 0: LNPayment.Status.INVGEN,
+ 1: LNPayment.Status.SETLED,
+ 2: LNPayment.Status.CANCEL,
+ 3: LNPayment.Status.LOCKED
+ }
+
+ stub = LNNode.invoicesstub
+
+ while True:
+ time.sleep(5)
+
+ # time it for debugging
+ t0 = time.time()
+ queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
+
+ debug = {}
+ debug['num_active_invoices'] = len(queryset)
+ debug['invoices'] = []
+
+ for idx, hold_lnpayment in enumerate(queryset):
+ old_status = LNPayment.Status(hold_lnpayment.status).label
+
+ try:
+ request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
+ response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
+
+ hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
+ # If it fails at finding the invoice it has definetely been canceled.
+ # On RoboSats DB we make a distinction between cancelled and returned (LND does not)
+ except:
+ hold_lnpayment.status = LNPayment.Status.CANCEL
+ continue
+
+ new_status = LNPayment.Status(hold_lnpayment.status).label
+
+ # Only save the hold_payments that change (otherwise this function does not scale)
+ changed = not old_status==new_status
+ if changed:
+ hold_lnpayment.save()
+
+ # Report for debugging
+ new_status = LNPayment.Status(hold_lnpayment.status).label
+ debug['invoices'].append({idx:{
+ 'payment_hash': str(hold_lnpayment.payment_hash),
+ 'status_changed': not old_status==new_status,
+ 'old_status': old_status,
+ 'new_status': new_status,
+ }})
+ debug['time']=time.time()-t0
+
+ self.stdout.write(str(debug))
+
+
+
\ No newline at end of file
diff --git a/api/models.py b/api/models.py
index a2d04adc..8debaf5c 100644
--- a/api/models.py
+++ b/api/models.py
@@ -50,11 +50,12 @@ class LNPayment(models.Model):
LOCKED = 1, 'Locked'
SETLED = 2, 'Settled'
RETNED = 3, 'Returned'
- EXPIRE = 4, 'Expired'
- VALIDI = 5, 'Valid'
- FLIGHT = 6, 'In flight'
- SUCCED = 7, 'Succeeded'
- FAILRO = 8, 'Routing failed'
+ CANCEL = 4, 'Cancelled'
+ EXPIRE = 5, 'Expired'
+ VALIDI = 6, 'Valid'
+ FLIGHT = 7, 'In flight'
+ SUCCED = 8, 'Succeeded'
+ FAILRO = 9, 'Routing failed'
# payment use details
diff --git a/api/tasks.py b/api/tasks.py
index 16219ac8..a17c8de9 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -1,26 +1,20 @@
from celery import shared_task
-from .lightning.node import LNNode
-from django.contrib.auth.models import User
-from .models import LNPayment, Order, Currency
-from .logics import Logics
-from .utils import get_exchange_rates
-
-from django.db.models import Q
-from datetime import timedelta
-from django.utils import timezone
-
-import time
-
@shared_task(name="users_cleansing")
def users_cleansing():
'''
Deletes users never used 12 hours after creation
'''
+ from django.contrib.auth.models import User
+ from django.db.models import Q
+ from .logics import Logics
+ from datetime import timedelta
+ from django.utils import timezone
+
# Users who's last login has not been in the last 12 hours
active_time_range = (timezone.now() - timedelta(hours=12), timezone.now())
queryset = User.objects.filter(~Q(last_login__range=active_time_range))
- queryset = queryset(is_staff=False) # Do not delete staff users
+ queryset = queryset.filter(is_staff=False) # Do not delete staff users
# And do not have an active trade or any pass finished trade.
deleted_users = []
@@ -46,8 +40,14 @@ def orders_expire(rest_secs):
Continuously checks order expiration times for 1 hour. If order
has expires, it calls the logics module for expiration handling.
'''
+ import time
+ from .models import Order
+ from .logics import Logics
+ from datetime import timedelta
+ from django.utils import timezone
+
now = timezone.now()
- end_time = now + timedelta(hours=1)
+ end_time = now + timedelta(minutes=60)
context = []
while now < end_time:
@@ -55,8 +55,12 @@ def orders_expire(rest_secs):
queryset = queryset.filter(expires_at__lt=now) # expires at lower than now
for order in queryset:
- if Logics.order_expires(order): # Order send to expire here
- context.append(str(order)+ " was "+ Order.Status(order.status).label)
+ try: # TODO Fix, it might fail if returning an already returned bond.
+ info = str(order)+ " was "+ Order.Status(order.status).label
+ if Logics.order_expires(order): # Order send to expire here
+ context.append(info)
+ except:
+ pass
# Allow for some thread rest.
time.sleep(rest_secs)
@@ -72,23 +76,51 @@ def orders_expire(rest_secs):
return results
-@shared_task
-def follow_lnd_payment():
- ''' Makes a payment and follows it.
- Updates the LNpayment object, and retries
- until payment is done'''
+@shared_task(name='follow_send_payment')
+def follow_send_payment(lnpayment):
+ '''Sends sats to buyer, continuous update'''
- pass
+ from decouple import config
+ from base64 import b64decode
-@shared_task
-def follow_lnd_hold_invoice():
- ''' Follows and updates LNpayment object
- until settled or canceled'''
+ from api.lightning.node import LNNode
+ from api.models import LNPayment
- pass
+ MACAROON = b64decode(config('LND_MACAROON_BASE64'))
+
+ fee_limit_sat = max(lnpayment.num_satoshis * 0.0002, 10) # 200 ppm or 10 sats max
+ request = LNNode.routerrpc.SendPaymentRequest(
+ payment_request=lnpayment.invoice,
+ fee_limit_sat=fee_limit_sat,
+ timeout_seconds=60)
+
+ for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
+ if response.status == 0 : # Status 0 'UNKNOWN'
+ pass
+
+ if response.status == 1 : # Status 1 'IN_FLIGHT'
+ lnpayment.status = LNPayment.Status.FLIGHT
+ lnpayment.save()
+
+ if response.status == 3 : # Status 3 'FAILED'
+ lnpayment.status = LNPayment.Status.FAILRO
+ lnpayment.save()
+ context = LNNode.payment_failure_context[response.failure_reason]
+ return False, context
+
+ if response.status == 2 : # Status 2 'SUCCEEDED'
+ lnpayment.status = LNPayment.Status.SUCCED
+ lnpayment.save()
+ return True, None
@shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market():
+
+ from .models import Currency
+ from .utils import get_exchange_rates
+
+ from django.utils import timezone
+
exchange_rates = get_exchange_rates(list(Currency.currency_dict.values()))
results = {}
for val in Currency.currency_dict:
diff --git a/api/utils.py b/api/utils.py
index b7ea9459..467753b4 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -5,7 +5,7 @@ import numpy as np
market_cache = {}
-# @ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds
+# @ring.dict(market_cache, expire=5) #keeps in cache for 5 seconds
def get_exchange_rates(currencies):
'''
Params: list of currency codes.
diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js
index a0800075..5516ee36 100644
--- a/frontend/src/components/BottomBar.js
+++ b/frontend/src/components/BottomBar.js
@@ -115,10 +115,10 @@ export default class BottomBar extends Component {
Community
Support is only offered via public channels.
- Writte us on our Telegram community if you have
+ Join our Telegram community if you have
questions or want to hang out with other cool robots.
- If you find a bug or want to see new features, use
- the Github Issues page.
+ Please, use our Github Issues if you find a bug or want
+ to see new features!
diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js
index b5a26bf7..453867ed 100644
--- a/frontend/src/components/OrderPage.js
+++ b/frontend/src/components/OrderPage.js
@@ -237,7 +237,7 @@ export default class OrderPage extends Component {
- {this.state.type ? "Sell " : "Buy "} Order Details
+ Order Details
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index fb359736..17b3dc26 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -499,7 +499,7 @@ handleRatingChange=(e)=>{
- TradeBox
+ Contract Box
{/* Maker and taker Bond request */}
diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py
index df462750..00eb5af7 100644
--- a/robosats/celery/__init__.py
+++ b/robosats/celery/__init__.py
@@ -31,7 +31,6 @@ app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler'
# Configure the periodic tasks
app.conf.beat_schedule = {
- # User cleansing every 6 hours
'users-cleansing': { # Cleans abandoned users every 6 hours
'task': 'users_cleansing',
'schedule': timedelta(hours=6),
From 0db73c7c821000e695c627d1782f9c908fbc229e Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Mon, 17 Jan 2022 10:11:44 -0800
Subject: [PATCH 23/37] Convert order cleaning task into admin command
---
api/logics.py | 51 +++++++++++++++++-----
api/management/commands/clean_orders.py | 41 +++++++++++++++++
api/management/commands/follow_invoices.py | 12 ++---
api/tasks.py | 44 -------------------
api/utils.py | 2 +-
robosats/celery/__init__.py | 5 ---
6 files changed, 87 insertions(+), 68 deletions(-)
create mode 100644 api/management/commands/clean_orders.py
diff --git a/api/logics.py b/api/logics.py
index c0a0383a..547cad90 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -113,6 +113,7 @@ class Logics():
elif order.status == Order.Status.WFB:
order.status = Order.Status.EXP
+ cls.cancel_bond(order.maker_bond)
order.maker = None
order.taker = None
order.save()
@@ -127,6 +128,7 @@ class Logics():
return True
elif order.status == Order.Status.TAK:
+ cls.cancel_bond(order.taker_bond)
cls.kick_taker(order)
return True
@@ -149,6 +151,7 @@ class Logics():
# If maker is seller, settle the bond and order goes to expired
if maker_is_seller:
cls.settle_bond(order.maker_bond)
+ cls.return_bond(order.taker_bond)
order.status = Order.Status.EXP
order.maker = None
order.taker = None
@@ -167,19 +170,20 @@ class Logics():
elif order.status == Order.Status.WFI:
# The trade could happen without a buyer invoice. However, this user
- # is most likely AFK since he did not submit an invoice; will most
- # likely desert the contract as well.
+ # is likely AFK since he did not submit an invoice; will probably
+ # desert the contract as well.
maker_is_buyer = cls.is_buyer(order, order.maker)
# If maker is buyer, settle the bond and order goes to expired
if maker_is_buyer:
cls.settle_bond(order.maker_bond)
+ cls.return_bond(order.taker_bond)
order.status = Order.Status.EXP
order.maker = None
order.taker = None
order.save()
return True
- # If maker is seller, settle the taker's bond order goes back to public
+ # If maker is seller settle the taker's bond, order goes back to public
else:
cls.settle_bond(order.taker_bond)
order.status = Order.Status.PUB
@@ -203,14 +207,13 @@ class Logics():
profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
profile.save()
- # Delete the taker_bond payment request, and make order public again
- if LNNode.cancel_return_hold_invoice(order.taker_bond.payment_hash):
- order.status = Order.Status.PUB
- order.taker = None
- order.taker_bond = None
- order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
- order.save()
- return True
+ # Make order public again
+ order.status = Order.Status.PUB
+ order.taker = None
+ order.taker_bond = None
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
+ order.save()
+ return True
@classmethod
def open_dispute(cls, order, user=None):
@@ -369,6 +372,7 @@ class Logics():
LNPayment "order.taker_bond" is deleted() '''
elif order.status == Order.Status.TAK and order.taker == user:
# adds a timeout penalty
+ cls.cancel_bond(order.taker_bond)
cls.kick_taker(order)
return True, None
@@ -495,6 +499,7 @@ class Logics():
# Do not gen and kick out the taker if order is older than expiry time
if order.expires_at < timezone.now():
+ cls.cancel_bond(order.taker_bond)
cls.kick_taker(order)
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
@@ -615,9 +620,31 @@ class Logics():
def return_bond(bond):
'''returns a bond'''
- if LNNode.cancel_return_hold_invoice(bond.payment_hash):
+ if bond == None:
+ return
+
+ try:
+ LNNode.cancel_return_hold_invoice(bond.payment_hash)
bond.status = LNPayment.Status.RETNED
return True
+ except Exception as e:
+ if 'invoice already settled' in str(e):
+ bond.status = LNPayment.Status.SETLED
+ return True
+
+ def cancel_bond(bond):
+ '''cancel a bond'''
+ # Same as return bond, but used when the invoice was never accepted
+ if bond == None:
+ return True
+ try:
+ LNNode.cancel_return_hold_invoice(bond.payment_hash)
+ bond.status = LNPayment.Status.CANCEL
+ return True
+ except Exception as e:
+ if 'invoice already settled' in str(e):
+ bond.status = LNPayment.Status.SETLED
+ return True
def pay_buyer_invoice(order):
''' Pay buyer invoice'''
diff --git a/api/management/commands/clean_orders.py b/api/management/commands/clean_orders.py
new file mode 100644
index 00000000..5ea0a71e
--- /dev/null
+++ b/api/management/commands/clean_orders.py
@@ -0,0 +1,41 @@
+from django.core.management.base import BaseCommand, CommandError
+
+import time
+from api.models import Order
+from api.logics import Logics
+from django.utils import timezone
+
+class Command(BaseCommand):
+ help = 'Follows all active hold invoices'
+
+ # def add_arguments(self, parser):
+ # parser.add_argument('debug', nargs='+', type=boolean)
+
+ def handle(self, *args, **options):
+ ''' Continuously checks order expiration times for 1 hour. If order
+ has expires, it calls the logics module for expiration handling.'''
+
+ do_nothing = [Order.Status.DEL, Order.Status.UCA,
+ Order.Status.EXP, Order.Status.FSE,
+ Order.Status.DIS, Order.Status.CCA,
+ Order.Status.PAY, Order.Status.SUC,
+ Order.Status.FAI, Order.Status.MLD,
+ Order.Status.TLD]
+
+ while True:
+ time.sleep(5)
+
+ queryset = Order.objects.exclude(status__in=do_nothing)
+ queryset = queryset.filter(expires_at__lt=timezone.now()) # expires at lower than now
+
+ debug = {}
+ debug['num_expired_orders'] = len(queryset)
+ debug['expired_orders'] = []
+
+ for idx, order in enumerate(queryset):
+ context = str(order)+ " was "+ Order.Status(order.status).label
+ if Logics.order_expires(order): # Order send to expire here
+ debug['expired_orders'].append({idx:context})
+
+ self.stdout.write(str(timezone.now()))
+ self.stdout.write(str(debug))
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index abee1eac..1956ba83 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -1,7 +1,6 @@
-from distutils.log import debug
-from re import L
-from xmlrpc.client import boolean
from django.core.management.base import BaseCommand, CommandError
+
+from django.utils import timezone
from api.lightning.node import LNNode
from decouple import config
from base64 import b64decode
@@ -53,9 +52,9 @@ class Command(BaseCommand):
try:
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
-
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
- # If it fails at finding the invoice it has definetely been canceled.
+
+ # If it fails at finding the invoice it has been canceled.
# On RoboSats DB we make a distinction between cancelled and returned (LND does not)
except:
hold_lnpayment.status = LNPayment.Status.CANCEL
@@ -76,9 +75,10 @@ class Command(BaseCommand):
'old_status': old_status,
'new_status': new_status,
}})
+
debug['time']=time.time()-t0
- self.stdout.write(str(debug))
+ self.stdout.write(str(timezone.now())+str(debug))
\ No newline at end of file
diff --git a/api/tasks.py b/api/tasks.py
index a17c8de9..84744f5f 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -30,50 +30,6 @@ def users_cleansing():
'num_deleted': len(deleted_users),
'deleted_users': deleted_users,
}
-
- return results
-
-
-@shared_task(name="orders_expire")
-def orders_expire(rest_secs):
- '''
- Continuously checks order expiration times for 1 hour. If order
- has expires, it calls the logics module for expiration handling.
- '''
- import time
- from .models import Order
- from .logics import Logics
- from datetime import timedelta
- from django.utils import timezone
-
- now = timezone.now()
- end_time = now + timedelta(minutes=60)
- context = []
-
- while now < end_time:
- queryset = Order.objects.exclude(status=Order.Status.EXP).exclude(status=Order.Status.UCA).exclude(status= Order.Status.CCA)
- queryset = queryset.filter(expires_at__lt=now) # expires at lower than now
-
- for order in queryset:
- try: # TODO Fix, it might fail if returning an already returned bond.
- info = str(order)+ " was "+ Order.Status(order.status).label
- if Logics.order_expires(order): # Order send to expire here
- context.append(info)
- except:
- pass
-
- # Allow for some thread rest.
- time.sleep(rest_secs)
-
- # Update 'now' for a new loop
- now = timezone.now()
-
- results = {
- 'num_expired': len(context),
- 'expired_orders_context': context,
- 'rest_param': rest_secs,
- }
-
return results
@shared_task(name='follow_send_payment')
diff --git a/api/utils.py b/api/utils.py
index 467753b4..1d240b4f 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -5,7 +5,7 @@ import numpy as np
market_cache = {}
-# @ring.dict(market_cache, expire=5) #keeps in cache for 5 seconds
+@ring.dict(market_cache, expire=5) #keeps in cache for 5 seconds
def get_exchange_rates(currencies):
'''
Params: list of currency codes.
diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py
index 00eb5af7..0d1999d2 100644
--- a/robosats/celery/__init__.py
+++ b/robosats/celery/__init__.py
@@ -39,11 +39,6 @@ app.conf.beat_schedule = {
'task': 'cache_external_market_prices',
'schedule': timedelta(seconds=60),
},
- 'orders_expire': { # Continuous order expire removal (1 hour long process, every hour reports results)
- 'task': 'orders_expire',
- 'schedule': timedelta(hours=1),
- 'args': [5], # Rest between checks (secs)
- },
}
app.conf.timezone = 'UTC'
\ No newline at end of file
From 28d18a484215682a6057214762143a28a3d46e66 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Mon, 17 Jan 2022 15:11:41 -0800
Subject: [PATCH 24/37] Add background order updates. Add confirm boxes for
Dispute and Fiat Received
---
api/logics.py | 92 +++++++++++-----
api/management/commands/clean_orders.py | 7 +-
api/management/commands/follow_invoices.py | 79 +++++++++-----
api/models.py | 13 +--
frontend/src/components/OrderPage.js | 2 +-
frontend/src/components/TradeBox.js | 118 +++++++++++++++++----
6 files changed, 231 insertions(+), 80 deletions(-)
diff --git a/api/logics.py b/api/logics.py
index 547cad90..42523e2b 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -1,11 +1,14 @@
-from datetime import time, timedelta
+from datetime import timedelta
from django.utils import timezone
-from .lightning.node import LNNode
+from api.lightning.node import LNNode
-from .models import Order, LNPayment, MarketTick, User, Currency
+from api.models import Order, LNPayment, MarketTick, User, Currency
from decouple import config
+from api.tasks import follow_send_payment
+
import math
+import ast
FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE'))
@@ -140,6 +143,7 @@ class Logics():
cls.settle_bond(order.maker_bond)
cls.settle_bond(order.taker_bond)
+ cls.cancel_escrow(order)
order.status = Order.Status.EXP
order.maker = None
order.taker = None
@@ -152,6 +156,7 @@ class Logics():
if maker_is_seller:
cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond)
+ cls.cancel_escrow(order)
order.status = Order.Status.EXP
order.maker = None
order.taker = None
@@ -161,22 +166,25 @@ class Logics():
# If maker is buyer, settle the taker's bond order goes back to public
else:
cls.settle_bond(order.taker_bond)
+ cls.cancel_escrow(order)
order.status = Order.Status.PUB
order.taker = None
order.taker_bond = None
+ order.trade_escrow = None
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
order.save()
return True
elif order.status == Order.Status.WFI:
# The trade could happen without a buyer invoice. However, this user
- # is likely AFK since he did not submit an invoice; will probably
- # desert the contract as well.
+ # is likely AFK; will probably desert the contract as well.
+
maker_is_buyer = cls.is_buyer(order, order.maker)
# If maker is buyer, settle the bond and order goes to expired
if maker_is_buyer:
cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond)
+ cls.return_escrow(order)
order.status = Order.Status.EXP
order.maker = None
order.taker = None
@@ -186,17 +194,19 @@ class Logics():
# If maker is seller settle the taker's bond, order goes back to public
else:
cls.settle_bond(order.taker_bond)
+ cls.return_escrow(order)
order.status = Order.Status.PUB
order.taker = None
order.taker_bond = None
+ order.trade_escrow = None
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
order.save()
return True
elif order.status == Order.Status.CHA:
# Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute
- # was opened. A seller-scammer could persuade a buyer to not click "fiat sent"
- # as of now, we assume this is a dispute case by default.
+ # was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
+ # sent", we assume this is a dispute case by default.
cls.open_dispute(order)
return True
@@ -219,12 +229,14 @@ class Logics():
def open_dispute(cls, order, user=None):
# Always settle the escrow during a dispute (same as with 'Fiat Sent')
+ # Dispute winner will have to submit a new invoice.
+
if not order.trade_escrow.status == LNPayment.Status.SETLED:
cls.settle_escrow(order)
order.is_disputed = True
order.status = Order.Status.DIS
- order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
order.save()
# User could be None if a dispute is open automatically due to weird expiration.
@@ -235,6 +247,7 @@ class Logics():
profile.save()
return True, None
+
def dispute_statement(order, user, statement):
''' Updates the dispute statements in DB'''
if not order.status == Order.Status.DIS:
@@ -319,16 +332,18 @@ class Logics():
def add_profile_rating(profile, rating):
''' adds a new rating to a user profile'''
+ # TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
profile.total_ratings = profile.total_ratings + 1
latest_ratings = profile.latest_ratings
- if len(latest_ratings) <= 1:
+ if latest_ratings == None:
profile.latest_ratings = [rating]
profile.avg_rating = rating
else:
- latest_ratings = list(latest_ratings).append(rating)
+ latest_ratings = ast.literal_eval(latest_ratings)
+ latest_ratings.append(rating)
profile.latest_ratings = latest_ratings
- profile.avg_rating = sum(latest_ratings) / len(latest_ratings)
+ profile.avg_rating = sum(list(map(int, latest_ratings))) / len(latest_ratings) # Just an average, but it is a list of strings. Has to be converted to int.
profile.save()
@@ -413,15 +428,20 @@ class Logics():
else:
return False, {'bad_request':'You cannot cancel this order'}
+ def publish_order(order):
+ if order.status == Order.Status.WFB:
+ order.status = Order.Status.PUB
+ # With the bond confirmation the order is extended 'public_order_duration' hours
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
+ order.save()
+ return
+
@classmethod
def is_maker_bond_locked(cls, order):
if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
order.maker_bond.status = LNPayment.Status.LOCKED
order.maker_bond.save()
- order.status = Order.Status.PUB
- # With the bond confirmation the order is extended 'public_order_duration' hours
- order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
- order.save()
+ cls.publish_order(order)
return True
return False
@@ -467,13 +487,12 @@ class Logics():
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
@classmethod
- def is_taker_bond_locked(cls, order):
- if order.taker_bond.status == LNPayment.Status.LOCKED:
- return True
+ def finalize_contract(cls, order):
+ ''' When the taker locks the taker_bond
+ the contract is final '''
- if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
- # (This is the last update to "last_satoshis", it becomes the escrow amount next!)
+ # (This is the last update to "last_satoshis", it becomes the escrow amount next)
order.last_satoshis = cls.satoshis_now(order)
order.taker_bond.status = LNPayment.Status.LOCKED
order.taker_bond.save()
@@ -492,6 +511,14 @@ class Logics():
order.status = Order.Status.WF2
order.save()
return True
+
+ @classmethod
+ def is_taker_bond_locked(cls, order):
+ if order.taker_bond.status == LNPayment.Status.LOCKED:
+ return True
+ if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
+ cls.finalize_contract(order)
+ return True
return False
@classmethod
@@ -618,11 +645,17 @@ class Logics():
order.trade_escrow.status = LNPayment.Status.RETNED
return True
+ def cancel_escrow(order):
+ '''returns the trade escrow'''
+ # Same as return escrow, but used when the invoice was never LOCKED
+ if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
+ order.trade_escrow.status = LNPayment.Status.CANCEL
+ return True
+
def return_bond(bond):
'''returns a bond'''
if bond == None:
return
-
try:
LNNode.cancel_return_hold_invoice(bond.payment_hash)
bond.status = LNPayment.Status.RETNED
@@ -631,10 +664,12 @@ class Logics():
if 'invoice already settled' in str(e):
bond.status = LNPayment.Status.SETLED
return True
+ else:
+ raise e
def cancel_bond(bond):
'''cancel a bond'''
- # Same as return bond, but used when the invoice was never accepted
+ # Same as return bond, but used when the invoice was never LOCKED
if bond == None:
return True
try:
@@ -645,11 +680,12 @@ class Logics():
if 'invoice already settled' in str(e):
bond.status = LNPayment.Status.SETLED
return True
+ else:
+ raise e
def pay_buyer_invoice(order):
''' Pay buyer invoice'''
- # TODO ERROR HANDLING
- suceeded, context = LNNode.pay_invoice(order.buyer_invoice.invoice, order.buyer_invoice.num_satoshis)
+ suceeded, context = follow_send_payment(order.buyer_invoice)
return suceeded, context
@classmethod
@@ -703,11 +739,15 @@ class Logics():
# If the trade is finished
if order.status > Order.Status.PAY:
# if maker, rates taker
- if order.maker == user:
+ if order.maker == user and order.maker_rated == False:
cls.add_profile_rating(order.taker.profile, rating)
+ order.maker_rated = True
+ order.save()
# if taker, rates maker
- if order.taker == user:
+ if order.taker == user and order.taker_rated == False:
cls.add_profile_rating(order.maker.profile, rating)
+ order.taker_rated = True
+ order.save()
else:
return False, {'bad_request':'You cannot rate your counterparty yet.'}
diff --git a/api/management/commands/clean_orders.py b/api/management/commands/clean_orders.py
index 5ea0a71e..033784c5 100644
--- a/api/management/commands/clean_orders.py
+++ b/api/management/commands/clean_orders.py
@@ -36,6 +36,7 @@ class Command(BaseCommand):
context = str(order)+ " was "+ Order.Status(order.status).label
if Logics.order_expires(order): # Order send to expire here
debug['expired_orders'].append({idx:context})
-
- self.stdout.write(str(timezone.now()))
- self.stdout.write(str(debug))
+
+ if debug['num_expired_orders'] > 0:
+ self.stdout.write(str(timezone.now()))
+ self.stdout.write(str(debug))
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index 1956ba83..e1edb6ec 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -1,21 +1,25 @@
from django.core.management.base import BaseCommand, CommandError
-from django.utils import timezone
from api.lightning.node import LNNode
+from api.models import LNPayment, Order
+from api.logics import Logics
+
+from django.utils import timezone
+from datetime import timedelta
from decouple import config
from base64 import b64decode
-from api.models import LNPayment
import time
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
class Command(BaseCommand):
'''
- Background: SubscribeInvoices stub iterator would be great to use here
- however it only sends updates when the invoice is OPEN (new) or SETTLED.
+ Background: SubscribeInvoices stub iterator would be great to use here.
+ However, it only sends updates when the invoice is OPEN (new) or SETTLED.
We are very interested on the other two states (CANCELLED and ACCEPTED).
Therefore, this thread (follow_invoices) will iterate over all LNpayment
- objects and do InvoiceLookupV2 to update their state 'live' '''
+ objects and do InvoiceLookupV2 every X seconds to update their state 'live'
+ '''
help = 'Follows all active hold invoices'
@@ -27,10 +31,10 @@ class Command(BaseCommand):
until settled or canceled'''
lnd_state_to_lnpayment_status = {
- 0: LNPayment.Status.INVGEN,
- 1: LNPayment.Status.SETLED,
- 2: LNPayment.Status.CANCEL,
- 3: LNPayment.Status.LOCKED
+ 0: LNPayment.Status.INVGEN, # OPEN
+ 1: LNPayment.Status.SETLED, # SETTLED
+ 2: LNPayment.Status.CANCEL, # CANCELLED
+ 3: LNPayment.Status.LOCKED # ACCEPTED
}
stub = LNNode.invoicesstub
@@ -45,6 +49,7 @@ class Command(BaseCommand):
debug = {}
debug['num_active_invoices'] = len(queryset)
debug['invoices'] = []
+ at_least_one_changed = False
for idx, hold_lnpayment in enumerate(queryset):
old_status = LNPayment.Status(hold_lnpayment.status).label
@@ -56,29 +61,55 @@ class Command(BaseCommand):
# If it fails at finding the invoice it has been canceled.
# On RoboSats DB we make a distinction between cancelled and returned (LND does not)
- except:
- hold_lnpayment.status = LNPayment.Status.CANCEL
- continue
+ except Exception as e:
+ if 'unable to locate invoice' in str(e):
+ hold_lnpayment.status = LNPayment.Status.CANCEL
+ else:
+ self.stdout.write(str(e))
new_status = LNPayment.Status(hold_lnpayment.status).label
# Only save the hold_payments that change (otherwise this function does not scale)
changed = not old_status==new_status
if changed:
+ # self.handle_status_change(hold_lnpayment, old_status)
hold_lnpayment.save()
+ self.update_order_status(hold_lnpayment)
- # Report for debugging
- new_status = LNPayment.Status(hold_lnpayment.status).label
- debug['invoices'].append({idx:{
- 'payment_hash': str(hold_lnpayment.payment_hash),
- 'status_changed': not old_status==new_status,
- 'old_status': old_status,
- 'new_status': new_status,
- }})
+ # Report for debugging
+ new_status = LNPayment.Status(hold_lnpayment.status).label
+ debug['invoices'].append({idx:{
+ 'payment_hash': str(hold_lnpayment.payment_hash),
+ 'old_status': old_status,
+ 'new_status': new_status,
+ }})
- debug['time']=time.time()-t0
-
- self.stdout.write(str(timezone.now())+str(debug))
+ at_least_one_changed = at_least_one_changed or changed
+
+ debug['time']=time.time()-t0
+
+ if at_least_one_changed:
+ self.stdout.write(str(timezone.now()))
+ self.stdout.write(str(debug))
-
\ No newline at end of file
+ def update_order_status(self, lnpayment):
+ ''' Background process following LND hold invoices
+ might catch LNpayments changing status. If they do,
+ the order status might have to change status too.'''
+
+ # If the LNPayment goes to LOCKED (ACCEPTED)
+ if lnpayment.status == LNPayment.Status.LOCKED:
+
+ # It is a maker bond => Publish order.
+ order = lnpayment.order_made
+ if not order == None:
+ Logics.publish_order(order)
+ return
+
+ # It is a taker bond => close contract.
+ order = lnpayment.order_taken
+ if not order == None:
+ if order.status == Order.Status.TAK:
+ Logics.finalize_contract(order)
+ return
\ No newline at end of file
diff --git a/api/models.py b/api/models.py
index 8debaf5c..a65515e7 100644
--- a/api/models.py
+++ b/api/models.py
@@ -146,16 +146,16 @@ class Order(models.Model):
# LNpayments
# Order collateral
- maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ maker_bond = models.OneToOneField(LNPayment, related_name='order_made', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ taker_bond = models.OneToOneField(LNPayment, related_name='order_taken', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
# buyer payment LN invoice
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- # Unused so far. Cancel LN invoices // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
- # maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- # taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ # ratings
+ maker_rated = models.BooleanField(default=False, null=False)
+ taker_rated = models.BooleanField(default=False, null=False)
t_to_expire = {
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
@@ -182,6 +182,7 @@ class Order(models.Model):
def __str__(self):
# Make relational back to ORDER
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
+
@receiver(pre_delete, sender=Order)
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js
index 453867ed..2b05a63b 100644
--- a/frontend/src/components/OrderPage.js
+++ b/frontend/src/components/OrderPage.js
@@ -61,7 +61,7 @@ export default class OrderPage extends Component {
"8": 10000, //'Waiting only for buyer invoice'
"9": 10000, //'Sending fiat - In chatroom'
"10": 15000, //'Fiat sent - In chatroom'
- "11": 300000, //'In dispute'
+ "11": 60000, //'In dispute'
"12": 9999999,//'Collaboratively cancelled'
"13": 120000, //'Sending satoshis to buyer'
"14": 9999999,//'Sucessful trade'
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index 17b3dc26..e8ed00a8 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -1,5 +1,5 @@
import React, { Component } from "react";
-import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon} from "@mui/material"
+import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import QRCode from "react-qr-code";
import Chat from "./Chat"
@@ -37,11 +37,100 @@ export default class TradeBox extends Component {
constructor(props) {
super(props);
this.state = {
+ openConfirmFiatReceived: false,
+ openConfirmDispute: false,
badInvoice: false,
badStatement: false,
}
}
-
+
+ handleClickOpenConfirmDispute = () => {
+ this.setState({openConfirmDispute: true});
+ };
+ handleClickCloseConfirmDispute = () => {
+ this.setState({openConfirmDispute: false});
+ };
+
+ handleClickAgreeDisputeButton=()=>{
+ const requestOptions = {
+ method: 'POST',
+ headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
+ body: JSON.stringify({
+ 'action': "dispute",
+ }),
+ };
+ fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
+ .then((response) => response.json())
+ .then((data) => (this.props.data = data));
+ this.handleClickCloseConfirmDispute();
+ }
+
+ ConfirmDisputeDialog =() =>{
+ return(
+
+ )
+ }
+
+ handleClickOpenConfirmFiatReceived = () => {
+ this.setState({openConfirmFiatReceived: true});
+ };
+ handleClickCloseConfirmFiatReceived = () => {
+ this.setState({openConfirmFiatReceived: false});
+ };
+
+ handleClickTotallyConfirmFiatReceived = () =>{
+ this.handleClickConfirmButton();
+ this.handleClickCloseConfirmFiatReceived();
+ };
+
+ ConfirmFiatReceivedDialog =() =>{
+ return(
+
+ )
+ }
+
showQRInvoice=()=>{
return (
@@ -275,7 +364,7 @@ export default class TradeBox extends Component {
/>
-
+
{this.showBondIsLocked()}
@@ -382,18 +471,7 @@ export default class TradeBox extends Component {
.then((response) => response.json())
.then((data) => (this.props.data = data));
}
-handleClickOpenDisputeButton=()=>{
- const requestOptions = {
- method: 'POST',
- headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
- body: JSON.stringify({
- 'action': "dispute",
- }),
- };
- fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
- .then((response) => response.json())
- .then((data) => (this.props.data = data));
-}
+
handleRatingChange=(e)=>{
const requestOptions = {
method: 'POST',
@@ -419,11 +497,9 @@ handleRatingChange=(e)=>{
}
showFiatReceivedButton(){
- // TODO, show alert and ask for double confirmation (Have you check you received the fiat? Confirming fiat received settles the trade.)
- // Ask for double confirmation.
return(
-
+
)
}
@@ -432,7 +508,7 @@ handleRatingChange=(e)=>{
// TODO, show alert about how opening a dispute might involve giving away personal data and might mean losing the bond. Ask for double confirmation.
return(
-
+
)
}
@@ -487,7 +563,7 @@ handleRatingChange=(e)=>{
-
+
)
@@ -497,6 +573,8 @@ handleRatingChange=(e)=>{
render() {
return (
+
+
Contract Box
From ae3a8cc0f5980298119101a3392cc98e54922cad Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Mon, 17 Jan 2022 15:22:44 -0800
Subject: [PATCH 25/37] Rework active order validation to only a subset of
status
---
api/logics.py | 20 ++++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/api/logics.py b/api/logics.py
index 42523e2b..a47ee882 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -31,17 +31,25 @@ FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
class Logics():
def validate_already_maker_or_taker(user):
- '''Checks if the user is already partipant of an order'''
- queryset = Order.objects.filter(maker=user)
+ '''Validates if a use is already not part of an active order'''
+
+ active_order_status = [Order.Status.WFB, Order.Status.PUB, Order.Status.TAK,
+ Order.Status.WF2, Order.Status.WFE, Order.Status.WFI,
+ Order.Status.CHA, Order.Status.FSE, Order.Status.DIS,
+ Order.Status.WFR]
+ '''Checks if the user is already partipant of an active order'''
+ queryset = Order.objects.filter(maker=user, status__in=active_order_status)
if queryset.exists():
- return False, {'bad_request':'You are already maker of an order'}
- queryset = Order.objects.filter(taker=user)
+ return False, {'bad_request':'You are already maker of an active order'}
+
+ queryset = Order.objects.filter(taker=user, status__in=active_order_status)
if queryset.exists():
- return False, {'bad_request':'You are already taker of an order'}
+ return False, {'bad_request':'You are already taker of an active order'}
+
return True, None
def validate_order_size(order):
- '''Checks if order is withing limits at t0'''
+ '''Validates if order is withing limits in satoshis at t0'''
if order.t0_satoshis > MAX_TRADE:
return False, {'bad_request': 'Your order is too big. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MAX_TRADE)+ ' Sats'}
if order.t0_satoshis < MIN_TRADE:
From a005b3509d407354aeab0b6d44e71cceff790246 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Mon, 17 Jan 2022 16:50:54 -0800
Subject: [PATCH 26/37] Add meta onion-location pointer
---
api/views.py | 23 +++++++++++++++++++++--
frontend/templates/frontend/index.html | 2 +-
frontend/views.py | 5 +++--
3 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/api/views.py b/api/views.py
index 812b635b..b89fc61f 100644
--- a/api/views.py
+++ b/api/views.py
@@ -123,7 +123,7 @@ class OrderView(viewsets.ViewSet):
return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN)
# 3.b If order is between public and WF2
- if order.status >= Order.Status.PUB and order.status > Order.Status.WFB:
+ if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
data['price_now'], data['premium_now'] = Logics.price_and_premium_now(order)
# 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
@@ -136,7 +136,7 @@ class OrderView(viewsets.ViewSet):
elif not data['is_participant'] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK)
- # For participants add positions, nicks and status as a message
+ # For participants add positions, nicks and status as a message and hold invoices status
data['is_buyer'] = Logics.is_buyer(order,request.user)
data['is_seller'] = Logics.is_seller(order,request.user)
data['maker_nick'] = str(order.maker)
@@ -146,6 +146,25 @@ class OrderView(viewsets.ViewSet):
data['is_disputed'] = order.is_disputed
data['ur_nick'] = request.user.username
+ # Add whether hold invoices are LOCKED (ACCEPTED)
+ # Is there a maker bond? If so, True if locked, False otherwise
+ if order.maker_bond:
+ data['maker_locked'] = order.maker_bond.status == LNPayment.Status.LOCKED
+ else:
+ data['maker_locked'] = False
+
+ # Is there a taker bond? If so, True if locked, False otherwise
+ if order.taker_bond:
+ data['taker_locked'] = order.taker_bond.status == LNPayment.Status.LOCKED
+ else:
+ data['taker_locked'] = False
+
+ # Is there an escrow? If so, True if locked, False otherwise
+ if order.trade_escrow:
+ data['escrow_locked'] = order.trade_escrow.status == LNPayment.Status.LOCKED
+ else:
+ data['escrow_locked'] = False
+
# If both bonds are locked, participants can see the final trade amount in sats.
if order.taker_bond:
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
diff --git a/frontend/templates/frontend/index.html b/frontend/templates/frontend/index.html
index 34f11a4a..b23f90c9 100644
--- a/frontend/templates/frontend/index.html
+++ b/frontend/templates/frontend/index.html
@@ -1,7 +1,7 @@
-
+
{% comment %} TODO Add a proper fav icon {% endcomment %}
diff --git a/frontend/views.py b/frontend/views.py
index 5124628e..6b59e3cd 100644
--- a/frontend/views.py
+++ b/frontend/views.py
@@ -1,6 +1,7 @@
from django.shortcuts import render
-
+from decouple import config
# Create your views here.
def index(request, *args, **kwargs):
- return render(request, 'frontend/index.html')
\ No newline at end of file
+ context={'ONION_LOCATION': config('ONION_LOCATION')}
+ return render(request, 'frontend/index.html', context=context)
\ No newline at end of file
From e31bc1adad11b69cd079df2d2ededecb45fb58ea Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 05:20:19 -0800
Subject: [PATCH 27/37] Bug fix invoice follow. Copy user token button.
---
.env-sample | 5 +-
api/admin.py | 4 +-
api/logics.py | 31 +++++---
api/management/commands/follow_invoices.py | 24 +++---
api/models.py | 5 +-
api/tasks.py | 16 +++-
api/utils.py | 2 +-
frontend/src/components/OrderPage.js | 2 +-
frontend/src/components/UserGenPage.js | 86 ++++++++++++++--------
9 files changed, 111 insertions(+), 64 deletions(-)
diff --git a/.env-sample b/.env-sample
index f5612fb4..3eb14fbb 100644
--- a/.env-sample
+++ b/.env-sample
@@ -9,9 +9,12 @@ REDIS_URL=''
# List of market price public APIs. If the currency is available in more than 1 API, will use median price.
MARKET_PRICE_APIS = https://blockchain.info/ticker, https://api.yadio.io/exrates/BTC
-# Host e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
+# Host e.g. robosats.com
HOST_NAME = ''
+# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
+ONION_LOCATION = ''
+
# Trade fee in percentage %
FEE = 0.002
# Bond size in percentage %
diff --git a/api/admin.py b/api/admin.py
index 8507a508..db50098f 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -31,8 +31,8 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('id','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link')
- list_display_links = ('id','concept')
+ list_display = ('id','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link','order_made','order_taken','order_escrow','order_paid')
+ list_display_links = ('id','concept','order_made','order_taken','order_escrow','order_paid')
change_links = ('sender','receiver')
list_filter = ('type','concept','status')
diff --git a/api/logics.py b/api/logics.py
index a47ee882..85ff5de5 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -446,7 +446,9 @@ class Logics():
@classmethod
def is_maker_bond_locked(cls, order):
- if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
+ if order.maker_bond.status == LNPayment.Status.LOCKED:
+ return True
+ elif LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
order.maker_bond.status = LNPayment.Status.LOCKED
order.maker_bond.save()
cls.publish_order(order)
@@ -524,7 +526,7 @@ class Logics():
def is_taker_bond_locked(cls, order):
if order.taker_bond.status == LNPayment.Status.LOCKED:
return True
- if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
+ elif LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
cls.finalize_contract(order)
return True
return False
@@ -574,20 +576,25 @@ class Logics():
order.save()
return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
+ def trade_escrow_received(order):
+ ''' Moves the order forward'''
+ # If status is 'Waiting for both' move to Waiting for invoice
+ if order.status == Order.Status.WF2:
+ order.status = Order.Status.WFI
+ # If status is 'Waiting for invoice' move to Chat
+ elif order.status == Order.Status.WFE:
+ order.status = Order.Status.CHA
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
+ order.save()
@classmethod
def is_trade_escrow_locked(cls, order):
- if LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
+ if order.trade_escrow.status == LNPayment.Status.LOCKED:
+ return True
+ elif LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
order.trade_escrow.status = LNPayment.Status.LOCKED
order.trade_escrow.save()
- # If status is 'Waiting for both' move to Waiting for invoice
- if order.status == Order.Status.WF2:
- order.status = Order.Status.WFI
- # If status is 'Waiting for invoice' move to Chat
- elif order.status == Order.Status.WFE:
- order.status = Order.Status.CHA
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
- order.save()
+ cls.trade_escrow_received(order)
return True
return False
@@ -607,7 +614,7 @@ class Logics():
elif order.trade_escrow.status == LNPayment.Status.INVGEN:
return True, {'escrow_invoice':order.trade_escrow.invoice, 'escrow_satoshis':order.trade_escrow.num_satoshis}
- # If there was no taker_bond object yet, generates one
+ # If there was no taker_bond object yet, generate one
escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked
description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index e1edb6ec..6a590cc7 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -22,6 +22,7 @@ class Command(BaseCommand):
'''
help = 'Follows all active hold invoices'
+ rest = 5 # seconds between consecutive checks for invoice updates
# def add_arguments(self, parser):
# parser.add_argument('debug', nargs='+', type=boolean)
@@ -40,7 +41,7 @@ class Command(BaseCommand):
stub = LNNode.invoicesstub
while True:
- time.sleep(5)
+ time.sleep(self.rest)
# time it for debugging
t0 = time.time()
@@ -95,21 +96,24 @@ class Command(BaseCommand):
def update_order_status(self, lnpayment):
''' Background process following LND hold invoices
- might catch LNpayments changing status. If they do,
+ can catch LNpayments changing status. If they do,
the order status might have to change status too.'''
# If the LNPayment goes to LOCKED (ACCEPTED)
if lnpayment.status == LNPayment.Status.LOCKED:
# It is a maker bond => Publish order.
- order = lnpayment.order_made
- if not order == None:
- Logics.publish_order(order)
+ if not lnpayment.order_made == None:
+ Logics.publish_order(lnpayment.order_made)
return
# It is a taker bond => close contract.
- order = lnpayment.order_taken
- if not order == None:
- if order.status == Order.Status.TAK:
- Logics.finalize_contract(order)
- return
\ No newline at end of file
+ elif not lnpayment.order_taken == None:
+ if lnpayment.order_taken.status == Order.Status.TAK:
+ Logics.finalize_contract(lnpayment.order_taken)
+ return
+
+ # It is a trade escrow => move foward order status.
+ elif not lnpayment.order_escrow == None:
+ Logics.trade_escrow_received(lnpayment.order_escrow)
+ return
\ No newline at end of file
diff --git a/api/models.py b/api/models.py
index a65515e7..2285a928 100644
--- a/api/models.py
+++ b/api/models.py
@@ -151,7 +151,7 @@ class Order(models.Model):
trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
# buyer payment LN invoice
- buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ buyer_invoice = models.OneToOneField(LNPayment, related_name='order_paid', on_delete=models.SET_NULL, null=True, default=None, blank=True)
# ratings
maker_rated = models.BooleanField(default=False, null=False)
@@ -180,9 +180,8 @@ class Order(models.Model):
}
def __str__(self):
- # Make relational back to ORDER
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
-
+
@receiver(pre_delete, sender=Order)
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
diff --git a/api/tasks.py b/api/tasks.py
index 84744f5f..9f1aaaa8 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -40,7 +40,7 @@ def follow_send_payment(lnpayment):
from base64 import b64decode
from api.lightning.node import LNNode
- from api.models import LNPayment
+ from api.models import LNPayment, Order
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
@@ -48,25 +48,37 @@ def follow_send_payment(lnpayment):
request = LNNode.routerrpc.SendPaymentRequest(
payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat,
- timeout_seconds=60)
+ timeout_seconds=60) # time out payment in 60 seconds
+ order = lnpayment.order_paid
for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
if response.status == 0 : # Status 0 'UNKNOWN'
+ # Not sure when this status happens
pass
if response.status == 1 : # Status 1 'IN_FLIGHT'
+ print('IN_FLIGHT')
lnpayment.status = LNPayment.Status.FLIGHT
lnpayment.save()
+ order.status = Order.Status.PAY
+ order.save()
if response.status == 3 : # Status 3 'FAILED'
+ print('FAILED')
lnpayment.status = LNPayment.Status.FAILRO
lnpayment.save()
+ order.status = Order.Status.FAI
+ order.save()
context = LNNode.payment_failure_context[response.failure_reason]
+ # Call for a retry here
return False, context
if response.status == 2 : # Status 2 'SUCCEEDED'
+ print('SUCCEEDED')
lnpayment.status = LNPayment.Status.SUCCED
lnpayment.save()
+ order.status = Order.Status.SUC
+ order.save()
return True, None
@shared_task(name="cache_external_market_prices", ignore_result=True)
diff --git a/api/utils.py b/api/utils.py
index 1d240b4f..4f747657 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -5,7 +5,7 @@ import numpy as np
market_cache = {}
-@ring.dict(market_cache, expire=5) #keeps in cache for 5 seconds
+@ring.dict(market_cache, expire=3) #keeps in cache for 3 seconds
def get_exchange_rates(currencies):
'''
Params: list of currency codes.
diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js
index 2b05a63b..7ca10bda 100644
--- a/frontend/src/components/OrderPage.js
+++ b/frontend/src/components/OrderPage.js
@@ -63,7 +63,7 @@ export default class OrderPage extends Component {
"10": 15000, //'Fiat sent - In chatroom'
"11": 60000, //'In dispute'
"12": 9999999,//'Collaboratively cancelled'
- "13": 120000, //'Sending satoshis to buyer'
+ "13": 3000, //'Sending satoshis to buyer'
"14": 9999999,//'Sucessful trade'
"15": 10000, //'Failed lightning network routing'
"16": 9999999,//'Maker lost dispute'
diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js
index 796e756e..45eb625c 100644
--- a/frontend/src/components/UserGenPage.js
+++ b/frontend/src/components/UserGenPage.js
@@ -1,8 +1,10 @@
import React, { Component } from "react";
-import { Button , Dialog, Grid, Typography, TextField, ButtonGroup} from "@mui/material"
+import { Button , Dialog, Grid, Typography, TextField, ButtonGroup, CircularProgress, IconButton} from "@mui/material"
import { Link } from 'react-router-dom'
import Image from 'material-ui-image'
import InfoDialog from './InfoDialog'
+import ContentCopyIcon from '@mui/icons-material/ContentCopy';
+import ContentCopy from "@mui/icons-material/ContentCopy";
function getCookie(name) {
let cookieValue = null;
@@ -27,6 +29,7 @@ export default class UserGenPage extends Component {
this.state = {
token: this.genBase62Token(34),
openInfo: false,
+ showRobosat: true,
};
this.getGeneratedUser(this.state.token);
}
@@ -53,6 +56,7 @@ export default class UserGenPage extends Component {
shannon_entropy: data.token_shannon_entropy,
bad_request: data.bad_request,
found: data.found,
+ showRobosat:true,
});
});
}
@@ -69,10 +73,12 @@ export default class UserGenPage extends Component {
handleAnotherButtonPressed=(e)=>{
this.delGeneratedUser()
- this.setState({
- token: this.genBase62Token(34),
- });
- this.getGeneratedUser(this.state.token);
+ // this.setState({
+ // showRobosat: false,
+ // token: this.genBase62Token(34),
+ // });
+ // this.getGeneratedUser(this.state.token);
+ window.location.reload();
}
handleChangeToken=(e)=>{
@@ -81,6 +87,7 @@ export default class UserGenPage extends Component {
token: e.target.value,
})
this.getGeneratedUser(e.target.value);
+ this.setState({showRobosat: false})
}
handleClickOpenInfo = () => {
@@ -109,20 +116,26 @@ export default class UserGenPage extends Component {
render() {
return (
-
-
- {this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""}
-
-
-
-
+ : }
{
this.state.found ?
@@ -134,21 +147,30 @@ export default class UserGenPage extends Component {
:
""
}
-
-
+
+
+ navigator.clipboard.writeText(this.state.token)}>
+
+
+
+
-
+
From c58070f4372384a7c7b6da85e26fdab1d43db30b Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 07:23:57 -0800
Subject: [PATCH 28/37] Improve logics around locked bonds. Add frontend
confirm cancel dialog.
---
api/logics.py | 33 ++++-----
api/views.py | 16 ++---
frontend/src/components/BookPage.js | 4 +-
frontend/src/components/OrderPage.js | 96 ++++++++++++++++++--------
frontend/src/components/TradeBox.js | 3 +-
frontend/src/components/UserGenPage.js | 6 +-
6 files changed, 95 insertions(+), 63 deletions(-)
diff --git a/api/logics.py b/api/logics.py
index 85ff5de5..4edb0c09 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -125,7 +125,7 @@ class Logics():
elif order.status == Order.Status.WFB:
order.status = Order.Status.EXP
cls.cancel_bond(order.maker_bond)
- order.maker = None
+ order.maker = None # TODO with the new validate_already_maker_taker there is no need to kick out participants on expired orders.
order.taker = None
order.save()
return True
@@ -175,12 +175,10 @@ class Logics():
else:
cls.settle_bond(order.taker_bond)
cls.cancel_escrow(order)
- order.status = Order.Status.PUB
order.taker = None
order.taker_bond = None
order.trade_escrow = None
- order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
- order.save()
+ cls.publish_order(order)
return True
elif order.status == Order.Status.WFI:
@@ -203,12 +201,10 @@ class Logics():
else:
cls.settle_bond(order.taker_bond)
cls.return_escrow(order)
- order.status = Order.Status.PUB
order.taker = None
order.taker_bond = None
order.trade_escrow = None
- order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
- order.save()
+ cls.publish_order(order)
return True
elif order.status == Order.Status.CHA:
@@ -218,7 +214,8 @@ class Logics():
cls.open_dispute(order)
return True
- def kick_taker(order):
+ @classmethod
+ def kick_taker(cls, order):
''' The taker did not lock the taker_bond. Now he has to go'''
# Add a time out to the taker
profile = order.taker.profile
@@ -226,11 +223,9 @@ class Logics():
profile.save()
# Make order public again
- order.status = Order.Status.PUB
order.taker = None
order.taker_bond = None
- order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
- order.save()
+ cls.publish_order(order)
return True
@classmethod
@@ -417,14 +412,12 @@ class Logics():
# 4.b) When taker cancel after bond (before escrow)
'''The order into cancelled status if maker cancels.'''
- elif order.status > Order.Status.TAK and order.status < Order.Status.CHA and order.taker == user:
+ elif order.status in [Order.Status.WF2, Order.Status.WFE] and order.taker == user:
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_bond(order.taker_bond)
if valid:
order.taker = None
- order.status = Order.Status.PUB
- # order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard.
- order.save()
+ cls.publish_order(order)
return True, None
# 5) When trade collateral has been posted (after escrow)
@@ -437,12 +430,10 @@ class Logics():
return False, {'bad_request':'You cannot cancel this order'}
def publish_order(order):
- if order.status == Order.Status.WFB:
- order.status = Order.Status.PUB
- # With the bond confirmation the order is extended 'public_order_duration' hours
- order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
- order.save()
- return
+ order.status = Order.Status.PUB
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
+ order.save()
+ return
@classmethod
def is_maker_bond_locked(cls, order):
diff --git a/api/views.py b/api/views.py
index b89fc61f..2efcded3 100644
--- a/api/views.py
+++ b/api/views.py
@@ -166,14 +166,14 @@ class OrderView(viewsets.ViewSet):
data['escrow_locked'] = False
# If both bonds are locked, participants can see the final trade amount in sats.
- if order.taker_bond:
- if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
- # Seller sees the amount he sends
- if data['is_seller']:
- data['trade_satoshis'] = order.last_satoshis
- # Buyer sees the amount he receives
- elif data['is_buyer']:
- data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount']
+ # if order.taker_bond:
+ # if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
+ # # Seller sees the amount he sends
+ # if data['is_seller']:
+ # data['trade_satoshis'] = order.last_satoshis
+ # # Buyer sees the amount he receives
+ # elif data['is_buyer']:
+ # data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount']
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice.
if order.status == Order.Status.WFB and data['is_maker']:
diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js
index 8de89617..138e5eb7 100644
--- a/frontend/src/components/BookPage.js
+++ b/frontend/src/components/BookPage.js
@@ -166,10 +166,10 @@ export default class BookPage extends Component {
style: {textAlign:"center"}
}}
onChange={this.handleCurrencyChange}
- >
+ >
{
Object.entries(this.state.currencies_dict)
- .map( ([key, value]) => )
+ .map( ([key, value]) => )
}
diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js
index 7ca10bda..b415a07b 100644
--- a/frontend/src/components/OrderPage.js
+++ b/frontend/src/components/OrderPage.js
@@ -1,5 +1,5 @@
import React, { Component } from "react";
-import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress} from "@mui/material"
+import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown';
import TradeBox from "./TradeBox";
import getFlags from './getFlags'
@@ -11,6 +11,7 @@ import PriceChangeIcon from '@mui/icons-material/PriceChange';
import PaymentsIcon from '@mui/icons-material/Payments';
import MoneyIcon from '@mui/icons-material/Money';
import ArticleIcon from '@mui/icons-material/Article';
+import ContentCopy from "@mui/icons-material/ContentCopy";
function getCookie(name) {
let cookieValue = null;
@@ -43,6 +44,7 @@ export default class OrderPage extends Component {
currencies_dict: {"1":"USD"},
total_secs_expiry: 300,
loading: true,
+ openCancel: false,
};
this.orderId = this.props.match.params.orderId;
this.getCurrencyDict();
@@ -144,8 +146,8 @@ export default class OrderPage extends Component {
countdownRenderer = ({ total, hours, minutes, seconds, completed }) => {
if (completed) {
// Render a completed state
- this.getOrderDetails();
- return null;
+ return ( The order has expired);
+
} else {
var col = 'black'
var fraction_left = (total/1000) / this.state.total_secs_expiry
@@ -218,7 +220,7 @@ export default class OrderPage extends Component {
return code
}
- handleClickCancelOrderButton=()=>{
+ handleClickConfirmCancelButton=()=>{
console.log(this.state)
const requestOptions = {
method: 'POST',
@@ -230,6 +232,64 @@ export default class OrderPage extends Component {
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json())
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
+ this.handleClickCloseConfirmCancelDialog();
+ }
+
+ handleClickOpenConfirmCancelDialog = () => {
+ this.setState({openCancel: true});
+ };
+ handleClickCloseConfirmCancelDialog = () => {
+ this.setState({openCancel: false});
+ };
+
+ CancelDialog =() =>{
+ return(
+
+ )
+ }
+
+ CancelButton = () => {
+
+ // If maker and Waiting for Bond. Or if taker and Waiting for bond.
+ // Simply allow to cancel without showing the cancel dialog.
+ if ((this.state.isMaker & this.state.statusCode == 0) || this.state.isTaker & this.state.statusCode == 3){
+ return(
+
+
+
+ )}
+ // If the order does not yet have an escrow deposited. Show dialog
+ // to confirm forfeiting the bond
+ if (this.state.statusCode < 8){
+ return(
+
+
+
+
+ )}
+
+ // TODO If the escrow is Locked, show the collaborative cancel button.
+
+ // If none of the above do not return a cancel button.
+ return(null)
}
orderBox=()=>{
@@ -346,8 +406,10 @@ export default class OrderPage extends Component {
- {/* Participants cannot see the Back or Take Order buttons */}
- {this.state.isParticipant ? "" :
+ {/* Participants can see the "Cancel" Button, but cannot see the "Back" or "Take Order" buttons */}
+ {this.state.isParticipant ?
+
+ :
<>
@@ -358,27 +420,7 @@ export default class OrderPage extends Component {
>
}
- {/* Makers can cancel before trade escrow deposited (status <9)*/}
- {/* Only free cancel before bond locked (status 0)*/}
- {this.state.isMaker & this.state.statusCode < 9 ?
-
-
-
- :""}
- {this.state.isMaker & this.state.statusCode > 0 & this.state.statusCode < 9 ?
-
- Cancelling now forfeits the maker bond
-
- :""}
-
- {/* Takers can cancel before commiting the bond (status 3)*/}
- {this.state.isTaker & this.state.statusCode == 3 ?
-
-
-
- :""}
-
-
+
)
}
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index e8ed00a8..89f0cd8a 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -183,7 +183,7 @@ export default class TradeBox extends Component {
return (
-
+ Deposit {pn(this.props.data.escrowSatoshis)} Sats as trade collateral
@@ -569,7 +569,6 @@ handleRatingChange=(e)=>{
)
}
-
render() {
return (
diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js
index 45eb625c..48341242 100644
--- a/frontend/src/components/UserGenPage.js
+++ b/frontend/src/components/UserGenPage.js
@@ -150,15 +150,15 @@ export default class UserGenPage extends Component {
navigator.clipboard.writeText(this.state.token)}>
-
+
Date: Tue, 18 Jan 2022 07:45:04 -0800
Subject: [PATCH 29/37] Improve Order Expired behaviour
---
api/logics.py | 13 -------------
api/views.py | 20 ++++++++------------
frontend/src/components/TradeBox.js | 15 ++++++++++++++-
3 files changed, 22 insertions(+), 26 deletions(-)
diff --git a/api/logics.py b/api/logics.py
index 4edb0c09..307a382a 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -125,16 +125,12 @@ class Logics():
elif order.status == Order.Status.WFB:
order.status = Order.Status.EXP
cls.cancel_bond(order.maker_bond)
- order.maker = None # TODO with the new validate_already_maker_taker there is no need to kick out participants on expired orders.
- order.taker = None
order.save()
return True
elif order.status == Order.Status.PUB:
cls.return_bond(order.maker_bond)
order.status = Order.Status.EXP
- order.maker = None
- order.taker = None
order.save()
return True
@@ -153,8 +149,6 @@ class Logics():
cls.settle_bond(order.taker_bond)
cls.cancel_escrow(order)
order.status = Order.Status.EXP
- order.maker = None
- order.taker = None
order.save()
return True
@@ -166,8 +160,6 @@ class Logics():
cls.return_bond(order.taker_bond)
cls.cancel_escrow(order)
order.status = Order.Status.EXP
- order.maker = None
- order.taker = None
order.save()
return True
@@ -192,8 +184,6 @@ class Logics():
cls.return_bond(order.taker_bond)
cls.return_escrow(order)
order.status = Order.Status.EXP
- order.maker = None
- order.taker = None
order.save()
return True
@@ -369,7 +359,6 @@ class Logics():
'''The order never shows up on the book and order
status becomes "cancelled". That's it.'''
if order.status == Order.Status.WFB and order.maker == user:
- order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
@@ -380,7 +369,6 @@ class Logics():
elif order.status == Order.Status.PUB and order.maker == user:
#Settle the maker bond (Maker loses the bond for cancelling public order)
if cls.settle_bond(order.maker_bond):
- order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
@@ -405,7 +393,6 @@ class Logics():
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_bond(order.maker_bond)
if valid:
- order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
diff --git a/api/views.py b/api/views.py
index 2efcded3..10f0381f 100644
--- a/api/views.py
+++ b/api/views.py
@@ -95,10 +95,6 @@ class OrderView(viewsets.ViewSet):
# This is our order.
order = order[0]
- # 1) If order has expired
- if order.status == Order.Status.EXP:
- return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
-
# 2) If order has been cancelled
if order.status == Order.Status.UCA:
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
@@ -166,14 +162,14 @@ class OrderView(viewsets.ViewSet):
data['escrow_locked'] = False
# If both bonds are locked, participants can see the final trade amount in sats.
- # if order.taker_bond:
- # if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
- # # Seller sees the amount he sends
- # if data['is_seller']:
- # data['trade_satoshis'] = order.last_satoshis
- # # Buyer sees the amount he receives
- # elif data['is_buyer']:
- # data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount']
+ if order.taker_bond:
+ if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
+ # Seller sees the amount he sends
+ if data['is_seller']:
+ data['trade_satoshis'] = order.last_satoshis
+ # Buyer sees the amount he receives
+ elif data['is_buyer']:
+ data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount']
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice.
if order.status == Order.Status.WFB and data['is_maker']:
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index 89f0cd8a..1d3c6e72 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -513,6 +513,18 @@ handleRatingChange=(e)=>{
)
}
+ showOrderExpired(){
+ return(
+
+
+
+ The order has expired
+
+
+
+ )
+ }
+
showChat(sendFiatButton, receivedFiatButton, openDisputeButton){
return(
@@ -610,7 +622,8 @@ handleRatingChange=(e)=>{
{/* Trade Finished - TODO Needs more planning */}
{this.props.data.statusCode == 11 ? this.showInDisputeStatement() : ""}
-
+ {/* Order has expired */}
+ {this.props.data.statusCode == 5 ? this.showOrderExpired() : ""}
{/* TODO */}
{/* */}
{/* */}
From 5e0639cfb331d24d7a250ce5c824ffe5f55d9a3e Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 08:57:55 -0800
Subject: [PATCH 30/37] Make payment_hash the primary key of LNpayment model
---
api/admin.py | 5 +--
api/management/commands/follow_invoices.py | 36 +++++++++++++---------
api/models.py | 17 +++++++---
3 files changed, 37 insertions(+), 21 deletions(-)
diff --git a/api/admin.py b/api/admin.py
index db50098f..ff7f2c68 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -22,6 +22,7 @@ class EUserAdmin(UserAdmin):
def avatar_tag(self, obj):
return obj.profile.avatar_tag()
+
@admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
@@ -31,8 +32,8 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('id','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link','order_made','order_taken','order_escrow','order_paid')
- list_display_links = ('id','concept','order_made','order_taken','order_escrow','order_paid')
+ list_display = ('hash','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link','order_made','order_taken','order_escrow','order_paid')
+ list_display_links = ('hash','concept','order_made','order_taken','order_escrow','order_paid')
change_links = ('sender','receiver')
list_filter = ('type','concept','status')
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index 6a590cc7..d00c5e68 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -97,23 +97,31 @@ class Command(BaseCommand):
def update_order_status(self, lnpayment):
''' Background process following LND hold invoices
can catch LNpayments changing status. If they do,
- the order status might have to change status too.'''
+ the order status might have to change too.'''
# If the LNPayment goes to LOCKED (ACCEPTED)
if lnpayment.status == LNPayment.Status.LOCKED:
- # It is a maker bond => Publish order.
- if not lnpayment.order_made == None:
- Logics.publish_order(lnpayment.order_made)
- return
-
- # It is a taker bond => close contract.
- elif not lnpayment.order_taken == None:
- if lnpayment.order_taken.status == Order.Status.TAK:
- Logics.finalize_contract(lnpayment.order_taken)
+ try:
+ # It is a maker bond => Publish order.
+ if not lnpayment.order_made == None:
+ Logics.publish_order(lnpayment.order_made)
return
- # It is a trade escrow => move foward order status.
- elif not lnpayment.order_escrow == None:
- Logics.trade_escrow_received(lnpayment.order_escrow)
- return
\ No newline at end of file
+ # It is a taker bond => close contract.
+ elif not lnpayment.order_taken == None:
+ if lnpayment.order_taken.status == Order.Status.TAK:
+ Logics.finalize_contract(lnpayment.order_taken)
+ return
+
+ # It is a trade escrow => move foward order status.
+ elif not lnpayment.order_escrow == None:
+ Logics.trade_escrow_received(lnpayment.order_escrow)
+ return
+ except Exception as e:
+ self.stdout.write(str(e))
+
+ # TODO If an lnpayment goes from LOCKED to INVGED. Totally weird
+ # halt the order
+ if lnpayment.status == LNPayment.Status.LOCKED:
+ pass
\ No newline at end of file
diff --git a/api/models.py b/api/models.py
index 2285a928..673e25d8 100644
--- a/api/models.py
+++ b/api/models.py
@@ -2,6 +2,7 @@ from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
from django.db.models.signals import post_save, pre_delete
+from django.template.defaultfilters import truncatechars
from django.dispatch import receiver
from django.utils.html import mark_safe
import uuid
@@ -59,15 +60,14 @@ class LNPayment(models.Model):
# payment use details
- id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
# payment info
+ payment_hash = models.CharField(max_length=100, unique=True, default=None, blank=True, primary_key=True)
invoice = models.CharField(max_length=1200, unique=True, null=True, default=None, blank=True) # Some invoices with lots of routing hints might be long
- payment_hash = models.CharField(max_length=100, unique=True, null=True, default=None, blank=True)
preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True)
description = models.CharField(max_length=500, unique=False, null=True, default=None, blank=True)
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
@@ -79,12 +79,19 @@ class LNPayment(models.Model):
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
def __str__(self):
- return (f'LN-{str(self.id)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
+ return (f'LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
class Meta:
verbose_name = 'Lightning payment'
verbose_name_plural = 'Lightning payments'
+ @property
+ def hash(self):
+ # Payment hash is the primary key of LNpayments
+ # However it is too long for the admin panel.
+ # We created a truncated property for display 'hash'
+ return truncatechars(self.payment_hash, 10)
+
class Order(models.Model):
class Types(models.IntegerChoices):
@@ -141,8 +148,8 @@ class Order(models.Model):
# in dispute
is_disputed = models.BooleanField(default=False, null=False)
- maker_statement = models.TextField(max_length=5000, unique=True, null=True, default=None, blank=True)
- taker_statement = models.TextField(max_length=5000, unique=True, null=True, default=None, blank=True)
+ maker_statement = models.TextField(max_length=5000, null=True, default=None, blank=True)
+ taker_statement = models.TextField(max_length=5000, null=True, default=None, blank=True)
# LNpayments
# Order collateral
From ce9845cbc23073836a54c49953496db64edb94a1 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 09:42:45 -0800
Subject: [PATCH 31/37] Users stay logged in when re-entering home if there is
an active order or is an old account
---
README.md | 3 +++
api/views.py | 23 +++++++++++++++--------
2 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 8fc2e3b4..58af713d 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,9 @@ RoboSats is a simple and private way to exchange bitcoin for national currencies
## Contribute to the Robotic Satoshis Open Source Project
See [CONTRIBUTING.md](CONTRIBUTING.md)
+## Original idea
+A simple, custody-minimized, lightning exchange using hold invoices is heavily inspired by [P2PLNBOT](https://github.com/grunch/p2plnbot) by @grunch
+
## License
The Robotic Satoshis Open Source Project is released under the terms of the AGPL3.0 license. See [LICENSE](LICENSE) for more details.
diff --git a/api/views.py b/api/views.py
index 10f0381f..3683fe7c 100644
--- a/api/views.py
+++ b/api/views.py
@@ -315,14 +315,21 @@ class UserView(APIView):
Response with Avatar and Nickname.
'''
- # if request.user.id:
- # context = {}
- # context['nickname'] = request.user.username
- # participant = not Logics.validate_already_maker_or_taker(request.user)
- # context['bad_request'] = f'You are already logged in as {request.user}'
- # if participant:
- # context['bad_request'] = f'You are already logged in as as {request.user} and have an active order'
- # return Response(context,status.HTTP_200_OK)
+ # If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
+ if request.user.is_authenticated:
+ context = {'nickname': request.user.username}
+ not_participant, _ = Logics.validate_already_maker_or_taker(request.user)
+
+ # Does not allow this 'mistake' if an active order
+ if not not_participant:
+ context['bad_request'] = f'You are already logged in as {request.user} and have an active order'
+ return Response(context, status.HTTP_400_BAD_REQUEST)
+
+ # Does not allow this 'mistake' if the last login was sometime ago (5 minutes)
+ if request.user.last_login < timezone.now() - timedelta(minutes=5):
+ context['bad_request'] = f'You are already logged in as {request.user}'
+ return Response(context, status.HTTP_400_BAD_REQUEST)
+
token = request.GET.get(self.lookup_url_kwarg)
From f010fe9bb081d50a662649899a49171cc29e9b68 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 09:52:48 -0800
Subject: [PATCH 32/37] Fix today active robots
---
api/tasks.py | 6 +++---
api/views.py | 14 +++++---------
frontend/src/components/BottomBar.js | 16 ++++++++--------
robosats/celery/__init__.py | 4 ++--
4 files changed, 18 insertions(+), 22 deletions(-)
diff --git a/api/tasks.py b/api/tasks.py
index 9f1aaaa8..82da436a 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -11,12 +11,12 @@ def users_cleansing():
from datetime import timedelta
from django.utils import timezone
- # Users who's last login has not been in the last 12 hours
- active_time_range = (timezone.now() - timedelta(hours=12), timezone.now())
+ # Users who's last login has not been in the last 6 hours
+ active_time_range = (timezone.now() - timedelta(hours=6), timezone.now())
queryset = User.objects.filter(~Q(last_login__range=active_time_range))
queryset = queryset.filter(is_staff=False) # Do not delete staff users
- # And do not have an active trade or any pass finished trade.
+ # And do not have an active trade or any past contract.
deleted_users = []
for user in queryset:
if not user.profile.total_contracts == 0:
diff --git a/api/views.py b/api/views.py
index 3683fe7c..d1fb2427 100644
--- a/api/views.py
+++ b/api/views.py
@@ -329,7 +329,6 @@ class UserView(APIView):
if request.user.last_login < timezone.now() - timedelta(minutes=5):
context['bad_request'] = f'You are already logged in as {request.user}'
return Response(context, status.HTTP_400_BAD_REQUEST)
-
token = request.GET.get(self.lookup_url_kwarg)
@@ -345,10 +344,10 @@ class UserView(APIView):
context['bad_request'] = 'The token does not have enough entropy'
return Response(context, status=status.HTTP_400_BAD_REQUEST)
- # Hashes the token, only 1 iteration. Maybe more is better.
+ # Hash the token, only 1 iteration.
hash = hashlib.sha256(str.encode(token)).hexdigest()
- # Generate nickname
+ # Generate nickname deterministically
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
context['nickname'] = nickname
@@ -357,13 +356,12 @@ class UserView(APIView):
rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
# Does not replace image if existing (avoid re-avatar in case of nick collusion)
-
image_path = avatar_path.joinpath(nickname+".png")
if not image_path.exists():
with open(image_path, "wb") as f:
rh.img.save(f, format="png")
- # Create new credentials and log in if nickname is new
+ # Create new credentials and login if nickname is new
if len(User.objects.filter(username=nickname)) == 0:
User.objects.create_user(username=nickname, password=token, is_staff=False)
user = authenticate(request, username=nickname, password=token)
@@ -451,12 +449,10 @@ class InfoView(ListAPIView):
context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB))
# Number of active users (logged in in last 30 minutes)
- active_user_time_range = (timezone.now() - timedelta(minutes=120), timezone.now())
- context['num_active_robotsats'] = len(User.objects.filter(last_login__range=active_user_time_range))
+ today = datetime.today()
+ context['active_robots_today'] = len(User.objects.filter(last_login__day=today.day))
# Compute average premium and volume of today
- today = datetime.today()
-
queryset = MarketTick.objects.filter(timestamp__day=today.day)
if not len(queryset) == 0:
weighted_premiums = []
diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js
index 5516ee36..df924fa6 100644
--- a/frontend/src/components/BottomBar.js
+++ b/frontend/src/components/BottomBar.js
@@ -21,12 +21,12 @@ export default class BottomBar extends Component {
this.state = {
openStatsForNerds: false,
openCommuniy: false,
- num_public_buy_orders: null,
- num_active_robotsats: null,
- num_public_sell_orders: null,
- fee: null,
- today_avg_nonkyc_btc_premium: null,
- today_total_volume: null,
+ num_public_buy_orders: 0,
+ num_public_sell_orders: 0,
+ active_robots_today: 0,
+ fee: 0,
+ today_avg_nonkyc_btc_premium: 0,
+ today_total_volume: 0,
};
this.getInfo();
}
@@ -200,8 +200,8 @@ export default class BottomBar extends Component {
+ primary={this.state.active_robots_today}
+ secondary="Today Active Robots" />
diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py
index 0d1999d2..a176b226 100644
--- a/robosats/celery/__init__.py
+++ b/robosats/celery/__init__.py
@@ -31,9 +31,9 @@ app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler'
# Configure the periodic tasks
app.conf.beat_schedule = {
- 'users-cleansing': { # Cleans abandoned users every 6 hours
+ 'users-cleansing': { # Cleans abandoned users every hour
'task': 'users_cleansing',
- 'schedule': timedelta(hours=6),
+ 'schedule': timedelta(hours=1),
},
'cache-market-prices': { # Cache market prices every minutes for now.
'task': 'cache_external_market_prices',
From 980a38552823551161424e07520b168977f97fb9 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 10:24:45 -0800
Subject: [PATCH 33/37] Premium percentile computing
---
api/utils.py | 22 ++++++++++++++++++++--
api/views.py | 10 +++++-----
frontend/src/components/TradeBox.js | 5 +++--
3 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/api/utils.py b/api/utils.py
index 4f747657..e138d322 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -3,6 +3,8 @@ import requests, ring, os
from decouple import config
import numpy as np
+from api.models import Order
+
market_cache = {}
@ring.dict(market_cache, expire=3) #keeps in cache for 3 seconds
@@ -49,7 +51,6 @@ def get_exchange_rates(currencies):
return median_rates.tolist()
lnd_v_cache = {}
-
@ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds
def get_lnd_version():
@@ -59,7 +60,6 @@ def get_lnd_version():
return lnd_version
robosats_commit_cache = {}
-
@ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats():
@@ -67,4 +67,22 @@ def get_commit_robosats():
lnd_version = stream.read()
return lnd_version
+
+premium_percentile = {}
+@ring.dict(premium_percentile, expire=300)
+def compute_premium_percentile(order):
+
+ queryset = Order.objects.filter(currency=order.currency, status=Order.Status.PUB)
+
+ print(len(queryset))
+ if len(queryset) <= 1:
+ return 0.5
+
+ order_rate = float(order.last_satoshis) / float(order.amount)
+ rates = []
+ for similar_order in queryset:
+ rates.append(float(similar_order.last_satoshis) / float(similar_order.amount))
+
+ rates = np.array(rates)
+ return round(np.sum(rates < order_rate) / len(rates),2)
diff --git a/api/views.py b/api/views.py
index d1fb2427..6e529a40 100644
--- a/api/views.py
+++ b/api/views.py
@@ -11,7 +11,7 @@ from django.contrib.auth.models import User
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
from .models import LNPayment, MarketTick, Order, Currency
from .logics import Logics
-from .utils import get_lnd_version, get_commit_robosats
+from .utils import get_lnd_version, get_commit_robosats, compute_premium_percentile
from .nick_generator.nick_generator import NickGenerator
from robohash import Robohash
@@ -125,7 +125,7 @@ class OrderView(viewsets.ViewSet):
# 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
if data['is_maker'] and order.status == Order.Status.PUB:
data['robots_in_book'] = None # TODO
- data['premium_percentile'] = None # TODO
+ data['premium_percentile'] = compute_premium_percentile(order)
data['num_similar_orders'] = len(Order.objects.filter(currency=order.currency, status=Order.Status.PUB))
# 4) Non participants can view details (but only if PUB)
@@ -326,9 +326,9 @@ class UserView(APIView):
return Response(context, status.HTTP_400_BAD_REQUEST)
# Does not allow this 'mistake' if the last login was sometime ago (5 minutes)
- if request.user.last_login < timezone.now() - timedelta(minutes=5):
- context['bad_request'] = f'You are already logged in as {request.user}'
- return Response(context, status.HTTP_400_BAD_REQUEST)
+ # if request.user.last_login < timezone.now() - timedelta(minutes=5):
+ # context['bad_request'] = f'You are already logged in as {request.user}'
+ # return Response(context, status.HTTP_400_BAD_REQUEST)
token = request.GET.get(self.lookup_url_kwarg)
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index 1d3c6e72..d0bfab8d 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -256,7 +256,7 @@ export default class TradeBox extends Component {
-
+
@@ -272,7 +272,8 @@ export default class TradeBox extends Component {
-
+
From a0b38d831f74ca078d76a1bc1ddf605a2c98b9a4 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 10:40:56 -0800
Subject: [PATCH 34/37] Fix bug penalizing a non existing taker
---
api/logics.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/api/logics.py b/api/logics.py
index 307a382a..6944aa03 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -45,7 +45,6 @@ class Logics():
queryset = Order.objects.filter(taker=user, status__in=active_order_status)
if queryset.exists():
return False, {'bad_request':'You are already taker of an active order'}
-
return True, None
def validate_order_size(order):
@@ -208,9 +207,10 @@ class Logics():
def kick_taker(cls, order):
''' The taker did not lock the taker_bond. Now he has to go'''
# Add a time out to the taker
- profile = order.taker.profile
- profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
- profile.save()
+ if order.taker:
+ profile = order.taker.profile
+ profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
+ profile.save()
# Make order public again
order.taker = None
From 7a6c29fe642eaa7fbc47befabf4cab9a48d63485 Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 12:52:10 -0800
Subject: [PATCH 35/37] Silence initialization of nick generator
---
api/management/commands/follow_invoices.py | 12 ++++++++----
api/nick_generator/nick_generator.py | 15 ++++++++-------
2 files changed, 16 insertions(+), 11 deletions(-)
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index d00c5e68..4248ff1d 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -59,12 +59,16 @@ class Command(BaseCommand):
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
-
- # If it fails at finding the invoice it has been canceled.
- # On RoboSats DB we make a distinction between cancelled and returned (LND does not)
+
except Exception as e:
+ # If it fails at finding the invoice it has been canceled.
+ # On RoboSats DB we make a distinction between cancelled and returned (LND does not)
if 'unable to locate invoice' in str(e):
hold_lnpayment.status = LNPayment.Status.CANCEL
+ # LND restarted.
+ if 'wallet locked, unlock it to enable full RPC access' in str(e):
+ self.stdout.write(str(timezone.now())+':: Wallet Locked')
+ # Other write to logs
else:
self.stdout.write(str(e))
@@ -121,7 +125,7 @@ class Command(BaseCommand):
except Exception as e:
self.stdout.write(str(e))
- # TODO If an lnpayment goes from LOCKED to INVGED. Totally weird
+ # TODO If a lnpayment goes from LOCKED to INVGED. Totally weird
# halt the order
if lnpayment.status == LNPayment.Status.LOCKED:
pass
\ No newline at end of file
diff --git a/api/nick_generator/nick_generator.py b/api/nick_generator/nick_generator.py
index 29e5e84a..93371619 100755
--- a/api/nick_generator/nick_generator.py
+++ b/api/nick_generator/nick_generator.py
@@ -41,13 +41,14 @@ class NickGenerator:
else:
raise ValueError("Language not implemented.")
- print(
- f"{lang} SHA256 Nick Generator initialized with:"
- + f"\nUp to {len(adverbs)} adverbs."
- + f"\nUp to {len(adjectives)} adjectives."
- + f"\nUp to {len(nouns)} nouns."
- + f"\nUp to {max_num+1} numerics.\n"
- )
+ if verbose:
+ print(
+ f"{lang} SHA256 Nick Generator initialized with:"
+ + f"\nUp to {len(adverbs)} adverbs."
+ + f"\nUp to {len(adjectives)} adjectives."
+ + f"\nUp to {len(nouns)} nouns."
+ + f"\nUp to {max_num+1} numerics.\n"
+ )
self.use_adv = use_adv
self.use_adj = use_adj
From 285f85aaf21ebea57929357633fd803f2416b5ba Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Tue, 18 Jan 2022 14:17:41 -0800
Subject: [PATCH 36/37] Add check if attribute order exists when triggering an
order status
---
api/logics.py | 1 -
api/management/commands/follow_invoices.py | 9 ++++-----
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/api/logics.py b/api/logics.py
index 6944aa03..7059ea10 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -520,7 +520,6 @@ class Logics():
# Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting.
if order.taker_bond:
- # Check if status is INVGEN and still not expired
if cls.is_taker_bond_locked(order):
return False, None
elif order.taker_bond.status == LNPayment.Status.INVGEN:
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index 4248ff1d..6ab3b6c1 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -59,7 +59,7 @@ class Command(BaseCommand):
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
-
+
except Exception as e:
# If it fails at finding the invoice it has been canceled.
# On RoboSats DB we make a distinction between cancelled and returned (LND does not)
@@ -105,21 +105,20 @@ class Command(BaseCommand):
# If the LNPayment goes to LOCKED (ACCEPTED)
if lnpayment.status == LNPayment.Status.LOCKED:
-
try:
# It is a maker bond => Publish order.
- if not lnpayment.order_made == None:
+ if hasattr(lnpayment, 'order_made' ):
Logics.publish_order(lnpayment.order_made)
return
# It is a taker bond => close contract.
- elif not lnpayment.order_taken == None:
+ elif hasattr(lnpayment, 'order_taken' ):
if lnpayment.order_taken.status == Order.Status.TAK:
Logics.finalize_contract(lnpayment.order_taken)
return
# It is a trade escrow => move foward order status.
- elif not lnpayment.order_escrow == None:
+ elif hasattr(lnpayment, 'order_escrow' ):
Logics.trade_escrow_received(lnpayment.order_escrow)
return
except Exception as e:
From bfa0cd84d1284631e3e7cba5a14a7821ec68151c Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi
Date: Wed, 19 Jan 2022 05:32:54 -0800
Subject: [PATCH 37/37] Update readme
---
README.md | 34 +-
api/management/commands/follow_invoices.py | 4 +-
frontend/package-lock.json | 576 ---------------------
frontend/package.json | 1 -
frontend/src/components/HomePage.js | 3 -
frontend/src/components/InfoDialog.js | 12 +-
frontend/src/components/InfoPageMd.js | 35 --
frontend/src/components/TradeBox.js | 6 +-
frontend/static/assets/info.md | 81 ---
setup.md | 1 -
10 files changed, 37 insertions(+), 716 deletions(-)
delete mode 100644 frontend/src/components/InfoPageMd.js
delete mode 100644 frontend/static/assets/info.md
diff --git a/README.md b/README.md
index 58af713d..71503250 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,45 @@
-# RoboSats: Buy and sell Satoshis Privately.
-## What is RoboSats?
+## RoboSats - Buy and sell Satoshis Privately.
+[![release](https://img.shields.io/badge/release-v0.1.0%20MVP-orange)](https://github.com/Reckless-Satoshi/robosats/releases)
+[![AGPL-3.0 license](https://img.shields.io/badge/license-AGPL--3.0-blue)](https://github.com/Reckless-Satoshi/robosats/blob/main/LICENSE)
+[![Telegram](https://img.shields.io/badge/chat-telegram-brightgreen)](https://t.me/robosats)
-RoboSats is a simple and private way to exchange bitcoin for national currencies. Robosats aims to simplify the peer-to-peer experience and uses lightning hodl invoices to minimize the trust needed to trade. In addition, your Robotic Satoshi will help you stick to best privacy practices.
+RoboSats is a simple and private way to exchange bitcoin for national currencies. Robosats simplifies the peer-to-peer user experience and uses lightning hold invoices to minimize custody and trust requirements. The deterministically generated avatars help users stick to best privacy practices.
## Try it out
**Bitcoin mainnet:**
-- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Not active)
-- Url: robosats.com (Registered - Not active)
+- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Coming soon)
+- Url: robosats.com (Coming soon)
- Version: v0.0.0 (Last stable)
**Bitcoin testnet:**
- Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion (Active - On Dev Node)
-- Url: testnet.robosats.com (Registered - Not active)
-- Commit height: v0.0.0 Latest commit.
+- Url: testnet.robosats.com (Coming soon)
+- Commit height: Latest commit.
*Always use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.*
+## How it works
+
+Alice wants to buy satoshis privately:
+1. Alice generates an avatar (AdequateAlice01) using her private random token.
+2. Alice stores safely the token in case she needs to recover AdequateAlice01 in the future.
+3. Alice makes a new order and locks a small hold invoice to publish it (maker bond).
+4. Bob wants to sell satoshis, sees Alice's order in the book and takes it.
+5. Bob scans a small hold invoice as his taker bond. The contract is final.
+6. Bob posts the traded satoshis with a hold invoice. While Alice submits her payout invoice.
+7. On a private chat, Bob tells Alice how to send him fiat.
+8. Alice pays Bob, then they confirm the fiat has been sent and received.
+9. Bob's trade hold invoice is charged and the satoshis are sent to Alice.
+10. Bob and Alice's bonds return automatically, since they complied by the rules.
+11. The bonds would be charged (lost) in case of unilateral cancellation or cheating (lost dispute).
+
+
## Contribute to the Robotic Satoshis Open Source Project
See [CONTRIBUTING.md](CONTRIBUTING.md)
## Original idea
-A simple, custody-minimized, lightning exchange using hold invoices is heavily inspired by [P2PLNBOT](https://github.com/grunch/p2plnbot) by @grunch
+The concept of a simple custody-minimized lightning exchange using hold invoices is heavily inspired by [P2PLNBOT](https://github.com/grunch/p2plnbot) by @grunch
## License
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index 6ab3b6c1..52253a2a 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -63,10 +63,10 @@ class Command(BaseCommand):
except Exception as e:
# If it fails at finding the invoice it has been canceled.
# On RoboSats DB we make a distinction between cancelled and returned (LND does not)
- if 'unable to locate invoice' in str(e):
+ if 'unable to locate invoice' in str(e):
hold_lnpayment.status = LNPayment.Status.CANCEL
# LND restarted.
- if 'wallet locked, unlock it to enable full RPC access' in str(e):
+ if 'wallet locked, unlock it' in str(e):
self.stdout.write(str(timezone.now())+':: Wallet Locked')
# Other write to logs
else:
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index d16fcc61..62e583c3 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -2125,14 +2125,6 @@
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
- "@types/debug": {
- "version": "4.1.7",
- "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
- "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
- "requires": {
- "@types/ms": "*"
- }
- },
"@types/eslint": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz",
@@ -2167,14 +2159,6 @@
"@types/node": "*"
}
},
- "@types/hast": {
- "version": "2.3.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
- "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
- "requires": {
- "@types/unist": "*"
- }
- },
"@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -2202,24 +2186,6 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
- "@types/mdast": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
- "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==",
- "requires": {
- "@types/unist": "*"
- }
- },
- "@types/mdurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
- "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA=="
- },
- "@types/ms": {
- "version": "0.7.31",
- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
- "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
- },
"@types/node": {
"version": "17.0.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.6.tgz",
@@ -2273,11 +2239,6 @@
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
- "@types/unist": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
- "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
- },
"@types/yargs": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@@ -2769,11 +2730,6 @@
"babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0"
}
},
- "bail": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
- "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="
- },
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3017,11 +2973,6 @@
}
}
},
- "character-entities": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.1.tgz",
- "integrity": "sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ=="
- },
"chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@@ -3145,11 +3096,6 @@
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
},
- "comma-separated-tokens": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz",
- "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg=="
- },
"command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
@@ -3373,14 +3319,6 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
- "decode-named-character-reference": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz",
- "integrity": "sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w==",
- "requires": {
- "character-entities": "^2.0.0"
- }
- },
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@@ -3454,21 +3392,11 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
- "dequal": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
- "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
- },
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
- "diff": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
- "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
- },
"dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@@ -3786,11 +3714,6 @@
}
}
},
- "extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
- },
"extend-shallow": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@@ -4131,11 +4054,6 @@
}
}
},
- "hast-util-whitespace": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz",
- "integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg=="
- },
"hermes-engine": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.9.0.tgz",
@@ -4262,11 +4180,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
- "inline-style-parser": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
- "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
- },
"interpret": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
@@ -4390,11 +4303,6 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
- "is-plain-obj": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz",
- "integrity": "sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw=="
- },
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -5140,78 +5048,11 @@
"prop-types": "^15.5.8"
}
},
- "mdast-util-definitions": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz",
- "integrity": "sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ==",
- "requires": {
- "@types/mdast": "^3.0.0",
- "@types/unist": "^2.0.0",
- "unist-util-visit": "^3.0.0"
- },
- "dependencies": {
- "unist-util-visit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz",
- "integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==",
- "requires": {
- "@types/unist": "^2.0.0",
- "unist-util-is": "^5.0.0",
- "unist-util-visit-parents": "^4.0.0"
- }
- }
- }
- },
- "mdast-util-from-markdown": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz",
- "integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==",
- "requires": {
- "@types/mdast": "^3.0.0",
- "@types/unist": "^2.0.0",
- "decode-named-character-reference": "^1.0.0",
- "mdast-util-to-string": "^3.1.0",
- "micromark": "^3.0.0",
- "micromark-util-decode-numeric-character-reference": "^1.0.0",
- "micromark-util-decode-string": "^1.0.0",
- "micromark-util-normalize-identifier": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.0",
- "unist-util-stringify-position": "^3.0.0",
- "uvu": "^0.5.0"
- }
- },
- "mdast-util-to-hast": {
- "version": "11.3.0",
- "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-11.3.0.tgz",
- "integrity": "sha512-4o3Cli3hXPmm1LhB+6rqhfsIUBjnKFlIUZvudaermXB+4/KONdd/W4saWWkC+LBLbPMqhFSSTSRgafHsT5fVJw==",
- "requires": {
- "@types/hast": "^2.0.0",
- "@types/mdast": "^3.0.0",
- "@types/mdurl": "^1.0.0",
- "mdast-util-definitions": "^5.0.0",
- "mdurl": "^1.0.0",
- "unist-builder": "^3.0.0",
- "unist-util-generated": "^2.0.0",
- "unist-util-position": "^4.0.0",
- "unist-util-visit": "^4.0.0"
- }
- },
- "mdast-util-to-string": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz",
- "integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA=="
- },
"mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
},
- "mdurl": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
- "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
- },
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -5680,218 +5521,6 @@
"nullthrows": "^1.1.1"
}
},
- "micromark": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.10.tgz",
- "integrity": "sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==",
- "requires": {
- "@types/debug": "^4.0.0",
- "debug": "^4.0.0",
- "decode-named-character-reference": "^1.0.0",
- "micromark-core-commonmark": "^1.0.1",
- "micromark-factory-space": "^1.0.0",
- "micromark-util-character": "^1.0.0",
- "micromark-util-chunked": "^1.0.0",
- "micromark-util-combine-extensions": "^1.0.0",
- "micromark-util-decode-numeric-character-reference": "^1.0.0",
- "micromark-util-encode": "^1.0.0",
- "micromark-util-normalize-identifier": "^1.0.0",
- "micromark-util-resolve-all": "^1.0.0",
- "micromark-util-sanitize-uri": "^1.0.0",
- "micromark-util-subtokenize": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.1",
- "uvu": "^0.5.0"
- }
- },
- "micromark-core-commonmark": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz",
- "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==",
- "requires": {
- "decode-named-character-reference": "^1.0.0",
- "micromark-factory-destination": "^1.0.0",
- "micromark-factory-label": "^1.0.0",
- "micromark-factory-space": "^1.0.0",
- "micromark-factory-title": "^1.0.0",
- "micromark-factory-whitespace": "^1.0.0",
- "micromark-util-character": "^1.0.0",
- "micromark-util-chunked": "^1.0.0",
- "micromark-util-classify-character": "^1.0.0",
- "micromark-util-html-tag-name": "^1.0.0",
- "micromark-util-normalize-identifier": "^1.0.0",
- "micromark-util-resolve-all": "^1.0.0",
- "micromark-util-subtokenize": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.1",
- "uvu": "^0.5.0"
- }
- },
- "micromark-factory-destination": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz",
- "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==",
- "requires": {
- "micromark-util-character": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.0"
- }
- },
- "micromark-factory-label": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz",
- "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==",
- "requires": {
- "micromark-util-character": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.0",
- "uvu": "^0.5.0"
- }
- },
- "micromark-factory-space": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz",
- "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==",
- "requires": {
- "micromark-util-character": "^1.0.0",
- "micromark-util-types": "^1.0.0"
- }
- },
- "micromark-factory-title": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz",
- "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==",
- "requires": {
- "micromark-factory-space": "^1.0.0",
- "micromark-util-character": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.0",
- "uvu": "^0.5.0"
- }
- },
- "micromark-factory-whitespace": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz",
- "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==",
- "requires": {
- "micromark-factory-space": "^1.0.0",
- "micromark-util-character": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.0"
- }
- },
- "micromark-util-character": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz",
- "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==",
- "requires": {
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.0"
- }
- },
- "micromark-util-chunked": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz",
- "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==",
- "requires": {
- "micromark-util-symbol": "^1.0.0"
- }
- },
- "micromark-util-classify-character": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz",
- "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==",
- "requires": {
- "micromark-util-character": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.0"
- }
- },
- "micromark-util-combine-extensions": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz",
- "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==",
- "requires": {
- "micromark-util-chunked": "^1.0.0",
- "micromark-util-types": "^1.0.0"
- }
- },
- "micromark-util-decode-numeric-character-reference": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz",
- "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==",
- "requires": {
- "micromark-util-symbol": "^1.0.0"
- }
- },
- "micromark-util-decode-string": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz",
- "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==",
- "requires": {
- "decode-named-character-reference": "^1.0.0",
- "micromark-util-character": "^1.0.0",
- "micromark-util-decode-numeric-character-reference": "^1.0.0",
- "micromark-util-symbol": "^1.0.0"
- }
- },
- "micromark-util-encode": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz",
- "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA=="
- },
- "micromark-util-html-tag-name": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz",
- "integrity": "sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g=="
- },
- "micromark-util-normalize-identifier": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz",
- "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==",
- "requires": {
- "micromark-util-symbol": "^1.0.0"
- }
- },
- "micromark-util-resolve-all": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz",
- "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==",
- "requires": {
- "micromark-util-types": "^1.0.0"
- }
- },
- "micromark-util-sanitize-uri": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz",
- "integrity": "sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==",
- "requires": {
- "micromark-util-character": "^1.0.0",
- "micromark-util-encode": "^1.0.0",
- "micromark-util-symbol": "^1.0.0"
- }
- },
- "micromark-util-subtokenize": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz",
- "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==",
- "requires": {
- "micromark-util-chunked": "^1.0.0",
- "micromark-util-symbol": "^1.0.0",
- "micromark-util-types": "^1.0.0",
- "uvu": "^0.5.0"
- }
- },
- "micromark-util-symbol": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz",
- "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ=="
- },
- "micromark-util-types": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz",
- "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w=="
- },
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -5974,11 +5603,6 @@
"minimist": "^1.2.5"
}
},
- "mri": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
- "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
- },
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -6473,11 +6097,6 @@
}
}
},
- "property-information": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
- "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
- },
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -6562,27 +6181,6 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
- "react-markdown": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-7.1.2.tgz",
- "integrity": "sha512-ibMcc0EbfmbwApqJD8AUr0yls8BSrKzIbHaUsPidQljxToCqFh34nwtu3CXNEItcVJNzpjDHrhK8A+MAh2JW3A==",
- "requires": {
- "@types/hast": "^2.0.0",
- "@types/unist": "^2.0.0",
- "comma-separated-tokens": "^2.0.0",
- "hast-util-whitespace": "^2.0.0",
- "prop-types": "^15.0.0",
- "property-information": "^6.0.0",
- "react-is": "^17.0.0",
- "remark-parse": "^10.0.0",
- "remark-rehype": "^9.0.0",
- "space-separated-tokens": "^2.0.0",
- "style-to-object": "^0.3.0",
- "unified": "^10.0.0",
- "unist-util-visit": "^4.0.0",
- "vfile": "^5.0.0"
- }
- },
"react-native": {
"version": "0.66.4",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.66.4.tgz",
@@ -7014,27 +6612,6 @@
}
}
},
- "remark-parse": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz",
- "integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==",
- "requires": {
- "@types/mdast": "^3.0.0",
- "mdast-util-from-markdown": "^1.0.0",
- "unified": "^10.0.0"
- }
- },
- "remark-rehype": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-9.1.0.tgz",
- "integrity": "sha512-oLa6YmgAYg19zb0ZrBACh40hpBLteYROaPLhBXzLgjqyHQrN+gVP9N/FJvfzuNNuzCutktkroXEZBrxAxKhh7Q==",
- "requires": {
- "@types/hast": "^2.0.0",
- "@types/mdast": "^3.0.0",
- "mdast-util-to-hast": "^11.0.0",
- "unified": "^10.0.0"
- }
- },
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@@ -7141,14 +6718,6 @@
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
},
- "sade": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
- "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
- "requires": {
- "mri": "^1.1.0"
- }
- },
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -7714,11 +7283,6 @@
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="
},
- "space-separated-tokens": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz",
- "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw=="
- },
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@@ -7838,14 +7402,6 @@
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true
},
- "style-to-object": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
- "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
- "requires": {
- "inline-style-parser": "0.1.1"
- }
- },
"stylis": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz",
@@ -8000,11 +7556,6 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
- "trough": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/trough/-/trough-2.0.2.tgz",
- "integrity": "sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w=="
- },
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -8073,27 +7624,6 @@
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz",
"integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ=="
},
- "unified": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.1.tgz",
- "integrity": "sha512-v4ky1+6BN9X3pQrOdkFIPWAaeDsHPE1svRDxq7YpTc2plkIqFMwukfqM+l0ewpP9EfwARlt9pPFAeWYhHm8X9w==",
- "requires": {
- "@types/unist": "^2.0.0",
- "bail": "^2.0.0",
- "extend": "^3.0.0",
- "is-buffer": "^2.0.0",
- "is-plain-obj": "^4.0.0",
- "trough": "^2.0.0",
- "vfile": "^5.0.0"
- },
- "dependencies": {
- "is-buffer": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
- "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
- }
- }
- },
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -8105,67 +7635,6 @@
"set-value": "^2.0.1"
}
},
- "unist-builder": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz",
- "integrity": "sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==",
- "requires": {
- "@types/unist": "^2.0.0"
- }
- },
- "unist-util-generated": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz",
- "integrity": "sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw=="
- },
- "unist-util-is": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz",
- "integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ=="
- },
- "unist-util-position": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.1.tgz",
- "integrity": "sha512-mgy/zI9fQ2HlbOtTdr2w9lhVaiFUHWQnZrFF2EUoVOqtAUdzqMtNiD99qA5a1IcjWVR8O6aVYE9u7Z2z1v0SQA=="
- },
- "unist-util-stringify-position": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.0.tgz",
- "integrity": "sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA==",
- "requires": {
- "@types/unist": "^2.0.0"
- }
- },
- "unist-util-visit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz",
- "integrity": "sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==",
- "requires": {
- "@types/unist": "^2.0.0",
- "unist-util-is": "^5.0.0",
- "unist-util-visit-parents": "^5.0.0"
- },
- "dependencies": {
- "unist-util-visit-parents": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz",
- "integrity": "sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==",
- "requires": {
- "@types/unist": "^2.0.0",
- "unist-util-is": "^5.0.0"
- }
- }
- }
- },
- "unist-util-visit-parents": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz",
- "integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==",
- "requires": {
- "@types/unist": "^2.0.0",
- "unist-util-is": "^5.0.0"
- }
- },
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -8267,24 +7736,6 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
- "uvu": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz",
- "integrity": "sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==",
- "requires": {
- "dequal": "^2.0.0",
- "diff": "^5.0.0",
- "kleur": "^4.0.3",
- "sade": "^1.7.3"
- },
- "dependencies": {
- "kleur": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
- "integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA=="
- }
- }
- },
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
@@ -8295,33 +7746,6 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
- "vfile": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.0.tgz",
- "integrity": "sha512-Tj44nY/48OQvarrE4FAjUfrv7GZOYzPbl5OD65HxVKwLJKMPU7zmfV8cCgCnzKWnSfYG2f3pxu+ALqs7j22xQQ==",
- "requires": {
- "@types/unist": "^2.0.0",
- "is-buffer": "^2.0.0",
- "unist-util-stringify-position": "^3.0.0",
- "vfile-message": "^3.0.0"
- },
- "dependencies": {
- "is-buffer": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
- "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
- }
- }
- },
- "vfile-message": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.0.tgz",
- "integrity": "sha512-4QJbBk+DkPEhBXq3f260xSaWtjE4gPKOfulzfMFF8ZNwaPZieWsg3iVlcmF04+eebzpcpeXOOFMfrYzJHVYg+g==",
- "requires": {
- "@types/unist": "^2.0.0",
- "unist-util-stringify-position": "^3.0.0"
- }
- },
"vlq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 6d055c42..3a098c13 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -32,7 +32,6 @@
"@mui/x-data-grid": "^5.2.2",
"material-ui-image": "^3.3.2",
"react-countdown": "^2.3.2",
- "react-markdown": "^7.1.2",
"react-native": "^0.66.4",
"react-native-svg": "^12.1.1",
"react-qr-code": "^2.0.3",
diff --git a/frontend/src/components/HomePage.js b/frontend/src/components/HomePage.js
index e47a17e8..011fa492 100644
--- a/frontend/src/components/HomePage.js
+++ b/frontend/src/components/HomePage.js
@@ -5,8 +5,6 @@ import UserGenPage from "./UserGenPage";
import MakerPage from "./MakerPage";
import BookPage from "./BookPage";
import OrderPage from "./OrderPage";
-import InfoPage from "./InfoPageMd";
-
export default class HomePage extends Component {
constructor(props) {
super(props);
@@ -18,7 +16,6 @@ export default class HomePage extends Component {
You are at the start page
-
diff --git a/frontend/src/components/InfoDialog.js b/frontend/src/components/InfoDialog.js
index 3815edcf..d7e8adae 100644
--- a/frontend/src/components/InfoDialog.js
+++ b/frontend/src/components/InfoDialog.js
@@ -44,9 +44,9 @@ export default class InfoDialog extends Component {
Are there trade limits?
Maximum single trade size is 500,000 Satoshis to minimize lightning
- routing. There is no limits to the number of trades per day. A robot
+ routing failure. There is no limits to the number of trades per day. A robot
can only have one order at a time. However, you can use multiple
- Robots simultatenously in different browsers (remember to back up the tokens!).
+ robots simultatenously in different browsers (remember to back up your robot tokens!).
Is RoboSats private?
@@ -67,14 +67,14 @@ export default class InfoDialog extends Component {
The seller faces the same chargeback risk as with any
other peer-to-peer service. Paypal or credit cards are
- not adviced.
+ not recommened.
What is the trust model?
The buyer and the seller never have to trust each other.
Some trust on RoboSats staff is needed since linking
- the seller's hold invoice and buyer payment is not atomic.
+ the seller's hold invoice and buyer payment is not atomic (yet).
In addition, disputes are solved by the RoboSats staff.
Your sats will most likely return to you. Any hold invoice that is not
settled would be automatically returned even if RoboSats goes down
forever. This is true for both, locked bonds and trading escrows. However,
- in the window between the buyer confirms FIAT SENT and the moment the
- seller releases the satoshis, the fund could be lost.
+ there is a small window between the buyer confirms FIAT SENT and the moment
+ the seller releases the satoshis when the funds could be lost.
diff --git a/frontend/src/components/InfoPageMd.js b/frontend/src/components/InfoPageMd.js
deleted file mode 100644
index bde547b5..00000000
--- a/frontend/src/components/InfoPageMd.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import ReactMarkdown from 'react-markdown'
-import {Paper, Grid, CircularProgress, Button, Link} from "@mui/material"
-import React, { Component } from 'react'
-
-export default class InfoPage extends Component {
- constructor(props) {
- super(props);
- this.state = {
- info: null,
- loading: true,
- };
- this.getInfoMarkdown()
- }
-
- getInfoMarkdown() {
- fetch('/static/assets/info.md')
- .then((response) => response.text())
- .then((data) => this.setState({info:data, loading:false}));
- }
-
- render() {
- return (
-
- {this.state.loading ?
-
- : ""}
-
-
-
-
-
-
- )
- }
- }
\ No newline at end of file
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index d0bfab8d..1c25c9f1 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -422,7 +422,7 @@ export default class TradeBox extends Component {
- Your invoice looks good!
+ Your invoice looks good!🎉
@@ -443,7 +443,7 @@ export default class TradeBox extends Component {
- The trade collateral is locked! :D
+ The trade collateral is locked! 🎉
@@ -537,7 +537,7 @@ handleRatingChange=(e)=>{
{this.props.data.isSeller ?
- Say hi! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}.
+ Say hi! Be helpful and concise. Let them know how to send you {this.props.data.currencyCode}.
:
diff --git a/frontend/static/assets/info.md b/frontend/static/assets/info.md
deleted file mode 100644
index 82e59f0c..00000000
--- a/frontend/static/assets/info.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# Simple and Private P2P Bitcoin Exchanging in the Lightning Network.
-
-## What is this?
-
-{project_name} is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies matchmaking and minimizes the trust needed to trade with a peer.
-
-## That’s cool, so how it works?
-
-Alice wants to sell sats, posts a sell order. Bob wants to buy sats, and takes Alice's order. Alice posts the sats as collateral using a hodl LN invoice. Bob also posts some sats as a bond to prove he is real. {project_name} locks the sats until Bob confirms he sent the fiat to Alice. Once Alice confirms she received the fiat, she tells {project_name} to release her sats to Bob. Enjoy your sats Bob!
-
-At no point, Alice and Bob have to trust the funds to each other. In case Alice and Bob have a conflict, {project_name} staff will resolve the dispute.
-
-(TODO: Long explanation and tutorial step by step, link)
-
-## Nice, and fiat payments method are...?
-
-Basically all of them. It is up to you to select your preferred payment methods. You will need to search for a peer who also accepts that method. Lightning is fast, so we highly recommend using instant fiat payment rails. Be aware trades have a expiry time of 8 hours. Paypal or credit card are not advice due to chargeback risk.
-
-## Trust
-
-The buyer and the seller never have to trust each other. Some trust on {project_name} is needed. Linking the seller’s hodl invoice and buyer payment is not atomic (yet, research ongoing). In addition, disputes are solved by the {project_name} staff.
-
-Note: this is not an escrow service. While trust requirements are minimized, {project_name} could run away with your sats. It could be argued that it is not worth it, as it would instantly destroy {project_name} reputation. However, you should hesitate and only trade small quantities at a time. For larger amounts and safety assurance use an escrow service such as Bisq or Hodlhodl.
-
-You can build more trust on {project_name} by inspecting the source code, link.
-
-## If {project_name} suddenly disappears during a trade my sats…
-
-Your sats will most likely return to you. Any hodl invoice that is not settled would be automatically returned even if {project_name} goes down forever. This is true for both, locked bonds and traded sats. However, in the window between the buyer confirms FIAT SENT and the sats have not been released yet by the seller, the fund could be lost.
-
-## Limits
-
-Max trade size is 500K Sats to minimize failures in lightning routing. The limit will be raised as LN grows.
-
-## Privacy
-
-User token is generated locally as the unique identifier (back it up on paper! If lost {project_name} cannot help recover it). {project_name} doesn’t know anything about you and doesn’t want to know.
-
-Your trading peer is the only one who can potentially guess anything about you. Keep chat short and concise. Avoid providing non-essential information other than strictly necessary for the fiat payment.
-
-The chat with your peer is end-to-end encrypted, {project_name} cannot read. It can only be decrypted with your user token. The chat encryption makes it hard to resolve disputes. Therefore, by opening a dispute you are sending a viewkey to {project_name} staff. The encrypted chat cannot be revisited as it is deleted automatically when the trade is finalized (check the source code).
-
-For best anonymity use Tor Browser and access the .onion hidden service.
-
-## So {project_name} is a decentralized exchange?
-Not quite, though it shares some elements.
-
-A simple comparisson:
-* Privacy worst to best: Coinbase/Binance/others < hodlhodl < {project_name} < Bisq
-* Safety (not your keys, not your coins): Coinbase/Binance/others < {project_name} < hodlhodl < Bisq
-*(take with a pinch of salt)*
-
-So, if bisq is best for both privacy and safety, why {project_name} exists? Bisq is great, but it is difficult, slow, high-fee and needs extra steps to move to lightning. {project_name} aims to be as easy as Binance/Coinbase greatly improving on privacy and requiring minimal trust.
-
-## Any risk?
-
-Sure, this is a beta bot, things could go wrong. Trade small amounts!
-
-The seller faces the same chargeback risk as with any other peer-to-peer exchange. Avoid accepting payment methods with easy chargeback!
-
-## What are the fees?
-
-{project_name} takes a 0.2% fee of the trade to cover lightning routing costs. This is akin to a Binance trade fee (but hey, you do not have to sell your soul to the devil, nor pay the withdrawal fine...).
-
-The loser of a dispute pays a 1% fee that is slashed from the collateral posted when the trade starts. This fee is necessary to disincentive cheating and keep the site healthy. It also helps to cover the staff cost of dispute solving.
-
-Note: your selected fiat payment rails might have other fees, these are to be covered by the buyer.
-
-## I am a pro and {project_name} is too simple, it lacks features…
-
-Indeed, this site is a simple front-end that aims for user friendliness and forces best privacy for casual users.
-
-If you are a big maker, liquidity provider, or want to create many trades simultaneously use the API: {API_LINK_DOCUMENTATION}
-
-## Is it legal to use {project_name} in my country?
-
-In many countries using {project_name} is not different than buying something from a peer on Ebay or Craiglist. Your regulation may vary, you need to figure out.
-
-## Disclaimer
-
-This tool is provided as is. It is in active development and can be buggy. Be aware that you could lose your funds: trade with the utmost caution. There is no private support. Support is only offered via public channels (link telegram groups). {project_name} will never contact you. And {project_name} will definitely never ask for your user token.
\ No newline at end of file
diff --git a/setup.md b/setup.md
index ccc946aa..1c691458 100644
--- a/setup.md
+++ b/setup.md
@@ -122,7 +122,6 @@ npm install react-native
npm install react-native-svg
npm install react-qr-code
npm install @mui/material
-npm install react-markdown
npm install websocket
npm install react-countdown
npm install @mui/icons-material