Implement backend handle range amounts, order take amount input and order book ranges

This commit is contained in:
Reckless_Satoshi 2022-03-22 10:49:57 -07:00
parent bf80986005
commit 8ae2406275
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
9 changed files with 263 additions and 103 deletions

View File

@ -46,7 +46,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"maker_link",
"taker_link",
"status",
"amount",
"amt",
"currency_link",
"t0_satoshis",
"is_disputed",
@ -69,7 +69,13 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"trade_escrow",
)
list_filter = ("is_disputed", "is_fiat_sent", "type", "currency", "status")
search_fields = ["id","amount"]
search_fields = ["id","amount","min_amount","max_amount"]
def amt(self, obj):
if obj.has_range and obj.amount == None:
return str(float(obj.min_amount))+"-"+ str(float(obj.max_amount))
else:
return float(obj.amount)
@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):

View File

@ -118,16 +118,16 @@ class Logics:
elif max_sats > MAX_TRADE:
return False, {
"bad_request":
"Your order upper range value (max_amount) is too big. It is worth " +
"{:,}".format(max_sats) +
"Your order maximum amount is too big. It is worth " +
"{:,}".format(int(max_sats)) +
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
" Sats"
}
elif min_sats < MIN_TRADE:
return False, {
"bad_request":
"Your order lower range value (min_mount) is too small. It is worth " +
"{:,}".format(min_sats) +
"Your order minimum amount is too small. It is worth " +
"{:,}".format(int(min_sats)) +
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
" Sats"
}
@ -139,6 +139,15 @@ class Logics:
return True, None
def validate_amount_within_range(order, amount):
if amount > float(order.max_amount) or amount < float(order.min_amount):
return False, {
"bad_request":
"The amount specified is outside the range specified by the maker"
}
return True, None
def user_activity_status(last_seen):
if last_seen > (timezone.now() - timedelta(minutes=2)):
return "Active"
@ -148,7 +157,7 @@ class Logics:
return "Inactive"
@classmethod
def take(cls, order, user):
def take(cls, order, user, amount=None):
is_penalized, time_out = cls.is_penalized(user)
if is_penalized:
return False, {
@ -156,6 +165,8 @@ class Logics:
f"You need to wait {time_out} seconds to take an order",
}
else:
if order.has_range:
order.amount= amount
order.taker = user
order.status = Order.Status.TAK
order.expires_at = timezone.now() + timedelta(
@ -702,6 +713,8 @@ class Logics:
order.status = Order.Status.PUB
order.expires_at = order.created_at + timedelta(
seconds=order.t_to_expire(Order.Status.PUB))
if order.has_range:
order.amount = None
order.save()
# send_message.delay(order.id,'order_published') # too spammy
return

View File

@ -339,7 +339,11 @@ class Order(models.Model):
taker_platform_rated = models.BooleanField(default=False, null=False)
def __str__(self):
return f"Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}"
if self.has_range and self.amount == None:
amt = str(float(self.min_amount))+"-"+ str(float(self.max_amount))
else:
amt = float(self.amount)
return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}"
def t_to_expire(self, status):

View File

@ -21,7 +21,7 @@ class ListOrderSerializer(serializers.ModelSerializer):
"is_explicit",
"premium",
"satoshis",
"bondless_taker"
"bondless_taker",
"maker",
"taker",
)
@ -75,6 +75,7 @@ class UpdateOrderSerializer(serializers.Serializer):
allow_blank=True,
default=None,
)
amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None)
class ClaimRewardSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000,

View File

