From abb1bdd0beaadbce2efc348bc5b64830ec7f27bf Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 13 Jan 2022 16:43:26 -0800 Subject: [PATCH 01/37] Add dynamic countdown. Attach countdown to expiry progress bar. --- chat/consumers.py | 10 ++- chat/urls.py | 8 +-- chat/views.py | 11 ++-- frontend/package-lock.json | 8 +++ frontend/package.json | 1 + frontend/src/components/Chat.js | 17 +++-- frontend/src/components/OrderPage.js | 93 ++++++++++++++++------------ frontend/src/components/TradeBox.js | 2 +- robosats/urls.py | 2 +- setup.md | 1 + 10 files changed, 93 insertions(+), 60 deletions(-) diff --git a/chat/consumers.py b/chat/consumers.py index 2013b34a..a65ed459 100644 --- a/chat/consumers.py +++ b/chat/consumers.py @@ -56,8 +56,16 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): message = event['message'] nick = event['nick'] + # Insert a white space in words longer than 22 characters. + # Helps when messages overflow in a single line. + words = message.split(' ') + fix_message = '' + for word in words: + word = ' '.join(word[i:i+22] for i in range(0, len(word), 22)) + fix_message = fix_message +' '+ word + await self.send(text_data=json.dumps({ - 'message': message, + 'message': fix_message, 'user_nick': nick, })) diff --git a/chat/urls.py b/chat/urls.py index 4ed60135..f0107bfe 100644 --- a/chat/urls.py +++ b/chat/urls.py @@ -2,7 +2,7 @@ from django.urls import path from . import views -urlpatterns = [ - path('', views.index, name='index'), - path('/', views.room, name='order_chat'), -] \ No newline at end of file +# urlpatterns = [ +# path('', views.index, name='index'), +# path('/', views.room, name='order_chat'), +# ] \ No newline at end of file diff --git a/chat/views.py b/chat/views.py index c0f94a87..1198a822 100644 --- a/chat/views.py +++ b/chat/views.py @@ -1,10 +1,7 @@ from django.shortcuts import render -def index(request): - return render(request, 'index.html', {}) - -def room(request, order_id): - return render(request, 'chatroom.html', { - 'order_id': order_id - }) \ No newline at end of file +# def room(request, order_id): +# return render(request, 'chatroom.html', { +# 'order_id': order_id +# }) \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 69c66f59..90a612e0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6503,6 +6503,14 @@ "object-assign": "^4.1.1" } }, + "react-countdown": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz", + "integrity": "sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-devtools-core": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.22.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 28aa9147..15fb2128 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "@mui/material": "^5.2.7", "@mui/system": "^5.2.6", "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", diff --git a/frontend/src/components/Chat.js b/frontend/src/components/Chat.js index 9a1f6a66..e1b3dc22 100644 --- a/frontend/src/components/Chat.js +++ b/frontend/src/components/Chat.js @@ -13,15 +13,13 @@ export default class Chat extends Component { state = { messages: [], value:'', - orderId: 2, }; - client = new W3CWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.data.orderId + '/'); + client = new W3CWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.orderId + '/'); componentDidMount() { this.client.onopen = () => { console.log('WebSocket Client Connected') - console.log(this.props.data) } this.client.onmessage = (message) => { const dataFromServer = JSON.parse(message.data); @@ -43,11 +41,19 @@ export default class Chat extends Component { } } + componentDidUpdate() { + this.scrollToBottom(); + } + + scrollToBottom = () => { + this.messagesEnd.scrollIntoView({ behavior: "smooth" }); + } + onButtonClicked = (e) => { this.client.send(JSON.stringify({ type: "message", message: this.state.value, - nick: this.props.data.urNick, + nick: this.props.urNick, })); this.state.value = '' e.preventDefault(); @@ -60,7 +66,7 @@ export default class Chat extends Component { {this.state.messages.map(message => <> {/* If message sender is not our nick, gray color, if it is our nick, green color */} - {message.userNick == this.props.data.urNick ? + {message.userNick == this.props.urNick ? } )} +
{ this.messagesEnd = el; }}>
diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 84f225b6..f4bee0a2 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -1,44 +1,8 @@ import React, { Component } from "react"; import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress} from "@mui/material" +import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown'; import TradeBox from "./TradeBox"; -function msToTime(duration) { - var seconds = Math.floor((duration / 1000) % 60), - minutes = Math.floor((duration / (1000 * 60)) % 60), - hours = Math.floor((duration / (1000 * 60 * 60)) % 24); - - minutes = (minutes < 10) ? "0" + minutes : minutes; - seconds = (seconds < 10) ? "0" + seconds : seconds; - - return hours + "h " + minutes + "m " + seconds + "s"; -} - -// TO DO fix Progress bar to go from 100 to 0, from total_expiration time, showing time_left -function LinearDeterminate() { - const [progress, setProgress] = React.useState(0); - - React.useEffect(() => { - const timer = setInterval(() => { - setProgress((oldProgress) => { - if (oldProgress === 0) { - return 100; - } - const diff = 1; - return Math.max(oldProgress - diff, 0); - }); - }, 500); - - return () => { - clearInterval(timer); - }; - }, []); - - return ( - - - - ); -} function getCookie(name) { let cookieValue = null; @@ -67,8 +31,9 @@ export default class OrderPage extends Component { super(props); this.state = { isExplicit: false, - delay: 2000, // Refresh every 2 seconds by default - currencies_dict: {"1":"USD"} + delay: 60000, // Refresh every 60 seconds by default + currencies_dict: {"1":"USD"}, + total_secs_expiry: 300, }; this.orderId = this.props.match.params.orderId; this.getCurrencyDict(); @@ -136,6 +101,50 @@ export default class OrderPage extends Component { window.history.back(); } + // Countdown Renderer callback with condition + countdownRenderer = ({ total, hours, minutes, seconds, completed }) => { + if (completed) { + // Render a completed state + this.getOrderDetails(); + } else { + var col = 'black' + var fraction_left = (total/1000) / this.state.total_secs_expiry + console.log(fraction_left) + // Make orange at -25% of time left + if (fraction_left < 0.25){col = 'orange'} + // Make red at 10% of time left + if (fraction_left < 0.1){col = 'red'} + // Render a countdown + return ( + fraction_left < 0.25 ? {hours}h {zeroPad(minutes)}m {zeroPad(seconds)}s + :{hours}h {zeroPad(minutes)}m {zeroPad(seconds)}s + ); + } + }; + + LinearDeterminate =()=> { + const [progress, setProgress] = React.useState(0); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((oldProgress) => { + var left = calcTimeDelta( new Date(this.state.expiresAt)).total /1000; + return (left / this.state.total_secs_expiry) * 100; + }); + }, 1000); + + return () => { + clearInterval(timer); + }; + }, []); + + return ( + + + + ); + } + handleClickTakeOrderButton=()=>{ console.log(this.state) const requestOptions = { @@ -246,9 +255,11 @@ export default class OrderPage extends Component { - + + + - + {/* If the user has a penalty/limit */} diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 1720270a..450bb0a3 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -377,7 +377,7 @@ handleRatingChange=(e)=>{ - + {openDisputeButton ? this.showOpenDisputeButton() : ""} diff --git a/robosats/urls.py b/robosats/urls.py index eb7315bd..2e44786c 100644 --- a/robosats/urls.py +++ b/robosats/urls.py @@ -19,6 +19,6 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('api.urls')), - path('chat/', include('chat.urls')), + # path('chat/', include('chat.urls')), path('', include('frontend.urls')), ] diff --git a/setup.md b/setup.md index f530c3d8..caca25f6 100644 --- a/setup.md +++ b/setup.md @@ -114,6 +114,7 @@ npm install react-qr-code npm install @mui/material npm install react-markdown npm install websocket +npm install react-countdown ``` Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed) From 135c4c59e562f56c5c3bbb80e341148f05e44dbf Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 04:00:53 -0800 Subject: [PATCH 02/37] Add UI info and icons on order pager --- api/models.py | 21 ++++++++++ api/views.py | 31 +++++++++----- frontend/package-lock.json | 8 ++++ frontend/package.json | 1 + frontend/src/components/OrderPage.js | 60 +++++++++++++++++++++++----- frontend/src/components/TradeBox.js | 2 +- setup.md | 1 + 7 files changed, 102 insertions(+), 22 deletions(-) diff --git a/api/models.py b/api/models.py index bc7edd9f..9ad4be95 100644 --- a/api/models.py +++ b/api/models.py @@ -133,6 +133,27 @@ class Order(models.Model): 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 = { + 0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond' + 1 : 60*60*int(config('PUBLIC_ORDER_DURATION')), # 'Public' + 2 : 0, # 'Deleted' + 3 : int(config('EXP_TAKER_BOND_INVOICE')), # 'Waiting for taker bond' + 4 : 0, # 'Cancelled' + 5 : 0, # 'Expired' + 6 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting for trade collateral and buyer invoice' + 7 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for seller trade collateral' + 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' + 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' + } + 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)]}') diff --git a/api/views.py b/api/views.py index 46dbd2d3..4d428cab 100644 --- a/api/views.py +++ b/api/views.py @@ -106,6 +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] # if user is under a limit (penalty), inform him. is_penalized, time_out = Logics.is_penalized(request.user) @@ -116,12 +117,21 @@ class OrderView(viewsets.ViewSet): data['is_maker'] = order.maker == request.user data['is_taker'] = order.taker == request.user data['is_participant'] = data['is_maker'] or data['is_taker'] - data['ur_nick'] = request.user.username - # 3) If not a participant and order is not public, forbid. + # 3.a) If not a participant and order is not public, forbid. if not data['is_participant'] and order.status != Order.Status.PUB: return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN) + # 3.b If public + if order.status == Order.Status.PUB: + 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. + if data['is_maker']: + data['robots_in_book'] = None # TODO + data['premium_percentile'] = None # TODO + 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) elif not data['is_participant'] and order.status != Order.Status.PUB: return Response(data, status=status.HTTP_200_OK) @@ -134,6 +144,7 @@ class OrderView(viewsets.ViewSet): data['status_message'] = Order.Status(order.status).label data['is_fiat_sent'] = order.is_fiat_sent data['is_disputed'] = order.is_disputed + data['ur_nick'] = request.user.username # If both bonds are locked, participants can see the final trade amount in sats. if order.taker_bond: @@ -281,14 +292,14 @@ 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 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) token = request.GET.get(self.lookup_url_kwarg) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 90a612e0..cf8cd9f7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1581,6 +1581,14 @@ "react-is": "^17.0.2" } }, + "@mui/icons-material": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.2.5.tgz", + "integrity": "sha512-uQiUz+l0xy+2jExyKyU19MkMAR2F7bQFcsQ5hdqAtsB14Jw2zlmIAD55mV6f0NxKCut7Rx6cA3ZpfzlzAfoK8Q==", + "requires": { + "@babel/runtime": "^7.16.3" + } + }, "@mui/material": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.7.tgz", diff --git a/frontend/package.json b/frontend/package.json index 15fb2128..55a83e36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "@emotion/styled": "^11.6.0", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", + "@mui/icons-material": "^5.2.5", "@mui/material": "^5.2.7", "@mui/system": "^5.2.6", "material-ui-image": "^3.3.2", diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index f4bee0a2..5a1820c9 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -1,8 +1,15 @@ import React, { Component } from "react"; -import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, 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} from "@mui/material" import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown'; import TradeBox from "./TradeBox"; +// icons +import AccessTimeIcon from '@mui/icons-material/AccessTime'; +import NumbersIcon from '@mui/icons-material/Numbers'; +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'; function getCookie(name) { let cookieValue = null; @@ -75,6 +82,13 @@ export default class OrderPage extends Component { escrowInvoice: data.escrow_invoice, escrowSatoshis: data.escrow_satoshis, invoiceAmount: data.invoice_amount, + total_secs_expiry: data.total_secs_exp, + numSimilarOrders: data.num_similar_orders, + priceNow: data.price_now, + premiumNow: data.premium_now, + robotsInBook: data.robots_in_book, + premiumPercentile: data.premium_percentile, + numSimilarOrders: data.num_similar_orders }) }); } @@ -106,15 +120,15 @@ export default class OrderPage extends Component { if (completed) { // Render a completed state this.getOrderDetails(); + return null; } else { var col = 'black' var fraction_left = (total/1000) / this.state.total_secs_expiry - console.log(fraction_left) - // Make orange at -25% of time left + // Make orange at 25% of time left if (fraction_left < 0.25){col = 'orange'} // Make red at 10% of time left if (fraction_left < 0.1){col = 'red'} - // Render a countdown + // Render a countdown, bold when less than 25% return ( fraction_left < 0.25 ? {hours}h {zeroPad(minutes)}m {zeroPad(seconds)}s :{hours}h {zeroPad(minutes)}m {zeroPad(seconds)}s @@ -226,6 +240,9 @@ export default class OrderPage extends Component { "" } + + + @@ -234,28 +251,49 @@ export default class OrderPage extends Component { } + + + + + + + + {/* If there is live Price and Premium data, show it. Otherwise show the order maker settings */} - {this.state.isExplicit ? - - : - - } + + + + {this.state.priceNow? + + : + (this.state.isExplicit ? + + : + + ) + } - + + + + - + + + + diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 450bb0a3..972d3dfe 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -163,7 +163,7 @@ export default class TradeBox extends Component { - + diff --git a/setup.md b/setup.md index caca25f6..366747dc 100644 --- a/setup.md +++ b/setup.md @@ -115,6 +115,7 @@ npm install @mui/material npm install react-markdown npm install websocket npm install react-countdown +npm install @mui/icons-material ``` Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed) From 806c469b64780a5bdd8710914e081820f200a6a0 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 05:31:54 -0800 Subject: [PATCH 03/37] Add tradebox icons --- frontend/src/components/TradeBox.js | 32 +++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 972d3dfe..74856773 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -1,9 +1,17 @@ import React, { Component } from "react"; -import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material" +import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon} from "@mui/material" import QRCode from "react-qr-code"; import Chat from "./Chat" +// Icons +import LockIcon from '@mui/icons-material/Lock'; +import SmartToyIcon from '@mui/icons-material/SmartToy'; +import PercentIcon from '@mui/icons-material/Percent'; +import BookIcon from '@mui/icons-material/Book'; + + + function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { @@ -77,9 +85,16 @@ export default class TradeBox extends Component { showBondIsLocked=()=>{ return ( - - 🔒 Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is safely locked - + + + + + + + Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is locked + + + ); } @@ -158,16 +173,25 @@ export default class TradeBox extends Component { {/* TODO API sends data for a more confortable wait */} + + + + + + + + + From 032d3a1369616b8fbc1917329bedc6a85fe7dd02 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 06:19:25 -0800 Subject: [PATCH 04/37] Add taker bond lock expiry --- api/logics.py | 9 ++------- api/views.py | 9 +++++---- frontend/src/components/OrderPage.js | 6 ++++-- frontend/src/components/TradeBox.js | 21 ++++++--------------- 4 files changed, 17 insertions(+), 28 deletions(-) diff --git a/api/logics.py b/api/logics.py index c8da042a..bdf2ddd9 100644 --- a/api/logics.py +++ b/api/logics.py @@ -238,13 +238,7 @@ class Logics(): LNPayment "order.taker_bond" is deleted() ''' elif order.status == Order.Status.TAK and order.taker == user: # adds a timeout penalty - user.profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT) - user.profile.save() - - order.taker = None - order.status = Order.Status.PUB - order.save() - + cls.kick_taker(order) return True, None # 4) When taker or maker cancel after bond (before escrow) @@ -395,6 +389,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.save() return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis} diff --git a/api/views.py b/api/views.py index 4d428cab..b41826d8 100644 --- a/api/views.py +++ b/api/views.py @@ -122,12 +122,12 @@ class OrderView(viewsets.ViewSet): if not data['is_participant'] and order.status != Order.Status.PUB: return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN) - # 3.b If public - if order.status == Order.Status.PUB: + # 3.b If order is between public and WF2 + if order.status >= Order.Status.PUB and order.status > Order.Status.WFB: 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. - if data['is_maker']: + if data['is_maker'] and order.status == Order.Status.PUB: data['robots_in_book'] = None # TODO data['premium_percentile'] = None # TODO data['num_similar_orders'] = len(Order.objects.filter(currency=order.currency, status=Order.Status.PUB)) @@ -227,10 +227,11 @@ class OrderView(viewsets.ViewSet): if order.status == Order.Status.PUB: valid, context = Logics.validate_already_maker_or_taker(request.user) if not valid: return Response(context, status=status.HTTP_409_CONFLICT) - valid, context = Logics.take(order, request.user) if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN) + return self.get(request) + else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST) # Any other action is only allowed if the user is a participant diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 5a1820c9..30e01a08 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -41,6 +41,7 @@ export default class OrderPage extends Component { delay: 60000, // Refresh every 60 seconds by default currencies_dict: {"1":"USD"}, total_secs_expiry: 300, + loading: true, }; this.orderId = this.props.match.params.orderId; this.getCurrencyDict(); @@ -53,6 +54,7 @@ export default class OrderPage extends Component { .then((response) => response.json()) .then((data) => {console.log(data) & this.setState({ + loading: false, id: data.id, statusCode: data.status, statusText: data.status_message, @@ -294,7 +296,7 @@ export default class OrderPage extends Component { - + @@ -380,7 +382,7 @@ export default class OrderPage extends Component { render (){ return ( // Only so nothing shows while requesting the first batch of data - (this.state.statusCode == null & this.state.badRequest == null) ? : this.orderDetailsPage() + this.state.loading ? : this.orderDetailsPage() ); } } diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 74856773..011f348b 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -68,14 +68,12 @@ export default class TradeBox extends Component { @@ -85,16 +83,9 @@ export default class TradeBox extends Component { showBondIsLocked=()=>{ return ( - - - - - - - Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is locked - - - + + 🔒 Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is locked + ); } @@ -118,7 +109,7 @@ export default class TradeBox extends Component { size="small" defaultValue={this.props.data.escrowInvoice} disabled="true" - helperText="This is a hold invoice. It will simply freeze in your wallet. It will be charged once the buyer confirms he sent the fiat." + helperText="This is a hold invoice. It will be charged once the buyer confirms he sent the fiat." color = "secondary" /> From ad58f669681adebee8a6e91314dc51565cd91e03 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 06:57:56 -0800 Subject: [PATCH 05/37] Add second market price API. Median price is used. --- .env-sample | 4 ++-- api/logics.py | 1 - api/utils.py | 26 ++++++++++++++++++++------ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.env-sample b/.env-sample index 54d6637c..f5612fb4 100644 --- a/.env-sample +++ b/.env-sample @@ -6,8 +6,8 @@ LND_GRPC_HOST='127.0.0.1:10009' REDIS_URL='' -# Market price public API -MARKET_PRICE_API = 'https://blockchain.info/ticker' +# 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_NAME = '' diff --git a/api/logics.py b/api/logics.py index bdf2ddd9..df5592b6 100644 --- a/api/logics.py +++ b/api/logics.py @@ -10,7 +10,6 @@ import math FEE = float(config('FEE')) BOND_SIZE = float(config('BOND_SIZE')) -MARKET_PRICE_API = config('MARKET_PRICE_API') ESCROW_USERNAME = config('ESCROW_USERNAME') PENALTY_TIMEOUT = int(config('PENALTY_TIMEOUT')) diff --git a/api/utils.py b/api/utils.py index 6c23d924..f97cb569 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,18 +1,32 @@ import requests, ring, os from decouple import config - +from statistics import median market_cache = {} @ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds def get_exchange_rate(currency): - # TODO Add fallback Public APIs and error handling - # Think about polling price data in a different way (e.g. store locally every t seconds) + ''' + Checks for exchange rates in several public APIs. + Returns the median price. + ''' + + APIS = config('MARKET_PRICE_APIS', cast=lambda v: [s.strip() for s in v.split(',')]) + exchange_rates = [] - market_prices = requests.get(config('MARKET_PRICE_API')).json() - exchange_rate = float(market_prices[currency]['last']) + for api_url in APIS: + print(api_url) + try: + if 'blockchain.info' in api_url: + blockchain_prices = requests.get(api_url).json() + exchange_rates.append(float(blockchain_prices[currency]['last'])) + elif 'yadio.io' in api_url: + yadio_prices = requests.get(api_url).json() + exchange_rates.append(float(yadio_prices['BTC'][currency])) + except: + pass - return exchange_rate + return median(exchange_rates) lnd_v_cache = {} From 4087bb99366501208a64b33e0196c1ba6a780462 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 08:21:42 -0800 Subject: [PATCH 06/37] Add bottom bar --- api/views.py | 4 +- frontend/src/components/App.js | 12 ++- frontend/src/components/BottomBar.js | 119 +++++++++++++++++++++++++++ frontend/static/assets/info.md | 2 +- frontend/static/css/index.css | 12 +++ 5 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/BottomBar.js diff --git a/api/views.py b/api/views.py index b41826d8..404615da 100644 --- a/api/views.py +++ b/api/views.py @@ -438,8 +438,8 @@ class InfoView(ListAPIView): avg_premium = sum(premiums) / len(premiums) total_volume = sum(volumes) else: - avg_premium = None - total_volume = None + avg_premium = 0 + total_volume = 0 context['today_avg_nonkyc_btc_premium'] = avg_premium context['today_total_volume'] = total_volume diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js index 6ac372c9..6cf5659a 100644 --- a/frontend/src/components/App.js +++ b/frontend/src/components/App.js @@ -2,6 +2,7 @@ import React, { Component } from "react"; import { render } from "react-dom"; import HomePage from "./HomePage"; +import BottomBar from "./BottomBar"; export default class App extends Component { constructor(props) { @@ -10,9 +11,14 @@ export default class App extends Component { render() { return ( -
- -
+ <> +
+ +
+
+ +
+ ); } } diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js new file mode 100644 index 00000000..2d6bf033 --- /dev/null +++ b/frontend/src/components/BottomBar.js @@ -0,0 +1,119 @@ +import React, { Component } from 'react' +import {Paper, Grid, IconButton, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon} from "@mui/material"; + +// Icons +import SettingsIcon from '@mui/icons-material/Settings'; +import SupportAgentIcon from '@mui/icons-material/SupportAgent'; +import InventoryIcon from '@mui/icons-material/Inventory'; +import SellIcon from '@mui/icons-material/Sell'; +import SmartToyIcon from '@mui/icons-material/SmartToy'; +import PercentIcon from '@mui/icons-material/Percent'; + +export default class BottomBar extends Component { + constructor(props) { + super(props); + this.state = { + num_public_buy_orders: 0, + num_active_robotsats: 0, + num_public_sell_orders: 0, + fee: 0, + today_avg_nonkyc_btc_premium: 0, + today_volume: 0, + }; + this.getInfo(); + } + + handleClickSuppport = () => { + window.open("https://t.me/robosats"); + }; + + getInfo() { + this.setState(null) + fetch('/api/info/') + .then((response) => response.json()) + .then((data) => {console.log(data) & + this.setState(data) + }); + } + + render() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + } +} diff --git a/frontend/static/assets/info.md b/frontend/static/assets/info.md index a5f43bab..82e59f0c 100644 --- a/frontend/static/assets/info.md +++ b/frontend/static/assets/info.md @@ -1,4 +1,4 @@ -# Buy and sell non-KYC Bitcoin using the lightning network. +# Simple and Private P2P Bitcoin Exchanging in the Lightning Network. ## What is this? diff --git a/frontend/static/css/index.css b/frontend/static/css/index.css index def12e47..096f9b29 100644 --- a/frontend/static/css/index.css +++ b/frontend/static/css/index.css @@ -25,6 +25,18 @@ body { transform: translate(-50%,-50%); } +.bottomBar { + position: fixed; + bottom: 0; + width: 100%; + height: 40px; +} + +.bottomItem { + margin: 0; + top: -14px; +} + .newAvatar { background-color:white; border-radius: 50%; From 1a99b78229a11141679de6174f1a21206e5c77b5 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 09:35:27 -0800 Subject: [PATCH 07/37] Fix small mistakes --- api/views.py | 16 ++++++------- frontend/src/components/BottomBar.js | 36 +++++++++++++++++++++------- frontend/src/components/OrderPage.js | 2 +- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/api/views.py b/api/views.py index 404615da..587a98c4 100644 --- a/api/views.py +++ b/api/views.py @@ -293,14 +293,14 @@ 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 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) token = request.GET.get(self.lookup_url_kwarg) diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js index 2d6bf033..eb30284c 100644 --- a/frontend/src/components/BottomBar.js +++ b/frontend/src/components/BottomBar.js @@ -8,17 +8,18 @@ import InventoryIcon from '@mui/icons-material/Inventory'; import SellIcon from '@mui/icons-material/Sell'; import SmartToyIcon from '@mui/icons-material/SmartToy'; import PercentIcon from '@mui/icons-material/Percent'; +import PriceChangeIcon from '@mui/icons-material/PriceChange'; export default class BottomBar extends Component { constructor(props) { super(props); this.state = { - num_public_buy_orders: 0, - num_active_robotsats: 0, - num_public_sell_orders: 0, - fee: 0, - today_avg_nonkyc_btc_premium: 0, - today_volume: 0, + num_public_buy_orders: null, + num_active_robotsats: null, + num_public_sell_orders: null, + fee: null, + today_avg_nonkyc_btc_premium: null, + today_volume: null, }; this.getInfo(); } @@ -40,11 +41,13 @@ export default class BottomBar extends Component { return ( + + @@ -57,6 +60,7 @@ export default class BottomBar extends Component { secondary="Public Buy Orders" /> + @@ -69,6 +73,7 @@ export default class BottomBar extends Component { secondary="Public Sell Orders" /> + @@ -81,10 +86,11 @@ export default class BottomBar extends Component { secondary="Num Active RoboSats" /> + - + - + + + + + + + + + + @@ -111,6 +130,7 @@ export default class BottomBar extends Component { + diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 30e01a08..c50fd40a 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -38,7 +38,7 @@ export default class OrderPage extends Component { super(props); this.state = { isExplicit: false, - delay: 60000, // Refresh every 60 seconds by default + delay: 3000, // Refresh every 3 seconds by default currencies_dict: {"1":"USD"}, total_secs_expiry: 300, loading: true, From 03653cb09a1462590104a69cae43338a605ab873 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 10:00:41 -0800 Subject: [PATCH 08/37] Update domain --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8dc24bcb..eeb59a3e 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ RoboSats is a simple and private way to exchange bitcoin for national currencies ## Try it out **Bitcoin mainnet:** -- Tor: robosatsbkpis32grrxz7vliwjuivdmsyjx4d7zrlffo3nul44ck5sad.onion -- Url: robosats.org (Not active) +- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Not active) +- Url: robosats.com (Registered - Not active) - Version: v0.0.0 (Last stable) **Bitcoin testnet:** -- Tor: robotescktg6eqthfvatugczhzo3rj5zzk7rrkp6n5pa5qrz2mdikwid.onion -- Url: testnet.robosats.org (Not active) +- Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion (Active - On Dev Node) +- Url: testnet.robosats.com (Registered - Not active) - Commit height: v0.0.0 Latest commit. *Use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.* From 7a6cf943eefdac48b7acc4b4f72b822785230eba Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 13:40:54 -0800 Subject: [PATCH 09/37] Rework book as datagrid --- api/logics.py | 2 +- frontend/package-lock.json | 16 ++++ frontend/package.json | 1 + frontend/src/components/BookPage.js | 115 ++++++++++++++-------------- setup.md | 1 + 5 files changed, 78 insertions(+), 57 deletions(-) diff --git a/api/logics.py b/api/logics.py index df5592b6..56953e24 100644 --- a/api/logics.py +++ b/api/logics.py @@ -92,7 +92,7 @@ class Logics(): premium = int(premium*100) # 2 decimals left price = order_rate - significant_digits = 6 + significant_digits = 5 price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1) return price, premium diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cf8cd9f7..d16fcc61 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1674,6 +1674,17 @@ "react-is": "^17.0.2" } }, + "@mui/x-data-grid": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-5.2.2.tgz", + "integrity": "sha512-FI/fwsMMATUdEHwiGGkBdiw/p3G6+iUlkoklBzzsB6MY0Mb+Voj+s/waoFM3uyNJ+h4jof8NTS/Gs8IfDiyciA==", + "requires": { + "@mui/utils": "^5.2.3", + "clsx": "^1.1.1", + "prop-types": "^15.8.0", + "reselect": "^4.1.5" + } + }, "@popperjs/core": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", @@ -7049,6 +7060,11 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, + "reselect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz", + "integrity": "sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ==" + }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 55a83e36..6d055c42 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,6 +29,7 @@ "@mui/icons-material": "^5.2.5", "@mui/material": "^5.2.7", "@mui/system": "^5.2.6", + "@mui/x-data-grid": "^5.2.2", "material-ui-image": "^3.3.2", "react-countdown": "^2.3.2", "react-markdown": "^7.1.2", diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 6c305a0c..a6a6ceb5 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -1,26 +1,27 @@ import React, { Component } from "react"; -import { Paper, Button , Divider, CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material"; +import { Paper, Button , CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material"; import { Link } from 'react-router-dom' +import { DataGrid } from '@mui/x-data-grid'; export default class BookPage extends Component { constructor(props) { super(props); this.state = { - orders: new Array(), + orders: new Array({id:0,}), currency: 0, type: 1, currencies_dict: {"0":"ANY"}, loading: true, }; this.getCurrencyDict() - this.getOrderDetails(this.state.type,this.state.currency) + this.getOrderDetails(this.state.type, this.state.currency) this.state.currencyCode = this.getCurrencyCode(this.state.currency) } - getOrderDetails(type,currency) { + getOrderDetails(type, currency) { fetch('/api/book' + '?currency=' + currency + "&type=" + type) .then((response) => response.json()) - .then((data) => + .then((data) => console.log(data) & this.setState({ orders: data, not_found: data.not_found, @@ -28,7 +29,7 @@ export default class BookPage extends Component { })); } - handleCardClick=(e)=>{ + handleRowClick=(e)=>{ console.log(e) this.props.history.push('/order/' + e); } @@ -67,54 +68,56 @@ export default class BookPage extends Component { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } - bookListItems=()=>{ - return (this.state.orders.map((order) => - <> - this.handleCardClick(order.id)}> + bookListTable=()=>{ + return ( +
+ + ({id: order.id, + avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png', + robosat: order.maker_nick, + type: order.type ? "Sell": "Buy", + amount: parseFloat(parseFloat(order.amount).toFixed(4)), + currency: this.getCurrencyCode(order.currency), + payment_method: order.payment_method, + price: order.price, + premium: order.premium, + }) + )} - - - - - - - {order.maker_nick+" "} - - - - - - {order.type ? " Sells ": " Buys "} BTC for {parseFloat( - parseFloat(order.amount).toFixed(4))+" "+ this.getCurrencyCode(order.currency)+" "} - - + columns={[ + // { field: 'id', headerName: 'ID', width: 40 }, + { field: 'robosat', headerName: 'RoboSat', width: 240, + renderCell: (params) => {return ( + + + + + + + ); + } }, + { field: 'type', headerName: 'Type', width: 60 }, + { field: 'amount', headerName: 'Amount', type: 'number', width: 80 }, + { field: 'currency', headerName: 'Currency', width: 100 }, + { field: 'payment_method', headerName: 'Payment Method', width: 180 }, + { field: 'price', headerName: 'Price', type: 'number', width: 140, + renderCell: (params) => {return ( +
{this.pn(params.row.price) + " " +params.row.currency+ "/BTC" }
+ )} }, + { field: 'premium', headerName: 'Premium', type: 'number', width: 100, + renderCell: (params) => {return ( +
{parseFloat(parseFloat(params.row.premium).toFixed(4))+"%" }
+ )} }, + ]} - - - via {order.payment_method} - - - - - - at {this.pn(order.price) + " " + this.getCurrencyCode(order.currency)}/BTC - - - - - - {order.premium > 1 ? "🔴" : "🔵" } {parseFloat(parseFloat(order.premium).toFixed(4))}% - - - - - - - - )); + pageSize={7} + onRowClick={(params) => this.handleRowClick(params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places. + rowsPerPageOptions={[7]} + /> +
+ ); } render() { @@ -196,10 +199,10 @@ export default class BookPage extends Component { ) : - - - {this.bookListItems()} - + + + {this.state.loading ? null : this.bookListTable()} + } diff --git a/setup.md b/setup.md index 366747dc..032a374f 100644 --- a/setup.md +++ b/setup.md @@ -116,6 +116,7 @@ npm install react-markdown npm install websocket npm install react-countdown npm install @mui/icons-material +npm install @mui/x-data-grid ``` Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed) From a995a4e2eadde81afcddb412a9169c5c9a206542 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Fri, 14 Jan 2022 16:28:19 -0800 Subject: [PATCH 10/37] Add Info and Stats for nerds popups --- api/utils.py | 3 +- frontend/src/components/BottomBar.js | 55 +++++++- frontend/src/components/HomePage.js | 2 +- frontend/src/components/InfoDialog.js | 126 ++++++++++++++++++ .../components/{InfoPage.js => InfoPageMd.js} | 3 - frontend/src/components/UserGenPage.js | 36 +++-- 6 files changed, 207 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/InfoDialog.js rename frontend/src/components/{InfoPage.js => InfoPageMd.js} (88%) diff --git a/api/utils.py b/api/utils.py index f97cb569..645d8084 100644 --- a/api/utils.py +++ b/api/utils.py @@ -10,12 +10,11 @@ def get_exchange_rate(currency): Checks for exchange rates in several public APIs. Returns the median price. ''' - + APIS = config('MARKET_PRICE_APIS', cast=lambda v: [s.strip() for s in v.split(',')]) exchange_rates = [] for api_url in APIS: - print(api_url) try: if 'blockchain.info' in api_url: blockchain_prices = requests.get(api_url).json() diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js index eb30284c..7b1d7c0f 100644 --- a/frontend/src/components/BottomBar.js +++ b/frontend/src/components/BottomBar.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import {Paper, Grid, IconButton, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon} from "@mui/material"; +import {Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, Divider, Dialog, DialogContent} from "@mui/material"; // Icons import SettingsIcon from '@mui/icons-material/Settings'; @@ -9,11 +9,15 @@ import SellIcon from '@mui/icons-material/Sell'; import SmartToyIcon from '@mui/icons-material/SmartToy'; import PercentIcon from '@mui/icons-material/Percent'; import PriceChangeIcon from '@mui/icons-material/PriceChange'; +import BoltIcon from '@mui/icons-material/Bolt'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import EqualizerIcon from '@mui/icons-material/Equalizer'; export default class BottomBar extends Component { constructor(props) { super(props); this.state = { + openStatsForNerds: false, num_public_buy_orders: null, num_active_robotsats: null, num_public_sell_orders: null, @@ -37,13 +41,60 @@ export default class BottomBar extends Component { }); } + handleClickOpenStatsForNerds = () => { + this.setState({openStatsForNerds: true}); + }; + + handleClickCloseStatsForNerds = () => { + this.setState({openStatsForNerds: false}); + }; + + StatsDialog =() =>{ + return( + + + Stats For Nerds + + + + + + + + + + + {this.state.robosats_running_commit_hash} + + + + + + + + + + + + ) + } + render() { return ( + - + diff --git a/frontend/src/components/HomePage.js b/frontend/src/components/HomePage.js index f3413355..e47a17e8 100644 --- a/frontend/src/components/HomePage.js +++ b/frontend/src/components/HomePage.js @@ -5,7 +5,7 @@ import UserGenPage from "./UserGenPage"; import MakerPage from "./MakerPage"; import BookPage from "./BookPage"; import OrderPage from "./OrderPage"; -import InfoPage from "./InfoPage"; +import InfoPage from "./InfoPageMd"; export default class HomePage extends Component { constructor(props) { diff --git a/frontend/src/components/InfoDialog.js b/frontend/src/components/InfoDialog.js new file mode 100644 index 00000000..3490e6bb --- /dev/null +++ b/frontend/src/components/InfoDialog.js @@ -0,0 +1,126 @@ + +import {Typography, DialogTitle, DialogContent, DialogContentText, Button } from "@mui/material" +import React, { Component } from 'react' + +export default class InfoDialog extends Component { + render() { + return ( +
+ + What is RoboSats? + +

It is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies + matchmaking and minimizes the trust needed to trade with a peer.

+ +

RoboSats is an open source project (GitHub). +

+
+ + How does it work? + +

AdequateAlice01 wants to sell bitcoin, so she posts a sell order. + BafflingBob02 wants to buy bitcoin and he takes Alice's order. + Both have to post a small bond using lightning to prove they are real + robots. Then, Alice posts the trade collateral also using a lightning + hold invoice. RobotSats locks the invoice until Bob confirms he sent + the fiat to Alice. Once Alice confirms she received the fiat, she + tells RoboSats to release the satoshis to Bob. Enjoy your satoshis, + Bob!

+ +

At no point, AdequateAlice01 and BafflingBob02 have to trust the + bitcoin to each other. In case they have a conflict, RoboSats staff + will help resolving the dispute.

+
+ + What payment methods are accepted? + +

Basically all of them. You can write down your preferred payment + method(s). You will have to match with a peer who also accepts + that method. Lightning is fast, so we highly recommend using instant + fiat payment rails.

+
+ + Are there trade limits? + +

Maximum single trade size is 500,000 Satoshis to minimize lightninh + 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.

+
+ + Is RoboSats private? + +

RoboSats will never ask you for your name, country or ID. For + best anonymity use Tor Browser and access the .onion hidden service.

+ +

Your trading peer is the only one who can potentially guess + anything about you. Keep your chat short and concise. Avoid + providing non-essential information other than strictly necessary + for the fiat payment.

+
+ + What are the risks? + +

This is an experimental application, things could go wrong. + Trade small amounts!

+ +

The seller faces the same chargeback risk as with any + other peer-to-peer service. Paypal or credit cards are + not adviced.

+
+ + 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. + In addition, disputes are solved by the RoboSats staff. +

+ +

While trust requirements are minimized, RoboSats could + run away with your satoshis. It could be argued that it is not + worth it, as it would instantly destroy RoboSats reputation. + However, you should hesitate and only trade small quantities at a + time. For large amounts use an onchain escrow service such as Bisq +

+ +

You can build more trust on RoboSats by + inspecting the source code

+
+ + What happens if RoboSats suddently disapears? + +

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 moment + the seller releases the satoshis, the fund could be lost. +

+
+ + It 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 + to comply. +

+
+ + Disclaimer + +

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. +

+
+ + +
+ +
+ ) + } +} \ No newline at end of file diff --git a/frontend/src/components/InfoPage.js b/frontend/src/components/InfoPageMd.js similarity index 88% rename from frontend/src/components/InfoPage.js rename to frontend/src/components/InfoPageMd.js index 9146d8f5..bde547b5 100644 --- a/frontend/src/components/InfoPage.js +++ b/frontend/src/components/InfoPageMd.js @@ -28,9 +28,6 @@ export default class InfoPage extends Component {
- ) diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index 06491bb6..5ed55d18 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -1,7 +1,8 @@ import React, { Component } from "react"; -import { Button , Grid, Typography, TextField, ButtonGroup} from "@mui/material" +import { Button , Dialog, Grid, Typography, TextField, ButtonGroup} from "@mui/material" import { Link } from 'react-router-dom' import Image from 'material-ui-image' +import InfoDialog from './InfoDialog' function getCookie(name) { let cookieValue = null; @@ -25,6 +26,7 @@ export default class UserGenPage extends Component { super(props); this.state = { token: this.genBase62Token(34), + openInfo: false, }; this.getGeneratedUser(this.state.token); } @@ -65,16 +67,11 @@ export default class UserGenPage extends Component { .then((data) => console.log(data)); } - // Fix next two handler functions so they work sequentially - // at the moment they make the request generate a new user in parallel - // to updating the token in the state. So the it works a bit weird. - handleAnotherButtonPressed=(e)=>{ this.delGeneratedUser() this.setState({ token: this.genBase62Token(34), }) - this.reload_for_csrf_to_work(); } handleChangeToken=(e)=>{ @@ -85,11 +82,29 @@ export default class UserGenPage extends Component { this.getGeneratedUser(e.target.value); } - // TO FIX CSRF TOKEN IS NOT UPDATED UNTIL WINDOW IS RELOADED - reload_for_csrf_to_work=()=>{ - window.location.reload() + handleClickOpenInfo = () => { + this.setState({openInfo: true}); + }; + + handleCloseInfo = () => { + this.setState({openInfo: false}); + }; + + InfoDialog =() =>{ + return( + + + + ) } + render() { return ( @@ -137,7 +152,8 @@ export default class UserGenPage extends Component { - + + From a03cfdc01d83678c4e327a665aadcefcee2f193c Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Sat, 15 Jan 2022 02:21:36 -0800 Subject: [PATCH 11/37] Add community pop up --- frontend/src/components/BottomBar.js | 74 ++++++++++++++++++++++++--- frontend/src/components/InfoDialog.js | 10 ++-- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js index 7b1d7c0f..c422987f 100644 --- a/frontend/src/components/BottomBar.js +++ b/frontend/src/components/BottomBar.js @@ -1,9 +1,9 @@ import React, { Component } from 'react' -import {Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, Divider, Dialog, DialogContent} from "@mui/material"; +import {Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material"; // Icons import SettingsIcon from '@mui/icons-material/Settings'; -import SupportAgentIcon from '@mui/icons-material/SupportAgent'; +import PeopleIcon from '@mui/icons-material/People'; import InventoryIcon from '@mui/icons-material/Inventory'; import SellIcon from '@mui/icons-material/Sell'; import SmartToyIcon from '@mui/icons-material/SmartToy'; @@ -12,12 +12,14 @@ import PriceChangeIcon from '@mui/icons-material/PriceChange'; 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'; export default class BottomBar extends Component { constructor(props) { super(props); this.state = { openStatsForNerds: false, + openCommuniy: false, num_public_buy_orders: null, num_active_robotsats: null, num_public_sell_orders: null, @@ -42,11 +44,10 @@ export default class BottomBar extends Component { } handleClickOpenStatsForNerds = () => { - this.setState({openStatsForNerds: true}); + this.setState({openStatsForNerds: true}); }; - handleClickCloseStatsForNerds = () => { - this.setState({openStatsForNerds: false}); + this.setState({openStatsForNerds: false}); }; StatsDialog =() =>{ @@ -85,10 +86,66 @@ export default class BottomBar extends Component { ) } + handleClickOpenCommunity = () => { + this.setState({openCommuniy: true}); + }; + handleClickCloseCommunity = () => { + this.setState({openCommuniy: false}); + }; + + CommunityDialog =() =>{ + return( + + + 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. +

+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ ) + } + + render() { return ( + @@ -177,8 +234,11 @@ export default class BottomBar extends Component { - - + + diff --git a/frontend/src/components/InfoDialog.js b/frontend/src/components/InfoDialog.js index 3490e6bb..25a1d57b 100644 --- a/frontend/src/components/InfoDialog.js +++ b/frontend/src/components/InfoDialog.js @@ -19,11 +19,11 @@ export default class InfoDialog extends Component { How does it work? -

AdequateAlice01 wants to sell bitcoin, so she posts a sell order. +

AdequateAlice01 wants to sell bitcoin. She posts a sell order. BafflingBob02 wants to buy bitcoin and he takes Alice's order. Both have to post a small bond using lightning to prove they are real robots. Then, Alice posts the trade collateral also using a lightning - hold invoice. RobotSats locks the invoice until Bob confirms he sent + hold invoice. RoboSats locks the invoice until Bob confirms he sent the fiat to Alice. Once Alice confirms she received the fiat, she tells RoboSats to release the satoshis to Bob. Enjoy your satoshis, Bob!

@@ -43,7 +43,7 @@ export default class InfoDialog extends Component { Are there trade limits? -

Maximum single trade size is 500,000 Satoshis to minimize lightninh +

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.

@@ -94,8 +94,8 @@ export default class InfoDialog extends Component {

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 moment - the seller releases the satoshis, the fund could be lost. + in the window between the buyer confirms FIAT SENT and the moment the + seller releases the satoshis, the fund could be lost.

From 8e609b9e471ba5251ece1ae8bff0ba417ba77d8f Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Sat, 15 Jan 2022 04:00:11 -0800 Subject: [PATCH 12/37] Add dynamic refresh rate to OrderPage based on order status --- README.md | 6 ++--- api/logics.py | 7 +++--- api/utils.py | 1 + frontend/src/components/OrderPage.js | 32 ++++++++++++++++++++---- frontend/src/components/TradeBox.js | 37 ++++++++++++++-------------- 5 files changed, 53 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index eeb59a3e..8fc2e3b4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RoboSats: Buy and sell non-KYC Satoshis. +# RoboSats: Buy and sell Satoshis Privately. ## What is 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. @@ -15,11 +15,11 @@ RoboSats is a simple and private way to exchange bitcoin for national currencies - Url: testnet.robosats.com (Registered - Not active) - Commit height: v0.0.0 Latest commit. -*Use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.* +*Always use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.* ## Contribute to the Robotic Satoshis Open Source Project See [CONTRIBUTING.md](CONTRIBUTING.md) ## License -RoboSats is released under the terms of the AGPL3.0 license. See [LICENSE](LICENSE) for more details. +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/logics.py b/api/logics.py index 56953e24..d752618e 100644 --- a/api/logics.py +++ b/api/logics.py @@ -369,7 +369,8 @@ 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 {order.amount} - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel." + description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {float(order.amount) + 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 hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) @@ -482,8 +483,8 @@ class Logics(): def pay_buyer_invoice(order): ''' Pay buyer invoice''' # TODO ERROR HANDLING - if LNNode.pay_invoice(order.buyer_invoice.invoice, order.buyer_invoice.num_satoshis): - return True + suceeded, context = LNNode.pay_invoice(order.buyer_invoice.invoice, order.buyer_invoice.num_satoshis) + return suceeded, context @classmethod def confirm_fiat(cls, order, user): diff --git a/api/utils.py b/api/utils.py index 645d8084..96e58ffa 100644 --- a/api/utils.py +++ b/api/utils.py @@ -2,6 +2,7 @@ import requests, ring, os from decouple import config from statistics import median + market_cache = {} @ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index c50fd40a..b1f00449 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -38,7 +38,7 @@ export default class OrderPage extends Component { super(props); this.state = { isExplicit: false, - delay: 3000, // Refresh every 3 seconds by default + delay: 60000, // Refresh every 60 seconds by default currencies_dict: {"1":"USD"}, total_secs_expiry: 300, loading: true, @@ -46,6 +46,28 @@ export default class OrderPage extends Component { this.orderId = this.props.match.params.orderId; this.getCurrencyDict(); this.getOrderDetails(); + + // Change refresh delay according to Order status + this.statusToDelay = { + "0": 3000, //'Waiting for maker bond' + "1": 30000, //'Public' + "2": 999999, //'Deleted' + "3": 3000, //'Waiting for taker bond' + "4": 999999, //'Cancelled' + "5": 999999, //'Expired' + "6": 3000, //'Waiting for trade collateral and buyer invoice' + "7": 3000, //'Waiting only for seller trade collateral' + "8": 10000, //'Waiting only for buyer invoice' + "9": 10000, //'Sending fiat - In chatroom' + "10": 15000, //'Fiat sent - In chatroom' + "11": 300000, //'In dispute' + "12": 999999, //'Collaboratively cancelled' + "13": 120000, //'Sending satoshis to buyer' + "14": 999999, //'Sucessful trade' + "15": 15000, //'Failed lightning network routing' + "16": 999999, //'Maker lost dispute' + "17": 999999, //'Taker lost dispute' + } } getOrderDetails() { @@ -55,6 +77,7 @@ export default class OrderPage extends Component { .then((data) => {console.log(data) & this.setState({ loading: false, + delay: this.statusToDelay[data.status.toString()], id: data.id, statusCode: data.status, statusText: data.status_message, @@ -99,12 +122,11 @@ export default class OrderPage extends Component { componentDidMount() { this.interval = setInterval(this.tick, this.state.delay); } - componentDidUpdate(prevProps, prevState) { - if (prevState.delay !== this.state.delay) { - clearInterval(this.interval); + componentDidUpdate() { + clearInterval(this.interval); this.interval = setInterval(this.tick, this.state.delay); - } } + componentWillUnmount() { clearInterval(this.interval); } diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 011f348b..c71f2f08 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -5,7 +5,6 @@ import QRCode from "react-qr-code"; import Chat from "./Chat" // Icons -import LockIcon from '@mui/icons-material/Lock'; import SmartToyIcon from '@mui/icons-material/SmartToy'; import PercentIcon from '@mui/icons-material/Percent'; import BookIcon from '@mui/icons-material/Book'; @@ -238,24 +237,24 @@ export default class TradeBox extends Component { valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis.
- - - - - - - - + + + + + + + + {this.showBondIsLocked()}
) From f390a8f2f1cd9a851287c7987aecd5c8c9b77d54 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Sat, 15 Jan 2022 06:22:07 -0800 Subject: [PATCH 13/37] Fix delay when bad request. Fixavg premium to weighted avg premium --- api/logics.py | 2 +- api/views.py | 12 +++++++----- frontend/src/components/BottomBar.js | 8 ++++---- frontend/src/components/InfoDialog.js | 2 +- frontend/src/components/OrderPage.js | 23 ++++++++++++++--------- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/api/logics.py b/api/logics.py index d752618e..2f0eea25 100644 --- a/api/logics.py +++ b/api/logics.py @@ -369,7 +369,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 {float(order.amount) + Order.currency_dict[str(order.currency)]}" + description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + 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/views.py b/api/views.py index 587a98c4..e779498f 100644 --- a/api/views.py +++ b/api/views.py @@ -207,7 +207,7 @@ class OrderView(viewsets.ViewSet): def take_update_confirm_dispute_cancel(self, request, format=None): ''' - Here takes place all of updatesto the order object. + Here takes place all of the updates to the order object. That is: take, confim, cancel, dispute, update_invoice or rate. ''' order_id = request.GET.get(self.lookup_url_kwarg) @@ -430,18 +430,20 @@ class InfoView(ListAPIView): queryset = MarketTick.objects.filter(timestamp__day=today.day) if not len(queryset) == 0: - premiums = [] + weighted_premiums = [] volumes = [] for tick in queryset: - premiums.append(tick.premium) + weighted_premiums.append(tick.premium*tick.volume) volumes.append(tick.volume) - avg_premium = sum(premiums) / len(premiums) + total_volume = sum(volumes) + # Avg_premium is the weighted average of the premiums by volume + avg_premium = sum(weighted_premiums) / total_volume else: avg_premium = 0 total_volume = 0 - context['today_avg_nonkyc_btc_premium'] = avg_premium + context['today_avg_nonkyc_btc_premium'] = round(avg_premium,2) context['today_total_volume'] = total_volume context['lnd_version'] = get_lnd_version() context['robosats_running_commit_hash'] = get_commit_robosats() diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js index c422987f..d36a1b1b 100644 --- a/frontend/src/components/BottomBar.js +++ b/frontend/src/components/BottomBar.js @@ -25,7 +25,7 @@ export default class BottomBar extends Component { num_public_sell_orders: null, fee: null, today_avg_nonkyc_btc_premium: null, - today_volume: null, + today_total_volume: null, }; this.getInfo(); } @@ -78,7 +78,7 @@ export default class BottomBar extends Component { - + @@ -203,8 +203,8 @@ export default class BottomBar extends Component { + primary={this.state.today_avg_nonkyc_btc_premium+"%"} + secondary="Today Non-KYC Avg Premium" /> diff --git a/frontend/src/components/InfoDialog.js b/frontend/src/components/InfoDialog.js index 25a1d57b..ac867a1a 100644 --- a/frontend/src/components/InfoDialog.js +++ b/frontend/src/components/InfoDialog.js @@ -89,7 +89,7 @@ export default class InfoDialog extends Component { inspecting the source code

- What happens if RoboSats suddently disapears? + What happens if RoboSats suddenly disapears?

Your sats will most likely return to you. Any hold invoice that is not settled would be automatically returned even if RoboSats goes down diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index b1f00449..1c44ce45 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -51,9 +51,9 @@ export default class OrderPage extends Component { this.statusToDelay = { "0": 3000, //'Waiting for maker bond' "1": 30000, //'Public' - "2": 999999, //'Deleted' + "2": 9999999, //'Deleted' "3": 3000, //'Waiting for taker bond' - "4": 999999, //'Cancelled' + "4": 9999999, //'Cancelled' "5": 999999, //'Expired' "6": 3000, //'Waiting for trade collateral and buyer invoice' "7": 3000, //'Waiting only for seller trade collateral' @@ -61,13 +61,13 @@ export default class OrderPage extends Component { "9": 10000, //'Sending fiat - In chatroom' "10": 15000, //'Fiat sent - In chatroom' "11": 300000, //'In dispute' - "12": 999999, //'Collaboratively cancelled' + "12": 9999999,//'Collaboratively cancelled' "13": 120000, //'Sending satoshis to buyer' - "14": 999999, //'Sucessful trade' - "15": 15000, //'Failed lightning network routing' - "16": 999999, //'Maker lost dispute' - "17": 999999, //'Taker lost dispute' - } + "14": 9999999,//'Sucessful trade' + "15": 10000, //'Failed lightning network routing' + "16": 9999999,//'Maker lost dispute' + "17": 9999999,//'Taker lost dispute' + } } getOrderDetails() { @@ -77,7 +77,7 @@ export default class OrderPage extends Component { .then((data) => {console.log(data) & this.setState({ loading: false, - delay: this.statusToDelay[data.status.toString()], + delay: this.setDelay(data.status), id: data.id, statusCode: data.status, statusText: data.status_message, @@ -207,6 +207,11 @@ 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; + } + getCurrencyCode(val){ let code = val ? this.state.currencies_dict[val.toString()] : "" return code From 91544642ae0a23ef2451b03edae959b03a72d978 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Sat, 15 Jan 2022 07:12:26 -0800 Subject: [PATCH 14/37] Add currency visual flags --- api/logics.py | 2 +- frontend/src/components/BookPage.js | 6 ++++- frontend/src/components/OrderPage.js | 8 +++---- frontend/src/components/getFlags.js | 33 ++++++++++++++++++++++++++ frontend/static/assets/currencies.json | 4 +++- 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/getFlags.js diff --git a/api/logics.py b/api/logics.py index 2f0eea25..cee59d8a 100644 --- a/api/logics.py +++ b/api/logics.py @@ -89,7 +89,7 @@ class Logics(): exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) order_rate = float(order.amount) / (float(order.satoshis) / 100000000) premium = order_rate / exchange_rate - 1 - premium = int(premium*100) # 2 decimals left + premium = int(premium*10000)/100 # 2 decimals left price = order_rate significant_digits = 5 diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index a6a6ceb5..8de89617 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -2,6 +2,7 @@ import React, { Component } from "react"; import { Paper, Button , CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material"; import { Link } from 'react-router-dom' import { DataGrid } from '@mui/x-data-grid'; +import getFlags from './getFlags' export default class BookPage extends Component { constructor(props) { @@ -100,7 +101,10 @@ export default class BookPage extends Component { } }, { field: 'type', headerName: 'Type', width: 60 }, { field: 'amount', headerName: 'Amount', type: 'number', width: 80 }, - { field: 'currency', headerName: 'Currency', width: 100 }, + { field: 'currency', headerName: 'Currency', width: 100, + renderCell: (params) => {return ( +

{params.row.currency + " " + getFlags(params.row.currency)}
+ )} }, { 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|0sJ0i13(>mQ28+RAu4`4jgo$ev|h}haIL)Y$FKN{n*70YwzYwCEaKemEqc4H zu=9M`t2#!?Sb&9wf-L_2|1~iQuy=Guvaf9E45(4plyD+|vYV9QXG$L&3#g2*w10Hv19L`O+dojh;`&ey!}-KcGgZjH_#EgfgSeElYwu9i~oh%Ycl`~V%;wpuX*_f7@V zA~h(uadd;@j_|Jenrm8T_#Gnn{+q5K3x7fCU1H3i0s$*NcP>sidwE)4^b?`XMiu(c zed;nGNGJivljLarW^j(ic4C;nSE>yJIn5ihTT6GC?DsfelH+o`G$(}3g5Of;esGO@ zkxoU`2Kg`&+!i86Vm;1SGI9Hr`JLttmW_vl$vlpV3#5ddIu+|_PE)#sad^L^;L+Rf z#GqHr(u)eDln|;8{}Nt9`ZItq*@f>F{09KA8)D*Ga^1aIu5jfAq3yqAm90kGZI4 zr+z-XRGliNJ5`;KlEP@dERoNjy2qY=7{+cru;-iT!X!OsYyb8*j7{_WW$Oq7Lg~uV z*Y2yxA&27S6R!-0h15~54*-!c`b3Ujk#`wxZZkPqN%<9dVkS5Jb5#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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..dbb03b5b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,140 @@ +aioredis==1.3.1 +aiorpcX==0.18.7 +amqp==5.0.9 +apturl==0.5.2 +asgiref==3.4.1 +async-timeout==4.0.2 +attrs==21.4.0 +autobahn==21.11.1 +Automat==20.2.0 +backports.zoneinfo==0.2.1 +bcrypt==3.1.7 +billiard==3.6.4.0 +blinker==1.4 +Brlapi==0.7.0 +celery==5.2.3 +certifi==2019.11.28 +cffi==1.15.0 +channels==3.0.4 +channels-redis==3.3.1 +chardet==3.0.4 +charge-lnd==0.2.4 +click==8.0.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 +colorama==0.4.4 +command-not-found==0.3 +constantly==15.1.0 +cryptography==36.0.1 +cupshelpers==1.0 +daphne==3.0.2 +dbus-python==1.2.16 +defer==1.0.6 +Deprecated==1.2.13 +distlib==0.3.4 +distro==1.4.0 +distro-info===0.23ubuntu1 +Django==3.2.11 +django-admin-relation-links==0.2.5 +django-celery-beat==2.2.1 +django-celery-results==2.2.0 +django-model-utils==4.2.0 +django-private-chat2==1.0.2 +django-redis==5.2.0 +django-timezone-field==4.2.3 +djangorestframework==3.13.1 +duplicity==0.8.12.0 +entrypoints==0.3 +fasteners==0.14.1 +filelock==3.4.2 +future==0.18.2 +googleapis-common-protos==1.53.0 +grpcio==1.39.0 +grpcio-tools==1.43.0 +hiredis==2.0.0 +httplib2==0.14.0 +hyperlink==21.0.0 +idna==2.8 +incremental==21.3.0 +keyring==18.0.1 +kombu==5.2.3 +language-selector==0.1 +launchpadlib==1.10.13 +lazr.restfulclient==0.14.2 +lazr.uri==1.0.3 +lockfile==0.12.2 +louis==3.12.0 +macaroonbakery==1.3.1 +Mako==1.1.0 +MarkupSafe==1.1.0 +monotonic==1.5 +msgpack==1.0.3 +natsort==8.0.2 +netifaces==0.10.4 +numpy==1.22.0 +oauthlib==3.1.0 +olefile==0.46 +packaging==21.3 +paramiko==2.6.0 +pbr==5.8.0 +pexpect==4.6.0 +Pillow==7.0.0 +platformdirs==2.4.1 +prompt-toolkit==3.0.24 +protobuf==3.17.3 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycairo==1.16.2 +pycparser==2.21 +pycups==1.9.73 +PyGObject==3.36.0 +PyJWT==1.7.1 +pymacaroons==0.13.0 +PyNaCl==1.3.0 +pyOpenSSL==21.0.0 +pyparsing==3.0.6 +pyRFC3339==1.1 +PySocks==1.7.1 +python-apt==2.0.0+ubuntu0.20.4.5 +python-crontab==2.6.0 +python-dateutil==2.7.3 +python-debian===0.1.36ubuntu1 +python-decouple==3.5 +pytz==2021.3 +pyxdg==0.26 +PyYAML==5.3.1 +redis==4.1.0 +reportlab==3.5.34 +requests==2.22.0 +requests-unixsocket==0.2.0 +ring==0.9.1 +robohash==1.1 +scipy==1.7.3 +SecretStorage==2.3.1 +service-identity==21.1.0 +simplejson==3.16.0 +six==1.16.0 +sqlparse==0.4.2 +stevedore==3.5.0 +systemd-python==234 +termcolor==1.1.0 +Twisted==21.7.0 +txaio==21.2.1 +typing-extensions==4.0.1 +ubuntu-advantage-tools==27.2 +ubuntu-drivers-common==0.0.0 +ufw==0.36 +unattended-upgrades==0.1 +urllib3==1.25.8 +usb-creator==0.3.7 +vine==5.0.0 +virtualenv==20.12.1 +virtualenv-clone==0.5.7 +virtualenvwrapper==4.8.4 +wadllib==1.3.3 +wcwidth==0.2.5 +wirerope==0.4.5 +wrapt==1.13.3 +xkit==0.0.0 +zope.interface==5.4.0 diff --git a/robosats/__init__.py b/robosats/__init__.py index e69de29b..d128d39c 100644 --- a/robosats/__init__.py +++ b/robosats/__init__.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import, unicode_literals + +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py new file mode 100644 index 00000000..797aa2c9 --- /dev/null +++ b/robosats/celery/__init__.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, unicode_literals +import os + +from celery import Celery +from celery.schedules import crontab + +# You can use rabbitmq instead here. +BASE_REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379') + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings') + +app = Celery('robosats') + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks() + +app.conf.broker_url = BASE_REDIS_URL + +# this allows schedule items in the Django admin. +app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler' + + +## Configure the periodic tasks +app.conf.beat_schedule = { + 'users-cleasing-every-hour': { + 'task': 'users_cleansing', + 'schedule': 60*60, + }, +} +app.conf.timezone = 'UTC' \ No newline at end of file diff --git a/robosats/celery/conf.py b/robosats/celery/conf.py new file mode 100644 index 00000000..6b1aa603 --- /dev/null +++ b/robosats/celery/conf.py @@ -0,0 +1,2 @@ +# This sets the django-celery-results backend +CELERY_RESULT_BACKEND = 'django-db' \ No newline at end of file diff --git a/robosats/settings.py b/robosats/settings.py index d41f249b..2bf7a7d3 100644 --- a/robosats/settings.py +++ b/robosats/settings.py @@ -40,10 +40,13 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'rest_framework', 'channels', + 'django_celery_beat', + 'django_celery_results', 'api', 'chat', 'frontend.apps.FrontendConfig', ] +from .celery.conf import * MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', diff --git a/setup.md b/setup.md index 032a374f..ccc946aa 100644 --- a/setup.md +++ b/setup.md @@ -45,8 +45,18 @@ pip install channels pip install django-redis pip install channels-redis ``` +## Install Celery for Django tasks +``` +pip install celery +pip install django-celery-beat +pip install django-celery-results +``` -*Django 4.0 at the time of writting* +Start up celery worker +`celery -A robosats worker --beat -l info -S django` + +*Django 3.2.11 at the time of writting* +*Celery 5.2.3* ### Launch the local development node From 185cc71e91ccd580275742ffd505ddb776e5fb49 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Sun, 16 Jan 2022 07:18:23 -0800 Subject: [PATCH 17/37] Improve market price fetching and caching --- api/admin.py | 9 ++++- api/logics.py | 10 ++--- api/models.py | 11 ++++- api/tasks.py | 26 +++++++++--- api/utils.py | 38 +++++++++++++----- frontend/src/components/getFlags.js | 2 +- frontend/static/assets/currencies.json | 2 +- .../static/assets/misc/unknown_avatar.png | Bin 0 -> 5347 bytes robosats/celery/__init__.py | 15 +++++-- 9 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 frontend/static/assets/misc/unknown_avatar.png diff --git a/api/admin.py b/api/admin.py index 6cba65a7..da92dcc5 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 +from .models import Order, LNPayment, Profile, MarketTick, CachedExchangeRate admin.site.unregister(Group) admin.site.unregister(User) @@ -31,7 +31,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): @admin.register(LNPayment) class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link') + list_display = ('id','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link') list_display_links = ('id','concept') change_links = ('sender','receiver') list_filter = ('type','concept','status') @@ -43,6 +43,11 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): change_links =['user'] readonly_fields = ['avatar_tag'] +@admin.register(CachedExchangeRate) +class CachedExchangeRateAdmin(admin.ModelAdmin): + list_display = ('currency','exchange_rate','timestamp') + readonly_fields = ('currency','exchange_rate','timestamp') + @admin.register(MarketTick) class MarketTickAdmin(admin.ModelAdmin): list_display = ('timestamp','price','volume','premium','currency','fee') diff --git a/api/logics.py b/api/logics.py index 1a4994e9..034dfdff 100644 --- a/api/logics.py +++ b/api/logics.py @@ -2,9 +2,8 @@ from datetime import time, timedelta from django.utils import timezone from .lightning.node import LNNode -from .models import Order, LNPayment, MarketTick, User +from .models import Order, LNPayment, MarketTick, User, CachedExchangeRate from decouple import config -from .utils import get_exchange_rate import math @@ -73,7 +72,7 @@ class Logics(): if order.is_explicit: satoshis_now = order.satoshis else: - exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) + exchange_rate = float(CachedExchangeRate.objects.get(currency=order.currency).exchange_rate) premium_rate = exchange_rate * (1+float(order.premium)/100) satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000 @@ -81,12 +80,11 @@ class Logics(): def price_and_premium_now(order): ''' computes order premium live ''' - exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) + exchange_rate = float(CachedExchangeRate.objects.get(currency=order.currency).exchange_rate) if not order.is_explicit: premium = order.premium price = exchange_rate * (1+float(premium)/100) else: - exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) order_rate = float(order.amount) / (float(order.satoshis) / 100000000) premium = order_rate / exchange_rate - 1 premium = int(premium*10000)/100 # 2 decimals left @@ -339,7 +337,7 @@ class Logics(): order.taker_bond.status = LNPayment.Status.LOCKED order.taker_bond.save() - # Both users profile have one more contract done + # 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 order.maker.profile.save() diff --git a/api/models.py b/api/models.py index 1505b303..8c2a561c 100644 --- a/api/models.py +++ b/api/models.py @@ -8,7 +8,6 @@ import uuid from decouple import config from pathlib import Path -from .utils import get_exchange_rate import json MIN_TRADE = int(config('MIN_TRADE')) @@ -220,6 +219,13 @@ 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. @@ -253,7 +259,8 @@ 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 - premium = 100 * (price / get_exchange_rate(Order.currency_dict[str(order.currency)]) - 1) + market_exchange_rate = float(CachedExchangeRate.objects.get(currency=order.currency).exchange_rate) + premium = 100 * (price / market_exchange_rate - 1) tick = MarketTick.objects.create( price=price, diff --git a/api/tasks.py b/api/tasks.py index 8876b84d..f41c2f03 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -2,10 +2,11 @@ from celery import shared_task from .lightning.node import LNNode from django.contrib.auth.models import User -from .models import LNPayment, Order +from .models import LNPayment, Order, CachedExchangeRate from .logics import Logics -from django.db.models import Q +from .utils import get_exchange_rates +from django.db.models import Q from datetime import timedelta from django.utils import timezone @@ -40,7 +41,7 @@ def users_cleansing(): return results -@shared_task +@shared_task(name="orders_expire") def orders_expire(): pass @@ -52,6 +53,21 @@ def follow_lnd_payment(): def query_all_lnd_invoices(): pass -@shared_task +@shared_task(name="cache_market", ignore_result=True) def cache_market(): - pass \ No newline at end of file + exchange_rates = get_exchange_rates(list(Order.currency_dict.values())) + results = {} + for val in Order.currency_dict: + rate = exchange_rates[int(val)-1] # currecies are indexed starting at 1 (USD) + results[val] = {Order.currency_dict[val], rate} + + # Create / Update database cached prices + CachedExchangeRate.objects.update_or_create( + currency = int(val), + # if there is a Cached Exchange rate matching that value, it updates it with defaults below + defaults = { + 'exchange_rate': rate, + 'timestamp': timezone.now(), + }) + + return results \ No newline at end of file diff --git a/api/utils.py b/api/utils.py index 96e58ffa..b7ea9459 100644 --- a/api/utils.py +++ b/api/utils.py @@ -1,32 +1,52 @@ import requests, ring, os from decouple import config -from statistics import median +import numpy as np market_cache = {} -@ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds -def get_exchange_rate(currency): +# @ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds +def get_exchange_rates(currencies): ''' + Params: list of currency codes. Checks for exchange rates in several public APIs. - Returns the median price. + Returns the median price list. ''' APIS = config('MARKET_PRICE_APIS', cast=lambda v: [s.strip() for s in v.split(',')]) - exchange_rates = [] + api_rates = [] for api_url in APIS: - try: + try: # If one API is unavailable pass if 'blockchain.info' in api_url: blockchain_prices = requests.get(api_url).json() - exchange_rates.append(float(blockchain_prices[currency]['last'])) + blockchain_rates = [] + for currency in currencies: + try: # If a currency is missing place a None + blockchain_rates.append(float(blockchain_prices[currency]['last'])) + except: + blockchain_rates.append(np.nan) + api_rates.append(blockchain_rates) + elif 'yadio.io' in api_url: yadio_prices = requests.get(api_url).json() - exchange_rates.append(float(yadio_prices['BTC'][currency])) + yadio_rates = [] + for currency in currencies: + try: + yadio_rates.append(float(yadio_prices['BTC'][currency])) + except: + yadio_rates.append(np.nan) + api_rates.append(yadio_rates) except: pass - return median(exchange_rates) + if len(api_rates) == 0: + return None # Wops there is not API available! + + exchange_rates = np.array(api_rates) + median_rates = np.nanmedian(exchange_rates, axis=0) + + return median_rates.tolist() lnd_v_cache = {} diff --git a/frontend/src/components/getFlags.js b/frontend/src/components/getFlags.js index bdfc98a0..676a1ec4 100644 --- a/frontend/src/components/getFlags.js +++ b/frontend/src/components/getFlags.js @@ -7,7 +7,7 @@ export default function getFlags(code){ if(code == 'CLP') return '🇨🇱'; if(code == 'CNY') return '🇨🇳'; if(code == 'EUR') return '🇪🇺'; - if(code == 'HKR') return '🇨🇷'; + if(code == 'HRK') return '🇨🇷'; if(code == 'CZK') return '🇨🇿'; if(code == 'DKK') return '🇩🇰'; if(code == 'GBP') return '🇬🇧'; diff --git a/frontend/static/assets/currencies.json b/frontend/static/assets/currencies.json index 3376cc5e..1cce4508 100644 --- a/frontend/static/assets/currencies.json +++ b/frontend/static/assets/currencies.json @@ -22,7 +22,7 @@ "21": "CLP", "22": "CZK", "23": "DKK", - "24": "HKR", + "24": "HRK", "25": "HUF", "26": "INR", "27": "ISK", diff --git a/frontend/static/assets/misc/unknown_avatar.png b/frontend/static/assets/misc/unknown_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..9e19b6a9857296d12ca124efe8c1f61aa0fa5431 GIT binary patch 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|0sJ0i13(>mQ28+RAu4`4jgo$ev|h}haIL)Y$FKN{n*70YwzYwCEaKemEqc4H zu=9M`t2#!?Sb&9wf-L_2|1~iQuy=Guvaf9E45(4plyD+|vYV9QXG$L&3#g2*w10Hv19L`O+dojh;`&ey!}-KcGgZjH_#EgfgSeElYwu9i~oh%Ycl`~V%;wpuX*_f7@V zA~h(uadd;@j_|Jenrm8T_#Gnn{+q5K3x7fCU1H3i0s$*NcP>sidwE)4^b?`XMiu(c zed;nGNGJivljLarW^j(ic4C;nSE>yJIn5ihTT6GC?DsfelH+o`G$(}3g5Of;esGO@ zkxoU`2Kg`&+!i86Vm;1SGI9Hr`JLttmW_vl$vlpV3#5ddIu+|_PE)#sad^L^;L+Rf z#GqHr(u)eDln|;8{}Nt9`ZItq*@f>F{09KA8)D*Ga^1aIu5jfAq3yqAm90kGZI4 zr+z-XRGliNJ5`;KlEP@dERoNjy2qY=7{+cru;-iT!X!OsYyb8*j7{_WW$Oq7Lg~uV z*Y2yxA&27S6R!-0h15~54*-!c`b3Ujk#`wxZZkPqN%<9dVkS5Jb5#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]) => {value} ) + .map( ([key, value]) => + {getFlags(value) + " " + 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( + + + {"Do you want to open a dispute?"} + + + + The RoboSats staff will examine the statements and evidence provided by the participants. + It is best if you provide a burner contact method on your statement for the staff to contact you. + The satoshis in the trade escrow will be sent to the dispute winner, while the dispute + loser will lose the bond. + + + + + + + + ) + } + + handleClickOpenConfirmFiatReceived = () => { + this.setState({openConfirmFiatReceived: true}); + }; + handleClickCloseConfirmFiatReceived = () => { + this.setState({openConfirmFiatReceived: false}); + }; + + handleClickTotallyConfirmFiatReceived = () =>{ + this.handleClickConfirmButton(); + this.handleClickCloseConfirmFiatReceived(); + }; + + ConfirmFiatReceivedDialog =() =>{ + return( + + + {"Confirm you received " +this.props.data.currencyCode+ "?"} + + + + Confirming that you received the fiat will finalize the trade. The satoshis + in the escrow will be released to the buyer. Only confirm after the {this.props.data.currencyCode+ " "} + has arrived to your account. In addition, if you have received {this.props.data.currencyCode+ " "} + and do not confirm the receipt, you risk losing your bond. + + + + + + + + ) + } + 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.showRobosat ? +
+ + + {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} - > ANY + > 🌍 ANY { Object.entries(this.state.currencies_dict) - .map( ([key, value]) => {value} ) + .map( ([key, value]) => {getFlags(value) + " " + 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( + + + {"Cancel the order?"} + + + + If the order is cancelled now you will lose your bond. + + + + + + + + ) + } + + 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.

@@ -94,8 +94,8 @@ export default class InfoDialog extends Component {

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