mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 20:21:35 +00:00
Implement min_amount max_amount model props and validation
This commit is contained in:
parent
2998c265d6
commit
bf80986005
@ -87,24 +87,56 @@ class Logics:
|
|||||||
|
|
||||||
return True, None, None
|
return True, None, None
|
||||||
|
|
||||||
def validate_order_size(order):
|
@classmethod
|
||||||
"""Validates if order is withing limits in satoshis at t0"""
|
def validate_order_size(cls, order):
|
||||||
if order.t0_satoshis > MAX_TRADE:
|
"""Validates if order size in Sats is within limits at t0"""
|
||||||
return False, {
|
if not order.has_range:
|
||||||
"bad_request":
|
if order.t0_satoshis > MAX_TRADE:
|
||||||
"Your order is too big. It is worth " +
|
return False, {
|
||||||
"{:,}".format(order.t0_satoshis) +
|
"bad_request":
|
||||||
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
|
"Your order is too big. It is worth " +
|
||||||
" Sats"
|
"{:,}".format(order.t0_satoshis) +
|
||||||
}
|
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
|
||||||
if order.t0_satoshis < MIN_TRADE:
|
" Sats"
|
||||||
return False, {
|
}
|
||||||
"bad_request":
|
if order.t0_satoshis < MIN_TRADE:
|
||||||
"Your order is too small. It is worth " +
|
return False, {
|
||||||
"{:,}".format(order.t0_satoshis) +
|
"bad_request":
|
||||||
" Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
|
"Your order is too small. It is worth " +
|
||||||
" Sats"
|
"{:,}".format(order.t0_satoshis) +
|
||||||
}
|
" Sats now, but the limit is " + "{:,}".format(MIN_TRADE) +
|
||||||
|
" Sats"
|
||||||
|
}
|
||||||
|
elif order.has_range:
|
||||||
|
min_sats = cls.calc_sats(order.min_amount, order.currency.exchange_rate, order.premium)
|
||||||
|
max_sats = cls.calc_sats(order.max_amount, order.currency.exchange_rate, order.premium)
|
||||||
|
if min_sats > max_sats/1.5:
|
||||||
|
return False, {
|
||||||
|
"bad_request":
|
||||||
|
"min_sats*1.5 has to be smaller than max_sats"
|
||||||
|
}
|
||||||
|
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) +
|
||||||
|
" 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) +
|
||||||
|
" Sats now, but the limit is " + "{:,}".format(MAX_TRADE) +
|
||||||
|
" Sats"
|
||||||
|
}
|
||||||
|
elif min_sats < max_sats/5:
|
||||||
|
return False, {
|
||||||
|
"bad_request":
|
||||||
|
f"Your order amount range is too large. Max amount can only be 5 times bigger than min amount"
|
||||||
|
}
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
def user_activity_status(last_seen):
|
def user_activity_status(last_seen):
|
||||||
@ -144,15 +176,19 @@ class Logics:
|
|||||||
return (is_maker and order.type == Order.Types.SELL) or (
|
return (is_maker and order.type == Order.Types.SELL) or (
|
||||||
is_taker and order.type == Order.Types.BUY)
|
is_taker and order.type == Order.Types.BUY)
|
||||||
|
|
||||||
def satoshis_now(order):
|
def calc_sats(amount, exchange_rate, premium):
|
||||||
|
exchange_rate = float(exchange_rate)
|
||||||
|
premium_rate = exchange_rate * (1 + float(premium) / 100)
|
||||||
|
return (float(amount) /premium_rate) * 100 * 1000 * 1000
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def satoshis_now(cls, order):
|
||||||
"""checks trade amount in sats"""
|
"""checks trade amount in sats"""
|
||||||
if order.is_explicit:
|
if order.is_explicit:
|
||||||
satoshis_now = order.satoshis
|
satoshis_now = order.satoshis
|
||||||
else:
|
else:
|
||||||
exchange_rate = float(order.currency.exchange_rate)
|
amount = order.amount if order.amount != None else order.max_amount
|
||||||
premium_rate = exchange_rate * (1 + float(order.premium) / 100)
|
satoshis_now = cls.calc_sats(amount, order.currency.exchange_rate, order.premium)
|
||||||
satoshis_now = (float(order.amount) /
|
|
||||||
premium_rate) * 100 * 1000 * 1000
|
|
||||||
|
|
||||||
return int(satoshis_now)
|
return int(satoshis_now)
|
||||||
|
|
||||||
|
@ -192,14 +192,15 @@ class Order(models.Model):
|
|||||||
currency = models.ForeignKey(Currency,
|
currency = models.ForeignKey(Currency,
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=models.SET_NULL)
|
on_delete=models.SET_NULL)
|
||||||
amount = models.DecimalField(max_digits=16,
|
amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||||
decimal_places=8,
|
has_range = models.BooleanField(default=False, null=False, blank=False)
|
||||||
validators=[MinValueValidator(0.00000001)])
|
min_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||||
|
max_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||||
payment_method = models.CharField(max_length=35,
|
payment_method = models.CharField(max_length=35,
|
||||||
null=False,
|
null=False,
|
||||||
default="not specified",
|
default="not specified",
|
||||||
blank=True)
|
blank=True)
|
||||||
|
bondless_taker = models.BooleanField(default=False, null=False, blank=False)
|
||||||
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
||||||
is_explicit = models.BooleanField(default=False, null=False)
|
is_explicit = models.BooleanField(default=False, null=False)
|
||||||
# marked to market
|
# marked to market
|
||||||
|
@ -14,10 +14,14 @@ class ListOrderSerializer(serializers.ModelSerializer):
|
|||||||
"type",
|
"type",
|
||||||
"currency",
|
"currency",
|
||||||
"amount",
|
"amount",
|
||||||
|
"has_range",
|
||||||
|
"min_amount",
|
||||||
|
"max_amount",
|
||||||
"payment_method",
|
"payment_method",
|
||||||
"is_explicit",
|
"is_explicit",
|
||||||
"premium",
|
"premium",
|
||||||
"satoshis",
|
"satoshis",
|
||||||
|
"bondless_taker"
|
||||||
"maker",
|
"maker",
|
||||||
"taker",
|
"taker",
|
||||||
)
|
)
|
||||||
@ -31,12 +35,16 @@ class MakeOrderSerializer(serializers.ModelSerializer):
|
|||||||
"type",
|
"type",
|
||||||
"currency",
|
"currency",
|
||||||
"amount",
|
"amount",
|
||||||
|
"has_range",
|
||||||
|
"min_amount",
|
||||||
|
"max_amount",
|
||||||
"payment_method",
|
"payment_method",
|
||||||
"is_explicit",
|
"is_explicit",
|
||||||
"premium",
|
"premium",
|
||||||
"satoshis",
|
"satoshis",
|
||||||
"public_duration",
|
"public_duration",
|
||||||
"bond_size",
|
"bond_size",
|
||||||
|
"bondless_taker",
|
||||||
)
|
)
|
||||||
|
|
||||||
class UpdateOrderSerializer(serializers.Serializer):
|
class UpdateOrderSerializer(serializers.Serializer):
|
||||||
|
49
api/views.py
49
api/views.py
@ -66,33 +66,64 @@ class MakerView(CreateAPIView):
|
|||||||
},
|
},
|
||||||
status.HTTP_400_BAD_REQUEST,
|
status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
# Only allow users who are not already engaged in an order
|
||||||
|
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
|
||||||
|
if not valid:
|
||||||
|
return Response(context, status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
type = serializer.data.get("type")
|
type = serializer.data.get("type")
|
||||||
currency = serializer.data.get("currency")
|
currency = serializer.data.get("currency")
|
||||||
amount = serializer.data.get("amount")
|
amount = serializer.data.get("amount")
|
||||||
|
has_range = serializer.data.get("has_range")
|
||||||
|
min_amount = serializer.data.get("min_amount")
|
||||||
|
max_amount = serializer.data.get("max_amount")
|
||||||
payment_method = serializer.data.get("payment_method")
|
payment_method = serializer.data.get("payment_method")
|
||||||
premium = serializer.data.get("premium")
|
premium = serializer.data.get("premium")
|
||||||
satoshis = serializer.data.get("satoshis")
|
satoshis = serializer.data.get("satoshis")
|
||||||
is_explicit = serializer.data.get("is_explicit")
|
is_explicit = serializer.data.get("is_explicit")
|
||||||
public_duration = serializer.data.get("public_duration")
|
public_duration = serializer.data.get("public_duration")
|
||||||
bond_size = serializer.data.get("bond_size")
|
bond_size = serializer.data.get("bond_size")
|
||||||
|
bondless_taker = serializer.data.get("bondless_taker")
|
||||||
|
|
||||||
# Optional params
|
# Optional params
|
||||||
if public_duration == None:
|
if public_duration == None: public_duration = PUBLIC_DURATION
|
||||||
public_duration = PUBLIC_DURATION
|
if bond_size == None: bond_size = BOND_SIZE
|
||||||
if bond_size == None:
|
if bondless_taker == None: bondless_taker = False
|
||||||
bond_size = BOND_SIZE
|
if has_range == None: has_range = False
|
||||||
|
|
||||||
valid, context, _ = Logics.validate_already_maker_or_taker(
|
# An order can either have an amount or a range (min_amount and max_amount)
|
||||||
request.user)
|
if has_range:
|
||||||
if not valid:
|
amount = None
|
||||||
return Response(context, status.HTTP_409_CONFLICT)
|
else:
|
||||||
|
min_amount = None
|
||||||
|
max_amount = None
|
||||||
|
|
||||||
|
# Either amount or min_max has to be specified.
|
||||||
|
if has_range and (min_amount == None or max_amount == None):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"bad_request":
|
||||||
|
"You must specify min_amount and max_amount for a range order"
|
||||||
|
},
|
||||||
|
status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
elif not has_range and amount == None:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"bad_request":
|
||||||
|
"You must specify an order amount"
|
||||||
|
},
|
||||||
|
status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Creates a new order
|
# Creates a new order
|
||||||
order = Order(
|
order = Order(
|
||||||
type=type,
|
type=type,
|
||||||
currency=Currency.objects.get(id=currency),
|
currency=Currency.objects.get(id=currency),
|
||||||
amount=amount,
|
amount=amount,
|
||||||
|
has_range=has_range,
|
||||||
|
min_amount=min_amount,
|
||||||
|
max_amount=max_amount,
|
||||||
payment_method=payment_method,
|
payment_method=payment_method,
|
||||||
premium=premium,
|
premium=premium,
|
||||||
satoshis=satoshis,
|
satoshis=satoshis,
|
||||||
@ -102,9 +133,9 @@ class MakerView(CreateAPIView):
|
|||||||
maker=request.user,
|
maker=request.user,
|
||||||
public_duration=public_duration,
|
public_duration=public_duration,
|
||||||
bond_size=bond_size,
|
bond_size=bond_size,
|
||||||
|
bondless_taker=bondless_taker,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO move to Order class method when new instance is created!
|
|
||||||
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
|
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
|
||||||
|
|
||||||
valid, context = Logics.validate_order_size(order)
|
valid, context = Logics.validate_order_size(order)
|
||||||
|
@ -78,9 +78,9 @@ export default class MakerPage extends Component {
|
|||||||
.then((data) => this.setState({
|
.then((data) => this.setState({
|
||||||
limits:data,
|
limits:data,
|
||||||
loadingLimits:false,
|
loadingLimits:false,
|
||||||
minAmount: Number(data[this.state.currency]['max_amount']*0.25),
|
minAmount: parseFloat(Number(data[this.state.currency]['max_amount']*0.25).toPrecision(2)),
|
||||||
maxAmount: Number(data[this.state.currency]['max_amount']*0.75),
|
maxAmount: parseFloat(Number(data[this.state.currency]['max_amount']*0.75).toPrecision(2))
|
||||||
amount: ""})
|
})
|
||||||
& console.log(this.state.limits));
|
& console.log(this.state.limits));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,8 +131,8 @@ export default class MakerPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
minAmount: lowerValue,
|
minAmount: parseFloat(Number(lowerValue).toPrecision(2)),
|
||||||
maxAmount: upperValue,
|
maxAmount: parseFloat(Number(upperValue).toPrecision(2)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,12 +192,16 @@ export default class MakerPage extends Component {
|
|||||||
type: this.state.type,
|
type: this.state.type,
|
||||||
currency: this.state.currency,
|
currency: this.state.currency,
|
||||||
amount: this.state.amount,
|
amount: this.state.amount,
|
||||||
|
has_range: this.state.enableAmountRange,
|
||||||
|
min_amount: this.state.minAmount,
|
||||||
|
max_amount: this.state.maxAmount,
|
||||||
payment_method: this.state.payment_method,
|
payment_method: this.state.payment_method,
|
||||||
is_explicit: this.state.is_explicit,
|
is_explicit: this.state.is_explicit,
|
||||||
premium: this.state.is_explicit ? null: this.state.premium,
|
premium: this.state.is_explicit ? null: this.state.premium,
|
||||||
satoshis: this.state.is_explicit ? this.state.satoshis: null,
|
satoshis: this.state.is_explicit ? this.state.satoshis: null,
|
||||||
public_duration: this.state.publicDuration,
|
public_duration: this.state.publicDuration,
|
||||||
bond_size: this.state.bondSize,
|
bond_size: this.state.bondSize,
|
||||||
|
bondless_taker: this.state.allowBondless,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
fetch("/api/make/",requestOptions)
|
fetch("/api/make/",requestOptions)
|
||||||
@ -253,6 +257,7 @@ export default class MakerPage extends Component {
|
|||||||
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Amount of fiat to exchange for bitcoin">
|
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Amount of fiat to exchange for bitcoin">
|
||||||
<TextField
|
<TextField
|
||||||
disabled = {this.state.enableAmountRange}
|
disabled = {this.state.enableAmountRange}
|
||||||
|
variant = {this.state.enableAmountRange ? 'filled' : 'outlined'}
|
||||||
error={this.state.amount <= 0 & this.state.amount != "" }
|
error={this.state.amount <= 0 & this.state.amount != "" }
|
||||||
helperText={this.state.amount <= 0 & this.state.amount != "" ? 'Invalid' : null}
|
helperText={this.state.amount <= 0 & this.state.amount != "" ? 'Invalid' : null}
|
||||||
label="Amount"
|
label="Amount"
|
||||||
@ -396,7 +401,7 @@ export default class MakerPage extends Component {
|
|||||||
}else{
|
}else{
|
||||||
var max_amount = this.state.limits[this.state.currency]['max_amount']
|
var max_amount = this.state.limits[this.state.currency]['max_amount']
|
||||||
}
|
}
|
||||||
return parseFloat(Number(max_amount).toPrecision(3))
|
return parseFloat(Number(max_amount).toPrecision(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinAmount = () => {
|
getMinAmount = () => {
|
||||||
@ -405,7 +410,7 @@ export default class MakerPage extends Component {
|
|||||||
}else{
|
}else{
|
||||||
var min_amount = this.state.limits[this.state.currency]['min_amount']
|
var min_amount = this.state.limits[this.state.currency]['min_amount']
|
||||||
}
|
}
|
||||||
return parseFloat(Number(min_amount).toPrecision(3))
|
return parseFloat(Number(min_amount).toPrecision(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -417,14 +422,13 @@ export default class MakerPage extends Component {
|
|||||||
|
|
||||||
<Grid item xs={12} align="center" spacing={1}>
|
<Grid item xs={12} align="center" spacing={1}>
|
||||||
<FormControl align="center">
|
<FormControl align="center">
|
||||||
|
<Tooltip enterDelay="800" enterTouchDelay="0" placement="top" title={"Set the skin-in-the-game, increase for higher safety assurance"}>
|
||||||
<FormHelperText>
|
<FormHelperText>
|
||||||
<Tooltip enterTouchDelay="0" placement="top" title={"Set the skin-in-the-game (increase for higher safety assurance)"}>
|
|
||||||
<div align="center" style={{display:'flex',flexWrap:'wrap', transform: 'translate(20%, 0)'}}>
|
<div align="center" style={{display:'flex',flexWrap:'wrap', transform: 'translate(20%, 0)'}}>
|
||||||
Fidelity Bond Size <LockIcon sx={{height:20,width:20}}/>
|
Fidelity Bond Size <LockIcon sx={{height:20,width:20}}/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
|
</Tooltip>
|
||||||
<Slider
|
<Slider
|
||||||
sx={{width:220, align:"center"}}
|
sx={{width:220, align:"center"}}
|
||||||
aria-label="Bond Size (%)"
|
aria-label="Bond Size (%)"
|
||||||
@ -462,7 +466,7 @@ export default class MakerPage extends Component {
|
|||||||
<Grid item xs={12} align="center" spacing={1}>
|
<Grid item xs={12} align="center" spacing={1}>
|
||||||
<FormControl align="center">
|
<FormControl align="center">
|
||||||
<FormHelperText>
|
<FormHelperText>
|
||||||
<Tooltip enterTouchDelay="0" title={"Let the taker chose an amount within a range"}>
|
<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'}}>
|
<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)}/>
|
<Checkbox onChange={(e)=>this.setState({enableAmountRange:e.target.checked}) & (e.target.checked ? this.getLimits() : null)}/>
|
||||||
Amount Range
|
Amount Range
|
||||||
@ -475,12 +479,12 @@ export default class MakerPage extends Component {
|
|||||||
<div style={{ display: this.state.loadingLimits == false ? '':'none'}}>
|
<div style={{ display: this.state.loadingLimits == false ? '':'none'}}>
|
||||||
<Slider
|
<Slider
|
||||||
disableSwap={true}
|
disableSwap={true}
|
||||||
sx={{width:200, align:"center"}}
|
sx={{width:190, align:"center"}}
|
||||||
disabled={!this.state.enableAmountRange || this.state.loadingLimits}
|
disabled={!this.state.enableAmountRange || this.state.loadingLimits}
|
||||||
value={[this.state.minAmount, this.state.maxAmount]}
|
value={[this.state.minAmount, this.state.maxAmount]}
|
||||||
step={this.getMaxAmount()/1000}
|
step={(this.getMaxAmount()-this.getMinAmount())/100}
|
||||||
//valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
valueLabelFormat={(x) => (Number(x).toPrecision(3)+" "+this.state.currencyCode)}
|
valueLabelFormat={(x) => (parseFloat(Number(x).toPrecision(2))+" "+this.state.currencyCode)}
|
||||||
marks={this.state.limits == null?
|
marks={this.state.limits == null?
|
||||||
null
|
null
|
||||||
:
|
:
|
||||||
@ -571,7 +575,8 @@ export default class MakerPage extends Component {
|
|||||||
: ""}
|
: ""}
|
||||||
<Typography component="subtitle2" variant="subtitle2">
|
<Typography component="subtitle2" variant="subtitle2">
|
||||||
<div align='center'>
|
<div align='center'>
|
||||||
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {pn(this.state.amount)} {this.state.currencyCode}
|
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.enableAmountRange & this.state.minAmount != null?
|
||||||
|
" "+this.state.minAmount+"-"+this.state.maxAmount : pn(this.state.amount)} {this.state.currencyCode}
|
||||||
{this.state.is_explicit ? " of " + pn(this.state.satoshis) + " Satoshis" :
|
{this.state.is_explicit ? " of " + pn(this.state.satoshis) + " Satoshis" :
|
||||||
(this.state.premium == 0 ? " at market price" :
|
(this.state.premium == 0 ? " at market price" :
|
||||||
(this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount")
|
(this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount")
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user