Add setup background threads. Minor fixes and cosmetic.

This commit is contained in:
Reckless_Satoshi 2022-01-30 07:18:03 -08:00
parent 64115a8bb5
commit 58ecb607c3
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
9 changed files with 147 additions and 52 deletions

View File

@ -122,22 +122,22 @@ class LNNode():
lnpayment.save() lnpayment.save()
return True return True
@classmethod # @classmethod
def check_until_invoice_locked(cls, payment_hash, expiration): # def check_until_invoice_locked(cls, payment_hash, expiration):
'''Checks until hold invoice is locked. # '''Checks until hold invoice is locked.
When invoice is locked, returns true. # When invoice is locked, returns true.
If time expires, return False.''' # If time expires, return False.'''
# Experimental, might need asyncio. Best if subscribing all invoices and running a background task # # Experimental, might need asyncio. Best if subscribing all invoices and running a background task
# Maybe best to pass LNpayment object and change status live. # # Maybe best to pass LNpayment object and change status live.
request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash) # request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
for invoice in cls.invoicesstub.SubscribeSingleInvoice(request): # for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
print(invoice) # print(invoice)
if timezone.now > expiration: # if timezone.now > expiration:
break # break
if invoice.state == 3: # True if hold invoice is accepted. # if invoice.state == 3: # True if hold invoice is accepted.
return True # return True
return False # return False
@classmethod @classmethod

View File

@ -50,9 +50,9 @@ class Logics():
def validate_order_size(order): def validate_order_size(order):
'''Validates if order is withing limits in satoshis at t0''' '''Validates if order is withing limits in satoshis at t0'''
if order.t0_satoshis > MAX_TRADE: 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'} return False, {'bad_request': 'Your order is too big. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now, but the limit is '+'{:,}'.format(MAX_TRADE)+ ' Sats'}
if order.t0_satoshis < MIN_TRADE: if order.t0_satoshis < MIN_TRADE:
return False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'} return False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now, but the limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'}
return True, None return True, None
@classmethod @classmethod
@ -386,11 +386,12 @@ class Logics():
return True, None return True, None
# 2) When maker cancels after bond # 2) When maker cancels after bond
'''The order dissapears from book and goes to cancelled. Maker is charged the bond to prevent DDOS '''The order dissapears from book and goes to cancelled. If strict, maker is charged the bond
on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)''' to prevent DDOS on the LN node and order book. If not strict, maker is returned
the bond (more user friendly).'''
elif order.status == Order.Status.PUB and order.maker == user: elif order.status == Order.Status.PUB and order.maker == user:
#Settle the maker bond (Maker loses the bond for cancelling public order) #Settle the maker bond (Maker loses the bond for cancelling public order)
if cls.settle_bond(order.maker_bond): if cls.return_bond(order.maker_bond): # strict: cls.settle_bond(order.maker_bond):
order.status = Order.Status.UCA order.status = Order.Status.UCA
order.save() order.save()
return True, None return True, None

View File

@ -39,6 +39,9 @@ class MakerView(CreateAPIView):
def post(self,request): def post(self,request):
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
if not request.user.is_authenticated:
return Response({'bad_request':'Woops! It seems you do not have a robot avatar'}, status.HTTP_400_BAD_REQUEST)
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST) if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
type = serializer.data.get('type') type = serializer.data.get('type')
@ -413,15 +416,16 @@ class UserView(APIView):
def delete(self,request): def delete(self,request):
''' Pressing "give me another" deletes the logged in user ''' ''' Pressing "give me another" deletes the logged in user '''
user = request.user user = request.user
if not user: if not user.is_authenticated:
return Response(status.HTTP_403_FORBIDDEN) return Response(status.HTTP_403_FORBIDDEN)
# Only delete if user life is shorter than 30 minutes. Helps deleting users by mistake # Only delete if user life is shorter than 30 minutes. Helps to avoid deleting users by mistake
if user.date_joined < (timezone.now() - timedelta(minutes=30)): if user.date_joined < (timezone.now() - timedelta(minutes=30)):
return Response(status.HTTP_400_BAD_REQUEST) return Response(status.HTTP_400_BAD_REQUEST)
# Check if it is not a maker or taker! # Check if it is not a maker or taker!
if not Logics.validate_already_maker_or_taker(user): not_participant, _, _ = Logics.validate_already_maker_or_taker(user)
if not not_participant:
return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST) return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
logout(request) logout(request)

