Advanced maker options v2 (#110)

* Add escrow/invoice time customization

* Add accordion for Expiry times

* Add current price on order maker

* Add deposit timeout limit on order page

* Minor aestetic fixes

* Implement pause/unpause and expiry reasons

* Add renew order

* Add highlight buy/sell on maker page

* Fix order renewal. Improve book visuals and response.

* Fix double renew requests

* Fix cancel orders. Fix paused status to delay

* Fix paused order layout and loading spinner

* Add telegram message: order is in chat
This commit is contained in:
Reckless_Satoshi 2022-04-29 18:54:20 +00:00 committed by GitHub
parent 0b7d2754d1
commit 755874b100
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 568 additions and 16237 deletions

View File

@ -38,6 +38,7 @@ class Logics:
active_order_status = [
Order.Status.WFB,
Order.Status.PUB,
Order.Status.PAU,
Order.Status.TAK,
Order.Status.WF2,
Order.Status.WFE,
@ -230,7 +231,6 @@ class Logics:
# Do not change order status if an order in any with
# any of these status is sent to expire here
does_not_expire = [
Order.Status.DEL,
Order.Status.UCA,
Order.Status.EXP,
Order.Status.TLD,
@ -247,13 +247,15 @@ class Logics:
elif order.status == Order.Status.WFB:
order.status = Order.Status.EXP
order.expiry_reason = Order.ExpiryReasons.NMBOND
cls.cancel_bond(order.maker_bond)
order.save()
return True
elif order.status == Order.Status.PUB:
elif order.status in [Order.Status.PUB, Order.Status.PAU]:
cls.return_bond(order.maker_bond)
order.status = Order.Status.EXP
order.expiry_reason = Order.ExpiryReasons.NTAKEN
order.save()
send_message.delay(order.id,'order_expired_untaken')
return True
@ -274,6 +276,7 @@ class Logics:
cls.settle_bond(order.taker_bond)
cls.cancel_escrow(order)
order.status = Order.Status.EXP
order.expiry_reason = Order.ExpiryReasons.NESINV
order.save()
return True
@ -289,6 +292,7 @@ class Logics:
except:
pass
order.status = Order.Status.EXP
order.expiry_reason = Order.ExpiryReasons.NESCRO
order.save()
# Reward taker with part of the maker bond
cls.add_slashed_rewards(order.maker_bond, order.taker.profile)
@ -324,6 +328,7 @@ class Logics:
cls.return_bond(order.taker_bond)
cls.return_escrow(order)
order.status = Order.Status.EXP
order.expiry_reason = Order.ExpiryReasons.NINVOI
order.save()
# Reward taker with part of the maker bond
cls.add_slashed_rewards(order.maker_bond, order.taker.profile)
@ -525,6 +530,7 @@ class Logics:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.CHA))
send_message.delay(order.id,'fiat_exchange_starts')
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2:
@ -536,6 +542,7 @@ class Logics:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.CHA))
send_message.delay(order.id,'fiat_exchange_starts')
else:
order.status = Order.Status.WFE
@ -590,7 +597,6 @@ class Logics:
# Do not change order status if an is in order
# any of these status
do_not_cancel = [
Order.Status.DEL,
Order.Status.UCA,
Order.Status.EXP,
Order.Status.TLD,
@ -618,7 +624,7 @@ class Logics:
"""The order dissapears from book and goes to cancelled. If strict, maker is charged the bond
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 in [Order.Status.PUB, Order.Status.PAU] and order.maker == user:
# Return the maker bond (Maker gets returned the bond for cancelling public order)
if cls.return_bond(order.maker_bond): # strict cancellation: cls.settle_bond(order.maker_bond):
order.status = Order.Status.UCA
@ -925,6 +931,7 @@ class Logics:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.CHA))
send_message.delay(order.id,'fiat_exchange_starts')
order.save()
@classmethod
@ -1132,8 +1139,30 @@ class Logics:
order.save()
return True, None
def pause_unpause_public_order(order,user):
if not order.maker == user:
return False, {
"bad_request":
"You cannot pause or unpause an order you did not make"
}
else:
if order.status == Order.Status.PUB:
order.status = Order.Status.PAU
elif order.status == Order.Status.PAU:
order.status = Order.Status.PUB
else:
return False, {
"bad_request":
"You can only pause/unpause an order that is either public or paused"
}
order.save()
return True, None
@classmethod
def rate_counterparty(cls, order, user, rating):
'''
Not in use
'''
rating_allowed_status = [
Order.Status.PAY,

View File

@ -91,6 +91,21 @@ class Telegram():
self.send_message(user, text)
return
def fiat_exchange_starts(self, order):
user = order.maker
if not user.profile.telegram_enabled:
return
lang = user.profile.telegram_lang_code
site = config('HOST_NAME')
if lang == 'es':
text = f'El depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{site}/order/{order.id} para hablar con tu contraparte.'
else:
text = f'The escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{site}/order/{order.id} to talk with your counterpart.'
self.send_message(user, text)
return
def order_expired_untaken(self, order):
user = order.maker
if not user.profile.telegram_enabled:
@ -99,9 +114,9 @@ class Telegram():
lang = user.profile.telegram_lang_code
site = config('HOST_NAME')
if lang == 'es':
text = f'Tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{site} para crear una nueva.'
text = f'Tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{site}/order/{order.id} para renovarla.'
else:
text = f'Your order with ID {order.id} has expired untaken. Visit http://{site} to create a new one.'
text = f'Your order with ID {order.id} has expired without a taker. Visit http://{site}/order/{order.id} to renew it.'
self.send_message(user, text)
return

View File

@ -162,7 +162,7 @@ class Order(models.Model):
class Status(models.IntegerChoices):
WFB = 0, "Waiting for maker bond"
PUB = 1, "Public"
DEL = 2, "Deleted"
PAU = 2, "Paused"
TAK = 3, "Waiting for taker bond"
UCA = 4, "Cancelled"
EXP = 5, "Expired"
@ -180,12 +180,23 @@ class Order(models.Model):
MLD = 17, "Maker lost dispute"
TLD = 18, "Taker lost dispute"
class ExpiryReasons(models.IntegerChoices):
NTAKEN = 0, "Expired not taken"
NMBOND = 1, "Maker bond not locked"
NESCRO = 2, "Escrow not locked"
NINVOI = 3, "Invoice not submitted"
NESINV = 4, "Neither escrow locked or invoice submitted"
# order info
status = models.PositiveSmallIntegerField(choices=Status.choices,
null=False,
default=Status.WFB)
created_at = models.DateTimeField(default=timezone.now)
expires_at = models.DateTimeField()
expiry_reason = models.PositiveSmallIntegerField(choices=ExpiryReasons.choices,
null=True,
blank=True,
default=None)
# order details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
@ -232,6 +243,18 @@ class Order(models.Model):
],
blank=False,
)
# optionally makers can choose the escrow lock / invoice submission step length (seconds)
escrow_duration = models.PositiveBigIntegerField(
default=60 * int(config("INVOICE_AND_ESCROW_DURATION"))-1,
null=False,
validators=[
MinValueValidator(60*30), # Min is 30 minutes
MaxValueValidator(60*60*8), # Max is 8 Hours
],
blank=False,
)
# optionally makers can choose the fidelity bond size of the maker and taker (%)
bond_size = models.DecimalField(
max_digits=4,
@ -354,7 +377,7 @@ class Order(models.Model):
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'
6: self.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'

View File

@ -24,6 +24,7 @@ class ListOrderSerializer(serializers.ModelSerializer):
"bondless_taker",
"maker",
"taker",
"escrow_duration",
)
@ -43,6 +44,7 @@ class MakeOrderSerializer(serializers.ModelSerializer):
"premium",
"satoshis",
"public_duration",
"escrow_duration",
"bond_size",
"bondless_taker",
)
@ -58,6 +60,7 @@ class UpdateOrderSerializer(serializers.Serializer):
default=None)
action = serializers.ChoiceField(
choices=(
"pause",
"take",
"update_invoice",
"submit_statement",

View File

@ -235,4 +235,8 @@ def send_message(order_id, message):
elif message == 'order_taken_confirmed':
telegram.order_taken_confirmed(order)
elif message == 'fiat_exchange_starts':
telegram.fiat_exchange_starts(order)
return

View File

@ -31,6 +31,7 @@ from decouple import config
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
RETRY_TIME = int(config("RETRY_TIME"))
PUBLIC_DURATION = 60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1
ESCROW_DURATION = 60 * int(config("INVOICE_AND_ESCROW_DURATION"))
BOND_SIZE = int(config("DEFAULT_BOND_SIZE"))
avatar_path = Path(settings.AVATAR_ROOT)
@ -82,11 +83,13 @@ class MakerView(CreateAPIView):
satoshis = serializer.data.get("satoshis")
is_explicit = serializer.data.get("is_explicit")
public_duration = serializer.data.get("public_duration")
escrow_duration = serializer.data.get("escrow_duration")
bond_size = serializer.data.get("bond_size")
bondless_taker = serializer.data.get("bondless_taker")
# Optional params
if public_duration == None: public_duration = PUBLIC_DURATION
if escrow_duration == None: escrow_duration = ESCROW_DURATION
if bond_size == None: bond_size = BOND_SIZE
if bondless_taker == None: bondless_taker = False
if has_range == None: has_range = False
@ -132,6 +135,7 @@ class MakerView(CreateAPIView):
seconds=EXP_MAKER_BOND_INVOICE),
maker=request.user,
public_duration=public_duration,
escrow_duration=escrow_duration,
bond_size=bond_size,
bondless_taker=bondless_taker,
)
@ -238,9 +242,9 @@ class OrderView(viewsets.ViewSet):
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
# 3. c) If maker and Public/Paused, add premium percentile
# num similar orders, and maker information to enable telegram notifications.
if data["is_maker"] and order.status == Order.Status.PUB:
if data["is_maker"] and order.status in [Order.Status.PUB, Order.Status.PAU]:
data["premium_percentile"] = compute_premium_percentile(order)
data["num_similar_orders"] = len(
Order.objects.filter(currency=order.currency,
@ -372,8 +376,7 @@ class OrderView(viewsets.ViewSet):
and order.payout.receiver == request.user
): # might not be the buyer if after a dispute where winner wins
data["retries"] = order.payout.routing_attempts
data[
"next_retry_time"] = order.payout.last_routing_time + timedelta(
data["next_retry_time"] = order.payout.last_routing_time + timedelta(
minutes=RETRY_TIME)
if order.payout.status == LNPayment.Status.EXPIRE:
@ -381,6 +384,15 @@ class OrderView(viewsets.ViewSet):
# Add invoice amount once again if invoice was expired.
data["invoice_amount"] = Logics.payout_amount(order,request.user)[1]["invoice_amount"]
# 10) If status is 'Expired', add expiry reason.
elif (order.status == Order.Status.EXP):
data["expiry_reason"] = order.expiry_reason
data["expiry_message"] = Order.ExpiryReasons(order.expiry_reason).label
# other pieces of info useful to renew an identical order
data["public_duration"] = order.public_duration
data["bond_size"] = order.bond_size
data["bondless_taker"] = order.bondless_taker
return Response(data, status.HTTP_200_OK)
def take_update_confirm_dispute_cancel(self, request, format=None):
@ -390,10 +402,6 @@ 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)
@ -481,12 +489,18 @@ class OrderView(viewsets.ViewSet):
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate_platform
# 7) If action is rate_platform
elif action == "rate_platform" and rating:
valid, context = Logics.rate_platform(request.user, rating)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 8) If action is rate_platform
elif action == "pause":
valid, context = Logics.pause_unpause_public_order(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# If nothing of the above... something else is going on. Probably not allowed!
else:
return Response(
@ -824,6 +838,7 @@ class LimitView(ListAPIView):
exchange_rate = float(currency.exchange_rate)
payload[currency.currency] = {
'code': code,
'price': exchange_rate,
'min_amount': min_trade * exchange_rate,
'max_amount': max_trade * exchange_rate,
'max_bondless_amount': max_bondless_trade * exchange_rate,

16219
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"@babel/preset-react": "^7.16.7",
"babel-loader": "^8.2.3",
"jest": "^27.5.1",
"openpgp": "^5.2.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"webpack": "^5.65.0",

View File

@ -25,7 +25,7 @@ class BookPage extends Component {
loading: true,
pageSize: 6,
};
this.getOrderDetails(2, this.props.currency)
this.getOrderDetails(2, 0)
}
getOrderDetails(type, currency) {
@ -158,7 +158,7 @@ class BookPage extends Component {
),
NoResultsOverlay: () => (
<Stack height="100%" alignItems="center" justifyContent="center">
{t("Local filter returns no result")}
{t("Filter has no results")}
</Stack>
)
}}
@ -328,7 +328,7 @@ class BookPage extends Component {
return (
<Grid className='orderBook' container spacing={1} sx={{minWidth:400}}>
<IconButton sx={{position:'fixed',right:'0px', top:'30px'}} onClick={()=>this.setState({loading: true}) & this.getOrderDetails(this.props.type, this.props.currency)}>
<IconButton sx={{position:'fixed',right:'0px', top:'30px'}} onClick={()=>this.setState({loading: true}) & this.getOrderDetails(2, 0)}>
<RefreshIcon/>
</IconButton>
@ -341,7 +341,15 @@ class BookPage extends Component {
<div style={{position:"relative", left:"20px"}}>
<FormControlLabel
control={<Checkbox defaultChecked={true} icon={<MoveToInboxIcon sx={{width:"30px",height:"30px"}} color="inherit"/>} checkedIcon={<MoveToInboxIcon sx={{width:"30px",height:"30px"}} color="primary"/>}/>}
label={<div style={{position:"relative",top:"-13px"}}><Typography style={{color:"#666666"}} variant="caption">{t("Buy")}</Typography></div>}
label={
<div style={{position:"relative",top:"-13px"}}>
{this.props.buyChecked ?
<Typography variant="caption" color="primary"><b>{t("Buy")}</b></Typography>
:
<Typography variant="caption" color="text.secondary">{t("Buy")}</Typography>
}
</div>
}
labelPlacement="bottom"
checked={this.props.buyChecked}
onChange={this.handleClickBuy}
@ -349,7 +357,15 @@ class BookPage extends Component {
</div>
<FormControlLabel
control={<Checkbox defaultChecked={true} icon={<OutboxIcon sx={{width:"30px",height:"30px"}} color="inherit"/>} checkedIcon={<OutboxIcon sx={{width:"30px",height:"30px"}} color="secondary"/>}/>}
label={<div style={{position:"relative",top:"-13px"}}><Typography style={{color:"#666666"}} variant="caption">{t("Sell")}</Typography></div>}
label={
<div style={{position:"relative",top:"-13px"}}>
{this.props.sellChecked ?
<Typography variant="caption" color="secondary"><b>{t("Sell")}</b></Typography>
:
<Typography variant="caption" color="text.secondary">{t("Sell")}</Typography>
}
</div>
}
labelPlacement="bottom"
checked={this.props.sellChecked}
onChange={this.handleClickSell}

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { withTranslation, Trans} from "react-i18next";
import {Button, Link, Badge, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, FormHelperText, Typography} from "@mui/material";
import ReconnectingWebSocket from 'reconnecting-websocket';
import * as openpgp from 'openpgp/lightweight';
class Chat extends Component {
constructor(props) {

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { withTranslation, Trans} from "react-i18next";
import { InputAdornment, LinearProgress, Link, Checkbox, Slider, Box, Tab, Tabs, SliderThumb, Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
import { withTranslation } from "react-i18next";
import { InputAdornment, LinearProgress, Accordion, AccordionDetails, AccordionSummary, Checkbox, Slider, Box, Tab, Tabs, SliderThumb, 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 as LinkRouter } from 'react-router-dom'
@ -15,7 +15,7 @@ import MoveToInboxIcon from '@mui/icons-material/MoveToInbox';
import OutboxIcon from '@mui/icons-material/Outbox';
import LockIcon from '@mui/icons-material/Lock';
import HourglassTopIcon from '@mui/icons-material/HourglassTop';
import DoubleArrowIcon from '@mui/icons-material/DoubleArrow';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { getCookie } from "../utils/cookies";
import { pn } from "../utils/prettyNumbers";
@ -45,14 +45,16 @@ class MakerPage extends Component {
showAdvanced: false,
allowBondless: false,
publicExpiryTime: new Date(0, 0, 0, 23, 59),
escrowExpiryTime: new Date(0, 0, 0, 3, 0),
enableAmountRange: false,
minAmount: null,
bondSize: 1,
limits: null,
minAmount: null,
maxAmount: null,
loadingLimits: false,
loadingLimits: true,
}
this.getLimits()
}
getLimits() {
@ -219,6 +221,7 @@ class MakerPage extends Component {
premium: this.state.is_explicit ? null: this.state.premium,
satoshis: this.state.is_explicit ? this.state.satoshis: null,
public_duration: this.state.publicDuration,
escrow_duration: this.state.escrowDuration,
bond_size: this.state.bondSize,
bondless_taker: this.state.allowBondless,
}),
@ -237,6 +240,20 @@ class MakerPage extends Component {
this.setState({bondSize: event.target.value === '' ? 1 : Number(event.target.value)});
};
priceNow = () => {
if (this.state.loadingLimits){
return "...";
}
else if (this.state.is_explicit & this.state.amount > 0 & this.state.satoshis > 0){
return parseFloat(Number(this.state.amount / (this.state.satoshis/100000000)).toPrecision(5));
}
else if (!this.state.is_explicit){
var price = this.state.limits[this.state.currency]['price'];
return parseFloat(Number(price*(1+this.state.premium/100)).toPrecision(5));
}
return "...";
}
StandardMakerOptions = () => {
const { t } = this.props;
return(
@ -244,38 +261,21 @@ class MakerPage extends Component {
<Grid item xs={12} align="center" spacing={1}>
<div style={{position:'relative', left:'5px'}}>
<FormControl component="fieldset">
<FormHelperText sx={{align:"center"}}>
<FormHelperText sx={{textAlign:"center"}}>
{t("Buy or Sell Bitcoin?")}
</FormHelperText>
{/* <RadioGroup row>
<div style={{position:"relative", left:"20px"}}>
<FormControlLabel
control={<Checkbox defaultChecked={true} icon={<MoveToInboxIcon sx={{width:"30px",height:"30px"}} color="inherit"/>} checkedIcon={<MoveToInboxIcon sx={{width:"30px",height:"30px"}} color="primary"/>}/>}
label={<div style={{position:"relative",top:"-13px"}}><Typography style={{color:"#666666"}} variant="caption">{t("Buy")}</Typography></div>}
labelPlacement="bottom"
checked={this.state.buyChecked}
onChange={this.handleClickBuy}
/>
</div>
<FormControlLabel
control={<Checkbox defaultChecked={true} icon={<OutboxIcon sx={{width:"30px",height:"30px"}} color="inherit"/>} checkedIcon={<OutboxIcon sx={{width:"30px",height:"30px"}} color="secondary"/>}/>}
label={<div style={{position:"relative",top:"-13px"}}><Typography style={{color:"#666666"}} variant="caption">{t("Sell")}</Typography></div>}
labelPlacement="bottom"
checked={this.state.sellChecked}
onChange={this.handleClickSell}
/> */}
<RadioGroup row defaultValue="0" onChange={this.handleTypeChange}>
<FormControlLabel
value="0"
control={<Radio icon={<MoveToInboxIcon sx={{width:"26px",height:"26px"}} color="inherit"/>} checkedIcon={<MoveToInboxIcon sx={{width:"26px",height:"26px"}} color="primary"/>}/>}
label={t("Buy")}
label={this.state.type == 0 ? <Typography color="primary"><b>{t("Buy")}</b></Typography>: <Typography color="text.secondary">{t("Buy")}</Typography>}
labelPlacement="end"
/>
<FormControlLabel
value="1"
control={<Radio icon={<OutboxIcon sx={{width:"26px",height:"26px"}} color="inherit"/>} checkedIcon={<OutboxIcon sx={{width:"26px",height:"26px"}} color="secondary"/>}/>}
label={t("Sell")}
label={this.state.type == 1 ? <Typography color="secondary"><b>{t("Sell")}</b></Typography>: <Typography color="text.secondary">{t("Sell")}</Typography>}
labelPlacement="end"
/>
</RadioGroup>
@ -398,6 +398,13 @@ class MakerPage extends Component {
onChange={this.handlePremiumChange}
/>
</div>
<Grid item>
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title={this.state.is_explicit? t("Your order fixed exchange rate"): t("Your order's current exchange rate. Rate will move with the market.")}>
<Typography variant="caption" color="text.secondary">
{(this.state.is_explicit ? t("Order rate:"): t("Order current rate:"))+" "+pn(this.priceNow())+" "+this.state.currencyCode+"/BTC"}
</Typography>
</Tooltip>
</Grid>
</Grid>
</Paper>
)
@ -411,12 +418,22 @@ class MakerPage extends Component {
var total_secs = hours*60*60 + minutes * 60;
this.setState({
changedPublicExpiryTime: true,
publicExpiryTime: date,
publicDuration: total_secs,
badDuration: false,
});
}
handleChangeEscrowDuration = (date) => {
let d = new Date(date),
hours = d.getHours(),
minutes = d.getMinutes();
var total_secs = hours*60*60 + minutes * 60;
this.setState({
escrowExpiryTime: date,
escrowDuration: total_secs,
});
}
getMaxAmount = () => {
@ -530,7 +547,7 @@ class MakerPage extends Component {
<FormHelperText>
<Tooltip enterTouchDelay="0" placement="top" align="center" title={t("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, is_explicit: false}) & (e.target.checked ? this.getLimits() : null)}/>
<Checkbox onChange={(e)=>this.setState({enableAmountRange:e.target.checked, is_explicit: false})}/>
{this.state.enableAmountRange & this.state.minAmount != null? <this.rangeText/> : t("Enable Amount Range")}
</div>
</Tooltip>
@ -562,34 +579,75 @@ class MakerPage extends Component {
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<LocalizationProvider dateAdapter={DateFnsUtils}>
<TimePicker
sx={{width:210, align:"center"}}
ampm={false}
openTo="hours"
views={['hours', 'minutes']}
inputFormat="HH:mm"
mask="__:__"
components={{
OpenPickerIcon: HourglassTopIcon
}}
open={this.state.openTimePicker}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<HourglassTopIcon />
</InputAdornment>)
}}
renderInput={(props) => <TextField {...props} />}
label={t("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>
<Accordion elevation={0} sx={{width:'280px', position:'relative', left:'-12px'}}>
<AccordionSummary expandIcon={<ExpandMoreIcon color="primary"/>}>
<Typography sx={{flexGrow: 1, textAlign: "center"}} color="text.secondary">{t("Expiry Timers")}</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container xs={12} spacing={1}>
<Grid item xs={12} align="center" spacing={1}>
<LocalizationProvider dateAdapter={DateFnsUtils}>
<TimePicker
sx={{width:210, align:"center"}}
ampm={false}
openTo="hours"
views={['hours', 'minutes']}
inputFormat="HH:mm"
mask="__:__"
components={{
OpenPickerIcon: HourglassTopIcon
}}
open={this.state.openTimePicker}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<HourglassTopIcon />
</InputAdornment>)
}}
renderInput={(props) => <TextField {...props} />}
label={t("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}>
<LocalizationProvider dateAdapter={DateFnsUtils}>
<TimePicker
sx={{width:210, align:"center"}}
ampm={false}
openTo="hours"
views={['hours', 'minutes']}
inputFormat="HH:mm"
mask="__:__"
components={{
OpenPickerIcon: HourglassTopIcon
}}
open={this.state.openTimePicker}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<HourglassTopIcon />
</InputAdornment>)
}}
renderInput={(props) => <TextField {...props} />}
label={t("Escrow Deposit Time-Out (HH:mm)")}
value={this.state.escrowExpiryTime}
onChange={this.handleChangeEscrowDuration}
minTime={new Date(0, 0, 0, 1, 0)}
maxTime={new Date(0, 0, 0, 8, 0)}
/>
</LocalizationProvider>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
<Grid item xs={12} align="center" spacing={1}>
<FormControl align="center">
<Tooltip enterDelay="800" enterTouchDelay="0" placement="top" title={t("Set the skin-in-the-game, increase for higher safety assurance")}>

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { withTranslation, Trans} from "react-i18next";
import { withTranslation} from "react-i18next";
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'
@ -17,6 +17,7 @@ import PriceChangeIcon from '@mui/icons-material/PriceChange';
import PaymentsIcon from '@mui/icons-material/Payments';
import ArticleIcon from '@mui/icons-material/Article';
import DoubleArrowIcon from '@mui/icons-material/DoubleArrow';
import HourglassTopIcon from '@mui/icons-material/HourglassTop';
import { getCookie } from "../utils/cookies";
import { pn } from "../utils/prettyNumbers";
@ -33,15 +34,15 @@ class OrderPage extends Component {
openCollaborativeCancel: false,
openInactiveMaker: false,
showContractBox: 1,
orderId: this.props.match.params.orderId,
};
this.orderId = this.props.match.params.orderId;
this.getOrderDetails();
this.getOrderDetails(this.props.match.params.orderId);
// Refresh delays according to Order status
this.statusToDelay = {
"0": 2000, //'Waiting for maker bond'
"1": 25000, //'Public'
"2": 999999, //'Deleted'
"2": 90000, //'Paused'
"3": 2000, //'Waiting for taker bond'
"4": 999999, //'Cancelled'
"5": 999999, //'Expired'
@ -67,6 +68,7 @@ class OrderPage extends Component {
// otherStateVars will fail to assign values
if (newStateVars.currency == null){
newStateVars.currency = this.state.currency
newStateVars.amount = this.state.amount
newStateVars.status = this.state.status
}
@ -83,11 +85,12 @@ class OrderPage extends Component {
this.setState(completeStateVars);
}
getOrderDetails() {
getOrderDetails =(id)=> {
this.setState(null)
fetch('/api/order' + '?order_id=' + this.orderId)
this.setState({orderId:id})
fetch('/api/order' + '?order_id=' + id)
.then((response) => response.json())
.then((data) => this.completeSetState(data));
.then((data) => (this.completeSetState(data) & this.setState({pauseLoading:false})));
}
// These are used to refresh the data
@ -103,7 +106,7 @@ class OrderPage extends Component {
clearInterval(this.interval);
}
tick = () => {
this.getOrderDetails();
this.getOrderDetails(this.state.orderId);
}
// Countdown Renderer callback with condition
@ -128,6 +131,14 @@ class OrderPage extends Component {
}
};
timerRenderer(seconds){
var hours = parseInt(seconds/3600);
var minutes = parseInt((seconds-hours*3600)/60);
return(
<span>{hours>0 ? hours+"h":""} {minutes>0 ? zeroPad(minutes)+"m":""} </span>
)
}
// Countdown Renderer callback with condition
countdownPenaltyRenderer = ({ minutes, seconds, completed }) => {
const { t } = this.props;
@ -267,7 +278,7 @@ class OrderPage extends Component {
'amount':this.state.takeAmount,
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
fetch('/api/order/' + '?order_id=' + this.state.orderId, requestOptions)
.then((response) => response.json())
.then((data) => this.completeSetState(data));
}
@ -291,9 +302,9 @@ class OrderPage extends Component {
'action':'cancel',
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
fetch('/api/order/' + '?order_id=' + this.state.orderId, requestOptions)
.then((response) => response.json())
.then((data) => this.getOrderDetails(data.id));
.then(() => (this.getOrderDetails(this.state.orderId) & this.setState({status:4})));
this.handleClickCloseConfirmCancelDialog();
}
@ -368,9 +379,9 @@ class OrderPage extends Component {
'action':'cancel',
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
fetch('/api/order/' + '?order_id=' + this.state.state.orderId, requestOptions)
.then((response) => response.json())
.then((data) => this.getOrderDetails(data.id));
.then(() => (this.getOrderDetails(this.state.orderId) & this.setState({status:4})));
this.handleClickCloseCollaborativeCancelDialog();
}
@ -531,10 +542,10 @@ class OrderPage extends Component {
</div>
</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={t("Amount range")}/>
<ListItemText primary={pn(parseFloat(Number(this.state.min_amount).toPrecision(4)))
+"-" + pn(parseFloat(Number(this.state.max_amount).toPrecision(4))) +" "+this.state.currencyCode} secondary={t("Amount range")}/>
:
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))
<ListItemText primary={pn(parseFloat(parseFloat(this.state.amount).toFixed(4)))
+" "+this.state.currencyCode} secondary={t("Amount")}/>
}
@ -566,12 +577,30 @@ class OrderPage extends Component {
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
<ListItem >
<ListItemIcon>
<NumbersIcon/>
</ListItemIcon>
<ListItemText primary={this.orderId} secondary={t("Order ID")}/>
<Grid container xs={12}>
<Grid item xs={4.5}>
<ListItemText primary={this.state.orderId} secondary={t("Order ID")}/>
</Grid>
<Grid item xs={7.5}>
<Grid container>
<Grid item xs={2}>
<ListItemIcon sx={{position:"relative",top:"12px",left:"-5px"}}><HourglassTopIcon/></ListItemIcon>
</Grid>
<Grid item xs={10}>
<ListItemText
primary={this.timerRenderer(this.state.escrow_duration)}
secondary={t("Deposit timer")}>
</ListItemText>
</Grid>
</Grid>
</Grid>
</Grid>
</ListItem>
<Divider />
<ListItem>
<ListItemIcon>
@ -652,7 +681,7 @@ class OrderPage extends Component {
{this.orderBox()}
</Grid>
<Grid item xs={6} align="left">
<TradeBox push={this.props.history.push} width={330} data={this.state} completeSetState={this.completeSetState} />
<TradeBox push={this.props.history.push} getOrderDetails={this.getOrderDetails} pauseLoading={this.state.pauseLoading} width={330} data={this.state} completeSetState={this.completeSetState} />
</Grid>
</Grid>
)
@ -688,7 +717,7 @@ class OrderPage extends Component {
{this.orderBox()}
</div>
<div style={{display: this.state.showContractBox == 1 ? '':'none'}}>
<TradeBox push={this.props.history.push} width={330} data={this.state} completeSetState={this.completeSetState} />
<TradeBox push={this.props.history.push} getOrderDetails={this.getOrderDetails} pauseLoading={this.state.pauseLoading} width={330} data={this.state} completeSetState={this.completeSetState} />
</div>
</Grid>
</Grid>

View File

@ -16,9 +16,12 @@ import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import BalanceIcon from '@mui/icons-material/Balance';
import ContentCopy from "@mui/icons-material/ContentCopy";
import PauseCircleIcon from '@mui/icons-material/PauseCircle';
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
import { getCookie } from "../utils/cookies";
import { pn } from "../utils/prettyNumbers";
import { t } from "i18next";
class TradeBox extends Component {
invoice_escrow_duration = 3;
@ -61,7 +64,7 @@ class TradeBox extends Component {
if(this.props.data.is_maker){
if (status == 0){
x = 1
} else if ([1,3].includes(status)){
} else if ([1,2,3].includes(status)){
x = 2
} else if ([6,7,8].includes(status)){
x = 3
@ -358,6 +361,26 @@ class TradeBox extends Component {
</Dialog>
)
}
depositHoursMinutes=()=>{
var hours = parseInt(this.props.data.escrow_duration/3600);
var minutes = parseInt((this.props.data.escrow_duration-hours*3600)/60);
var dict = {deposit_timer_hours:hours, deposit_timer_minutes:minutes}
return dict
}
handleClickPauseOrder=()=>{
this.props.completeSetState({pauseLoading:true})
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action': "pause",
}),
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => (this.props.getOrderDetails(data.id)));
}
showMakerWait=()=>{
const { t } = this.props;
@ -377,10 +400,11 @@ class TradeBox extends Component {
<Divider/>
<ListItem>
<Typography component="body2" variant="body2" align="left">
<p>{t("Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{invoice_escrow_duration}} hours to reply. If you do not reply, you risk losing your bond.", {invoice_escrow_duration: pn(this.invoice_escrow_duration)})} </p>
<p>{t("Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{deposit_timer_hours}}h {{deposit_timer_minutes}}m hours to reply. If you do not reply, you risk losing your bond.", this.depositHoursMinutes() )} </p>
<p>{t("If the order expires untaken, your bond will return to you (no action needed).")}</p>
</Typography>
</ListItem>
<Grid item xs={12} align="center">
{this.props.data.tg_enabled ?
<Typography color='primary' component="h6" variant="h6" align="center">{t("Telegram enabled")}</Typography>
@ -391,13 +415,32 @@ class TradeBox extends Component {
}
</Grid>
<Divider/>
<ListItem>
<ListItemIcon>
<BookIcon/>
</ListItemIcon>
<ListItemText primary={this.props.data.num_similar_orders} secondary={t("Public orders for {{currencyCode}}",{currencyCode: this.props.data.currencyCode})}/>
</ListItem>
<Grid container>
<Grid item xs={10}>
<ListItem>
<ListItemIcon>
<BookIcon/>
</ListItemIcon>
<ListItemText primary={this.props.data.num_similar_orders} secondary={t("Public orders for {{currencyCode}}",{currencyCode: this.props.data.currencyCode})}/>
</ListItem>
</Grid>
<Grid item xs={2}>
<div style={{position:"relative", top:"7px", right:"14px"}}>
{this.props.pauseLoading ?
<CircularProgress sx={{width:"30px",height:"30px"}}/>
:
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title={t("Pause the public order")}>
<Button color="primary" onClick={this.handleClickPauseOrder}>
<PauseCircleIcon sx={{width:"36px",height:"36px"}}/>
</Button>
</Tooltip>
}
</div>
</Grid>
</Grid>
<Divider/>
<ListItem>
<ListItemIcon>
@ -415,6 +458,44 @@ class TradeBox extends Component {
)
}
showPausedOrder=()=>{
const { t } = this.props;
return (
<Grid container align="center" spacing={1}>
<Grid item xs={12} align="center">
<Typography component="subtitle1" variant="subtitle1">
<b> {t("Your order is paused")} </b> {" " + this.stepXofY()}
</Typography>
</Grid>
<Grid item xs={12} align="center">
<List dense="true">
<Divider/>
<ListItem>
<Typography component="body2" variant="body2" align="left">
{t("Your public order has been paused. At the moment it cannot be seen or taken by other robots. You can choose to unpause it at any time.")}
</Typography>
</ListItem>
<Grid item xs={12} align="center">
{this.props.pauseLoading ?
<CircularProgress/>
:
<Button color="primary" onClick={this.handleClickPauseOrder}>
<PlayCircleIcon sx={{width:"36px",height:"36px"}}/>{t("Unpause Order")}
</Button>
}
</Grid>
<Divider/>
</List>
</Grid>
{this.showBondIsLocked()}
</Grid>
)
}
handleInputInvoiceChanged=(e)=>{
this.setState({
invoice: e.target.value,
@ -773,8 +854,41 @@ handleRatingRobosatsChange=(e)=>{
)
}
showOrderExpired(){
handleRenewOrderButtonPressed=()=>{
this.setState({renewLoading:true})
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
body: JSON.stringify({
type: this.props.data.type,
currency: this.props.data.currency,
amount: this.props.data.has_range ? null : this.props.data.amount,
has_range: this.props.data.has_range,
min_amount: this.props.data.min_amount,
max_amount: this.props.data.max_amount,
payment_method: this.props.data.payment_method,
is_explicit: this.props.data.is_explicit,
premium: this.props.data.is_explicit ? null: this.props.data.premium,
satoshis: this.props.data.is_explicit ? this.props.data.satoshis: null,
public_duration: this.props.data.public_duration,
escrow_duration: this.props.data.escrow_duration,
bond_size: this.props.data.bond_size,
bondless_taker: this.props.data.bondless_taker,
}),
};
fetch("/api/make/",requestOptions)
.then((response) => response.json())
.then((data) => (this.setState({badRequest:data.bad_request})
& (data.id ? this.props.push('/order/' + data.id)
& this.props.getOrderDetails(data.id)
:"")
));
}
showOrderExpired=()=>{
const { t } = this.props;
var show_renew = this.props.data.is_maker;
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
@ -782,6 +896,22 @@ handleRatingRobosatsChange=(e)=>{
<b>{t("The order has expired")}</b>
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2">
{t(this.props.data.expiry_message)}
</Typography>
</Grid>
{show_renew ?
<Grid item xs={12} align="center">
{this.state.renewLoading ?
<CircularProgress/>
:
<Button variant='contained' color='primary' onClick={this.handleRenewOrderButtonPressed}>{t("Renew Order")}</Button>
}
</Grid>
: null}
</Grid>
)
}
@ -1023,6 +1153,7 @@ handleRatingRobosatsChange=(e)=>{
{this.props.data.is_taker & this.props.data.status == 3 ? this.showQRInvoice() : ""}
{/* Waiting for taker and taker bond request */}
{this.props.data.is_maker & this.props.data.status == 2 ? this.showPausedOrder() : ""}
{this.props.data.is_maker & this.props.data.status == 1 ? this.showMakerWait() : ""}
{this.props.data.is_maker & this.props.data.status == 3 ? this.showTakerFound() : ""}

View File

@ -42,11 +42,17 @@
"Explicit":"Explicit",
"Set a fix amount of satoshis":"Set a fix amount of satoshis",
"Satoshis":"Satoshis",
"Fixed price:":"Fixed price:",
"Order current rate:":"Order current rate:",
"Your order fixed exchange rate":"Your order fixed exchange rate",
"Your order's current exchange rate. Rate will move with the market.":"Your order's current exchange rate. Rate will move with the market.",
"Let the taker chose an amount within the range":"Let the taker choose an amount within the range",
"Enable Amount Range":"Enable Amount Range",
"From": "From",
"to":"to",
"Expiry Timers":"Expiry Timers",
"Public Duration (HH:mm)":"Public Duration (HH:mm)",
"Escrow Deposit Time-Out (HH:mm)":"Escrow Deposit Time-Out (HH:mm)",
"Set the skin-in-the-game, increase for higher safety assurance":"Set the skin-in-the-game, increase for higher safety assurance",
"Fidelity Bond Size":"Fidelity Bond Size",
"Allow bondless takers":"Allow bondless takers",
@ -64,7 +70,6 @@
"Must be more than {{min}}%":"Must be more than {{min}}%",
"Must be less than {{maxSats}": "Must be less than {{maxSats}}",
"Must be more than {{minSats}}": "Must be more than {{minSats}}",
"PAYMENT METHODS - autocompletePayments.js": "Payment method strings",
"not specified":"Not specified",
@ -200,6 +205,7 @@
"Amount of Satoshis":"Amount of Satoshis",
"Premium over market price":"Premium over market price",
"Order ID":"Order ID",
"Deposit timer":"Deposit timer",
"Expires in":"Expires in",
"{{nickname}} is asking for a collaborative cancel":"{{nickname}} is asking for a collaborative cancel",
"You asked for a collaborative cancellation":"You asked for a collaborative cancellation",
@ -259,7 +265,7 @@
"Your maker bond was unlocked":"Your maker bond was unlocked",
"Your taker bond was unlocked":"Your taker bond was unlocked",
"Your order is public":"Your order is public",
"Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{invoice_escrow_duration}} hours to reply. If you do not reply, you risk losing your bond.":"Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{invoice_escrow_duration}} hours to reply. If you do not reply, you risk losing your bond.",
"Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{deposit_timer_hours}}h {{deposit_timer_minutes}}m hours to reply. If you do not reply, you risk losing your bond.":"Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{deposit_timer_hours}}h {{deposit_timer_minutes}}m hours to reply. If you do not reply, you risk losing your bond.",
"If the order expires untaken, your bond will return to you (no action needed).":"If the order expires untaken, your bond will return to you (no action needed).",
"Enable Telegram Notifications":"Enable Telegram Notifications",
"Enable TG Notifications":"Enable TG Notifications",
@ -325,6 +331,13 @@
"You can claim the dispute resolution amount (escrow and fidelity bond) from your profile rewards. If there is anything the staff can help with, do not hesitate to contact to robosats@protonmail.com (or via your provided burner contact method).":"You can claim the dispute resolution amount (escrow and fidelity bond) from your profile rewards. If there is anything the staff can help with, do not hesitate to contact to robosats@protonmail.com (or via your provided burner contact method).",
"You have lost the dispute":"You have lost the dispute",
"Unfortunately you have lost the dispute. If you think this is a mistake you can ask to re-open the case via email to robosats@protonmail.com. However, chances of it being investigated again are low.":"Unfortunately you have lost the dispute. If you think this is a mistake you can ask to re-open the case via email to robosats@protonmail.com. However, chances of it being investigated again are low.",
"Expired not taken":"Expired not taken",
"Maker bond not locked":"Maker bond not locked",
"Escrow not locked":"Escrow not locked",
"Invoice not submitted":"Invoice not submitted",
"Neither escrow locked or invoice submitted":"Neither escrow locked or invoice submitted",
"Renew Order":"Renew Order",
"INFO DIALOG - InfoDiagog.js":"App information and clarifications and terms of use",
"Close":"Close",

View File

@ -23,7 +23,7 @@
"MAKER PAGE - MakerPage.js": "This is the page where users can create new orders",
"Order":"Orden",
"Customize":"Personalizar",
"Buy or Sell Bitcoin?":"¿Comprar o Vender Bitcoin?",
"Buy or Sell Bitcoin?":"¿Comprar o vender bitcoin?",
"Buy":"Comprar",
"Sell":"Vender",
"Amount":"Monto",
@ -31,22 +31,28 @@
"Invalid":"No válido",
"Enter your preferred fiat payment methods. Fast methods are highly recommended.": "Introduce tus métodos de pago. Se recomiendan encarecidamente métodos rápidos.",
"Must be shorter than 65 characters":"Debe tener menos de 65 caracteres",
"Swap Destination(s)": "Destino(s) del Swap",
"Fiat Payment Method(s)":"Método(s) de Pago en Fiat",
"Swap Destination(s)": "Destino(s) del fwap",
"Fiat Payment Method(s)":"Método(s) de pago en fiat",
"You can add new methods":"Puedes añadir nuevos métodos",
"Add New":"Añadir nuevo",
"Choose a Pricing Method":"Elige Cómo Establecer el Precio",
"Choose a Pricing Method":"Elige cómo establecer el precio",
"Relative":"Relativo",
"Let the price move with the market":"EL precio se moverá relativo al mercado",
"Premium over Market (%)": "Prima sobre el mercado (%)",
"Explicit":"Fijo",
"Set a fix amount of satoshis": "Establece un monto fijo de Sats",
"Satoshis": "Satoshis",
"Fixed price:":"Precio fijo:",
"Order current rate:":"Precio actual:",
"Your order fixed exchange rate":"La tasa de cambio fija de tu orden",
"Your order's current exchange rate. Rate will move with the market.":"La tasa de cambio de tu orden justo en estos momentos. Se moverá relativo al mercado.",
"Let the taker chose an amount within the range":"Permite que el tomador elija un monto dentro del rango.",
"Enable Amount Range":"Activar Monto con Rango",
"Enable Amount Range":"Activar monto con rngo",
"From": "Desde",
"to":"a ",
"Expiry Timers":"Temporizadores",
"Public Duration (HH:mm)": "Duración pública (HH:mm)",
"Escrow Deposit Time-Out (HH:mm)":"Plazo límite depósito (HH:mm)",
"Set the skin-in-the-game, increase for higher safety assurance": "Establece la implicación requerida (aumentar para mayor seguridad)",
"Fidelity Bond Size": "Tamaño de la fianza",
"Allow bondless takers":"Permitir tomadores sin fianza",
@ -201,7 +207,8 @@
"Price and Premium":"Precio y prima",
"Amount of Satoshis": "Cantidad de Sats",
"Premium over market price":"Prima sobre el mercado",
"Order ID":"ID de la orden",
"Order ID":"Orden ID",
"Deposit timer":"Para depositar",
"Expires in":"Expira en",
"{{nickname}} is asking for a collaborative cancel":"{{nickname}} solicita cancelar colaborativamente",
"You asked for a collaborative cancellation":"Solicitaste cancelar colaborativamente",
@ -261,7 +268,7 @@
"Your maker bond was unlocked": "Tu fianza se ha desbloqueado",
"Your taker bond was unlocked": "Tu fianza se ha desbloqueado",
"Your order is public": "Tu orden es pública",
"Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{invoice_escrow_duration}} hours to reply. If you do not reply, you risk losing your bond.": "Se paciente hasta que un robot tome tu orden. Esta ventana sonará 🔊 una vez que algún Robot tome tu orden. Entonces tendrás {{invoice_escrow_duration}} horas para responder, si no respondes arriesgas perder tu fianza.",
"Be patient while robots check the book. This box will ring 🔊 once a robot takes your order, then you will have {{deposit_timer_hours}}h {{deposit_timer_minutes}}m hours to reply. If you do not reply, you risk losing your bond.": "Se paciente hasta que un robot tome tu orden. Esta ventana sonará 🔊 una vez que algún Robot tome tu orden. Entonces tendrás {{deposit_timer_hours}}h {{deposit_timer_minutes}}min para responder, si no respondes arriesgas perder tu fianza.",
"If the order expires untaken, your bond will return to you (no action needed).": "Si tu oferta expira sin ser tomada, tu fianza será desbloqueada en tu cartera automáticamente.",
"Enable Telegram Notifications": "Notificar en Telegram",
"Enable TG Notifications": "Activar Notificaciones TG",
@ -269,7 +276,7 @@
"Go back": "Volver",
"Enable": "Activar",
"Telegram enabled": "Telegram activado",
"Public orders for {{currencyCode}}": "Órdenes públicas para {{currencyCode}}",
"Public orders for {{currencyCode}}": "Órdenes públicas por {{currencyCode}}",
"Premium rank": "Percentil de la prima",
"Among public {{currencyCode}} orders (higher is cheaper)": "Entre las órdenes públicas de {{currencyCode}} (más alto, más barato)",
"A taker has been found!": "¡Un tomador ha sido encontrado!",
@ -327,6 +334,17 @@
"You can claim the dispute resolution amount (escrow and fidelity bond) from your profile rewards. If there is anything the staff can help with, do not hesitate to contact to robosats@protonmail.com (or via your provided burner contact method).": "Puedes retirar la cantidad de la resolución de la disputa (fianza y colateral) desde las recompensas de tu perfil. Si hay algo que el equipo pueda hacer, no dudes en contactar con robosats@protonmail.com (o a través del método de contacto de usar y tirar que especificaste).",
"You have lost the dispute": "Has perdido la disputa",
"Unfortunately you have lost the dispute. If you think this is a mistake you can ask to re-open the case via email to robosats@protonmail.com. However, chances of it being investigated again are low.": "Desafortunadamente has perdido la disputa. Si piensas que es un error también puedes pedir reabrir el caso por email a robosats@protonmail.com. De todas formas, las probabilidades de ser investigado de nuevo son bajas.",
"Expired not taken":"Expiró sin ser tomada",
"Maker bond not locked":"La fianza del creador no fue bloqueada",
"Escrow not locked":"El depósito de garantía no fue bloqueado",
"Invoice not submitted":"No se entregó factura del comprado",
"Neither escrow locked or invoice submitted":"Ni el depósito de garantía fue bloqueado ni se entregó factura del comprador",
"Renew Order":"Renovar Orden",
"Pause the public order":"Pausar la orden pública",
"Your order is paused":"Tu orden está en pausa",
"Your public order has been paused. At the moment it cannot be seen or taken by other robots. You can choose to unpause it at any time.":"Tu orden pública fue pausada. Ahora mismo, la orden no puede ser vista ni tomada por otros robots. Puedes volver a activarla cuando desees.",
"Unpause Order":"Activar Orden",
"INFO DIALOG - InfoDiagog.js": "App information and clarifications and terms of use",
"Close": "Cerrar",