diff --git a/.env-sample b/.env-sample
index 3eb14fbbe..f6c56edf5 100644
--- a/.env-sample
+++ b/.env-sample
@@ -21,12 +21,14 @@ FEE = 0.002
BOND_SIZE = 0.01
# Time out penalty for canceling takers in SECONDS
PENALTY_TIMEOUT = 60
+# Time between routing attempts of buyer invoice in MINUTES
+RETRY_TIME = 5
# Trade limits in satoshis
MIN_TRADE = 10000
MAX_TRADE = 500000
-# Expiration time for HODL invoices and returning collateral in HOURS
+# Expiration (CLTV_expiry) time for HODL invoices in HOURS // 7 min/block assumed
BOND_EXPIRY = 14
ESCROW_EXPIRY = 8
@@ -41,5 +43,10 @@ INVOICE_AND_ESCROW_DURATION = 30
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
FIAT_EXCHANGE_DURATION = 4
+# Proportional routing fee limit (fraction of total payout: % / 100)
+PROPORTIONAL_ROUTING_FEE_LIMIT = 0.0002
+# Base flat limit fee for routing in Sats (used only when proportional is lower than this)
+MIN_FLAT_ROUTING_FEE_LIMIT = 10
+
# Username for HTLCs escrows
ESCROW_USERNAME = 'admin'
\ No newline at end of file
diff --git a/README.md b/README.md
index 715032504..9bbc0fbd9 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,14 @@
-## RoboSats - Buy and sell Satoshis Privately.
-[![release](https://img.shields.io/badge/release-v0.1.0%20MVP-orange)](https://github.com/Reckless-Satoshi/robosats/releases)
+## RoboSats - Buy and sell Satoshis Privately
+[![release](https://img.shields.io/badge/release-v0.1.0%20MVP-red)](https://github.com/Reckless-Satoshi/robosats/releases)
[![AGPL-3.0 license](https://img.shields.io/badge/license-AGPL--3.0-blue)](https://github.com/Reckless-Satoshi/robosats/blob/main/LICENSE)
[![Telegram](https://img.shields.io/badge/chat-telegram-brightgreen)](https://t.me/robosats)
RoboSats is a simple and private way to exchange bitcoin for national currencies. Robosats simplifies the peer-to-peer user experience and uses lightning hold invoices to minimize custody and trust requirements. The deterministically generated avatars help users stick to best privacy practices.
## Try it out
+
+
+
**Bitcoin mainnet:**
- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Coming soon)
@@ -15,7 +18,7 @@ RoboSats is a simple and private way to exchange bitcoin for national currencies
**Bitcoin testnet:**
- Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion (Active - On Dev Node)
- Url: testnet.robosats.com (Coming soon)
-- Commit height: Latest commit.
+- Latest commit.
*Always use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.*
@@ -39,7 +42,7 @@ Alice wants to buy satoshis privately:
See [CONTRIBUTING.md](CONTRIBUTING.md)
## Original idea
-The concept of a simple custody-minimized lightning exchange using hold invoices is heavily inspired by [P2PLNBOT](https://github.com/grunch/p2plnbot) by @grunch
+The concept of a simple custody-minimized lightning exchange using hold invoices is heavily inspired in [P2PLNBOT](https://github.com/grunch/p2plnbot) by @grunch
## License
diff --git a/api/admin.py b/api/admin.py
index ff7f2c689..0e0d17934 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -19,23 +19,25 @@ class EUserAdmin(UserAdmin):
inlines = [ProfileInline]
list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
list_display_links = ('id','username')
+ ordering = ('-id',)
def avatar_tag(self, obj):
return obj.profile.avatar_tag()
@admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
+ list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'payout_link','maker_bond_link','taker_bond_link','trade_escrow_link')
list_display_links = ('id','type')
- change_links = ('maker','taker','currency','buyer_invoice','maker_bond','taker_bond','trade_escrow')
+ change_links = ('maker','taker','currency','payout','maker_bond','taker_bond','trade_escrow')
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('hash','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link','order_made','order_taken','order_escrow','order_paid')
- list_display_links = ('hash','concept','order_made','order_taken','order_escrow','order_paid')
- change_links = ('sender','receiver')
+ list_display = ('hash','concept','status','num_satoshis','type','expires_at','expiry_height','sender_link','receiver_link','order_made_link','order_taken_link','order_escrow_link','order_paid_link')
+ list_display_links = ('hash','concept')
+ change_links = ('sender','receiver','order_made','order_taken','order_escrow','order_paid')
list_filter = ('type','concept','status')
+ ordering = ('-expires_at',)
@admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@@ -49,9 +51,11 @@ class CurrencieAdmin(admin.ModelAdmin):
list_display = ('id','currency','exchange_rate','timestamp')
list_display_links = ('id','currency')
readonly_fields = ('currency','exchange_rate','timestamp')
+ ordering = ('id',)
@admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin):
list_display = ('timestamp','price','volume','premium','currency','fee')
readonly_fields = ('timestamp','price','volume','premium','currency','fee')
- list_filter = ['currency']
\ No newline at end of file
+ list_filter = ['currency']
+ ordering = ('-timestamp',)
\ No newline at end of file
diff --git a/api/lightning/node.py b/api/lightning/node.py
index 4f6cb3489..cbcdbf255 100644
--- a/api/lightning/node.py
+++ b/api/lightning/node.py
@@ -9,6 +9,7 @@
from datetime import timedelta, datetime
from django.utils import timezone
+from api.models import LNPayment
#######
# Should work with LND (c-lightning in the future if there are features that deserve the work)
#######
@@ -64,7 +65,7 @@ def settle_hold_invoice(cls, preimage):
return str(response)=="" # True if no response, false otherwise.
@classmethod
- def gen_hold_invoice(cls, num_satoshis, description, expiry):
+ def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry, cltv_expiry_secs):
'''Generates hold invoice'''
hold_payment = {}
@@ -73,12 +74,16 @@ def gen_hold_invoice(cls, num_satoshis, description, expiry):
# Its hash is used to generate the hold invoice
r_hash = hashlib.sha256(preimage).digest()
-
+
+ # timelock expiry for the last hop, computed based on a 10 minutes block with 30% padding (~7 min block)
+ cltv_expiry_blocks = int(cltv_expiry_secs / (7*60))
request = invoicesrpc.AddHoldInvoiceRequest(
memo=description,
value=num_satoshis,
hash=r_hash,
- expiry=expiry)
+ expiry=int(invoice_expiry*1.5), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
+ cltv_expiry=cltv_expiry_blocks,
+ )
response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())])
hold_payment['invoice'] = response.payment_request
@@ -87,18 +92,22 @@ def gen_hold_invoice(cls, num_satoshis, description, expiry):
hold_payment['payment_hash'] = payreq_decoded.payment_hash
hold_payment['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
hold_payment['expires_at'] = hold_payment['created_at'] + timedelta(seconds=payreq_decoded.expiry)
+ hold_payment['cltv_expiry'] = cltv_expiry_blocks
return hold_payment
@classmethod
- def validate_hold_invoice_locked(cls, payment_hash):
+ def validate_hold_invoice_locked(cls, lnpayment):
'''Checks if hold invoice is locked'''
- request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
+ request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(lnpayment.payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
print('status here')
print(response.state)
# TODO ERROR HANDLING
+ # Will fail if 'unable to locate invoice'. Happens if invoice expiry
+ # time has passed (but these are 15% padded at the moment). Should catch it
+ # and report back that the invoice has expired (better robustness)
if response.state == 0: # OPEN
print('STATUS: OPEN')
pass
@@ -108,31 +117,34 @@ def validate_hold_invoice_locked(cls, payment_hash):
pass
if response.state == 3: # ACCEPTED (LOCKED)
print('STATUS: ACCEPTED')
+ lnpayment.expiry_height = response.htlcs[0].expiry_height
+ lnpayment.status = LNPayment.Status.LOCKED
+ lnpayment.save()
return True
- @classmethod
- def check_until_invoice_locked(cls, payment_hash, expiration):
- '''Checks until hold invoice is locked.
- When invoice is locked, returns true.
- If time expires, return False.'''
- # Experimental, might need asyncio. Best if subscribing all invoices and running a background task
- # Maybe best to pass LNpayment object and change status live.
-
- request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
- for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
- print(invoice)
- if timezone.now > expiration:
- break
- if invoice.state == 3: # True if hold invoice is accepted.
- return True
- return False
+ # @classmethod
+ # def check_until_invoice_locked(cls, payment_hash, expiration):
+ # '''Checks until hold invoice is locked.
+ # When invoice is locked, returns true.
+ # If time expires, return False.'''
+ # # Experimental, might need asyncio. Best if subscribing all invoices and running a background task
+ # # Maybe best to pass LNpayment object and change status live.
+
+ # request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
+ # for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
+ # print(invoice)
+ # if timezone.now > expiration:
+ # break
+ # if invoice.state == 3: # True if hold invoice is accepted.
+ # return True
+ # return False
@classmethod
def validate_ln_invoice(cls, invoice, num_satoshis):
'''Checks if the submited LN invoice comforms to expectations'''
- buyer_invoice = {
+ payout = {
'valid': False,
'context': None,
'description': None,
@@ -145,36 +157,36 @@ def validate_ln_invoice(cls, invoice, num_satoshis):
payreq_decoded = cls.decode_payreq(invoice)
print(payreq_decoded)
except:
- buyer_invoice['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
- return buyer_invoice
+ payout['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
+ return payout
if payreq_decoded.num_satoshis == 0:
- buyer_invoice['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
- return buyer_invoice
+ payout['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
+ return payout
if not payreq_decoded.num_satoshis == num_satoshis:
- buyer_invoice['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
- return buyer_invoice
+ payout['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
+ return payout
- buyer_invoice['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
- buyer_invoice['expires_at'] = buyer_invoice['created_at'] + timedelta(seconds=payreq_decoded.expiry)
+ payout['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
+ payout['expires_at'] = payout['created_at'] + timedelta(seconds=payreq_decoded.expiry)
- if buyer_invoice['expires_at'] < timezone.now():
- buyer_invoice['context'] = {'bad_invoice':f'The invoice provided has already expired'}
- return buyer_invoice
+ if payout['expires_at'] < timezone.now():
+ payout['context'] = {'bad_invoice':f'The invoice provided has already expired'}
+ return payout
- buyer_invoice['valid'] = True
- buyer_invoice['description'] = payreq_decoded.description
- buyer_invoice['payment_hash'] = payreq_decoded.payment_hash
+ payout['valid'] = True
+ payout['description'] = payreq_decoded.description
+ payout['payment_hash'] = payreq_decoded.payment_hash
- return buyer_invoice
+ return payout
@classmethod
def pay_invoice(cls, invoice, num_satoshis):
'''Sends sats to buyer'''
- fee_limit_sat = max(num_satoshis * 0.0002, 10) # 200 ppm or 10 sats
+ fee_limit_sat = int(max(num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
request = routerrpc.SendPaymentRequest(
payment_request=invoice,
fee_limit_sat=fee_limit_sat,
diff --git a/api/logics.py b/api/logics.py
index 7059ea101..459f52f12 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -40,19 +40,19 @@ def validate_already_maker_or_taker(user):
'''Checks if the user is already partipant of an active order'''
queryset = Order.objects.filter(maker=user, status__in=active_order_status)
if queryset.exists():
- return False, {'bad_request':'You are already maker of an active order'}
+ return False, {'bad_request':'You are already maker of an active order'}, queryset[0]
queryset = Order.objects.filter(taker=user, status__in=active_order_status)
if queryset.exists():
- return False, {'bad_request':'You are already taker of an active order'}
- return True, None
+ return False, {'bad_request':'You are already taker of an active order'}, queryset[0]
+ return True, None, None
def validate_order_size(order):
'''Validates if order is withing limits in satoshis at t0'''
if order.t0_satoshis > MAX_TRADE:
- return False, {'bad_request': 'Your order is too big. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MAX_TRADE)+ ' Sats'}
+ return False, {'bad_request': 'Your order is too big. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now, but the limit is '+'{:,}'.format(MAX_TRADE)+ ' Sats'}
if order.t0_satoshis < MIN_TRADE:
- return False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'}
+ return False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now, but the limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'}
return True, None
@classmethod
@@ -89,7 +89,7 @@ def satoshis_now(order):
return int(satoshis_now)
def price_and_premium_now(order):
- ''' computes order premium live '''
+ ''' computes order price and premium with current rates '''
exchange_rate = float(order.currency.exchange_rate)
if not order.is_explicit:
premium = order.premium
@@ -111,14 +111,13 @@ def order_expires(cls, order):
# 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,
+ does_not_expire = [Order.Status.DEL, Order.Status.UCA,
+ Order.Status.EXP, Order.Status.TLD,
Order.Status.DIS, Order.Status.CCA,
Order.Status.PAY, Order.Status.SUC,
- Order.Status.FAI, Order.Status.MLD,
- Order.Status.TLD]
+ Order.Status.FAI, Order.Status.MLD]
- if order.status in do_nothing:
+ if order.status in does_not_expire:
return False
elif order.status == Order.Status.WFB:
@@ -196,8 +195,8 @@ def order_expires(cls, order):
cls.publish_order(order)
return True
- elif order.status == Order.Status.CHA:
- # Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute
+ elif order.status in [Order.Status.CHA, Order.Status.FSE]:
+ # Another weird case. The time to confirm 'fiat sent or received' expired. Yet no dispute
# was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
# sent", we assume this is a dispute case by default.
cls.open_dispute(order)
@@ -221,7 +220,7 @@ def kick_taker(cls, order):
@classmethod
def open_dispute(cls, order, user=None):
- # Always settle the escrow during a dispute (same as with 'Fiat Sent')
+ # Always settle the escrow during a dispute
# Dispute winner will have to submit a new invoice.
if not order.trade_escrow.status == LNPayment.Status.SETLED:
@@ -236,13 +235,17 @@ def open_dispute(cls, order, user=None):
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))
+ if profile.orders_disputes_started == None:
+ profile.orders_disputes_started = [str(order.id)]
+ else:
+ 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'''
+ ''' Updates the dispute statements'''
+
if not order.status == Order.Status.DIS:
return False, {'bad_request':'Only orders in dispute accept a dispute statements'}
@@ -263,7 +266,7 @@ def dispute_statement(order, user, statement):
return True, None
@classmethod
- def buyer_invoice_amount(cls, order, user):
+ def payout_amount(cls, order, user):
''' Computes buyer invoice amount. Uses order.last_satoshis,
that is the final trade amount set at Taker Bond time'''
@@ -276,33 +279,35 @@ def buyer_invoice_amount(cls, order, user):
def update_invoice(cls, order, user, invoice):
# only the buyer can post a buyer invoice
+
if not cls.is_buyer(order, user):
return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
if not order.taker_bond:
return False, {'bad_request':'Wait for your order to be taken.'}
if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED):
- return False, {'bad_request':'You cannot a invoice while bonds are not posted.'}
+ return False, {'bad_request':'You cannot submit a invoice while bonds are not locked.'}
- num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount']
- buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis)
+ num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount']
+ payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
- if not buyer_invoice['valid']:
- return False, buyer_invoice['context']
+ if not payout['valid']:
+ return False, payout['context']
- order.buyer_invoice, _ = LNPayment.objects.update_or_create(
+ order.payout, _ = LNPayment.objects.update_or_create(
concept = LNPayment.Concepts.PAYBUYER,
type = LNPayment.Types.NORM,
sender = User.objects.get(username=ESCROW_USERNAME),
+ order_paid = order, # In case this user has other payouts, update the one related to this order.
receiver= user,
# if there is a LNPayment matching these above, it updates that one with defaults below.
defaults={
'invoice' : invoice,
'status' : LNPayment.Status.VALIDI,
'num_satoshis' : num_satoshis,
- 'description' : buyer_invoice['description'],
- 'payment_hash' : buyer_invoice['payment_hash'],
- 'created_at' : buyer_invoice['created_at'],
- 'expires_at' : buyer_invoice['expires_at']}
+ 'description' : payout['description'],
+ 'payment_hash' : payout['payment_hash'],
+ 'created_at' : payout['created_at'],
+ 'expires_at' : payout['expires_at']}
)
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
@@ -312,12 +317,21 @@ def update_invoice(cls, order, user, invoice):
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2:
- # If the escrow is lock move to Chat.
- if order.trade_escrow.status == LNPayment.Status.LOCKED:
+ # If the escrow does not exist, or is not locked move to WFE.
+ if order.trade_escrow == None:
+ order.status = Order.Status.WFE
+ # If the escrow is locked move to Chat.
+ elif order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
else:
order.status = Order.Status.WFE
+
+ # If the order status is 'Failed Routing'. Retry payment.
+ if order.status == Order.Status.FAI:
+ # Double check the escrow is settled.
+ if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
+ follow_send_payment(order.payout)
order.save()
return True, None
@@ -326,7 +340,7 @@ def add_profile_rating(profile, rating):
''' adds a new rating to a user profile'''
# TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
- profile.total_ratings = profile.total_ratings + 1
+ profile.total_ratings += 1
latest_ratings = profile.latest_ratings
if latest_ratings == None:
profile.latest_ratings = [rating]
@@ -355,20 +369,32 @@ def is_penalized(user):
@classmethod
def cancel_order(cls, order, user, state=None):
+ # 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,
+ Order.Status.DIS, Order.Status.CCA,
+ Order.Status.PAY, Order.Status.SUC,
+ Order.Status.FAI, Order.Status.MLD]
+
+ if order.status in do_not_cancel:
+ return False, {'bad_request':'You cannot cancel this order'}
+
# 1) When maker cancels before bond
'''The order never shows up on the book and order
- status becomes "cancelled". That's it.'''
+ status becomes "cancelled" '''
if order.status == Order.Status.WFB and order.maker == user:
order.status = Order.Status.UCA
order.save()
return True, None
# 2) When maker cancels after bond
- '''The order dissapears from book and goes to cancelled. Maker is charged the bond to prevent DDOS
- on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)'''
+ '''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:
#Settle the maker bond (Maker loses the bond for cancelling public order)
- if cls.settle_bond(order.maker_bond):
+ if cls.return_bond(order.maker_bond): # strict: cls.settle_bond(order.maker_bond):
order.status = Order.Status.UCA
order.save()
return True, None
@@ -389,7 +415,7 @@ def cancel_order(cls, order, user, state=None):
# 4.a) When maker cancel after bond (before escrow)
'''The order into cancelled status if maker cancels.'''
- elif order.status > Order.Status.PUB and order.status < Order.Status.CHA and order.maker == user:
+ elif order.status in [Order.Status.PUB, Order.Status.TAK, Order.Status.WF2, Order.Status.WFE] and order.maker == user:
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_bond(order.maker_bond)
if valid:
@@ -408,14 +434,46 @@ def cancel_order(cls, order, user, state=None):
return True, None
# 5) When trade collateral has been posted (after escrow)
- '''Always goes to cancelled status. Collaboration is needed.
- When a user asks for cancel, 'order.is_pending_cancel' goes True.
+ '''Always goes to CCA status. Collaboration is needed.
+ When a user asks for cancel, 'order.m/t/aker_asked_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.'''
-
+ Must have a small cost for both parties to prevent node DDOS.'''
+ elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]:
+
+ # if the maker had asked, and now the taker does: cancel order, return everything
+ if order.maker_asked_cancel and user == order.taker:
+ cls.collaborative_cancel(order)
+ return True, None
+
+ # if the taker had asked, and now the maker does: cancel order, return everything
+ elif order.taker_asked_cancel and user == order.maker:
+ cls.collaborative_cancel(order)
+ return True, None
+
+ # Otherwise just make true the asked for cancel flags
+ elif user == order.taker:
+ order.taker_asked_cancel = True
+ order.save()
+ return True, None
+
+ elif user == order.maker:
+ order.maker_asked_cancel = True
+ order.save()
+ return True, None
+
+
else:
return False, {'bad_request':'You cannot cancel this order'}
+ @classmethod
+ def collaborative_cancel(cls, order):
+ cls.return_bond(order.maker_bond)
+ cls.return_bond(order.taker_bond)
+ cls.return_escrow(order)
+ order.status = Order.Status.CCA
+ order.save()
+ return
+
def publish_order(order):
order.status = Order.Status.PUB
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
@@ -426,9 +484,7 @@ def publish_order(order):
def is_maker_bond_locked(cls, order):
if order.maker_bond.status == LNPayment.Status.LOCKED:
return True
- elif LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
- order.maker_bond.status = LNPayment.Status.LOCKED
- order.maker_bond.save()
+ elif LNNode.validate_hold_invoice_locked(order.maker_bond):
cls.publish_order(order)
return True
return False
@@ -455,7 +511,10 @@ def gen_maker_hold_invoice(cls, order, user):
description = f"RoboSats - Publishing '{str(order)}' - This is a maker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel."
# Gen hold Invoice
- hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
+ hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
+ description,
+ invoice_expiry=Order.t_to_expire[Order.Status.WFB],
+ cltv_expiry_secs=BOND_EXPIRY*3600)
order.maker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.MAKEBOND,
@@ -469,7 +528,8 @@ def gen_maker_hold_invoice(cls, order, user):
description = description,
payment_hash = hold_payment['payment_hash'],
created_at = hold_payment['created_at'],
- expires_at = hold_payment['expires_at'])
+ expires_at = hold_payment['expires_at'],
+ cltv_expiry = hold_payment['cltv_expiry'])
order.save()
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
@@ -504,7 +564,7 @@ def finalize_contract(cls, order):
def is_taker_bond_locked(cls, order):
if order.taker_bond.status == LNPayment.Status.LOCKED:
return True
- elif LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
+ elif LNNode.validate_hold_invoice_locked(order.taker_bond):
cls.finalize_contract(order)
return True
return False
@@ -514,8 +574,7 @@ def gen_taker_hold_invoice(cls, order, user):
# Do not gen and kick out the taker if order is older than expiry time
if order.expires_at < timezone.now():
- cls.cancel_bond(order.taker_bond)
- cls.kick_taker(order)
+ cls.order_expires(order)
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
# Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting.
@@ -533,7 +592,10 @@ def gen_taker_hold_invoice(cls, order, user):
+ " - 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
- hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
+ hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
+ description,
+ invoice_expiry=Order.t_to_expire[Order.Status.TAK],
+ cltv_expiry_secs=BOND_EXPIRY*3600)
order.taker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.TAKEBOND,
@@ -547,7 +609,8 @@ def gen_taker_hold_invoice(cls, order, user):
description = description,
payment_hash = hold_payment['payment_hash'],
created_at = hold_payment['created_at'],
- expires_at = hold_payment['expires_at'])
+ expires_at = hold_payment['expires_at'],
+ cltv_expiry = hold_payment['cltv_expiry'])
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
order.save()
@@ -568,9 +631,7 @@ def trade_escrow_received(order):
def is_trade_escrow_locked(cls, order):
if order.trade_escrow.status == LNPayment.Status.LOCKED:
return True
- elif LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
- order.trade_escrow.status = LNPayment.Status.LOCKED
- order.trade_escrow.save()
+ elif LNNode.validate_hold_invoice_locked(order.trade_escrow):
cls.trade_escrow_received(order)
return True
return False
@@ -580,7 +641,7 @@ def gen_escrow_hold_invoice(cls, order, user):
# Do not generate if escrow deposit time has expired
if order.expires_at < timezone.now():
- cls.cancel_order(order,user)
+ cls.order_expires(order)
return False, {'bad_request':'Invoice expired. You did not send the escrow in time.'}
# Do not gen if an escrow invoice exist. Do not return if it is already locked. Return the old one if still waiting.
@@ -596,7 +657,10 @@ def gen_escrow_hold_invoice(cls, order, user):
description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
# Gen hold Invoice
- hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
+ hold_payment = LNNode.gen_hold_invoice(escrow_satoshis,
+ description,
+ invoice_expiry=Order.t_to_expire[Order.Status.WF2],
+ cltv_expiry_secs=ESCROW_EXPIRY*3600)
order.trade_escrow = LNPayment.objects.create(
concept = LNPayment.Concepts.TRESCROW,
@@ -610,7 +674,8 @@ def gen_escrow_hold_invoice(cls, order, user):
description = description,
payment_hash = hold_payment['payment_hash'],
created_at = hold_payment['created_at'],
- expires_at = hold_payment['expires_at'])
+ expires_at = hold_payment['expires_at'],
+ cltv_expiry = hold_payment['cltv_expiry'])
order.save()
return True, {'escrow_invoice':hold_payment['invoice'],'escrow_satoshis': escrow_satoshis}
@@ -635,6 +700,7 @@ 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
+ order.trade_escrow.save()
return True
def cancel_escrow(order):
@@ -642,6 +708,7 @@ def cancel_escrow(order):
# Same as return escrow, but used when the invoice was never LOCKED
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
order.trade_escrow.status = LNPayment.Status.CANCEL
+ order.trade_escrow.save()
return True
def return_bond(bond):
@@ -651,10 +718,12 @@ def return_bond(bond):
try:
LNNode.cancel_return_hold_invoice(bond.payment_hash)
bond.status = LNPayment.Status.RETNED
+ bond.save()
return True
except Exception as e:
if 'invoice already settled' in str(e):
bond.status = LNPayment.Status.SETLED
+ bond.save()
return True
else:
raise e
@@ -667,58 +736,53 @@ def cancel_bond(bond):
try:
LNNode.cancel_return_hold_invoice(bond.payment_hash)
bond.status = LNPayment.Status.CANCEL
+ bond.save()
return True
except Exception as e:
if 'invoice already settled' in str(e):
bond.status = LNPayment.Status.SETLED
+ bond.save()
return True
else:
raise e
- def pay_buyer_invoice(order):
- ''' Pay buyer invoice'''
- suceeded, context = follow_send_payment(order.buyer_invoice)
- return suceeded, context
-
@classmethod
def confirm_fiat(cls, order, user):
''' If Order is in the CHAT states:
- If user is buyer: mark FIAT SENT and settle escrow!
- If User is the seller and FIAT is SENT: Pay buyer invoice!'''
+ If user is buyer: fiat_sent goes to true.
+ If User is seller and fiat_sent is true: settle the escrow and pay buyer invoice!'''
if order.status == Order.Status.CHA or order.status == Order.Status.FSE: # TODO Alternatively, if all collateral is locked? test out
# If buyer, settle escrow and mark fiat sent
if cls.is_buyer(order, user):
- if cls.settle_escrow(order): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
- order.trade_escrow.status = LNPayment.Status.SETLED
- order.status = Order.Status.FSE
- order.is_fiat_sent = True
+ order.status = Order.Status.FSE
+ order.is_fiat_sent = True
- # If seller and fiat sent, pay buyer invoice
+ # If seller and fiat was sent, SETTLE ESCROW AND PAY BUYER INVOICE
elif cls.is_seller(order, user):
if not order.is_fiat_sent:
return False, {'bad_request':'You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer.'}
# Make sure the trade escrow is at least as big as the buyer invoice
- if order.trade_escrow.num_satoshis <= order.buyer_invoice.num_satoshis:
+ if order.trade_escrow.num_satoshis <= order.payout.num_satoshis:
return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'}
+
+ if cls.settle_escrow(order): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
+ order.trade_escrow.status = LNPayment.Status.SETLED
# Double check the escrow is settled.
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
- is_payed, context = cls.pay_buyer_invoice(order) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
+ # RETURN THE BONDS // Probably best also do it even if payment failed
+ cls.return_bond(order.taker_bond)
+ cls.return_bond(order.maker_bond)
+ is_payed, context = follow_send_payment(order.payout) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
if is_payed:
- order.status = Order.Status.SUC
- order.buyer_invoice.status = LNPayment.Status.SUCCED
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
order.save()
-
- # RETURN THE BONDS
- cls.return_bond(order.taker_bond)
- cls.return_bond(order.maker_bond)
+ return True, context
else:
# error handling here
- pass
+ return False, context
else:
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
@@ -727,9 +791,11 @@ def confirm_fiat(cls, order, user):
@classmethod
def rate_counterparty(cls, order, user, rating):
+
+ rating_allowed_status = [Order.Status.PAY, Order.Status.SUC, Order.Status.FAI, Order.Status.MLD, Order.Status.TLD]
# If the trade is finished
- if order.status > Order.Status.PAY:
+ if order.status in rating_allowed_status:
# if maker, rates taker
if order.maker == user and order.maker_rated == False:
cls.add_profile_rating(order.taker.profile, rating)
diff --git a/api/management/commands/clean_orders.py b/api/management/commands/clean_orders.py
index 033784c5f..9c814b3b9 100644
--- a/api/management/commands/clean_orders.py
+++ b/api/management/commands/clean_orders.py
@@ -11,16 +11,18 @@ class Command(BaseCommand):
# def add_arguments(self, parser):
# parser.add_argument('debug', nargs='+', type=boolean)
- def handle(self, *args, **options):
+ def clean_orders(self, *args, **options):
''' Continuously checks order expiration times for 1 hour. If order
has expires, it calls the logics module for expiration handling.'''
+ # TODO handle 'database is locked'
+
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]
+ Order.Status.TLD, Order.Status.WFR]
while True:
time.sleep(5)
@@ -34,9 +36,30 @@ def handle(self, *args, **options):
for idx, order in enumerate(queryset):
context = str(order)+ " was "+ Order.Status(order.status).label
- if Logics.order_expires(order): # Order send to expire here
- debug['expired_orders'].append({idx:context})
+ try:
+ if Logics.order_expires(order): # Order send to expire here
+ debug['expired_orders'].append({idx:context})
+
+ # It should not happen, but if it cannot locate the hold invoice
+ # it probably was cancelled by another thread, make it expire anyway.
+ except Exception as e:
+ if 'unable to locate invoice' in str(e):
+ self.stdout.write(str(e))
+ order.status = Order.Status.EXP
+ order.save()
+ debug['expired_orders'].append({idx:context})
+
if debug['num_expired_orders'] > 0:
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))
+
+ def handle(self, *args, **options):
+ ''' Never mind database locked error, keep going, print them out'''
+ try:
+ self.clean_orders()
+ except Exception as e:
+ if 'database is locked' in str(e):
+ self.stdout.write('database is locked')
+
+ self.stdout.write(str(e))
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index 52253a2af..7f7dbec92 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -1,6 +1,7 @@
from django.core.management.base import BaseCommand, CommandError
from api.lightning.node import LNNode
+from api.tasks import follow_send_payment
from api.models import LNPayment, Order
from api.logics import Logics
@@ -13,24 +14,37 @@
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
class Command(BaseCommand):
- '''
- Background: SubscribeInvoices stub iterator would be great to use here.
- However, it only sends updates when the invoice is OPEN (new) or SETTLED.
- We are very interested on the other two states (CANCELLED and ACCEPTED).
- Therefore, this thread (follow_invoices) will iterate over all LNpayment
- objects and do InvoiceLookupV2 every X seconds to update their state 'live'
- '''
help = 'Follows all active hold invoices'
rest = 5 # seconds between consecutive checks for invoice updates
- # def add_arguments(self, parser):
- # parser.add_argument('debug', nargs='+', type=boolean)
-
def handle(self, *args, **options):
- ''' Follows and updates LNpayment objects
- until settled or canceled'''
+ ''' Infinite loop to check invoices and retry payments.
+ ever mind database locked error, keep going, print out'''
+
+ while True:
+ time.sleep(self.rest)
+
+ try:
+ self.follow_hold_invoices()
+ self.retry_payments()
+ except Exception as e:
+ if 'database is locked' in str(e):
+ self.stdout.write('database is locked')
+
+ self.stdout.write(str(e))
+ def follow_hold_invoices(self):
+ ''' Follows and updates LNpayment objects
+ until settled or canceled
+
+ Background: SubscribeInvoices stub iterator would be great to use here.
+ However, it only sends updates when the invoice is OPEN (new) or SETTLED.
+ We are very interested on the other two states (CANCELLED and ACCEPTED).
+ Therefore, this thread (follow_invoices) will iterate over all LNpayment
+ objects and do InvoiceLookupV2 every X seconds to update their state 'live'
+ '''
+
lnd_state_to_lnpayment_status = {
0: LNPayment.Status.INVGEN, # OPEN
1: LNPayment.Status.SETLED, # SETTLED
@@ -40,63 +54,84 @@ def handle(self, *args, **options):
stub = LNNode.invoicesstub
- while True:
- time.sleep(self.rest)
-
- # time it for debugging
- t0 = time.time()
- queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
-
- debug = {}
- debug['num_active_invoices'] = len(queryset)
- debug['invoices'] = []
- at_least_one_changed = False
+ # time it for debugging
+ t0 = time.time()
+ queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
+
+ debug = {}
+ debug['num_active_invoices'] = len(queryset)
+ debug['invoices'] = []
+ at_least_one_changed = False
+
+ for idx, hold_lnpayment in enumerate(queryset):
+ old_status = LNPayment.Status(hold_lnpayment.status).label
+ try:
+ # this is similar to LNNnode.validate_hold_invoice_locked
+ request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
+ response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
+ hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
+
+ # try saving expiry height
+ if hasattr(response, 'htlcs' ):
+ try:
+ hold_lnpayment.expiry_height = response.htlcs[0].expiry_height
+ except:
+ pass
- for idx, hold_lnpayment in enumerate(queryset):
- old_status = LNPayment.Status(hold_lnpayment.status).label
-
- try:
- request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
- response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
- hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
-
- except Exception as e:
- # If it fails at finding the invoice it has been canceled.
- # On RoboSats DB we make a distinction between cancelled and returned (LND does not)
- if 'unable to locate invoice' in str(e):
- hold_lnpayment.status = LNPayment.Status.CANCEL
- # LND restarted.
- if 'wallet locked, unlock it' in str(e):
- self.stdout.write(str(timezone.now())+':: Wallet Locked')
- # Other write to logs
- else:
- self.stdout.write(str(e))
-
- new_status = LNPayment.Status(hold_lnpayment.status).label
-
- # Only save the hold_payments that change (otherwise this function does not scale)
- changed = not old_status==new_status
- if changed:
- # self.handle_status_change(hold_lnpayment, old_status)
- hold_lnpayment.save()
- self.update_order_status(hold_lnpayment)
-
- # Report for debugging
- new_status = LNPayment.Status(hold_lnpayment.status).label
- debug['invoices'].append({idx:{
- 'payment_hash': str(hold_lnpayment.payment_hash),
- 'old_status': old_status,
- 'new_status': new_status,
- }})
-
- at_least_one_changed = at_least_one_changed or changed
+ except Exception as e:
+ # If it fails at finding the invoice: it has been canceled.
+ # In RoboSats DB we make a distinction between cancelled and returned (LND does not)
+ if 'unable to locate invoice' in str(e):
+ self.stdout.write(str(e))
+ hold_lnpayment.status = LNPayment.Status.CANCEL
+
+ # LND restarted.
+ if 'wallet locked, unlock it' in str(e):
+ self.stdout.write(str(timezone.now())+' :: Wallet Locked')
+ # Other write to logs
+ else:
+ self.stdout.write(str(e))
- debug['time']=time.time()-t0
+ new_status = LNPayment.Status(hold_lnpayment.status).label
- if at_least_one_changed:
- self.stdout.write(str(timezone.now()))
- self.stdout.write(str(debug))
+ # Only save the hold_payments that change (otherwise this function does not scale)
+ changed = not old_status==new_status
+ if changed:
+ # self.handle_status_change(hold_lnpayment, old_status)
+ self.update_order_status(hold_lnpayment)
+ hold_lnpayment.save()
+ # Report for debugging
+ new_status = LNPayment.Status(hold_lnpayment.status).label
+ debug['invoices'].append({idx:{
+ 'payment_hash': str(hold_lnpayment.payment_hash),
+ 'old_status': old_status,
+ 'new_status': new_status,
+ }})
+
+ at_least_one_changed = at_least_one_changed or changed
+
+ debug['time']=time.time()-t0
+
+ if at_least_one_changed:
+ self.stdout.write(str(timezone.now()))
+ self.stdout.write(str(debug))
+
+ def retry_payments(self):
+ ''' Checks if any payment is due for retry, and tries to pay it'''
+
+ queryset = LNPayment.objects.filter(type=LNPayment.Types.NORM,
+ status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
+ routing_attempts__lt=4,
+ last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
+ for lnpayment in queryset:
+ success, _ = follow_send_payment(lnpayment)
+
+ # If already 3 attempts and last failed. Make it expire (ask for a new invoice) an reset attempts.
+ if not success and lnpayment.routing_attempts == 3:
+ lnpayment.status = LNPayment.Status.EXPIRE
+ lnpayment.routing_attempts = 0
+ lnpayment.save()
def update_order_status(self, lnpayment):
''' Background process following LND hold invoices
@@ -121,10 +156,27 @@ def update_order_status(self, lnpayment):
elif hasattr(lnpayment, 'order_escrow' ):
Logics.trade_escrow_received(lnpayment.order_escrow)
return
+
except Exception as e:
self.stdout.write(str(e))
- # TODO If a lnpayment goes from LOCKED to INVGED. Totally weird
+ # If the LNPayment goes to CANCEL from INVGEN, the invoice had expired
+ # If it goes to CANCEL from LOCKED the bond was relased. Order had expired in both cases.
+ # Testing needed for end of time trades!
+ if lnpayment.status == LNPayment.Status.CANCEL :
+ if hasattr(lnpayment, 'order_made' ):
+ Logics.order_expires(lnpayment.order_made)
+ return
+
+ elif hasattr(lnpayment, 'order_taken' ):
+ Logics.order_expires(lnpayment.order_taken)
+ return
+
+ elif hasattr(lnpayment, 'order_escrow' ):
+ Logics.order_expires(lnpayment.order_escrow)
+ return
+
+ # TODO If a lnpayment goes from LOCKED to INVGEN. Totally weird
# halt the order
- if lnpayment.status == LNPayment.Status.LOCKED:
+ if lnpayment.status == LNPayment.Status.INVGEN:
pass
\ No newline at end of file
diff --git a/api/models.py b/api/models.py
index 673e25d81..e45707e60 100644
--- a/api/models.py
+++ b/api/models.py
@@ -19,7 +19,7 @@
class Currency(models.Model):
- currency_dict = json.load(open('./frontend/static/assets/currencies.json'))
+ currency_dict = json.load(open('frontend/static/assets/currencies.json'))
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False, unique=True)
@@ -37,7 +37,7 @@ class Meta:
class LNPayment(models.Model):
class Types(models.IntegerChoices):
- NORM = 0, 'Regular invoice' # Only outgoing buyer payment will be a regular invoice (Non-hold)
+ NORM = 0, 'Regular invoice'
HOLD = 1, 'hold invoice'
class Concepts(models.IntegerChoices):
@@ -57,13 +57,11 @@ class Status(models.IntegerChoices):
FLIGHT = 7, 'In flight'
SUCCED = 8, 'Succeeded'
FAILRO = 9, 'Routing failed'
-
# payment use details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
- routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
# payment info
payment_hash = models.CharField(max_length=100, unique=True, default=None, blank=True, primary_key=True)
@@ -73,7 +71,13 @@ class Status(models.IntegerChoices):
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
created_at = models.DateTimeField()
expires_at = models.DateTimeField()
+ cltv_expiry = models.PositiveSmallIntegerField(null=True, default=None, blank=True)
+ expiry_height = models.PositiveBigIntegerField(null=True, default=None, blank=True)
+ # routing
+ routing_attempts = models.PositiveSmallIntegerField(null=False, default=0)
+ last_routing_time = models.DateTimeField(null=True, default=None, blank=True)
+
# involved parties
sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None)
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
@@ -141,9 +145,12 @@ class Status(models.IntegerChoices):
last_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE*2)], blank=True) # sats last time checked. Weird if 2* trade max...
# order participants
- maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order
+ maker = models.ForeignKey(User, related_name='maker', on_delete=models.SET_NULL, 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.
+ maker_last_seen = models.DateTimeField(null=True,default=None, blank=True)
+ taker_last_seen = models.DateTimeField(null=True,default=None, blank=True)
+ maker_asked_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
+ taker_asked_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
is_fiat_sent = models.BooleanField(default=False, null=False)
# in dispute
@@ -156,9 +163,8 @@ class Status(models.IntegerChoices):
maker_bond = models.OneToOneField(LNPayment, related_name='order_made', on_delete=models.SET_NULL, null=True, default=None, blank=True)
taker_bond = models.OneToOneField(LNPayment, related_name='order_taken', on_delete=models.SET_NULL, null=True, default=None, blank=True)
trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
-
# buyer payment LN invoice
- buyer_invoice = models.OneToOneField(LNPayment, related_name='order_paid', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ payout = models.OneToOneField(LNPayment, related_name='order_paid', on_delete=models.SET_NULL, null=True, default=None, blank=True)
# ratings
maker_rated = models.BooleanField(default=False, null=False)
@@ -176,12 +182,12 @@ class Status(models.IntegerChoices):
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 : 10*24*60*60, # 'In dispute'
+ 11 : 1*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, # 'Wait for dispute resolution'
+ 16 : 10*24*60*60, # 'Wait for dispute resolution'
17 : 24*60*60, # 'Maker lost dispute'
18 : 24*60*60, # 'Taker lost dispute'
}
@@ -192,7 +198,7 @@ def __str__(self):
@receiver(pre_delete, sender=Order)
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
- to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow)
+ to_delete = (instance.maker_bond, instance.payout, instance.taker_bond, instance.trade_escrow)
for lnpayment in to_delete:
try:
@@ -222,7 +228,7 @@ class Profile(models.Model):
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True)
# Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond)
- penalty_expiration = models.DateTimeField(null=True)
+ penalty_expiration = models.DateTimeField(null=True,default=None, blank=True)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
diff --git a/api/nick_generator/dicts/en/adjectives.py b/api/nick_generator/dicts/en/adjectives.py
index 07519d03f..4b9e22169 100755
--- a/api/nick_generator/dicts/en/adjectives.py
+++ b/api/nick_generator/dicts/en/adjectives.py
@@ -4773,6 +4773,7 @@
"Compound",
"Important",
"Robotic",
+ "Satoshi",
"Alltoocommon",
"Informative",
"Anxious",
diff --git a/api/nick_generator/dicts/en/nouns.py b/api/nick_generator/dicts/en/nouns.py
index ae3243e86..b11cfaaba 100755
--- a/api/nick_generator/dicts/en/nouns.py
+++ b/api/nick_generator/dicts/en/nouns.py
@@ -6346,6 +6346,7 @@
"Nair",
"Nairo",
"Naivete",
+ "Nakamoto",
"Name",
"Namesake",
"Nanometer",
@@ -12229,6 +12230,7 @@
"Sand",
"Sandwich",
"Satisfaction",
+ "Satoshi",
"Save",
"Savings",
"Scale",
diff --git a/api/tasks.py b/api/tasks.py
index 82da436ab..3d6ef69c1 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -21,7 +21,7 @@ def users_cleansing():
for user in queryset:
if not user.profile.total_contracts == 0:
continue
- valid, _ = Logics.validate_already_maker_or_taker(user)
+ valid, _, _ = Logics.validate_already_maker_or_taker(user)
if valid:
deleted_users.append(str(user))
user.delete()
@@ -38,6 +38,8 @@ def follow_send_payment(lnpayment):
from decouple import config
from base64 import b64decode
+ from django.utils import timezone
+ from datetime import timedelta
from api.lightning.node import LNNode
from api.models import LNPayment, Order
@@ -51,36 +53,54 @@ def follow_send_payment(lnpayment):
timeout_seconds=60) # time out payment in 60 seconds
order = lnpayment.order_paid
- for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
- if response.status == 0 : # Status 0 'UNKNOWN'
- # Not sure when this status happens
- pass
-
- if response.status == 1 : # Status 1 'IN_FLIGHT'
- print('IN_FLIGHT')
- lnpayment.status = LNPayment.Status.FLIGHT
- lnpayment.save()
- order.status = Order.Status.PAY
- order.save()
-
- if response.status == 3 : # Status 3 'FAILED'
- print('FAILED')
- lnpayment.status = LNPayment.Status.FAILRO
+ try:
+ for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
+ if response.status == 0 : # Status 0 'UNKNOWN'
+ # Not sure when this status happens
+ pass
+
+ if response.status == 1 : # Status 1 'IN_FLIGHT'
+ print('IN_FLIGHT')
+ lnpayment.status = LNPayment.Status.FLIGHT
+ lnpayment.save()
+ order.status = Order.Status.PAY
+ order.save()
+
+ if response.status == 3 : # Status 3 'FAILED'
+ print('FAILED')
+ lnpayment.status = LNPayment.Status.FAILRO
+ lnpayment.last_routing_time = timezone.now()
+ lnpayment.routing_attempts += 1
+ lnpayment.save()
+ order.status = Order.Status.FAI
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
+ order.save()
+ context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
+ print(context)
+ # Call a retry in 5 mins here?
+ return False, context
+
+ if response.status == 2 : # Status 2 'SUCCEEDED'
+ print('SUCCEEDED')
+ lnpayment.status = LNPayment.Status.SUCCED
+ lnpayment.save()
+ order.status = Order.Status.SUC
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
+ order.save()
+ return True, None
+
+ except Exception as e:
+ if "invoice expired" in str(e):
+ print('INVOICE EXPIRED')
+ lnpayment.status = LNPayment.Status.EXPIRE
+ lnpayment.last_routing_time = timezone.now()
lnpayment.save()
order.status = Order.Status.FAI
+ order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
order.save()
- context = LNNode.payment_failure_context[response.failure_reason]
- # Call for a retry here
+ context = {'routing_failed':'The payout invoice has expired'}
return False, context
- if response.status == 2 : # Status 2 'SUCCEEDED'
- print('SUCCEEDED')
- lnpayment.status = LNPayment.Status.SUCCED
- lnpayment.save()
- order.status = Order.Status.SUC
- order.save()
- return True, None
-
@shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market():
diff --git a/api/utils.py b/api/utils.py
index e138d322e..929f7b76a 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -72,7 +72,7 @@ def get_commit_robosats():
@ring.dict(premium_percentile, expire=300)
def compute_premium_percentile(order):
- queryset = Order.objects.filter(currency=order.currency, status=Order.Status.PUB)
+ queryset = Order.objects.filter(currency=order.currency, status=Order.Status.PUB).exclude(id=order.id)
print(len(queryset))
if len(queryset) <= 1:
diff --git a/api/views.py b/api/views.py
index 6e529a40c..2074f4e91 100644
--- a/api/views.py
+++ b/api/views.py
@@ -26,6 +26,7 @@
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
FEE = float(config('FEE'))
+RETRY_TIME = int(config('RETRY_TIME'))
avatar_path = Path('frontend/static/assets/avatars')
avatar_path.mkdir(parents=True, exist_ok=True)
@@ -38,6 +39,9 @@ class MakerView(CreateAPIView):
def post(self,request):
serializer = self.serializer_class(data=request.data)
+ if not request.user.is_authenticated:
+ return Response({'bad_request':'Woops! It seems you do not have a robot avatar'}, status.HTTP_400_BAD_REQUEST)
+
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
type = serializer.data.get('type')
@@ -48,7 +52,7 @@ def post(self,request):
satoshis = serializer.data.get('satoshis')
is_explicit = serializer.data.get('is_explicit')
- valid, context = Logics.validate_already_maker_or_taker(request.user)
+ valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
if not valid: return Response(context, status.HTTP_409_CONFLICT)
# Creates a new order
@@ -83,6 +87,9 @@ def get(self, request, format=None):
'''
order_id = request.GET.get(self.lookup_url_kwarg)
+ if not request.user.is_authenticated:
+ return Response({'bad_request':'You must have a robot avatar to see the order details'}, status=status.HTTP_400_BAD_REQUEST)
+
if order_id == None:
return Response({'bad_request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
@@ -107,7 +114,7 @@ def get(self, request, format=None):
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
if is_penalized:
- data['penalty'] = time_out
+ data['penalty'] = request.user.profile.penalty_expiration
# Add booleans if user is maker, taker, partipant, buyer or seller
data['is_maker'] = order.maker == request.user
@@ -118,6 +125,32 @@ def get(self, request, format=None):
if not data['is_participant'] and order.status != Order.Status.PUB:
return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN)
+ # WRITE Update last_seen for maker and taker.
+ # Note down that the taker/maker was here recently, so counterpart knows if the user is paying attention.
+ if order.maker == request.user:
+ order.maker_last_seen = timezone.now()
+ order.save()
+ if order.taker == request.user:
+ order.taker_last_seen = timezone.now()
+ order.save()
+
+ # Add activity status of participants based on last_seen
+ if order.taker_last_seen != None:
+ if order.taker_last_seen > (timezone.now() - timedelta(minutes=2)):
+ data['taker_status'] = 'active'
+ elif order.taker_last_seen > (timezone.now() - timedelta(minutes=10)):
+ data['taker_status'] = 'seen_recently'
+ else:
+ data['taker_status'] = 'inactive'
+
+ if order.maker_last_seen != None:
+ if order.maker_last_seen > (timezone.now() - timedelta(minutes=2)):
+ data['maker_status'] = 'active'
+ elif order.maker_last_seen > (timezone.now() - timedelta(minutes=10)):
+ data['maker_status'] = 'seen_recently'
+ else:
+ data['maker_status'] = 'inactive'
+
# 3.b If order is between public and WF2
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
data['price_now'], data['premium_now'] = Logics.price_and_premium_now(order)
@@ -169,7 +202,7 @@ def get(self, request, format=None):
data['trade_satoshis'] = order.last_satoshis
# Buyer sees the amount he receives
elif data['is_buyer']:
- data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount']
+ data['trade_satoshis'] = Logics.payout_amount(order, request.user)[1]['invoice_amount']
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice.
if order.status == Order.Status.WFB and data['is_maker']:
@@ -189,7 +222,6 @@ def get(self, request, format=None):
# 7 a. ) If seller and status is 'WF2' or 'WFE'
elif data['is_seller'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFE):
-
# If the two bonds are locked, reply with an ESCROW hold invoice.
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
valid, context = Logics.gen_escrow_hold_invoice(order, request.user)
@@ -203,20 +235,42 @@ def get(self, request, format=None):
# If the two bonds are locked, reply with an AMOUNT so he can send the buyer invoice.
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
- valid, context = Logics.buyer_invoice_amount(order, request.user)
+ valid, context = Logics.payout_amount(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED
- elif order.status == Order.Status.CHA or order.status == Order.Status.FSE: # TODO Add the other status
-
+ elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]:
# If all bonds are locked.
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
- # add whether a collaborative cancel is pending
- data['pending_cancel'] = order.is_pending_cancel
-
+ # add whether a collaborative cancel is pending or has been asked
+ if (data['is_maker'] and order.taker_asked_cancel) or (data['is_taker'] and order.maker_asked_cancel):
+ data['pending_cancel'] = True
+ elif (data['is_maker'] and order.maker_asked_cancel) or (data['is_taker'] and order.taker_asked_cancel):
+ data['asked_for_cancel'] = True
+ else:
+ data['asked_for_cancel'] = False
+
+ # 9) If status is 'DIS' and all HTLCS are in LOCKED
+ elif order.status == Order.Status.DIS:
+
+ # add whether the dispute statement has been received
+ if data['is_maker']:
+ data['statement_submitted'] = (order.maker_statement != None and order.maker_statement != "")
+ elif data['is_taker']:
+ data['statement_submitted'] = (order.taker_statement != None and order.maker_statement != "")
+
+ # 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 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(minutes=RETRY_TIME)
+
+ if order.payout.status == LNPayment.Status.EXPIRE:
+ data['invoice_expired'] = True
+ # Add invoice amount once again if invoice was expired.
+ data['invoice_amount'] = int(order.last_satoshis * (1-FEE))
return Response(data, status.HTTP_200_OK)
@@ -243,7 +297,7 @@ def take_update_confirm_dispute_cancel(self, request, format=None):
# 1) If action is take, it is a taker request!
if action == 'take':
if order.status == Order.Status.PUB:
- valid, context = Logics.validate_already_maker_or_taker(request.user)
+ valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
valid, context = Logics.take(order, request.user)
if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN)
@@ -318,7 +372,7 @@ def get(self,request, format=None):
# If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
if request.user.is_authenticated:
context = {'nickname': request.user.username}
- not_participant, _ = Logics.validate_already_maker_or_taker(request.user)
+ not_participant, _, _ = Logics.validate_already_maker_or_taker(request.user)
# Does not allow this 'mistake' if an active order
if not not_participant:
@@ -386,23 +440,22 @@ def get(self,request, format=None):
def delete(self,request):
''' Pressing "give me another" deletes the logged in user '''
user = request.user
- if not user:
+ if not user.is_authenticated:
return Response(status.HTTP_403_FORBIDDEN)
- # Only delete if user life is shorter than 30 minutes. Helps deleting users by mistake
+ # Only delete if user life is shorter than 30 minutes. Helps to avoid deleting users by mistake
if user.date_joined < (timezone.now() - timedelta(minutes=30)):
return Response(status.HTTP_400_BAD_REQUEST)
# Check if it is not a maker or taker!
- if not Logics.validate_already_maker_or_taker(user):
+ not_participant, _, _ = Logics.validate_already_maker_or_taker(user)
+ if not not_participant:
return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
logout(request)
user.delete()
return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY)
-
-
class BookView(ListAPIView):
serializer_class = ListOrderSerializer
queryset = Order.objects.filter(status=Order.Status.PUB)
@@ -424,7 +477,6 @@ def get(self,request, format=None):
if len(queryset)== 0:
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
- # queryset = queryset.order_by('created_at')
book_data = []
for order in queryset:
data = ListOrderSerializer(order).data
@@ -468,12 +520,27 @@ def get(self, request):
avg_premium = 0
total_volume = 0
+ queryset = MarketTick.objects.all()
+ if not len(queryset) == 0:
+ volume_settled = []
+ for tick in queryset:
+ volume_settled.append(tick.volume)
+ lifetime_volume_settled = int(sum(volume_settled)*100000000)
+ else:
+ lifetime_volume_settled = 0
+
context['today_avg_nonkyc_btc_premium'] = round(avg_premium,2)
context['today_total_volume'] = total_volume
+ context['lifetime_satoshis_settled'] = lifetime_volume_settled
context['lnd_version'] = get_lnd_version()
context['robosats_running_commit_hash'] = get_commit_robosats()
context['fee'] = FEE
context['bond_size'] = float(config('BOND_SIZE'))
+ if request.user.is_authenticated:
+ context['nickname'] = request.user.username
+ has_no_active_order, _, order = Logics.validate_already_maker_or_taker(request.user)
+ if not has_no_active_order:
+ context['active_order_id'] = order.id
return Response(context, status.HTTP_200_OK)
diff --git a/concept_v0.0.1.pdf b/concept_v0.0.1.pdf
deleted file mode 100644
index 192dc5fdc..000000000
Binary files a/concept_v0.0.1.pdf and /dev/null differ
diff --git a/faq.md b/faq.md
deleted file mode 100644
index a5f43babe..000000000
--- a/faq.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# Buy and sell non-KYC Bitcoin using the lightning network.
-
-## What is this?
-
-{project_name} is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies matchmaking and minimizes the trust needed to trade with a peer.
-
-## That’s cool, so how it works?
-
-Alice wants to sell sats, posts a sell order. Bob wants to buy sats, and takes Alice's order. Alice posts the sats as collateral using a hodl LN invoice. Bob also posts some sats as a bond to prove he is real. {project_name} locks the sats until Bob confirms he sent the fiat to Alice. Once Alice confirms she received the fiat, she tells {project_name} to release her sats to Bob. Enjoy your sats Bob!
-
-At no point, Alice and Bob have to trust the funds to each other. In case Alice and Bob have a conflict, {project_name} staff will resolve the dispute.
-
-(TODO: Long explanation and tutorial step by step, link)
-
-## Nice, and fiat payments method are...?
-
-Basically all of them. It is up to you to select your preferred payment methods. You will need to search for a peer who also accepts that method. Lightning is fast, so we highly recommend using instant fiat payment rails. Be aware trades have a expiry time of 8 hours. Paypal or credit card are not advice due to chargeback risk.
-
-## Trust
-
-The buyer and the seller never have to trust each other. Some trust on {project_name} is needed. Linking the seller’s hodl invoice and buyer payment is not atomic (yet, research ongoing). In addition, disputes are solved by the {project_name} staff.
-
-Note: this is not an escrow service. While trust requirements are minimized, {project_name} could run away with your sats. It could be argued that it is not worth it, as it would instantly destroy {project_name} reputation. However, you should hesitate and only trade small quantities at a time. For larger amounts and safety assurance use an escrow service such as Bisq or Hodlhodl.
-
-You can build more trust on {project_name} by inspecting the source code, link.
-
-## If {project_name} suddenly disappears during a trade my sats…
-
-Your sats will most likely return to you. Any hodl invoice that is not settled would be automatically returned even if {project_name} goes down forever. This is true for both, locked bonds and traded sats. However, in the window between the buyer confirms FIAT SENT and the sats have not been released yet by the seller, the fund could be lost.
-
-## Limits
-
-Max trade size is 500K Sats to minimize failures in lightning routing. The limit will be raised as LN grows.
-
-## Privacy
-
-User token is generated locally as the unique identifier (back it up on paper! If lost {project_name} cannot help recover it). {project_name} doesn’t know anything about you and doesn’t want to know.
-
-Your trading peer is the only one who can potentially guess anything about you. Keep chat short and concise. Avoid providing non-essential information other than strictly necessary for the fiat payment.
-
-The chat with your peer is end-to-end encrypted, {project_name} cannot read. It can only be decrypted with your user token. The chat encryption makes it hard to resolve disputes. Therefore, by opening a dispute you are sending a viewkey to {project_name} staff. The encrypted chat cannot be revisited as it is deleted automatically when the trade is finalized (check the source code).
-
-For best anonymity use Tor Browser and access the .onion hidden service.
-
-## So {project_name} is a decentralized exchange?
-Not quite, though it shares some elements.
-
-A simple comparisson:
-* Privacy worst to best: Coinbase/Binance/others < hodlhodl < {project_name} < Bisq
-* Safety (not your keys, not your coins): Coinbase/Binance/others < {project_name} < hodlhodl < Bisq
-*(take with a pinch of salt)*
-
-So, if bisq is best for both privacy and safety, why {project_name} exists? Bisq is great, but it is difficult, slow, high-fee and needs extra steps to move to lightning. {project_name} aims to be as easy as Binance/Coinbase greatly improving on privacy and requiring minimal trust.
-
-## Any risk?
-
-Sure, this is a beta bot, things could go wrong. Trade small amounts!
-
-The seller faces the same chargeback risk as with any other peer-to-peer exchange. Avoid accepting payment methods with easy chargeback!
-
-## What are the fees?
-
-{project_name} takes a 0.2% fee of the trade to cover lightning routing costs. This is akin to a Binance trade fee (but hey, you do not have to sell your soul to the devil, nor pay the withdrawal fine...).
-
-The loser of a dispute pays a 1% fee that is slashed from the collateral posted when the trade starts. This fee is necessary to disincentive cheating and keep the site healthy. It also helps to cover the staff cost of dispute solving.
-
-Note: your selected fiat payment rails might have other fees, these are to be covered by the buyer.
-
-## I am a pro and {project_name} is too simple, it lacks features…
-
-Indeed, this site is a simple front-end that aims for user friendliness and forces best privacy for casual users.
-
-If you are a big maker, liquidity provider, or want to create many trades simultaneously use the API: {API_LINK_DOCUMENTATION}
-
-## Is it legal to use {project_name} in my country?
-
-In many countries using {project_name} is not different than buying something from a peer on Ebay or Craiglist. Your regulation may vary, you need to figure out.
-
-## Disclaimer
-
-This tool is provided as is. It is in active development and can be buggy. Be aware that you could lose your funds: trade with the utmost caution. There is no private support. Support is only offered via public channels (link telegram groups). {project_name} will never contact you. And {project_name} will definitely never ask for your user token.
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 62e583c3c..80748d5ad 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -3253,6 +3253,11 @@
"which": "^2.0.1"
}
},
+ "css-mediaquery": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+ "integrity": "sha1-aiw3NEkoYYYxxUvTPO3TAdoYvqA="
+ },
"css-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
@@ -4820,6 +4825,11 @@
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
},
+ "jsqr": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
+ "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="
+ },
"jss": {
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/jss/-/jss-10.9.0.tgz",
@@ -5040,6 +5050,14 @@
"object-visit": "^1.0.0"
}
},
+ "matchmediaquery": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz",
+ "integrity": "sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==",
+ "requires": {
+ "css-mediaquery": "^0.1.2"
+ }
+ },
"material-ui-image": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/material-ui-image/-/material-ui-image-3.3.2.tgz",
@@ -6439,11 +6457,32 @@
"qr.js": "0.0.0"
}
},
+ "react-qr-reader": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-qr-reader/-/react-qr-reader-2.2.1.tgz",
+ "integrity": "sha512-EL5JEj53u2yAOgtpAKAVBzD/SiKWn0Bl7AZy6ZrSf1lub7xHwtaXe6XSx36Wbhl1VMGmvmrwYMRwO1aSCT2fwA==",
+ "requires": {
+ "jsqr": "^1.2.0",
+ "prop-types": "^15.7.2",
+ "webrtc-adapter": "^7.2.1"
+ }
+ },
"react-refresh": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
"integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA=="
},
+ "react-responsive": {
+ "version": "9.0.0-beta.6",
+ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.0-beta.6.tgz",
+ "integrity": "sha512-Flk6UrnpBBByreva6ja/TsbXiXq4BXOlDEKL6Ur+nshUs3CcN5W0BpGe6ClFWrKcORkMZAAYy7A4N4xlMmpgVw==",
+ "requires": {
+ "hyphenate-style-name": "^1.0.0",
+ "matchmediaquery": "^0.3.0",
+ "prop-types": "^15.6.1",
+ "shallow-equal": "^1.2.1"
+ }
+ },
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
@@ -6718,6 +6757,14 @@
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
},
+ "rtcpeerconnection-shim": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
+ "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
+ "requires": {
+ "sdp": "^2.6.0"
+ }
+ },
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -6965,6 +7012,11 @@
"ajv-keywords": "^3.5.2"
}
},
+ "sdp": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
+ "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
+ },
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -7076,6 +7128,11 @@
"kind-of": "^6.0.2"
}
},
+ "shallow-equal": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
+ "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
+ },
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7858,6 +7915,15 @@
"integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==",
"dev": true
},
+ "webrtc-adapter": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
+ "integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
+ "requires": {
+ "rtcpeerconnection-shim": "^1.2.15",
+ "sdp": "^2.12.0"
+ }
+ },
"websocket": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 3a098c139..8eb9727bd 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -35,6 +35,8 @@
"react-native": "^0.66.4",
"react-native-svg": "^12.1.1",
"react-qr-code": "^2.0.3",
+ "react-qr-reader": "^2.2.1",
+ "react-responsive": "^9.0.0-beta.6",
"react-router-dom": "^5.2.0",
"websocket": "^1.0.34"
}
diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js
index 6cf5659a5..7532249bc 100644
--- a/frontend/src/components/App.js
+++ b/frontend/src/components/App.js
@@ -7,17 +7,20 @@ import BottomBar from "./BottomBar";
export default class App extends Component {
constructor(props) {
super(props);
+ this.state = {
+ nickname: null,
+ token: null,
+ }
+ }
+
+ setAppState=(newState)=>{
+ this.setState(newState)
}
render() {
return (
<>
-
)
}
}
diff --git a/frontend/src/components/Chat.js b/frontend/src/components/Chat.js
index e1b3dc227..16cf2c714 100644
--- a/frontend/src/components/Chat.js
+++ b/frontend/src/components/Chat.js
@@ -1,8 +1,6 @@
import React, { Component } from 'react';
import { w3cwebsocket as W3CWebSocket } from "websocket";
-import {Button, TextField, Link, Grid, Typography, Container, Card, CardHeader, Paper, Avatar} from "@mui/material";
-import { withStyles } from "@mui/material";
-
+import {Button, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, FormHelperText} from "@mui/material";
export default class Chat extends Component {
@@ -53,7 +51,7 @@ export default class Chat extends Component {
this.client.send(JSON.stringify({
type: "message",
message: this.state.value,
- nick: this.props.urNick,
+ nick: this.props.ur_nick,
}));
this.state.value = ''
e.preventDefault();
@@ -66,7 +64,7 @@ export default class Chat extends Component {
{this.state.messages.map(message => <>
{/* If message sender is not our nick, gray color, if it is our nick, green color */}
- {message.userNick == this.props.urNick ?
+ {message.userNick == this.props.ur_nick ?
+ This chat has no memory. If you leave and come back the messages are lost.
)
}
diff --git a/frontend/src/components/HomePage.js b/frontend/src/components/HomePage.js
index 011fa4924..59d84bb62 100644
--- a/frontend/src/components/HomePage.js
+++ b/frontend/src/components/HomePage.js
@@ -1,25 +1,43 @@
import React, { Component } from "react";
-import { BrowserRouter as Router, Switch, Route, Link, Redirect } from "react-router-dom";
+import { BrowserRouter as Router, Switch, Route, Link, Redirect,useHistory } from "react-router-dom";
import UserGenPage from "./UserGenPage";
import MakerPage from "./MakerPage";
import BookPage from "./BookPage";
import OrderPage from "./OrderPage";
+import BottomBar from "./BottomBar";
+
export default class HomePage extends Component {
constructor(props) {
super(props);
+ this.state = {
+ nickname: null,
+ token: null,
+ }
+ }
+
+ setAppState=(newState)=>{
+ this.setState(newState)
+ }
+
+ redirectTo(location) {
+ this.props.history.push(location);
}
render() {
return (
-
-
-
You are at the start page
-
-
-
-
+
+
+ }/>
+
+
+
+
+
+
+
+
);
}
diff --git a/frontend/src/components/InfoDialog.js b/frontend/src/components/InfoDialog.js
index d7e8adaeb..24bad0de0 100644
--- a/frontend/src/components/InfoDialog.js
+++ b/frontend/src/components/InfoDialog.js
@@ -1,36 +1,69 @@
-import {Typography, DialogTitle, DialogContent, DialogContentText, Button } from "@mui/material"
+import {Typography, DialogActions, DialogContent, Button, Grid} from "@mui/material"
import React, { Component } from 'react'
+import Image from 'material-ui-image'
+import MediaQuery from 'react-responsive'
+import { maxWidth, minWidth } from "@mui/system"
export default class InfoDialog extends Component {
render() {
return (
- What is RoboSats?
+
+
+
+
+ What is RoboSats?
+
+
It is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies
+ matchmaking and minimizes the need of trust. RoboSats focuses in privacy and speed.
AdequateAlice01 wants to sell bitcoin. She posts a sell order.
+
AnonymousAlice01 wants to sell bitcoin. She posts a sell order.
BafflingBob02 wants to buy bitcoin and he takes Alice's order.
Both have to post a small bond using lightning to prove they are real
robots. Then, Alice posts the trade collateral also using a lightning
- hold invoice. RoboSats locks the invoice until Bob confirms he sent
- the fiat to Alice. Once Alice confirms she received the fiat, she
- tells RoboSats to release the satoshis to Bob. Enjoy your satoshis,
+ hold invoice. RoboSats locks the invoice until Alice confirms she
+ received the fiat, then the satoshis are released to Bob. Enjoy your satoshis,
Bob!
-
At no point, AdequateAlice01 and BafflingBob02 have to trust the
- bitcoin to each other. In case they have a conflict, RoboSats staff
- will help resolving the dispute.
+
At no point, AnonymousAlice01 and BafflingBob02 have to trust the
+ bitcoin funds to each other. In case they have a conflict, RoboSats staff
+ will help resolving the dispute. You can find a step-by-step
+ description of the trade pipeline in How it works
What payment methods are accepted?
@@ -51,8 +84,9 @@ export default class InfoDialog extends Component {
Is RoboSats private?
-
RoboSats will never ask you for your name, country or ID. For
- best anonymity use Tor Browser and access the .onion hidden service.
+
RoboSats will never ask you for your name, country or ID. RoboSats does
+ not custody your funds, and doesn't care who you are. For best anonymity
+ use Tor Browser and access the .onion hidden service.
Your trading peer is the only one who can potentially guess
anything about you. Keep your chat short and concise. Avoid
@@ -73,29 +107,33 @@ export default class InfoDialog extends Component {
What is the trust model?
The buyer and the seller never have to trust each other.
- Some trust on RoboSats staff is needed since linking
- the seller's hold invoice and buyer payment is not atomic (yet).
+ Some trust on RoboSats is needed since linking the
+ seller's hold invoice and buyer payment is not atomic (yet).
In addition, disputes are solved by the RoboSats staff.
-
While trust requirements are minimized, RoboSats could
- run away with your satoshis. It could be argued that it is not
- worth it, as it would instantly destroy RoboSats reputation.
+
To be totally clear. Trust requirements are minimized. However, there is still
+ one way RoboSats could run away with your satoshis: by not releasing
+ the satoshis to the buyer. It could be argued that such move is not in RoboSats'
+ interest as it would damage the reputation for a small payout.
However, you should hesitate and only trade small quantities at a
time. For large amounts use an onchain escrow service such as Bisq
Your sats will most likely return to you. Any hold invoice that is not
+
Your sats will return to you. Any hold invoice that is not
settled would be automatically returned even if RoboSats goes down
forever. This is true for both, locked bonds and trading escrows. However,
- there is a small window between the buyer confirms FIAT SENT and the moment
- the seller releases the satoshis when the funds could be lost.
+ there is a small window between the seller confirms FIAT RECEIVED and the moment
+ the buyer receives the satoshis when the funds could be permanentely lost if
+ RoboSats disappears. This window is about 1 second long. Make sure to have enough
+ inbound liquidity to avoid routing failures. If you have any problem, reach out
+ trough the RoboSats public channels.
@@ -116,9 +154,10 @@ export default class InfoDialog extends Component {
RoboSats will definitely never ask for your robot token.
-
+
+
+
-
:
- (this.state.isParticipant ?
-
-
- {this.orderBox()}
-
-
-
-
-
+ (this.state.is_participant ?
+ <>
+ {/* Desktop View */}
+
+
+
+
+ {/* SmarPhone View */}
+
+
+
+ >
:
-
+
{this.orderBox()}
)
)
diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js
index 1c25c9f19..ddc5ddfd0 100644
--- a/frontend/src/components/TradeBox.js
+++ b/frontend/src/components/TradeBox.js
@@ -1,15 +1,15 @@
import React, { Component } from "react";
-import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
+import { IconButton, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import QRCode from "react-qr-code";
-
+import Countdown from 'react-countdown';
import Chat from "./Chat"
+import MediaQuery from 'react-responsive'
+import QrReader from 'react-qr-reader'
// Icons
-import SmartToyIcon from '@mui/icons-material/SmartToy';
import PercentIcon from '@mui/icons-material/Percent';
import BookIcon from '@mui/icons-material/Book';
-
-
+import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
function getCookie(name) {
let cookieValue = null;
@@ -41,9 +41,21 @@ export default class TradeBox extends Component {
openConfirmDispute: false,
badInvoice: false,
badStatement: false,
+ qrscanner: false,
}
}
+ Sound = ({soundFileName}) => (
+ // Four filenames: "locked-invoice", "taker-found", "open-chat", "sucessful"
+
+ )
+
+ togglePlay = () => {
+ this.setState({ playSound: !this.state.playSound }, () => {
+ this.state.playSound ? this.audio.play() : this.audio.pause();
+ });
+ }
+
handleClickOpenConfirmDispute = () => {
this.setState({openConfirmDispute: true});
};
@@ -61,7 +73,7 @@ export default class TradeBox extends Component {
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
- .then((data) => (this.props.data = data));
+ .then((data) => this.props.completeSetState(data));
this.handleClickCloseConfirmDispute();
}
@@ -78,15 +90,15 @@ export default class TradeBox extends Component {
- The RoboSats staff will examine the statements and evidence provided by the participants.
- It is best if you provide a burner contact method on your statement for the staff to contact you.
- The satoshis in the trade escrow will be sent to the dispute winner, while the dispute
- loser will lose the bond.
+ The RoboSats staff will examine the statements and evidence provided. You need to build
+ a complete case, as the staff cannot read the chat. It is best to provide a burner contact
+ method with your statement. The satoshis in the trade escrow will be sent to the dispute winner,
+ while the dispute loser will lose the bond.
-
+
)
@@ -136,32 +148,32 @@ export default class TradeBox extends Component {
- Robosats show commitment to their peers
+ Robots show commitment to their peers
- {this.props.data.isMaker ?
+ {this.props.data.is_maker ?
- Lock {pn(this.props.data.bondSatoshis)} Sats to PUBLISH order
+ Lock {pn(this.props.data.bond_satoshis)} Sats to PUBLISH order
:
- Lock {pn(this.props.data.bondSatoshis)} Sats to TAKE the order
+ Lock {pn(this.props.data.bond_satoshis)} Sats to TAKE the order
}
-
-
+
+
@@ -173,7 +185,7 @@ export default class TradeBox extends Component {
return (
- 🔒 Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is locked
+ 🔒 Your {this.props.data.is_maker ? 'maker' : 'taker'} bond is locked
);
@@ -182,23 +194,25 @@ export default class TradeBox extends Component {
showEscrowQRInvoice=()=>{
return (
+ {/* Make confirmation sound for HTLC received. */}
+
- Deposit {pn(this.props.data.escrowSatoshis)} Sats as trade collateral
+ Deposit {pn(this.props.data.escrow_satoshis)} Sats as trade collateral
-
-
+
+
@@ -208,11 +222,10 @@ export default class TradeBox extends Component {
}
showTakerFound=()=>{
-
- // TODO Make some sound here! The maker might have been waiting for long
-
return (
+ {/* Make bell sound when taker is found */}
+ A taker has been found!
@@ -221,7 +234,9 @@ export default class TradeBox extends Component {
- Please wait for the taker to confirm his commitment by locking a bond.
+ Please wait for the taker to confirm by locking a bond.
+ If the taker does not lock a bond in time the orer will be made
+ public again.
{this.showBondIsLocked()}
@@ -232,6 +247,8 @@ export default class TradeBox extends Component {
showMakerWait=()=>{
return (
+ {/* Make confirmation sound for HTLC received. */}
+ Your order is public. Wait for a taker.
@@ -251,20 +268,12 @@ export default class TradeBox extends Component {
{/* TODO API sends data for a more confortable wait */}
-
-
-
-
-
-
-
-
-
+
@@ -272,7 +281,7 @@ export default class TradeBox extends Component {
-
@@ -305,7 +314,7 @@ export default class TradeBox extends Component {
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => this.setState({badInvoice:data.bad_invoice})
- & console.log(data));
+ & this.props.completeSetState(data));
}
handleInputDisputeChanged=(e)=>{
@@ -329,25 +338,38 @@ export default class TradeBox extends Component {
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));
-}
+ & this.props.completeSetState(data));
+ }
+
+ handleScan = data => {
+ if (data) {
+ this.setState({
+ invoice: data
+ })
+ }
+ }
+ handleError = err => {
+ console.error(err)
+ }
+
+ handleQRbutton = () => {
+ this.setState({qrscanner: !this.state.qrscanner});
+ }
showInputInvoice(){
return (
- // TODO Option to upload files and images
-
- Submit a LN invoice for {pn(this.props.data.invoiceAmount)} Sats
+ Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats
The taker is committed! Before letting you send {" "+ parseFloat(parseFloat(this.props.data.amount).toFixed(4))+
" "+ this.props.data.currencyCode}, we want to make sure you are able to receive the BTC. Please provide a
- valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis.
+ valid invoice for {pn(this.props.data.invoice_amount)} Satoshis.
@@ -357,14 +379,29 @@ export default class TradeBox extends Component {
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
label={"Payout Lightning Invoice"}
required
+ value={this.state.invoice}
inputProps={{
- style: {textAlign:"center"}
+ style: {textAlign:"center"},
+ maxHeight: 200,
}}
multiline
+ minRows={5}
+ maxRows={this.state.qrscanner ? 5 : 14}
onChange={this.handleInputInvoiceChanged}
/>
+ {this.state.qrscanner ?
+
+
+
+ : null }
+
@@ -374,44 +411,80 @@ export default class TradeBox extends Component {
}
// Asks the user for a dispute statement.
- showInDisputeStatement(){
- return (
-
- // TODO Option to upload files
+ showInDisputeStatement=()=>{
+ if(this.props.data.statement_submitted){
+ return (
+
+
+
+ We have received your statement
+
+
+
+
+ We are waiting for your trade counterparty statement.
+
+
+ {this.showBondIsLocked()}
+
+ )
+ }else{
+ 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()}
+
+ )}
+ }
+ showWaitForDisputeResolution=()=>{
+ return (
- A dispute has been opened
+ We have the statements
- 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.
+ Wait for the staff to resolve the dispute. The dispute winner
+ will be asked to submit a LN invoice.
-
-
-
-
-
-
-
-
{this.showBondIsLocked()}
)
@@ -427,10 +500,9 @@ export default class TradeBox extends Component {
-
We are waiting for the seller to deposit the full trade BTC amount
- into the escrow.
-
Just hang on for a moment. If the seller does not deposit,
- you will get your bond back automatically.
+
We are waiting for the seller lock the trade amount.
+
Just hang on for a moment. If the seller does not deposit,
+ you will get your bond back automatically.
{this.showBondIsLocked()}
@@ -441,6 +513,8 @@ export default class TradeBox extends Component {
showWaitingForBuyerInvoice(){
return(
+ {/* Make confirmation sound for HTLC received. */}
+ The trade collateral is locked! 🎉
@@ -470,7 +544,7 @@ export default class TradeBox extends Component {
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
- .then((data) => (this.props.data = data));
+ .then((data) => this.props.completeSetState(data));
}
handleRatingChange=(e)=>{
@@ -484,7 +558,7 @@ handleRatingChange=(e)=>{
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
- .then((data) => (this.props.data = data));
+ .then((data) => this.props.completeSetState(data));
}
showFiatSentButton(){
@@ -526,16 +600,42 @@ handleRatingChange=(e)=>{
)
}
- showChat(sendFiatButton, receivedFiatButton, openDisputeButton){
+ showChat=()=>{
+ //In Chatroom - No fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton)
+ if(this.props.data.is_buyer & this.props.data.status == 9){
+ var showSendButton=true;
+ var showReveiceButton=false;
+ var showDisputeButton=true;
+ }
+ if(this.props.data.is_seller & this.props.data.status == 9){
+ var showSendButton=false;
+ var showReveiceButton=false;
+ var showDisputeButton=true;
+ }
+
+ //In Chatroom - Fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton)
+ if(this.props.data.is_buyer & this.props.data.status == 10){
+ var showSendButton=false;
+ var showReveiceButton=false;
+ var showDisputeButton=true;
+ }
+ if(this.props.data.is_seller & this.props.data.status == 10){
+ var showSendButton=false;
+ var showReveiceButton=true;
+ var showDisputeButton=true;
+ }
+
return(
+ {/* Make confirmation sound for Chat Open. */}
+
- Chatting with {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}
+ Chatting with {this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}
- {this.props.data.isSeller ?
+ {this.props.data.is_seller ?
Say hi! Be helpful and concise. Let them know how to send you {this.props.data.currencyCode}.
@@ -547,12 +647,11 @@ handleRatingChange=(e)=>{
-
-
+
- {openDisputeButton ? this.showOpenDisputeButton() : ""}
- {sendFiatButton ? this.showFiatSentButton() : ""}
- {receivedFiatButton ? this.showFiatReceivedButton() : ""}
+ {showDisputeButton ? this.showOpenDisputeButton() : ""}
+ {showSendButton ? this.showFiatSentButton() : ""}
+ {showReveiceButton ? this.showFiatReceivedButton() : ""}
{this.showBondIsLocked()}
@@ -562,6 +661,8 @@ handleRatingChange=(e)=>{
showRateSelect(){
return(
+ {/* Make confirmation sound for Chat Open. */}
+
🎉Trade finished!🥳
@@ -569,7 +670,7 @@ handleRatingChange=(e)=>{
- What do you think of {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}?
+ What do you think of {this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}?
@@ -582,49 +683,132 @@ handleRatingChange=(e)=>{
)
}
+ showSendingPayment(){
+ return(
+
+
+
+ Attempting Lightning Payment
+
+
+
+
+ RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must
+ be online in order to receive payments.
+
+
+
+ )
+ }
+
+ showRoutingFailed=()=>{
+ // TODO If it has failed 3 times, ask for a new invoice.
+ if(this.props.data.invoice_expired){
+ return(
+
+
+
+ Lightning Routing Failed
+
+
+
+
+ Your invoice has expires or more than 3 payments have been attempted.
+
+
+
+
+ Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats
+
+
+
+
+
+
+
+
+
+ )
+ }else{
+ return(
+
+
+
+ Lightning Routing Failed
+
+
+
+
+ RoboSats will try to pay your invoice 3 times every 5 minutes. If it keeps failing, you
+ will be able to submit a new invoice. Check whether you have enough inboud liquidity.
+ Remember that lightning nodes must be online in order to receive payments.
+
+
+
+
+
+
+
+
+
+ )}
+ }
+
render() {
return (
-
+
-
- Contract Box
-
+
+
+ Contract Box
+
+
{/* Maker and taker Bond request */}
- {this.props.data.bondInvoice ? this.showQRInvoice() : ""}
+ {this.props.data.is_maker & this.props.data.status == 0 ? this.showQRInvoice() : ""}
+ {this.props.data.is_taker & this.props.data.status == 3 ? this.showQRInvoice() : ""}
{/* Waiting for taker and taker bond request */}
- {this.props.data.isMaker & this.props.data.statusCode == 1 ? this.showMakerWait() : ""}
- {this.props.data.isMaker & this.props.data.statusCode == 3 ? this.showTakerFound() : ""}
+ {this.props.data.is_maker & this.props.data.status == 1 ? this.showMakerWait() : ""}
+ {this.props.data.is_maker & this.props.data.status == 3 ? this.showTakerFound() : ""}
{/* Send Invoice (buyer) and deposit collateral (seller) */}
- {this.props.data.isSeller & this.props.data.escrowInvoice != null ? this.showEscrowQRInvoice() : ""}
- {this.props.data.isBuyer & this.props.data.invoiceAmount != null ? this.showInputInvoice() : ""}
- {this.props.data.isBuyer & this.props.data.statusCode == 7 ? this.showWaitingForEscrow() : ""}
- {this.props.data.isSeller & this.props.data.statusCode == 8 ? this.showWaitingForBuyerInvoice() : ""}
-
- {/* In Chatroom - No fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
- {this.props.data.isBuyer & this.props.data.statusCode == 9 ? this.showChat(true,false,true) : ""}
- {this.props.data.isSeller & this.props.data.statusCode == 9 ? this.showChat(false,false,true) : ""}
-
- {/* In Chatroom - Fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
- {this.props.data.isBuyer & this.props.data.statusCode == 10 ? this.showChat(false,false,true) : ""}
- {this.props.data.isSeller & this.props.data.statusCode == 10 ? this.showChat(false,true,true) : ""}
+ {this.props.data.is_seller & (this.props.data.status == 6 || this.props.data.status == 7 ) ? this.showEscrowQRInvoice() : ""}
+ {this.props.data.is_buyer & (this.props.data.status == 6 || this.props.data.status == 8 )? this.showInputInvoice() : ""}
+ {this.props.data.is_buyer & this.props.data.status == 7 ? this.showWaitingForEscrow() : ""}
+ {this.props.data.is_seller & this.props.data.status == 8 ? this.showWaitingForBuyerInvoice() : ""}
+
+ {/* In Chatroom */}
+ {this.props.data.status == 9 || this.props.data.status == 10 ? this.showChat(): ""}
{/* Trade Finished */}
- {(this.props.data.isSeller & this.props.data.statusCode > 12 & this.props.data.statusCode < 15) ? this.showRateSelect() : ""}
- {(this.props.data.isBuyer & this.props.data.statusCode == 14) ? this.showRateSelect() : ""}
+ {(this.props.data.is_seller & [13,14,15].includes(this.props.data.status)) ? this.showRateSelect() : ""}
+ {(this.props.data.is_buyer & this.props.data.status == 14) ? this.showRateSelect() : ""}
+
+ {/* Trade Finished - Payment Routing Failed */}
+ {this.props.data.is_buyer & this.props.data.status == 13 ? this.showSendingPayment() : ""}
{/* Trade Finished - Payment Routing Failed */}
- {this.props.data.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""}
+ {this.props.data.is_buyer & this.props.data.status == 15 ? this.showRoutingFailed() : ""}
{/* Trade Finished - TODO Needs more planning */}
- {this.props.data.statusCode == 11 ? this.showInDisputeStatement() : ""}
+ {this.props.data.status == 11 ? this.showInDisputeStatement() : ""}
+ {this.props.data.status == 16 ? this.showWaitForDisputeResolution() : ""}
{/* Order has expired */}
- {this.props.data.statusCode == 5 ? this.showOrderExpired() : ""}
+ {this.props.data.status == 5 ? this.showOrderExpired() : ""}
{/* TODO */}
{/* */}
{/* */}
diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js
index 48341242d..c263a5b57 100644
--- a/frontend/src/components/UserGenPage.js
+++ b/frontend/src/components/UserGenPage.js
@@ -3,7 +3,8 @@ import { Button , Dialog, Grid, Typography, TextField, ButtonGroup, CircularProg
import { Link } from 'react-router-dom'
import Image from 'material-ui-image'
import InfoDialog from './InfoDialog'
-import ContentCopyIcon from '@mui/icons-material/ContentCopy';
+import SmartToyIcon from '@mui/icons-material/SmartToy';
+import CasinoIcon from '@mui/icons-material/Casino';
import ContentCopy from "@mui/icons-material/ContentCopy";
function getCookie(name) {
@@ -29,7 +30,8 @@ export default class UserGenPage extends Component {
this.state = {
token: this.genBase62Token(34),
openInfo: false,
- showRobosat: true,
+ loadingRobot: true,
+ tokenHasChanged: false,
};
this.getGeneratedUser(this.state.token);
}
@@ -45,7 +47,7 @@ export default class UserGenPage extends Component {
.substring(0, length);
}
- getGeneratedUser(token) {
+ getGeneratedUser=(token)=>{
fetch('/api/user' + '?token=' + token)
.then((response) => response.json())
.then((data) => {
@@ -56,8 +58,18 @@ export default class UserGenPage extends Component {
shannon_entropy: data.token_shannon_entropy,
bad_request: data.bad_request,
found: data.found,
- showRobosat:true,
- });
+ loadingRobot:false,
+ })
+ &
+ // Add nick and token to App state (token only if not a bad request)
+ (data.bad_request ? this.props.setAppState({
+ nickname: data.nickname,
+ })
+ :
+ this.props.setAppState({
+ nickname: data.nickname,
+ token: this.state.token,
+ }));
});
}
@@ -71,23 +83,24 @@ export default class UserGenPage extends Component {
.then((data) => console.log(data));
}
- handleAnotherButtonPressed=(e)=>{
- this.delGeneratedUser()
- // this.setState({
- // showRobosat: false,
- // token: this.genBase62Token(34),
- // });
- // this.getGeneratedUser(this.state.token);
- window.location.reload();
+ handleClickNewRandomToken=()=>{
+ this.setState({
+ token: this.genBase62Token(34),
+ tokenHasChanged: true,
+ });
}
handleChangeToken=(e)=>{
- this.delGeneratedUser()
this.setState({
token: e.target.value,
+ tokenHasChanged: true,
})
- this.getGeneratedUser(e.target.value);
- this.setState({showRobosat: false})
+ }
+
+ handleClickSubmitToken=()=>{
+ this.delGeneratedUser()
+ this.getGeneratedUser(this.state.token);
+ this.setState({loadingRobot: true, tokenHasChanged: false})
}
handleClickOpenInfo = () => {
@@ -107,17 +120,16 @@ export default class UserGenPage extends Component {
aria-describedby="info-dialog-description"
scroll="paper"
>
-
+
)
}
-
render() {
return (
-
- {this.state.showRobosat ?
+
+ {!this.state.loadingRobot ?