diff --git a/api/logics.py b/api/logics.py
index e31937f5..dae70ab0 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -97,21 +97,104 @@ class Logics():
@classmethod
def order_expires(cls, order):
- ''' General case when time runs out. Only
- used when the maker does not lock a publishing bond'''
+ ''' General cases when time runs out.'''
- if order.status == Order.Status.WFB:
+ # Do not change order status if an order in any with
+ # any of these status is sent to expire here
+ do_nothing = [Order.Status.DEL, Order.Status.UCA,
+ Order.Status.EXP, Order.Status.FSE,
+ Order.Status.DIS, Order.Status.CCA,
+ Order.Status.PAY, Order.Status.SUC,
+ Order.Status.FAI, Order.Status.MLD,
+ Order.Status.TLD]
+
+ if order.status in do_nothing:
+ return False
+
+ elif order.status == Order.Status.WFB:
order.status = Order.Status.EXP
order.maker = None
order.taker = None
order.save()
+ return True
- if order.status == Order.Status.PUB:
+ elif order.status == Order.Status.PUB:
cls.return_bond(order.maker_bond)
order.status = Order.Status.EXP
order.maker = None
order.taker = None
order.save()
+ return True
+
+ elif order.status == Order.Status.TAK:
+ cls.kick_taker(order)
+ return True
+
+ elif order.status == Order.Status.WF2:
+ '''Weird case where an order expires and both participants
+ did not proceed with the contract. Likely the site was
+ down or there was a bug. Still bonds must be charged
+ to avoid service DDOS. '''
+
+ cls.settle_bond(order.maker_bond)
+ cls.settle_bond(order.taker_bond)
+ order.status = Order.Status.EXP
+ order.maker = None
+ order.taker = None
+ order.save()
+ return True
+
+ elif order.status == Order.Status.WFE:
+ maker_is_seller = cls.is_seller(order, order.maker)
+ # If maker is seller, settle the bond and order goes to expired
+ if maker_is_seller:
+ cls.settle_bond(order.maker_bond)
+ order.status = Order.Status.EXP
+ order.maker = None
+ order.taker = None
+ order.save()
+ return True
+
+ # If maker is buyer, settle the taker's bond order goes back to public
+ else:
+ cls.settle_bond(order.taker_bond)
+ order.status = Order.Status.PUB
+ order.taker = None
+ order.taker_bond = None
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
+ order.save()
+ return True
+
+ elif order.status == Order.Status.WFI:
+ # The trade could happen without a buyer invoice. However, this user
+ # is most likely AFK since he did not submit an invoice; will most
+ # likely desert the contract as well.
+ maker_is_buyer = cls.is_buyer(order, order.maker)
+ # If maker is buyer, settle the bond and order goes to expired
+ if maker_is_buyer:
+ cls.settle_bond(order.maker_bond)
+ order.status = Order.Status.EXP
+ order.maker = None
+ order.taker = None
+ order.save()
+ return True
+
+ # If maker is seller, settle the taker's bond order goes back to public
+ else:
+ cls.settle_bond(order.taker_bond)
+ order.status = Order.Status.PUB
+ order.taker = None
+ order.taker_bond = None
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
+ order.save()
+ return True
+
+ elif order.status == Order.Status.CHA:
+ # Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute
+ # was opened. A seller-scammer could persuade a buyer to not click "fiat sent"
+ # as of now, we assume this is a dispute case by default.
+ cls.open_dispute(order)
+ return True
def kick_taker(order):
''' The taker did not lock the taker_bond. Now he has to go'''
@@ -125,10 +208,48 @@ class Logics():
order.status = Order.Status.PUB
order.taker = None
order.taker_bond = None
- order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION) ## TO FIX. Restore the remaining order durantion, not all of it!
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
order.save()
return True
+ @classmethod
+ def open_dispute(cls, order, user=None):
+
+ # Always settle the escrow during a dispute (same as with 'Fiat Sent')
+ if not order.trade_escrow.status == LNPayment.Status.SETLED:
+ cls.settle_escrow(order)
+
+ order.is_disputed = True
+ order.status = Order.Status.DIS
+ order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
+ order.save()
+
+ # User could be None if a dispute is open automatically due to weird expiration.
+ if not user == None:
+ profile = user.profile
+ profile.num_disputes = profile.num_disputes + 1
+ profile.orders_disputes_started = list(profile.orders_disputes_started).append(str(order.id))
+ profile.save()
+
+ return True, None
+ def dispute_statement(order, user, statement):
+ ''' Updates the dispute statements in DB'''
+
+ if len(statement) > 5000:
+ return False, {'bad_statement':'The statement is longer than 5000 characters'}
+ if order.maker == user:
+ order.maker_statement = statement
+ else:
+ order.taker_statement = statement
+
+ # If both statements are in, move to wait for dispute resolution
+ if order.maker_statement != None and order.taker_statement != None:
+ order.status = Order.Status.WFR
+ order.expires_at = timezone.now() + Order.t_to_expire[Order.Status.WFR]
+
+ order.save()
+ return True, None
+
@classmethod
def buyer_invoice_amount(cls, order, user):
''' Computes buyer invoice amount. Uses order.last_satoshis,
@@ -234,7 +355,7 @@ class Logics():
on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)'''
elif order.status == Order.Status.PUB and order.maker == user:
#Settle the maker bond (Maker loses the bond for cancelling public order)
- if cls.settle_maker_bond(order):
+ if cls.settle_bond(order.maker_bond):
order.maker = None
order.status = Order.Status.UCA
order.save()
@@ -257,7 +378,7 @@ class Logics():
'''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.PUB and order.status < Order.Status.CHA and order.maker == user:
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
- valid = cls.settle_maker_bond(order)
+ valid = cls.settle_bond(order.maker_bond)
if valid:
order.maker = None
order.status = Order.Status.UCA
@@ -268,7 +389,7 @@ class Logics():
'''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.TAK and order.status < Order.Status.CHA and order.taker == user:
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
- valid = cls.settle_taker_bond(order)
+ valid = cls.settle_bond(order.taker_bond)
if valid:
order.taker = None
order.status = Order.Status.PUB
@@ -277,7 +398,7 @@ class Logics():
return True, None
# 5) When trade collateral has been posted (after escrow)
- '''Always goes to cancelled status. Collaboration is needed.
+ '''Always goes to cancelled status. Collaboration is needed.
When a user asks for cancel, 'order.is_pending_cancel' goes True.
When the second user asks for cancel. Order is totally cancelled.
Has a small cost for both parties to prevent node DDOS.'''
@@ -383,7 +504,7 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
- description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + str(order.currency)}"# Order.currency_dict[str(order.currency)]}"
+ description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}"
+ " - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel.")
# Gen hold Invoice
@@ -472,22 +593,20 @@ class Logics():
order.trade_escrow.save()
return True
- def settle_maker_bond(order):
- ''' Settles the maker bond hold invoice'''
+ def settle_bond(bond):
+ ''' Settles the bond hold invoice'''
# TODO ERROR HANDLING
- if LNNode.settle_hold_invoice(order.maker_bond.preimage):
- order.maker_bond.status = LNPayment.Status.SETLED
- order.maker_bond.save()
+ if LNNode.settle_hold_invoice(bond.preimage):
+ bond.status = LNPayment.Status.SETLED
+ bond.save()
return True
- def settle_taker_bond(order):
- ''' Settles the taker bond hold invoice'''
- # TODO ERROR HANDLING
- if LNNode.settle_hold_invoice(order.taker_bond.preimage):
- order.taker_bond.status = LNPayment.Status.SETLED
- order.taker_bond.save()
+ def return_escrow(order):
+ '''returns the trade escrow'''
+ if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
+ order.trade_escrow.status = LNPayment.Status.RETNED
return True
-
+
def return_bond(bond):
'''returns a bond'''
if LNNode.cancel_return_hold_invoice(bond.payment_hash):
diff --git a/api/models.py b/api/models.py
index ee718031..a2d04adc 100644
--- a/api/models.py
+++ b/api/models.py
@@ -107,8 +107,9 @@ class Order(models.Model):
PAY = 13, 'Sending satoshis to buyer'
SUC = 14, 'Sucessful trade'
FAI = 15, 'Failed lightning network routing'
- MLD = 16, 'Maker lost dispute'
- TLD = 17, 'Taker lost dispute'
+ WFR = 16, 'Wait for dispute resolution'
+ MLD = 17, 'Maker lost dispute'
+ TLD = 18, 'Taker lost dispute'
# order info
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
@@ -135,10 +136,14 @@ class Order(models.Model):
maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order
taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order
is_pending_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
- is_disputed = models.BooleanField(default=False, null=False)
is_fiat_sent = models.BooleanField(default=False, null=False)
- # HTLCs
+ # in dispute
+ is_disputed = models.BooleanField(default=False, null=False)
+ maker_statement = models.TextField(max_length=5000, unique=True, null=True, default=None, blank=True)
+ taker_statement = models.TextField(max_length=5000, unique=True, null=True, default=None, blank=True)
+
+ # LNpayments
# Order collateral
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
@@ -147,11 +152,11 @@ class Order(models.Model):
# buyer payment LN invoice
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- # cancel LN invoice // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
- maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ # Unused so far. Cancel LN invoices // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
+ # maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ # taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- total_time_to_expire = {
+ t_to_expire = {
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
1 : 60*60*int(config('PUBLIC_ORDER_DURATION')), # 'Public'
2 : 0, # 'Deleted'
@@ -163,13 +168,14 @@ class Order(models.Model):
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'
10 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Fiat sent - In chatroom'
- 11 : 24*60*60, # 'In dispute'
+ 11 : 10*24*60*60, # 'In dispute'
12 : 0, # 'Collaboratively cancelled'
13 : 24*60*60, # 'Sending satoshis to buyer'
14 : 24*60*60, # 'Sucessful trade'
15 : 24*60*60, # 'Failed lightning network routing'
- 16 : 24*60*60, # 'Maker lost dispute'
- 17 : 24*60*60, # 'Taker lost dispute'
+ 16 : 24*60*60, # 'Wait for dispute resolution'
+ 17 : 24*60*60, # 'Maker lost dispute'
+ 18 : 24*60*60, # 'Taker lost dispute'
}
def __str__(self):
@@ -201,6 +207,8 @@ class Profile(models.Model):
# Disputes
num_disputes = models.PositiveIntegerField(null=False, default=0)
lost_disputes = models.PositiveIntegerField(null=False, default=0)
+ num_disputes_started = models.PositiveIntegerField(null=False, default=0)
+ orders_disputes_started = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store ID of orders
# RoboHash
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True)
@@ -254,7 +262,7 @@ class MarketTick(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
- currency = models.PositiveSmallIntegerField(choices=Currency.currency_choices, null=True)
+ currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
timestamp = models.DateTimeField(auto_now_add=True)
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
diff --git a/api/serializers.py b/api/serializers.py
index 6beff335..88997f3d 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -13,5 +13,6 @@ class MakeOrderSerializer(serializers.ModelSerializer):
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None)
- action = serializers.ChoiceField(choices=('take','update_invoice','dispute','cancel','confirm','rate'), allow_null=False)
+ statement = serializers.CharField(max_length=10000, allow_null=True, allow_blank=True, default=None)
+ action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate'), allow_null=False)
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)
\ No newline at end of file
diff --git a/api/tasks.py b/api/tasks.py
index 6fe4f75c..16219ac8 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -43,8 +43,8 @@ def users_cleansing():
@shared_task(name="orders_expire")
def orders_expire(rest_secs):
'''
- Continuously checks order expiration times for 1 hour.
- If order is expires, it handles the actions.
+ Continuously checks order expiration times for 1 hour. If order
+ has expires, it calls the logics module for expiration handling.
'''
now = timezone.now()
end_time = now + timedelta(hours=1)
@@ -55,8 +55,8 @@ def orders_expire(rest_secs):
queryset = queryset.filter(expires_at__lt=now) # expires at lower than now
for order in queryset:
- context.append(str(order)+ " was "+ Order.Status(order.status).label)
- Logics.order_expires(order)
+ if Logics.order_expires(order): # Order send to expire here
+ context.append(str(order)+ " was "+ Order.Status(order.status).label)
# Allow for some thread rest.
time.sleep(rest_secs)
@@ -77,12 +77,14 @@ def follow_lnd_payment():
''' Makes a payment and follows it.
Updates the LNpayment object, and retries
until payment is done'''
+
pass
@shared_task
def follow_lnd_hold_invoice():
''' Follows and updates LNpayment object
until settled or canceled'''
+
pass
@shared_task(name="cache_external_market_prices", ignore_result=True)
diff --git a/api/views.py b/api/views.py
index 3ddbb600..812b635b 100644
--- a/api/views.py
+++ b/api/views.py
@@ -106,7 +106,7 @@ class OrderView(viewsets.ViewSet):
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
data = ListOrderSerializer(order).data
- data['total_secs_exp'] = Order.total_time_to_expire[order.status]
+ data['total_secs_exp'] = Order.t_to_expire[order.status]
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
@@ -217,10 +217,13 @@ class OrderView(viewsets.ViewSet):
order = Order.objects.get(id=order_id)
- # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' 6)'rate' (counterparty)
+ # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
+ # 6)'submit_statement' (in dispute), 7)'rate' (counterparty)
action = serializer.data.get('action')
invoice = serializer.data.get('invoice')
+ statement = serializer.data.get('statement')
rating = serializer.data.get('rating')
+
# 1) If action is take, it is a taker request!
if action == 'take':
@@ -255,7 +258,11 @@ class OrderView(viewsets.ViewSet):
# 5) If action is dispute
elif action == 'dispute':
- valid, context = Logics.open_dispute(order,request.user, rating)
+ valid, context = Logics.open_dispute(order,request.user)
+ if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
+
+ elif action == 'submit_statement':
+ valid, context = Logics.dispute_statement(order,request.user, statement)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate
diff --git a/chat/consumers.py b/chat/consumers.py
index a65ed459..cb3da612 100644
--- a/chat/consumers.py
+++ b/chat/consumers.py
@@ -21,10 +21,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
# if not (Logics.is_buyer(order[0], self.user) or Logics.is_seller(order[0], self.user)):
# print ("Outta this chat")
# return False
-
- print(self.user_nick)
- print(self.order_id)
-
+
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index c71f2f08..fb359736 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -38,6 +38,7 @@ export default class TradeBox extends Component {
super(props);
this.state = {
badInvoice: false,
+ badStatement: false,
}
}
@@ -200,8 +201,6 @@ export default class TradeBox extends Component {
});
}
- // Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage.
-
handleClickSubmitInvoiceButton=()=>{
this.setState({badInvoice:false});
@@ -219,10 +218,34 @@ export default class TradeBox extends Component {
& console.log(data));
}
+ handleInputDisputeChanged=(e)=>{
+ this.setState({
+ statement: e.target.value,
+ badStatement: false,
+ });
+ }
+
+ handleClickSubmitStatementButton=()=>{
+ this.setState({badInvoice:false});
+
+ const requestOptions = {
+ method: 'POST',
+ headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
+ body: JSON.stringify({
+ 'action':'submit_statement',
+ 'statement': this.state.statement,
+ }),
+ };
+ fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
+ .then((response) => response.json())
+ .then((data) => this.setState({badStatement:data.bad_statement})
+ & console.log(data));
+}
+
showInputInvoice(){
return (
- // TODO Camera option to read QR
+ // TODO Option to upload files and images
@@ -252,7 +275,51 @@ export default class TradeBox extends Component {
/>
-
+
+
+
+ {this.showBondIsLocked()}
+
+ )
+ }
+
+ // Asks the user for a dispute statement.
+ showInDisputeStatement(){
+ return (
+
+ // TODO Option to upload files
+
+
+
+
+ A dispute has been opened
+
+
+
+
+ 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.
+ Disputes are solved at the discretion of real robots (aka humans), so be as helpful
+ as possible to ensure a fair outcome. Max 5000 chars.
+
+
+
+
+
+
+
+
{this.showBondIsLocked()}
@@ -463,8 +530,8 @@ handleRatingChange=(e)=>{
{/* Trade Finished - Payment Routing Failed */}
{this.props.data.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""}
- {/* Trade Finished - Payment Routing Failed - TODO Needs more planning */}
- {this.props.data.statusCode == 11 ? this.showInDispute() : ""}
+ {/* Trade Finished - TODO Needs more planning */}
+ {this.props.data.statusCode == 11 ? this.showInDisputeStatement() : ""}
{/* TODO */}