View File

@ -93,7 +93,7 @@ export default class BookPage extends Component {
renderCell: (params) => {return ( renderCell: (params) => {return (
<ListItemButton style={{ cursor: "pointer" }}> <ListItemButton style={{ cursor: "pointer" }}>
<ListItemAvatar> <ListItemAvatar>
<Avatar alt={params.row.robosat} src={params.row.avatar} /> <Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={params.row.robosat}/> <ListItemText primary={params.row.robosat}/>
</ListItemButton> </ListItemButton>
@ -147,7 +147,7 @@ export default class BookPage extends Component {
{ field: 'robosat', headerName: 'Robot', width: 80, { field: 'robosat', headerName: 'Robot', width: 80,
renderCell: (params) => {return ( renderCell: (params) => {return (
<ListItemButton style={{ cursor: "pointer" }}> <ListItemButton style={{ cursor: "pointer" }}>
<Avatar alt={params.row.robosat} src={params.row.avatar} /> <Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
</ListItemButton> </ListItemButton>
); );
} }, } },

View File

@ -17,6 +17,7 @@ import SendIcon from '@mui/icons-material/Send';
import PublicIcon from '@mui/icons-material/Public'; import PublicIcon from '@mui/icons-material/Public';
import NumbersIcon from '@mui/icons-material/Numbers'; import NumbersIcon from '@mui/icons-material/Numbers';
import PasswordIcon from '@mui/icons-material/Password'; import PasswordIcon from '@mui/icons-material/Password';
import ContentCopy from "@mui/icons-material/ContentCopy";
// pretty numbers // pretty numbers
function pn(x) { function pn(x) {
@ -198,7 +199,7 @@ export default class BottomBar extends Component {
</Typography> </Typography>
</ListItemText> </ListItemText>
<ListItemAvatar> <ListItemAvatar>
<Avatar className='avatar' <Avatar className='profileAvatar'
sx={{ width: 65, height:65 }} sx={{ width: 65, height:65 }}
alt={this.props.nickname} alt={this.props.nickname}
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null} src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
@ -226,14 +227,21 @@ export default class BottomBar extends Component {
<ListItemIcon> <ListItemIcon>
<PasswordIcon/> <PasswordIcon/>
</ListItemIcon> </ListItemIcon>
<ListItemText secondary="Your token."> <ListItemText secondary="Your token">
{this.props.token ? {this.props.token ?
<TextField <TextField
disabled disabled
label='Store safely' label='Store safely'
value={this.props.token } value={this.props.token }
variant='filled' variant='filled'
size='small'/> size='small'
InputProps={{
endAdornment:
<IconButton onClick= {()=>navigator.clipboard.writeText(this.props.token)}>
<ContentCopy />
</IconButton>,
}}
/>
: :
'Cannot remember'} 'Cannot remember'}
</ListItemText> </ListItemText>
@ -258,7 +266,7 @@ bottomBarDesktop =()=>{
<ListItemButton onClick={this.handleClickOpenProfile} > <ListItemButton onClick={this.handleClickOpenProfile} >
<ListItemAvatar sx={{ width: 30, height: 30 }} > <ListItemAvatar sx={{ width: 30, height: 30 }} >
<Badge badgeContent={(this.state.active_order_id > 0 & !this.state.profileShown) ? "": null} color="primary"> <Badge badgeContent={(this.state.active_order_id > 0 & !this.state.profileShown) ? "": null} color="primary">
<Avatar className='rotatedAvatar' sx={{margin: 0, top: -13}} <Avatar className='flippedSmallAvatar' sx={{margin: 0, top: -13}}
alt={this.props.nickname} alt={this.props.nickname}
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null} src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
/> />
@ -462,7 +470,7 @@ bottomBarPhone =()=>{
<Grid item xs={1.6}> <Grid item xs={1.6}>
<IconButton onClick={this.handleClickOpenProfile} sx={{margin: 0, top: -13, }} > <IconButton onClick={this.handleClickOpenProfile} sx={{margin: 0, top: -13, }} >
<Badge badgeContent={(this.state.active_order_id >0 & !this.state.profileShown) ? "1": null} color="primary"> <Badge badgeContent={(this.state.active_order_id >0 & !this.state.profileShown) ? "1": null} color="primary">
<Avatar className='rotatedAvatar' <Avatar className='flippedSmallAvatar'
alt={this.props.nickname} alt={this.props.nickname}
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null} src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
/> />

View File

@ -309,7 +309,7 @@ export default class OrderPage extends Component {
// If maker and Waiting for Bond. Or if taker and Waiting for bond. // If maker and Waiting for Bond. Or if taker and Waiting for bond.
// Simply allow to cancel without showing the cancel dialog. // Simply allow to cancel without showing the cancel dialog.
if ((this.state.is_maker & this.state.status == 0) || this.state.is_taker & this.state.status == 3){ if ((this.state.is_maker & [0,1].includes(this.state.status)) || this.state.is_taker & this.state.status == 3){
return( return(
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button variant='contained' color='secondary' onClick={this.handleClickConfirmCancelButton}>Cancel</Button> <Button variant='contained' color='secondary' onClick={this.handleClickConfirmCancelButton}>Cancel</Button>
@ -317,7 +317,7 @@ export default class OrderPage extends Component {
)} )}
// If the order does not yet have an escrow deposited. Show dialog // If the order does not yet have an escrow deposited. Show dialog
// to confirm forfeiting the bond // to confirm forfeiting the bond
if ([1,3,6,7].includes(this.state.status)){ if ([3,6,7].includes(this.state.status)){
return( return(
<div id="openDialogCancelButton"> <div id="openDialogCancelButton">
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
@ -354,7 +354,7 @@ export default class OrderPage extends Component {
<List dense="true"> <List dense="true">
<ListItem > <ListItem >
<ListItemAvatar sx={{ width: 56, height: 56 }}> <ListItemAvatar sx={{ width: 56, height: 56 }}>
<Avatar <Avatar className="flippedSmallAvatar"
alt={this.state.maker_nick} alt={this.state.maker_nick}
src={window.location.origin +'/static/assets/avatars/' + this.state.maker_nick + '.png'} src={window.location.origin +'/static/assets/avatars/' + this.state.maker_nick + '.png'}
/> />
@ -370,7 +370,7 @@ export default class OrderPage extends Component {
<ListItem align="left"> <ListItem align="left">
<ListItemText primary={this.state.taker_nick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/> <ListItemText primary={this.state.taker_nick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/>
<ListItemAvatar > <ListItemAvatar >
<Avatar <Avatar className="smallAvatar"
alt={this.state.maker_nick} alt={this.state.maker_nick}
src={window.location.origin +'/static/assets/avatars/' + this.state.taker_nick + '.png'} src={window.location.origin +'/static/assets/avatars/' + this.state.taker_nick + '.png'}
/> />

View File

@ -3,7 +3,7 @@ import { Button , Dialog, Grid, Typography, TextField, ButtonGroup, CircularProg
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import Image from 'material-ui-image' import Image from 'material-ui-image'
import InfoDialog from './InfoDialog' import InfoDialog from './InfoDialog'
import PublishIcon from '@mui/icons-material/Publish'; import SmartToyIcon from '@mui/icons-material/SmartToy';
import CasinoIcon from '@mui/icons-material/Casino'; import CasinoIcon from '@mui/icons-material/Casino';
import ContentCopy from "@mui/icons-material/ContentCopy"; import ContentCopy from "@mui/icons-material/ContentCopy";
@ -161,14 +161,11 @@ export default class UserGenPage extends Component {
} }
<Grid container align="center"> <Grid container align="center">
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<IconButton sx={{top:6}} onClick= {()=>navigator.clipboard.writeText(this.state.token)}> <TextField sx={{maxWidth: 280}}
<ContentCopy sx={{width:18, height:18}} />
</IconButton>
<TextField
//sx={{ input: { color: 'purple' } }} //sx={{ input: { color: 'purple' } }}
InputLabelProps={{ // InputLabelProps={{
style: { color: 'green' }, // style: { color: 'green' },
}} // }}
error={this.state.bad_request} error={this.state.bad_request}
label='Store your token safely' label='Store your token safely'
required='true' required='true'
@ -183,15 +180,20 @@ export default class UserGenPage extends Component {
this.handleClickSubmitToken(); this.handleClickSubmitToken();
} }
}} }}
InputProps={{
startAdornment:
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.token)}>
<ContentCopy color={this.state.tokenHasChanged ? 'inherit' : 'primary' } sx={{width:18, height:18}} />
</IconButton>,
endAdornment:
<IconButton onClick={this.handleClickNewRandomToken}><CasinoIcon/></IconButton>,
}}
/> />
<IconButton sx={{top:8}} onClick={this.handleClickNewRandomToken}>
<CasinoIcon />
</IconButton>
</Grid> </Grid>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button disabled={!this.state.tokenHasChanged} type="submit" size='small' onClick= {this.handleClickSubmitToken}> <Button disabled={!this.state.tokenHasChanged} type="submit" size='small' onClick= {this.handleClickSubmitToken}>
<PublishIcon /> <SmartToyIcon sx={{width:18, height:18}} />
<span> Generate Robot</span> <span> Generate Robot</span>
</Button> </Button>
</Grid> </Grid>

View File

@ -39,7 +39,7 @@ body {
.profileNickname { .profileNickname {
margin: 0; margin: 0;
left: -22px; left: -16px;
} }
.newAvatar { .newAvatar {
@ -51,14 +51,19 @@ body {
width: 200px; width: 200px;
} }
.avatar { .profileAvatar {
border: 0.5px solid #555; border: 0.5px solid #555;
filter: drop-shadow(0.5px 0.5px 0.5px #000000); filter: drop-shadow(0.5px 0.5px 0.5px #000000);
left: 35px; left: 35px;
} }
.rotatedAvatar { .smallAvatar {
transform: scaleX(-1);
border: 0.5px solid #555; border: 0.5px solid #555;
filter: drop-shadow(0.5px 0.5px 0.5px #000000); filter: drop-shadow(0.5px 0.5px 0.5px #000000);
} }
.flippedSmallAvatar {
transform: scaleX(-1);
border: 0.3px solid #555;
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
}

View File

@ -134,3 +134,78 @@ Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4
### Launch the React render ### Launch the React render
from frontend/ directory from frontend/ directory
`npm run dev` `npm run dev`
## Robosats background threads.
There is 3 processes that run asynchronously: two admin commands and a celery beat scheduler.
The celery worker will run the task of caching external API market prices and cleaning(deleting) the generated robots that were never used.
`celery -A robosats worker --beat -l debug -S django`
The admin commands are used to keep an eye on the state of LND hold invoices and check whether orders have expired
```
python3 manage.py follow_invoices
python3 manage.py clean_order
```
It might be best to set up system services to continuously run these background processes.
### Follow invoices admin command as system service
Create `/etc/systemd/system/follow_invoices.service` and edit with:
```
[Unit]
Description=RoboSats Follow LND Invoices
After=lnd.service
StartLimitIntervalSec=0
[Service]
WorkingDirectory=/home/<USER>/robosats/
StandardOutput=file:/home/<USER>/robosats/follow_invoices.log
StandardError=file:/home/<USER>/robosats/follow_invoices.log
Type=simple
Restart=always
RestartSec=1
User=<USER>
ExecStart=python3 manage.py follow_invoices
[Install]
WantedBy=multi-user.target
```
Then launch it with
```
systemctl start follow_invoices
systemctl enable follow_invoices
```
### Clean orders admin command as system service
Create `/etc/systemd/system/clean_orders.service` and edit with (replace <USER> for your username):
```
[Unit]
Description=RoboSats Clean Orders
After=lnd.service
StartLimitIntervalSec=0
[Service]
WorkingDirectory=/home/<USER>/robosats/
StandardOutput=file:/home/<USER>/robosats/clean_orders.log
StandardError=file:/home/<USER>/robosats/clean_orders.log
Type=simple
Restart=always
RestartSec=1
User=<USER>
ExecStart=python3 manage.py clean_orders
[Install]
WantedBy=multi-user.target
```
Then launch it with
```
systemctl start clean_orders
systemctl enable clean_orders
```