@ -390,6 +390,10 @@ class OrderView(viewsets.ViewSet):
"""
order_id = request.GET.get(self.lookup_url_kwarg)
import sys
sys.stdout.write('AAAAAA')
print('BBBBB1')
serializer = UpdateOrderSerializer(data=request.data)
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
@ -410,7 +414,17 @@ class OrderView(viewsets.ViewSet):
request.user)
if not valid:
return Response(context, status=status.HTTP_409_CONFLICT)
valid, context = Logics.take(order, request.user)
# For order with amount range, set the amount now.
if order.has_range:
amount = float(serializer.data.get("amount"))
valid, context = Logics.validate_amount_within_range(order, amount)
if not valid:
return Response(context, status=status.HTTP_400_BAD_REQUEST)
valid, context = Logics.take(order, request.user, amount)
else:
valid, context = Logics.take(order, request.user)
if not valid:
return Response(context, status=status.HTTP_403_FORBIDDEN)

View File

@ -78,6 +78,15 @@ export default class BookPage extends Component {
if(status=='Seen recently'){return("warning")}
if(status=='Inactive'){return('error')}
}
amountToString = (amount,has_range,min_amount,max_amount) => {
if (has_range){
console.log(this.pn(parseFloat(Number(min_amount).toPrecision(2))))
console.log(this.pn(parseFloat(Number(min_amount).toPrecision(2)))+'-'+this.pn(parseFloat(Number(max_amount).toPrecision(2))))
return this.pn(parseFloat(Number(min_amount).toPrecision(2)))+'-'+this.pn(parseFloat(Number(max_amount).toPrecision(2)))
}else{
return this.pn(parseFloat(Number(amount).toPrecision(3)))
}
}
bookListTableDesktop=()=>{
return (
@ -90,7 +99,10 @@ export default class BookPage extends Component {
robot: order.maker_nick,
robot_status: order.maker_status,
type: order.type ? "Seller": "Buyer",
amount: parseFloat(parseFloat(order.amount).toFixed(5)),
amount: order.amount,
has_range: order.has_range,
min_amount: order.min_amount,
max_amount: order.max_amount,
currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method,
price: order.price,
@ -123,9 +135,9 @@ export default class BookPage extends Component {
);
} },
{ field: 'type', headerName: 'Is', width: 60 },
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80,
{ field: 'amount', headerName: 'Amount', type: 'number', width: 90,
renderCell: (params) => {return (
<div style={{ cursor: "pointer" }}>{this.pn(params.row.amount)}</div>
<div style={{ cursor: "pointer" }}>{this.amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)}</div>
)}},
{ field: 'currency', headerName: 'Currency', width: 100,
renderCell: (params) => {return (
@ -163,7 +175,10 @@ export default class BookPage extends Component {
robot: order.maker_nick,
robot_status: order.maker_status,
type: order.type ? "Seller": "Buyer",
amount: parseFloat(parseFloat(order.amount).toFixed(4)),
amount: order.amount,
has_range: order.has_range,
min_amount: order.min_amount,
max_amount: order.max_amount,
currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method,
price: order.price,
@ -191,10 +206,10 @@ export default class BookPage extends Component {
);
} },
{ field: 'type', headerName: 'Is', width: 60, hide:'true'},
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80,
{ field: 'amount', headerName: 'Amount', type: 'number', width: 90,
renderCell: (params) => {return (
<Tooltip placement="right" enterTouchDelay="0" title={params.row.type}>
<div style={{ cursor: "pointer" }}>{this.pn(params.row.amount)}</div>
<div style={{ cursor: "pointer" }}>{this.amountToString(params.row.amount,params.row.has_range, params.row.min_amount, params.row.max_amount)}</div>
</Tooltip>
)} },
{ field: 'currency', headerName: 'Currency', width: 100,

View File

@ -1,12 +1,12 @@
import React, { Component } from 'react';
import { LinearProgress, Checkbox, Slider, Switch, Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
import { LinearProgress, Checkbox, Slider, SliderThumb, Switch, Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
import { LocalizationProvider, TimePicker} from '@mui/lab';
import DateFnsUtils from "@date-io/date-fns";
import { Link } from 'react-router-dom'
import { styled } from '@mui/material/styles';
import getFlags from './getFlags'
import LockIcon from '@mui/icons-material/Lock';
import SelfImprovementIcon from '@mui/icons-material/SelfImprovement';
function getCookie(name) {
let cookieValue = null;
@ -43,8 +43,8 @@ export default class MakerPage extends Component {
minTradeSats = 20000;
maxTradeSats = 800000;
maxBondlessSats = 50000;
maxRangeAmountMultiple = 5;
minRangeAmountMultiple = 1.5;
maxRangeAmountMultiple = 4.9;
minRangeAmountMultiple = 1.6;
constructor(props) {
super(props);
@ -79,9 +79,8 @@ export default class MakerPage extends Component {
limits:data,
loadingLimits:false,
minAmount: parseFloat(Number(data[this.state.currency]['max_amount']*0.25).toPrecision(2)),
maxAmount: parseFloat(Number(data[this.state.currency]['max_amount']*0.75).toPrecision(2))
})
& console.log(this.state.limits));
maxAmount: parseFloat(Number(data[this.state.currency]['max_amount']*0.75).toPrecision(2)),
}));
}
handleTypeChange=(e)=>{
@ -94,6 +93,12 @@ export default class MakerPage extends Component {
currency: e.target.value,
currencyCode: this.getCurrencyCode(e.target.value),
});
if(this.state.enableAmountRange){
this.setState({
minAmount: parseFloat(Number(this.state.limits[e.target.value]['max_amount']*0.25).toPrecision(2)),
maxAmount: parseFloat(Number(this.state.limits[e.target.value]['max_amount']*0.75).toPrecision(2)),
})
}
}
handleAmountChange=(e)=>{
this.setState({
@ -101,7 +106,7 @@ export default class MakerPage extends Component {
});
}
handleRangeAmountChange = (e, activeThumb) => {
handleRangeAmountChange = (e, newValue, activeThumb) => {
var maxAmount = this.getMaxAmount();
var minAmount = this.getMinAmount();
var lowerValue = e.target.value[0];
@ -379,7 +384,6 @@ export default class MakerPage extends Component {
}
handleChangePublicDuration = (date) => {
console.log(date)
let d = new Date(date),
hours = d.getHours(),
minutes = d.getMinutes();
@ -401,7 +405,8 @@ export default class MakerPage extends Component {
}else{
var max_amount = this.state.limits[this.state.currency]['max_amount']
}
return parseFloat(Number(max_amount).toPrecision(2))
// times 0.98 to allow a bit of margin with respect to the backend minimum
return parseFloat(Number(max_amount*0.98).toPrecision(2))
}
getMinAmount = () => {
@ -410,10 +415,52 @@ export default class MakerPage extends Component {
}else{
var min_amount = this.state.limits[this.state.currency]['min_amount']
}
return parseFloat(Number(min_amount).toPrecision(2))
// times 1.1 to allow a bit of margin with respect to the backend minimum
return parseFloat(Number(min_amount*1.1).toPrecision(2))
}
RangeSlider = styled(Slider)(({ theme }) => ({
color: 'primary',
height: 3,
padding: '13px 0',
'& .MuiSlider-thumb': {
height: 27,
width: 27,
backgroundColor: '#fff',
border: '1px solid currentColor',
'&:hover': {
boxShadow: '0 0 0 8px rgba(58, 133, 137, 0.16)',
},
'& .range-bar': {
height: 9,
width: 1,
backgroundColor: 'currentColor',
marginLeft: 1,
marginRight: 1,
},
},
'& .MuiSlider-track': {
height: 3,
},
'& .MuiSlider-rail': {
color: theme.palette.mode === 'dark' ? '#bfbfbf' : '#d8d8d8',
opacity: theme.palette.mode === 'dark' ? undefined : 1,
height: 3,
},
}));
RangeThumbComponent(props) {
const { children, ...other } = props;
return (
<SliderThumb {...other}>
{children}
<span className="range-bar" />
<span className="range-bar" />
<span className="range-bar" />
</SliderThumb>
);
}
AdvancedMakerOptions = () => {
return(
<Paper elevation={12} style={{ padding: 8, width:250, align:'center'}}>
@ -421,6 +468,61 @@ export default class MakerPage extends Component {
<Grid container xs={12} spacing={1}>
<Grid item xs={12} align="center" spacing={1}>
<FormControl align="center">
<FormHelperText>
<Tooltip enterTouchDelay="0" placement="top" align="center"title={"Let the taker chose an amount within the range"}>
<div align="center" style={{display:'flex',alignItems:'center', flexWrap:'wrap'}}>
<Checkbox onChange={(e)=>this.setState({enableAmountRange:e.target.checked}) & (e.target.checked ? this.getLimits() : null)}/>
{this.state.enableAmountRange & this.state.minAmount != null?
"From "+this.state.minAmount+" to "+this.state.maxAmount +" "+this.state.currencyCode: "Enable Amount Range"}
</div>
</Tooltip>
</FormHelperText>
<div style={{ display: this.state.loadingLimits == true ? '':'none'}}>
<LinearProgress />
</div>
<div style={{ display: this.state.loadingLimits == false ? '':'none'}}>
<this.RangeSlider
disableSwap={true}
sx={{width:200, align:"center"}}
disabled={!this.state.enableAmountRange || this.state.loadingLimits}
value={[this.state.minAmount, this.state.maxAmount]}
step={(this.getMaxAmount()-this.getMinAmount())/100}
valueLabelDisplay="auto"
components={{ Thumb: this.RangeThumbComponent }}
valueLabelFormat={(x) => (parseFloat(Number(x).toPrecision(2))+" "+this.state.currencyCode)}
marks={this.state.limits == null?
null
:
[{value: this.getMinAmount(),label: this.getMinAmount()+" "+ this.state.currencyCode},
{value: this.getMaxAmount(),label: this.getMaxAmount()+" "+this.state.currencyCode}]}
min={this.getMinAmount()}
max={this.getMaxAmount()}
onChange={this.handleRangeAmountChange}
/>
</div>
</FormControl>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<LocalizationProvider dateAdapter={DateFnsUtils}>
<TimePicker
ampm={false}
openTo="hours"
views={['hours', 'minutes']}
inputFormat="HH:mm"
mask="__:__"
renderInput={(props) => <TextField {...props} />}
label="Public Duration (HH:mm)"
value={this.state.publicExpiryTime}
onChange={this.handleChangePublicDuration}
minTime={new Date(0, 0, 0, 0, 10)}
maxTime={new Date(0, 0, 0, 23, 59)}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<FormControl align="center">
<Tooltip enterDelay="800" enterTouchDelay="0" placement="top" title={"Set the skin-in-the-game, increase for higher safety assurance"}>
<FormHelperText>
@ -445,59 +547,6 @@ export default class MakerPage extends Component {
</FormControl>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<LocalizationProvider dateAdapter={DateFnsUtils}>
<TimePicker
ampm={false}
openTo="hours"
views={['hours', 'minutes']}
inputFormat="HH:mm"
mask="__:__"
renderInput={(props) => <TextField {...props} />}
label="Public Duration (HH:mm)"
value={this.state.publicExpiryTime}
onChange={this.handleChangePublicDuration}
minTime={new Date(0, 0, 0, 0, 10)}
maxTime={new Date(0, 0, 0, 23, 59)}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<FormControl align="center">
<FormHelperText>
<Tooltip enterTouchDelay="0" placement="top" align="center"title={"Let the taker chose an amount within the range"}>
<div align="center" style={{display:'flex',alignItems:'center', flexWrap:'wrap'}}>
<Checkbox onChange={(e)=>this.setState({enableAmountRange:e.target.checked}) & (e.target.checked ? this.getLimits() : null)}/>
Amount Range
</div>
</Tooltip>
</FormHelperText>
<div style={{ display: this.state.loadingLimits == true ? '':'none'}}>
<LinearProgress />
</div>
<div style={{ display: this.state.loadingLimits == false ? '':'none'}}>
<Slider
disableSwap={true}
sx={{width:190, align:"center"}}
disabled={!this.state.enableAmountRange || this.state.loadingLimits}
value={[this.state.minAmount, this.state.maxAmount]}
step={(this.getMaxAmount()-this.getMinAmount())/100}
valueLabelDisplay="auto"
valueLabelFormat={(x) => (parseFloat(Number(x).toPrecision(2))+" "+this.state.currencyCode)}
marks={this.state.limits == null?
null
:
[{value: this.getMinAmount(),label: this.getMinAmount()+" "+ this.state.currencyCode},
{value: this.getMaxAmount(),label: this.getMaxAmount()+" "+this.state.currencyCode}]}
min={this.getMinAmount()}
max={this.getMaxAmount()}
onChange={this.handleRangeAmountChange}
/>
</div>
</FormControl>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<Tooltip enterTouchDelay="0" title={"COMING SOON - High risk! Limited to "+ this.maxBondlessSats/1000 +"K Sats"}>
<FormControlLabel
@ -554,8 +603,8 @@ export default class MakerPage extends Component {
<Grid item xs={12} align="center">
{/* conditions to disable the make button */}
{(this.state.amount == null ||
this.state.amount <= 0 ||
{(this.state.amount == null & (this.state.enableAmountRange == false & this.state.minAmount != null) ||
this.state.amount <= 0 & !this.state.enableAmountRange ||
(this.state.is_explicit & (this.state.badSatoshis != null || this.state.satoshis == null)) ||
(!this.state.is_explicit & this.state.badPremium != null))
?

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Chip, Tooltip, Badge, Tab, Tabs, Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import {TextField,Chip, Tooltip, Badge, Tab, Tabs, 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 MediaQuery from 'react-responsive'
@ -89,6 +89,7 @@ export default class OrderPage extends Component {
}
var otherStateVars = {
amount: newStateVars.amount ? newStateVars.amount : null,
loading: false,
delay: this.setDelay(newStateVars.status),
currencyCode: this.getCurrencyCode(newStateVars.currency),
@ -157,32 +158,81 @@ export default class OrderPage extends Component {
}
};
countdownTakeOrderRenderer = ({ seconds, completed }) => {
if(isNaN(seconds)){
return (
<>
handleTakeAmountChange = (e) => {
if (e.target.value != "" & e.target.value != null){
this.setState({takeAmount: parseFloat(e.target.value)})
}else{
this.setState({takeAmount: e.target.value})
}
}
amountHelperText=()=>{
if(this.state.takeAmount < this.state.min_amount & this.state.takeAmount != ""){
return "Too low"
}else if (this.state.takeAmount > this.state.max_amount & this.state.takeAmount != ""){
return "Too high"
}else{
return null
}
}
takeOrderButton = () => {
if(this.state.has_range){
return(
<Grid containter xs={12} align="center" alignItems="stretch" justifyContent="center" style={{ display: "flex"}}>
<this.InactiveMakerDialog/>
<div style={{maxWidth:120}}>
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Amount of fiat to exchange for bitcoin">
<Paper sx={{maxHeight:40}}>
<TextField
error={(this.state.takeAmount < this.state.min_amount || this.state.takeAmount > this.state.max_amount) & this.state.takeAmount != "" }
helperText={this.amountHelperText()}
label={"Amount "+this.state.currencyCode}
size="small"
type="number"
required="true"
value={this.state.takeAmount}
inputProps={{
min:this.state.min_amount ,
max:this.state.max_amount ,
style: {textAlign:"center"}
}}
onChange={this.handleTakeAmountChange}
/>
</Paper>
</Tooltip>
</div>
<div style={{height:38, top:'1px', position:'relative'}}>
<Button sx={{height:38}} variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}
disabled={this.state.takeAmount < this.state.min_amount || this.state.takeAmount > this.state.max_amount || this.state.takeAmount == "" || this.state.takeAmount == null } >
Take Order
</Button>
</div>
</Grid>
)
}else{
return(
<>
<this.InactiveMakerDialog/>
<Button variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>)
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>
)
}
}
countdownTakeOrderRenderer = ({ seconds, completed }) => {
if(isNaN(seconds)){return (<this.takeOrderButton/>)}
if (completed) {
// Render a completed state
return (
<>
<this.InactiveMakerDialog/>
<Button variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>
);
return ( <this.takeOrderButton/>);
} else{
return(
<Tooltip enterTouchDelay="0" title="Wait until you can take an order"><div>
<Button disabled={true} variant='contained' color='primary' onClick={this.takeOrder}>Take Order</Button>
<Button disabled={true} variant='contained' color='primary'>Take Order</Button>
</div></Tooltip>)
}
};
@ -212,12 +262,13 @@ export default class OrderPage extends Component {
takeOrder=()=>{
this.setState({loading:true})
console.log(this.state.takeAmount)
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'take',
'amount':this.state.takeAmount,
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
@ -483,10 +534,17 @@ export default class OrderPage extends Component {
<ListItemIcon>
{getFlags(this.state.currencyCode)}
</ListItemIcon>
{this.state.has_range & this.state.amount == null ?
<ListItemText primary={parseFloat(Number(this.state.min_amount).toPrecision(2))
+"-" + parseFloat(Number(this.state.max_amount).toPrecision(2)) +" "+this.state.currencyCode} secondary="Amount range"/>
:
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))
+" "+this.state.currencyCode} secondary="Amount"/>
}
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<PaymentsIcon/>

File diff suppressed because one or more lines are too long