mirror of
https://github.com/RoboSats/robosats.git
synced 2025-02-07 13:59:06 +00:00
Add setup background threads. Minor fixes and cosmetic.
This commit is contained in:
parent
64115a8bb5
commit
58ecb607c3
@ -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
|
||||||
|
@ -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
|
||||||
|
10
api/views.py
10
api/views.py
@ -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)
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
} },
|
} },
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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'}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
}
|
75
setup.md
75
setup.md
@ -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
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user