Merge pull request #54 from Reckless-Satoshi/dispute-system-v2

A collection of fixes and small improvements.
This commit is contained in:
Reckless_Satoshi 2022-03-02 11:42:22 +00:00 committed by GitHub
commit bd97148132
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 587 additions and 51 deletions

View File

@ -58,6 +58,9 @@ PENALTY_TIMEOUT = 60
# Time between routing attempts of buyer invoice in MINUTES # Time between routing attempts of buyer invoice in MINUTES
RETRY_TIME = 5 RETRY_TIME = 5
# Platform activity limits
MAX_PUBLIC_ORDERS = 100
# Trade limits in satoshis # Trade limits in satoshis
MIN_TRADE = 10000 MIN_TRADE = 10000
MAX_TRADE = 500000 MAX_TRADE = 500000

View File

@ -103,6 +103,7 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"avatar_tag", "avatar_tag",
"id", "id",
"user_link", "user_link",
"telegram_enabled",
"total_contracts", "total_contracts",
"platform_rating", "platform_rating",
"total_ratings", "total_ratings",

View File

@ -4,7 +4,7 @@ from api.lightning.node import LNNode
from django.db.models import Q from django.db.models import Q
from api.models import Order, LNPayment, MarketTick, User, Currency from api.models import Order, LNPayment, MarketTick, User, Currency
from api.messages import Telegram from api.tasks import send_message
from decouple import config from decouple import config
import math import math
@ -30,7 +30,6 @@ FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION"))
class Logics: class Logics:
telegram = Telegram()
@classmethod @classmethod
def validate_already_maker_or_taker(cls, user): def validate_already_maker_or_taker(cls, user):
"""Validates if a use is already not part of an active order""" """Validates if a use is already not part of an active order"""
@ -129,7 +128,7 @@ class Logics:
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.TAK]) seconds=Order.t_to_expire[Order.Status.TAK])
order.save() order.save()
cls.telegram.order_taken(order) send_message.delay(order.id,'order_taken')
return True, None return True, None
def is_buyer(order, user): def is_buyer(order, user):
@ -208,11 +207,13 @@ class Logics:
cls.return_bond(order.maker_bond) cls.return_bond(order.maker_bond)
order.status = Order.Status.EXP order.status = Order.Status.EXP
order.save() order.save()
send_message.delay(order.id,'order_expired_untaken')
return True return True
elif order.status == Order.Status.TAK: elif order.status == Order.Status.TAK:
cls.cancel_bond(order.taker_bond) cls.cancel_bond(order.taker_bond)
cls.kick_taker(order) cls.kick_taker(order)
send_message.delay(order.id,'taker_expired_b4bond')
return True return True
elif order.status == Order.Status.WF2: elif order.status == Order.Status.WF2:
@ -342,13 +343,18 @@ class Logics:
if not order.status == Order.Status.DIS: if not order.status == Order.Status.DIS:
return False, { return False, {
"bad_request": "bad_request":
"Only orders in dispute accept a dispute statements" "Only orders in dispute accept dispute statements"
} }
if len(statement) > 5000: if len(statement) > 5000:
return False, { return False, {
"bad_statement": "The statement is longer than 5000 characters" "bad_statement": "The statement is longer than 5000 characters"
} }
if len(statement) < 100:
return False, {
"bad_statement": "The statement is too short. Make sure to be thorough."
}
if order.maker == user: if order.maker == user:
order.maker_statement = statement order.maker_statement = statement
@ -356,7 +362,7 @@ class Logics:
order.taker_statement = statement order.taker_statement = statement
# If both statements are in, move status to wait for dispute resolution # If both statements are in, move status to wait for dispute resolution
if order.maker_statement != None and order.taker_statement != None: if order.maker_statement not in [None,""] and order.taker_statement not in [None,""]:
order.status = Order.Status.WFR order.status = Order.Status.WFR
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.WFR]) seconds=Order.t_to_expire[Order.Status.WFR])
@ -519,11 +525,11 @@ class Logics:
to prevent DDOS on the LN node and order book. If not strict, maker is returned to prevent DDOS on the LN node and order book. If not strict, maker is returned
the bond (more user friendly).""" 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) # Return the maker bond (Maker gets returned the bond for cancelling public order)
if cls.return_bond(order.maker_bond if cls.return_bond(order.maker_bond): # strict cancellation: cls.settle_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()
send_message.delay(order.id,'public_order_cancelled')
return True, None return True, None
# 3) When taker cancels before bond # 3) When taker cancels before bond
@ -533,6 +539,7 @@ class Logics:
# adds a timeout penalty # adds a timeout penalty
cls.cancel_bond(order.taker_bond) cls.cancel_bond(order.taker_bond)
cls.kick_taker(order) cls.kick_taker(order)
send_message.delay(order.id,'taker_canceled_b4bond')
return True, None return True, None
# 4) When taker or maker cancel after bond (before escrow) # 4) When taker or maker cancel after bond (before escrow)
@ -612,6 +619,7 @@ class Logics:
order.expires_at = order.created_at + timedelta( order.expires_at = order.created_at + timedelta(
seconds=Order.t_to_expire[Order.Status.PUB]) seconds=Order.t_to_expire[Order.Status.PUB])
order.save() order.save()
send_message.delay(order.id,'order_published')
return return
@classmethod @classmethod
@ -999,14 +1007,9 @@ class Logics:
order.payout.status = LNPayment.Status.FLIGHT order.payout.status = LNPayment.Status.FLIGHT
order.payout.save() order.payout.save()
order.save() order.save()
send_message.delay(order.id,'trade_successful')
return True, None return True, None
# is_payed, context = follow_send_payment(order.payout) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
# if is_payed:
# order.save()
# return True, context
# else:
# # error handling here
# return False, context
else: else:
return False, { return False, {
"bad_request": "bad_request":

View File

@ -31,7 +31,12 @@ class Command(BaseCommand):
if len(list(response['result'])) == 0: if len(list(response['result'])) == 0:
continue continue
for result in response['result']: for result in response['result']:
text = result['message']['text']
try: # if there is no key message, skips this result.
text = result['message']['text']
except:
continue
splitted_text = text.split(' ') splitted_text = text.split(' ')
if splitted_text[0] == '/start': if splitted_text[0] == '/start':
token = splitted_text[-1] token = splitted_text[-1]

View File

@ -2,6 +2,7 @@ from decouple import config
from secrets import token_urlsafe from secrets import token_urlsafe
from api.models import Order from api.models import Order
from api.utils import get_tor_session from api.utils import get_tor_session
import time
class Telegram(): class Telegram():
''' Simple telegram messages by requesting to API''' ''' Simple telegram messages by requesting to API'''
@ -33,23 +34,33 @@ class Telegram():
chat_id = user.profile.telegram_chat_id chat_id = user.profile.telegram_chat_id
message_url = f'https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}' message_url = f'https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}'
response = self.session.get(message_url).json() # telegram messaging is atm inserted dangerously in the logics module
print(response) # if it fails, it should keep trying
while True:
return try:
self.session.get(message_url).json()
return
except:
pass
def welcome(self, user): def welcome(self, user):
lang = user.profile.telegram_lang_code lang = user.profile.telegram_lang_code
order = Order.objects.get(maker=user)
# In weird cases the order cannot be found (e.g. it is cancelled)
queryset = Order.objects.filter(maker=user)
order = queryset.last()
print(str(order.id)) print(str(order.id))
if lang == 'es': if lang == 'es':
text = f'Hola ⚡{user.username}⚡, Te enviaré un mensaje cuando tu orden con ID {str(order.id)} haya sido tomada.' text = f'Hola {user.username}, te enviaré un mensaje cuando tu orden con ID {str(order.id)} haya sido tomada.'
else: else:
text = f"Hey ⚡{user.username}⚡, I will send you a message when someone takes your order with ID {str(order.id)}." text = f"Hey {user.username}, I will send you a message when someone takes your order with ID {str(order.id)}."
self.send_message(user, text) self.send_message(user, text)
user.profile.telegram_welcomed = True
user.profile.save()
return return
def order_taken(self, order): def order_taken(self, order):
user = order.maker user = order.maker
if not user.profile.telegram_enabled: if not user.profile.telegram_enabled:
@ -59,9 +70,102 @@ class Telegram():
taker_nick = order.taker.username taker_nick = order.taker.username
site = config('HOST_NAME') site = config('HOST_NAME')
if lang == 'es': if lang == 'es':
text = f'Tu orden con ID {order.id} ha sido tomada por {taker_nick}!🥳 Visita http://{site}/order/{order.id} para continuar.' text = f'¡Tu orden con ID {order.id} ha sido tomada por {taker_nick}!🥳 Visita http://{site}/order/{order.id} para continuar.'
else: else:
text = f'Your order with ID {order.id} was taken by {taker_nick}!🥳 Visit http://{site}/order/{order.id} to proceed with the trade.' text = f'Your order with ID {order.id} was taken by {taker_nick}!🥳 Visit http://{site}/order/{order.id} to proceed with the trade.'
self.send_message(user, text) self.send_message(user, text)
return
def order_expired_untaken(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'Tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{site} para crear una nueva.'
else:
text = f'Your order with ID {order.id} has expired untaken. Visit http://{site} to create a new one.'
self.send_message(user, text)
return
def trade_successful(self, order):
user = order.maker
if not user.profile.telegram_enabled:
return
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Unase a @robosats_es y ayudanos a mejorar.'
else:
text = f'Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve.'
self.send_message(user, text)
return
def public_order_cancelled(self, order):
user = order.maker
if not user.profile.telegram_enabled:
return
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'Has cancelado tu orden pública con ID {order.id}.'
else:
text = f'You have cancelled your public order with ID {order.id}.'
self.send_message(user, text)
return
def taker_canceled_b4bond(self, order):
user = order.maker
if not user.profile.telegram_enabled:
return
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'El tomador ha cancelado antes de bloquear su fianza.'
else:
text = f'The taker has canceled before locking the bond.'
self.send_message(user, text)
return
def taker_expired_b4bond(self, order):
user = order.maker
if not user.profile.telegram_enabled:
return
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'El tomador no ha bloqueado la fianza a tiempo.'
else:
text = f'The taker has not locked the bond in time.'
self.send_message(user, text)
return
def order_published(self, order):
time.sleep(1) # Just so this message always arrives after the previous two
user = order.maker
lang = user.profile.telegram_lang_code
# In weird cases the order cannot be found (e.g. it is cancelled)
queryset = Order.objects.filter(maker=user)
order = queryset.last()
print(str(order.id))
if lang == 'es':
text = f'Tu orden con ID {str(order.id)} es pública en el libro de ordenes.'
else:
text = f"Your order with ID {str(order.id)} is public in the order book."
self.send_message(user, text)
user.profile.telegram_welcomed = True
user.profile.save()
return return

View File

@ -152,3 +152,37 @@ def cache_market():
) )
return results return results
@shared_task(name="send_message", ignore_result=True)
def send_message(order_id, message):
from api.models import Order
order = Order.objects.get(id=order_id)
if not order.maker.profile.telegram_enabled:
return
from api.messages import Telegram
telegram = Telegram()
if message == 'order_taken':
telegram.order_taken(order)
elif message == 'order_expired_untaken':
telegram.order_expired_untaken(order)
elif message == 'trade_successful':
telegram.trade_successful(order)
elif message == 'public_order_cancelled':
telegram.public_order_cancelled(order)
elif message == 'taker_expired_b4bond':
telegram.taker_expired_b4bond(order)
elif message == 'taker_canceled_b4bond':
telegram.taker_canceled_b4bond(order)
elif message == 'order_published':
telegram.order_published(order)
return

View File

@ -55,6 +55,16 @@ class MakerView(CreateAPIView):
if not serializer.is_valid(): if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
# In case it gets overwhelming. Limit the number of public orders.
if Order.objects.filter(status=Order.Status.PUB).count() >= int(config("MAX_PUBLIC_ORDERS")):
return Response(
{
"bad_request":
"Woah! RoboSats' book is at full capacity! Try again later"
},
status.HTTP_400_BAD_REQUEST,
)
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")
@ -311,7 +321,7 @@ class OrderView(viewsets.ViewSet):
and order.maker_statement != "") and order.maker_statement != "")
elif data["is_taker"]: elif data["is_taker"]:
data["statement_submitted"] = (order.taker_statement != None data["statement_submitted"] = (order.taker_statement != None
and order.maker_statement != "") and order.taker_statement != "")
# 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third. # 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third.
elif (order.status == Order.Status.FAI elif (order.status == Order.Status.FAI

View File

@ -219,10 +219,10 @@ export default class MakerPage extends Component {
</Grid> </Grid>
<br/> <br/>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Enter your prefered payment methods"> <Tooltip placement="top" enterTouchDelay="300" enterDelay="700" enterNextDelay="2000" title="Enter your prefered fiat payment methods (instant recommended)">
<TextField <TextField
sx={{width:240}} sx={{width:240}}
label={this.state.currency==1000 ? "Swap Destination (e.g. rBTC)":"Instant Payment Method(s)"} label={this.state.currency==1000 ? "Swap Destination (e.g. rBTC)":"Fiat Payment Method(s)"}
error={this.state.badPaymentMethod} error={this.state.badPaymentMethod}
helperText={this.state.badPaymentMethod ? "Must be shorter than 35 characters":""} helperText={this.state.badPaymentMethod ? "Must be shorter than 35 characters":""}
type="text" type="text"

View File

@ -63,8 +63,8 @@ export default class OrderPage extends Component {
"3": 2000, //'Waiting for taker bond' "3": 2000, //'Waiting for taker bond'
"4": 999999, //'Cancelled' "4": 999999, //'Cancelled'
"5": 999999, //'Expired' "5": 999999, //'Expired'
"6": 3000, //'Waiting for trade collateral and buyer invoice' "6": 6000, //'Waiting for trade collateral and buyer invoice'
"7": 3000, //'Waiting only for seller trade collateral' "7": 8000, //'Waiting only for seller trade collateral'
"8": 8000, //'Waiting only for buyer invoice' "8": 8000, //'Waiting only for buyer invoice'
"9": 10000, //'Sending fiat - In chatroom' "9": 10000, //'Sending fiat - In chatroom'
"10": 10000, //'Fiat sent - In chatroom' "10": 10000, //'Fiat sent - In chatroom'

View File

@ -195,6 +195,26 @@ export default class TradeBox extends Component {
); );
} }
showBondIsSettled=()=>{
return (
<Grid item xs={12} align="center">
<Typography color="error" component="subtitle1" variant="subtitle1" align="center">
Your {this.props.data.is_maker ? 'maker' : 'taker'} bond was settled
</Typography>
</Grid>
);
}
showBondIsReturned=()=>{
return (
<Grid item xs={12} align="center">
<Typography color="green" component="subtitle1" variant="subtitle1" align="center">
🔓 Your {this.props.data.is_maker ? 'maker' : 'taker'} bond was unlocked
</Typography>
</Grid>
);
}
showEscrowQRInvoice=()=>{ showEscrowQRInvoice=()=>{
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
@ -476,10 +496,16 @@ export default class TradeBox extends Component {
</Grid> </Grid>
<Grid item xs={12} align="left"> <Grid item xs={12} align="left">
<Typography component="body2" variant="body2"> <Typography component="body2" variant="body2">
We are waiting for your trade counterparty statement. <p>We are waiting for your trade counterparty statement. If you are hesitant about
the state of the dispute or want to add more information, contact robosats@protonmail.com.</p>
<p>Please, save the information needed to identificate your order and your payments: order ID;
payment hashes of the bonds or escrow (check on your lightning wallet); exact amount of
satoshis; and robot nickname. You will have to identify yourself as the user involved
in this trade via email (or other contact methods).</p>
</Typography> </Typography>
</Grid> </Grid>
{this.showBondIsLocked()} {this.showBondIsSettled()}
</Grid> </Grid>
) )
}else{ }else{
@ -496,7 +522,7 @@ export default class TradeBox extends Component {
<Grid item xs={12} align="left"> <Grid item xs={12} align="left">
<Typography component="body2" variant="body2"> <Typography component="body2" variant="body2">
Please, submit your statement. Be clear and specific about what happened and provide the necessary Please, submit your statement. Be clear and specific about what happened and provide the necessary
evidence. It is best to provide a burner email, XMPP or telegram username to follow up with the staff. evidence. You MUST provide a contact method: burner email, XMPP or telegram username to follow up with the staff.
Disputes are solved at the discretion of real robots <i>(aka humans)</i>, so be as helpful Disputes are solved at the discretion of real robots <i>(aka humans)</i>, so be as helpful
as possible to ensure a fair outcome. Max 5000 chars. as possible to ensure a fair outcome. Max 5000 chars.
</Typography> </Typography>
@ -519,9 +545,8 @@ export default class TradeBox extends Component {
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button onClick={this.handleClickSubmitStatementButton} variant='contained' color='primary'>Submit</Button> <Button onClick={this.handleClickSubmitStatementButton} variant='contained' color='primary'>Submit</Button>
</Grid> </Grid>
{this.showBondIsSettled()}
{this.showBondIsLocked()} </Grid>
</Grid>
)} )}
} }
@ -535,11 +560,60 @@ export default class TradeBox extends Component {
</Grid> </Grid>
<Grid item xs={12} align="left"> <Grid item xs={12} align="left">
<Typography component="body2" variant="body2"> <Typography component="body2" variant="body2">
Wait for the staff to resolve the dispute. The dispute winner <p>Both statements have been received, wait for the staff to resolve the dispute.
will be asked to submit a LN invoice. The dispute winner will be asked to submit a LN invoice via the contact methods provided.
If you are hesitant about the state of the dispute or want to add more information,
contact robosats@protonmail.com. If you did not provide a contact method, write us inmediately. </p>
<p>Please, save the information needed to identificate your order and your payments: order ID;
payment hashes of the bonds or escrow (check on your lightning wallet); exact amount of
satoshis; and robot nickname. You will have to identify yourself as the user involved
in this trade via email (or other contact methods).</p>
</Typography> </Typography>
</Grid> </Grid>
{this.showBondIsLocked()} {this.showBondIsSettled()}
</Grid>
)
}
showDisputeWinner=()=>{
return (
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography color="primary" component="subtitle1" variant="subtitle1">
<b> You have won the dispute </b>
</Typography>
</Grid>
<Grid item xs={12} align="left">
<Typography component="body2" variant="body2">
You will be sent the satoshis of the escrow and your fidelity bond.
This is not an automatic process, instead it will be sent manually by the staff.
Please coordinate with the staff by writing to robosats@protonmail.com (or via your provided
burner contact method). You will be asked to submit a new invoice together with identificative
information about this order (bond payment hash, robot nicknames, exact amount in satoshis and order ID).
</Typography>
</Grid>
{this.showBondIsSettled()}
</Grid>
)
}
showDisputeLoser=()=>{
return (
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography color="error" component="subtitle1" variant="subtitle1">
<b> You have lost the dispute </b>
</Typography>
</Grid>
<Grid item xs={12} align="left">
<Typography component="body2" variant="body2">
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.
</Typography>
</Grid>
{this.showBondIsSettled()}
</Grid> </Grid>
) )
} }
@ -776,7 +850,8 @@ handleRatingRobosatsChange=(e)=>{
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button color='primary' onClick={() => {this.props.push('/')}}>Start Again</Button> <Button color='primary' onClick={() => {this.props.push('/')}}>Start Again</Button>
</Grid> </Grid>
</Grid> {this.showBondIsReturned()}
</Grid>
) )
} }
@ -851,7 +926,8 @@ handleRatingRobosatsChange=(e)=>{
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button> <Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button>
</Grid> </Grid>
</Grid> {this.showBondIsReturned()}
</Grid>
) )
}else{ }else{
return( return(
@ -874,7 +950,8 @@ handleRatingRobosatsChange=(e)=>{
</ListItemText> </ListItemText>
</List> </List>
</Grid> </Grid>
</Grid> {this.showBondIsReturned()}
</Grid>
)} )}
} }
@ -920,7 +997,9 @@ handleRatingRobosatsChange=(e)=>{
{/* Trade Finished - TODO Needs more planning */} {/* Trade Finished - TODO Needs more planning */}
{this.props.data.status == 11 ? this.showInDisputeStatement() : ""} {this.props.data.status == 11 ? this.showInDisputeStatement() : ""}
{this.props.data.status == 16 ? this.showWaitForDisputeResolution() : ""} {this.props.data.status == 16 ? this.showWaitForDisputeResolution() : ""}
{(this.props.data.status == 17 & this.props.data.is_taker) || (this.props.data.status == 18 & this.props.data.is_maker) ? this.showDisputeWinner() : ""}
{(this.props.data.status == 18 & this.props.data.is_taker) || (this.props.data.status == 17 & this.props.data.is_maker) ? this.showDisputeLoser() : ""}
{/* Order has expired */} {/* Order has expired */}
{this.props.data.status == 5 ? this.showOrderExpired() : ""} {this.props.data.status == 5 ? this.showOrderExpired() : ""}
{/* TODO */} {/* TODO */}

View File

@ -38,6 +38,31 @@ export default function getFlags(code){
if(code == 'UYU') return '🇺🇾'; if(code == 'UYU') return '🇺🇾';
if(code == 'PYG') return '🇵🇾'; if(code == 'PYG') return '🇵🇾';
if(code == 'BOB') return '🇧🇴'; if(code == 'BOB') return '🇧🇴';
if(code == 'IDR') return '🇮🇩';
if(code == 'ANG') return '🇧🇶';
if(code == 'CRC') return '🇨🇷';
if(code == 'CUP') return '🇨🇺';
if(code == 'DOP') return '🇩🇴';
if(code == 'GHS') return '🇬🇭';
if(code == 'GTQ') return '🇬🇹';
if(code == 'ILS') return '🇮🇱';
if(code == 'JMD') return '🇯🇲';
if(code == 'KES') return '🇰🇪';
if(code == 'KZT') return '🇰🇿';
if(code == 'MYR') return '🇲🇲';
if(code == 'NAD') return '🇳🇦';
if(code == 'NGN') return '🇳🇬';
if(code == 'AZN') return '🇦🇿';
if(code == 'PAB') return '🇵🇦';
if(code == 'PHP') return '🇵🇭';
if(code == 'PKR') return '🇵🇰';
if(code == 'QAR') return '🇶🇦';
if(code == 'SAR') return '🇸🇦';
if(code == 'THB') return '🇹🇭';
if(code == 'TTD') return '🇹🇹';
if(code == 'VND') return '🇻🇳';
if(code == 'XOF') return '🇸🇳';
if(code == 'XAU') return '🟨';
if(code == 'BTC') return <SwapCallsIcon color="primary"/>; if(code == 'BTC') return <SwapCallsIcon color="primary"/>;
return '🏳'; return '🏳';
}; };

View File

@ -35,5 +35,30 @@
"34":"UYU", "34":"UYU",
"35":"PYG", "35":"PYG",
"36":"BOB", "36":"BOB",
"37":"IDR",
"38":"ANG",
"39":"CRC",
"40":"CUP",
"41":"DOP",
"42":"GHS",
"43":"GTQ",
"44":"ILS",
"45":"JMD",
"46":"KES",
"47":"KZT",
"48":"MYR",
"49":"NAD",
"50":"NGN",
"51":"AZN",
"52":"PAB",
"53":"PHP",
"54":"PKR",
"55":"QAR",
"56":"SAR",
"57":"THB",
"58":"TTD",
"59":"VND",
"60":"XOF",
"300":"XAU",
"1000":"BTC" "1000":"BTC"
} }

View File

@ -0,0 +1,252 @@
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/static/css/fonts/roboto-1.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/static/css/fonts/roboto-2.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/static/css/fonts/roboto-3.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/static/css/fonts/roboto-4.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/static/css/fonts/roboto-5.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/static/css/fonts/roboto-6.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/static/css/fonts/roboto-7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/css/fonts/roboto-8.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/css/fonts/roboto-9.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/css/fonts/roboto-10.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/css/fonts/roboto-11.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/css/fonts/roboto-12.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/css/fonts/roboto-13.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/static/css/fonts/roboto-14.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/css/fonts/roboto-15.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/css/fonts/roboto-16.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/css/fonts/roboto-17.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/css/fonts/roboto-18.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/css/fonts/roboto-19.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/css/fonts/roboto-20.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/static/css/fonts/roboto-21.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/static/css/fonts/roboto-22.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/static/css/fonts/roboto-23.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/static/css/fonts/roboto-24.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/static/css/fonts/roboto-25.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/static/css/fonts/roboto-26.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/static/css/fonts/roboto-27.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/static/css/fonts/roboto-28.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -11,13 +11,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RoboSats - Simple and Private Bitcoin Exchange</title> <title>RoboSats - Simple and Private Bitcoin Exchange</title>
{% load static %} {% load static %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <link rel="stylesheet" href="{% static "css/fonts.css" %}"/>
<link <link rel="stylesheet" type="text/css" href="{% static "css/index.css" %}"/>
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<link rel="stylesheet" type="text/css" href="{% static "css/index.css" %}"
/>
</head> </head>
<body> <body>
<noscript> <noscript>