Merge pull request #36 from Reckless-Satoshi/polish-and-debug-before-0.1.0-mvp
Merging debugging and polishing before v0.1.0-mvp Fixes #22 #30 #32
@ -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'
|
11
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
|
||||
<div align="center">
|
||||
<img width="75%" src="https://raw.githubusercontent.com/Reckless-Satoshi/robosats/frontend/static/assets/images/robosats_0.1.0_banner.png">
|
||||
</div>
|
||||
|
||||
**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
|
||||
|
||||
|
16
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']
|
||||
list_filter = ['currency']
|
||||
ordering = ('-timestamp',)
|
@ -9,6 +9,7 @@ from base64 import b64decode
|
||||
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 @@ class LNNode():
|
||||
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 @@ class LNNode():
|
||||
|
||||
# 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 @@ class LNNode():
|
||||
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 @@ class LNNode():
|
||||
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.
|
||||
# @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
|
||||
# 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 @@ class LNNode():
|
||||
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,
|
||||
|
222
api/logics.py
@ -40,19 +40,19 @@ class Logics():
|
||||
'''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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
|
||||
# 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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
@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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
|
||||
# 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 @@ class Logics():
|
||||
''' 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 @@ class Logics():
|
||||
@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 @@ class Logics():
|
||||
|
||||
# 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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
|
||||
# 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 @@ class Logics():
|
||||
+ " - 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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
|
||||
# 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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
'''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 @@ class Logics():
|
||||
# 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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
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 @@ class Logics():
|
||||
|
||||
@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)
|
||||
|
@ -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 @@ class Command(BaseCommand):
|
||||
|
||||
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))
|
||||
|
@ -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 @@ import time
|
||||
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 @@ class Command(BaseCommand):
|
||||
|
||||
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])
|
||||
|
||||
# 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
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
# try saving expiry height
|
||||
if hasattr(response, 'htlcs' ):
|
||||
try:
|
||||
hold_lnpayment.expiry_height = response.htlcs[0].expiry_height
|
||||
except:
|
||||
pass
|
||||
|
||||
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 @@ class Command(BaseCommand):
|
||||
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
|
@ -19,7 +19,7 @@ BOND_SIZE = float(config('BOND_SIZE'))
|
||||
|
||||
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 Currency(models.Model):
|
||||
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 LNPayment(models.Model):
|
||||
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 LNPayment(models.Model):
|
||||
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 Order(models.Model):
|
||||
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 Order(models.Model):
|
||||
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 Order(models.Model):
|
||||
8 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for buyer invoice'
|
||||
9 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Sending fiat - In chatroom'
|
||||
10 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Fiat sent - In chatroom'
|
||||
11 : 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 @@ class Order(models.Model):
|
||||
|
||||
@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):
|
||||
|
@ -4773,6 +4773,7 @@ adjectives = [
|
||||
"Compound",
|
||||
"Important",
|
||||
"Robotic",
|
||||
"Satoshi",
|
||||
"Alltoocommon",
|
||||
"Informative",
|
||||
"Anxious",
|
||||
|
@ -6346,6 +6346,7 @@ nouns = [
|
||||
"Nair",
|
||||
"Nairo",
|
||||
"Naivete",
|
||||
"Nakamoto",
|
||||
"Name",
|
||||
"Namesake",
|
||||
"Nanometer",
|
||||
@ -12229,6 +12230,7 @@ nouns = [
|
||||
"Sand",
|
||||
"Sandwich",
|
||||
"Satisfaction",
|
||||
"Satoshi",
|
||||
"Save",
|
||||
"Savings",
|
||||
"Scale",
|
||||
|
68
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
|
||||
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 == 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
|
||||
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():
|
||||
|
||||
|
@ -72,7 +72,7 @@ premium_percentile = {}
|
||||
@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:
|
||||
|
101
api/views.py
@ -26,6 +26,7 @@ from decouple import config
|
||||
|
||||
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 @@ class MakerView(CreateAPIView):
|
||||
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 @@ class OrderView(viewsets.ViewSet):
|
||||
'''
|
||||
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 @@ class OrderView(viewsets.ViewSet):
|
||||
# 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 @@ class OrderView(viewsets.ViewSet):
|
||||
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 @@ class OrderView(viewsets.ViewSet):
|
||||
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 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# 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 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# 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 @@ class OrderView(viewsets.ViewSet):
|
||||
# 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 @@ class UserView(APIView):
|
||||
# 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 @@ class UserView(APIView):
|
||||
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 @@ class BookView(ListAPIView):
|
||||
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 @@ class InfoView(ListAPIView):
|
||||
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)
|
||||
|
||||
|
81
faq.md
@ -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.
|
66
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
<div className='appCenter'>
|
||||
<HomePage />
|
||||
</div>
|
||||
<div className='bottomBar'>
|
||||
<BottomBar />
|
||||
</div>
|
||||
<HomePage setAppState={this.setAppState}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import React, { Component } from "react";
|
||||
import { Paper, Button , CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material";
|
||||
import { Link } from 'react-router-dom'
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import MediaQuery from 'react-responsive'
|
||||
|
||||
import getFlags from './getFlags'
|
||||
|
||||
export default class BookPage extends Component {
|
||||
@ -10,7 +12,7 @@ export default class BookPage extends Component {
|
||||
this.state = {
|
||||
orders: new Array({id:0,}),
|
||||
currency: 0,
|
||||
type: 1,
|
||||
type: 2,
|
||||
currencies_dict: {"0":"ANY"},
|
||||
loading: true,
|
||||
};
|
||||
@ -22,8 +24,7 @@ export default class BookPage extends Component {
|
||||
getOrderDetails(type, currency) {
|
||||
fetch('/api/book' + '?currency=' + currency + "&type=" + type)
|
||||
.then((response) => response.json())
|
||||
.then((data) => console.log(data) &
|
||||
this.setState({
|
||||
.then((data) => this.setState({
|
||||
orders: data,
|
||||
not_found: data.not_found,
|
||||
loading: false,
|
||||
@ -31,7 +32,6 @@ export default class BookPage extends Component {
|
||||
}
|
||||
|
||||
handleRowClick=(e)=>{
|
||||
console.log(e)
|
||||
this.props.history.push('/order/' + e);
|
||||
}
|
||||
|
||||
@ -68,8 +68,8 @@ export default class BookPage extends Component {
|
||||
pn(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
bookListTable=()=>{
|
||||
|
||||
bookListTableDesktop=()=>{
|
||||
return (
|
||||
<div style={{ height: 475, width: '100%' }}>
|
||||
<DataGrid
|
||||
@ -93,7 +93,7 @@ export default class BookPage extends Component {
|
||||
renderCell: (params) => {return (
|
||||
<ListItemButton style={{ cursor: "pointer" }}>
|
||||
<ListItemAvatar>
|
||||
<Avatar alt={params.row.robosat} src={params.row.avatar} />
|
||||
<Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={params.row.robosat}/>
|
||||
</ListItemButton>
|
||||
@ -124,9 +124,61 @@ export default class BookPage extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
bookListTablePhone=()=>{
|
||||
return (
|
||||
<div style={{ height: 425, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={
|
||||
this.state.orders.map((order) =>
|
||||
({id: order.id,
|
||||
avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png',
|
||||
robosat: order.maker_nick,
|
||||
type: order.type ? "Sell": "Buy",
|
||||
amount: parseFloat(parseFloat(order.amount).toFixed(4)),
|
||||
currency: this.getCurrencyCode(order.currency),
|
||||
payment_method: order.payment_method,
|
||||
price: order.price,
|
||||
premium: order.premium,
|
||||
})
|
||||
)}
|
||||
|
||||
columns={[
|
||||
// { field: 'id', headerName: 'ID', width: 40 },
|
||||
{ field: 'robosat', headerName: 'Robot', width: 80,
|
||||
renderCell: (params) => {return (
|
||||
<ListItemButton style={{ cursor: "pointer" }}>
|
||||
<Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
|
||||
</ListItemButton>
|
||||
);
|
||||
} },
|
||||
{ field: 'type', headerName: 'Type', width: 60, hide:'true'},
|
||||
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80 },
|
||||
{ field: 'currency', headerName: 'Currency', width: 100,
|
||||
renderCell: (params) => {return (
|
||||
<div style={{ cursor: "pointer" }}>{params.row.currency + " " + getFlags(params.row.currency)}</div>
|
||||
)} },
|
||||
{ field: 'payment_method', headerName: 'Payment Method', width: 180, hide:'true'},
|
||||
{ field: 'price', headerName: 'Price', type: 'number', width: 140, hide:'true',
|
||||
renderCell: (params) => {return (
|
||||
<div style={{ cursor: "pointer" }}>{this.pn(params.row.price) + " " +params.row.currency+ "/BTC" }</div>
|
||||
)} },
|
||||
{ field: 'premium', headerName: 'Premium', type: 'number', width: 85,
|
||||
renderCell: (params) => {return (
|
||||
<div style={{ cursor: "pointer" }}>{parseFloat(parseFloat(params.row.premium).toFixed(4))+"%" }</div>
|
||||
)} },
|
||||
]}
|
||||
|
||||
pageSize={6}
|
||||
onRowClick={(params) => this.handleRowClick(params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
|
||||
rowsPerPageOptions={[6]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid className='orderBook' container spacing={1}>
|
||||
<Grid className='orderBook' container spacing={1} sx={{minWidth:400}}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h2" variant="h2">
|
||||
Order Book
|
||||
@ -194,20 +246,31 @@ export default class BookPage extends Component {
|
||||
No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<br/>
|
||||
<Grid item>
|
||||
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||
</Grid>
|
||||
<Typography component="body1" variant="body1">
|
||||
Be the first one to create an order
|
||||
<br/>
|
||||
<br/>
|
||||
</Typography>
|
||||
</Grid>)
|
||||
:
|
||||
<Grid item xs={12} align="center">
|
||||
<Paper elevation={0} style={{width: 910, maxHeight: 500, overflow: 'auto'}}>
|
||||
{/* Desktop Book */}
|
||||
<MediaQuery minWidth={920}>
|
||||
<Paper elevation={0} style={{width: 910, maxHeight: 500, overflow: 'auto'}}>
|
||||
{this.state.loading ? null : this.bookListTableDesktop()}
|
||||
</Paper>
|
||||
</MediaQuery>
|
||||
|
||||
{this.state.loading ? null : this.bookListTable()}
|
||||
|
||||
</Paper>
|
||||
{/* Smartphone Book */}
|
||||
<MediaQuery maxWidth={919}>
|
||||
<Paper elevation={0} style={{width: 380, maxHeight: 450, overflow: 'auto'}}>
|
||||
{this.state.loading ? null : this.bookListTablePhone()}
|
||||
</Paper>
|
||||
</MediaQuery>
|
||||
</Grid>
|
||||
}
|
||||
<Grid item xs={12} align="center">
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import {Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
|
||||
import {Badge, TextField, ListItemAvatar, Avatar,Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
|
||||
import MediaQuery from 'react-responsive'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
// Icons
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
@ -14,6 +16,14 @@ import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import EqualizerIcon from '@mui/icons-material/Equalizer';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||
import PasswordIcon from '@mui/icons-material/Password';
|
||||
import ContentCopy from "@mui/icons-material/ContentCopy";
|
||||
|
||||
// pretty numbers
|
||||
function pn(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
export default class BottomBar extends Component {
|
||||
constructor(props) {
|
||||
@ -21,16 +31,21 @@ export default class BottomBar extends Component {
|
||||
this.state = {
|
||||
openStatsForNerds: false,
|
||||
openCommuniy: false,
|
||||
openExchangeSummary:false,
|
||||
num_public_buy_orders: 0,
|
||||
num_public_sell_orders: 0,
|
||||
active_robots_today: 0,
|
||||
fee: 0,
|
||||
today_avg_nonkyc_btc_premium: 0,
|
||||
today_total_volume: 0,
|
||||
lifetime_satoshis_settled: 0,
|
||||
robosats_running_commit_hash: '000000000000000',
|
||||
openProfile: false,
|
||||
profileShown: false,
|
||||
};
|
||||
this.getInfo();
|
||||
}
|
||||
|
||||
|
||||
handleClickSuppport = () => {
|
||||
window.open("https://t.me/robosats");
|
||||
};
|
||||
@ -39,19 +54,20 @@ export default class BottomBar extends Component {
|
||||
this.setState(null)
|
||||
fetch('/api/info/')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {console.log(data) &
|
||||
this.setState(data)
|
||||
});
|
||||
.then((data) => this.setState(data) &
|
||||
this.props.setAppState({nickname:data.nickname}));
|
||||
}
|
||||
|
||||
handleClickOpenStatsForNerds = () => {
|
||||
this.setState({openStatsForNerds: true});
|
||||
};
|
||||
|
||||
handleClickCloseStatsForNerds = () => {
|
||||
this.setState({openStatsForNerds: false});
|
||||
};
|
||||
|
||||
StatsDialog =() =>{
|
||||
|
||||
return(
|
||||
<Dialog
|
||||
open={this.state.openStatsForNerds}
|
||||
@ -61,7 +77,7 @@ export default class BottomBar extends Component {
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography component="h5" variant="h5">Stats For Nerds</Typography>
|
||||
<List>
|
||||
<List dense>
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon><BoltIcon/></ListItemIcon>
|
||||
@ -71,9 +87,9 @@ export default class BottomBar extends Component {
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon><GitHubIcon/></ListItemIcon>
|
||||
<ListItemText secondary="Currently running commit height">
|
||||
<ListItemText secondary="Currently running commit hash">
|
||||
<a href={"https://github.com/Reckless-Satoshi/robosats/tree/"
|
||||
+ this.state.robosats_running_commit_hash}>{this.state.robosats_running_commit_hash}
|
||||
+ this.state.robosats_running_commit_hash}>{this.state.robosats_running_commit_hash.slice(0, 12)+"..."}
|
||||
</a>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
@ -84,6 +100,12 @@ export default class BottomBar extends Component {
|
||||
<ListItemText primary={this.state.today_total_volume+" BTC"} secondary="Today traded volume"/>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon><EqualizerIcon/></ListItemIcon>
|
||||
<ListItemText primary={pn(this.state.lifetime_satoshis_settled)+" Sats"} secondary="Lifetime settled volume"/>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon><PublicIcon/></ListItemIcon>
|
||||
@ -104,6 +126,7 @@ export default class BottomBar extends Component {
|
||||
};
|
||||
|
||||
CommunityDialog =() =>{
|
||||
|
||||
return(
|
||||
<Dialog
|
||||
open={this.state.openCommuniy}
|
||||
@ -150,20 +173,108 @@ export default class BottomBar extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
handleClickOpenProfile = () => {
|
||||
this.getInfo();
|
||||
this.setState({openProfile: true, profileShown: true});
|
||||
};
|
||||
handleClickCloseProfile = () => {
|
||||
this.setState({openProfile: false});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Paper elevation={6} style={{height:40}}>
|
||||
dialogProfile =() =>{
|
||||
return(
|
||||
<Dialog
|
||||
open={this.state.openProfile}
|
||||
onClose={this.handleClickCloseProfile}
|
||||
aria-labelledby="profile-title"
|
||||
aria-describedby="profile-description"
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography component="h5" variant="h5">Your Profile</Typography>
|
||||
<List>
|
||||
<Divider/>
|
||||
<ListItem className="profileNickname">
|
||||
<ListItemText secondary="Your robot">
|
||||
<Typography component="h6" variant="h6">
|
||||
{this.props.nickname ? "⚡"+this.props.nickname+"⚡" : ""}
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
<ListItemAvatar>
|
||||
<Avatar className='profileAvatar'
|
||||
sx={{ width: 65, height:65 }}
|
||||
alt={this.props.nickname}
|
||||
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
{this.state.active_order_id ?
|
||||
// TODO Link to router and do this.props.history.push
|
||||
<ListItemButton onClick={this.handleClickCloseProfile} to={'/order/'+this.state.active_order_id} component={Link}>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent="" color="primary">
|
||||
<NumbersIcon color="primary"/>
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={'One active order #'+this.state.active_order_id} secondary="Your current order"/>
|
||||
</ListItemButton>
|
||||
:
|
||||
<ListItem>
|
||||
<ListItemIcon><NumbersIcon/></ListItemIcon>
|
||||
<ListItemText primary="No active orders" secondary="Your current order"/>
|
||||
</ListItem>
|
||||
}
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PasswordIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary="Your token">
|
||||
{this.props.token ?
|
||||
<TextField
|
||||
disabled
|
||||
label='Store safely'
|
||||
value={this.props.token }
|
||||
variant='filled'
|
||||
size='small'
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<IconButton onClick= {()=>navigator.clipboard.writeText(this.props.token)}>
|
||||
<ContentCopy />
|
||||
</IconButton>,
|
||||
}}
|
||||
/>
|
||||
:
|
||||
'Cannot remember'}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
bottomBarDesktop =()=>{
|
||||
return(
|
||||
<Paper elevation={6} style={{height:40}}>
|
||||
<this.StatsDialog/>
|
||||
<this.CommunityDialog/>
|
||||
<this.dialogProfile/>
|
||||
<Grid container xs={12}>
|
||||
|
||||
<Grid item xs={1}>
|
||||
<IconButton color="primary"
|
||||
aria-label="Stats for Nerds"
|
||||
onClick={this.handleClickOpenStatsForNerds} >
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
<Grid item xs={2}>
|
||||
<ListItemButton onClick={this.handleClickOpenProfile} >
|
||||
<ListItemAvatar sx={{ width: 30, height: 30 }} >
|
||||
<Badge badgeContent={(this.state.active_order_id > 0 & !this.state.profileShown) ? "": null} color="primary">
|
||||
<Avatar className='flippedSmallAvatar' sx={{margin: 0, top: -13}}
|
||||
alt={this.props.nickname}
|
||||
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
|
||||
/>
|
||||
</Badge>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={this.props.nickname}/>
|
||||
</ListItemButton>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
@ -214,11 +325,11 @@ export default class BottomBar extends Component {
|
||||
primaryTypographyProps={{fontSize: '14px'}}
|
||||
secondaryTypographyProps={{fontSize: '12px'}}
|
||||
primary={this.state.today_avg_nonkyc_btc_premium+"%"}
|
||||
secondary="Today Non-KYC Avg Premium" />
|
||||
secondary="Today Avg Premium" />
|
||||
</ListItem>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<Grid item xs={1}>
|
||||
<ListItem className="bottomItem">
|
||||
<ListItemIcon size="small">
|
||||
<PercentIcon/>
|
||||
@ -227,12 +338,11 @@ export default class BottomBar extends Component {
|
||||
primaryTypographyProps={{fontSize: '14px'}}
|
||||
secondaryTypographyProps={{fontSize: '12px'}}
|
||||
primary={this.state.fee*100}
|
||||
secondary="Trading Fee" />
|
||||
secondary="Trade Fee" />
|
||||
</ListItem>
|
||||
</Grid>
|
||||
|
||||
<Grid container item xs={1}>
|
||||
<Grid item xs={2}/>
|
||||
<Grid item xs={6}>
|
||||
<Select
|
||||
size = 'small'
|
||||
@ -243,10 +353,186 @@ export default class BottomBar extends Component {
|
||||
<MenuItem value={1}>EN</MenuItem>
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid item xs={3}>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Telegram"
|
||||
aria-label="Community"
|
||||
onClick={this.handleClickOpenCommunity} >
|
||||
<PeopleIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<IconButton color="primary"
|
||||
aria-label="Stats for Nerds"
|
||||
onClick={this.handleClickOpenStatsForNerds} >
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
handleClickOpenExchangeSummary = () => {
|
||||
this.getInfo();
|
||||
this.setState({openExchangeSummary: true});
|
||||
};
|
||||
handleClickCloseExchangeSummary = () => {
|
||||
this.setState({openExchangeSummary: false});
|
||||
};
|
||||
|
||||
exchangeSummaryDialogPhone =() =>{
|
||||
return(
|
||||
<Dialog
|
||||
open={this.state.openExchangeSummary}
|
||||
onClose={this.handleClickCloseExchangeSummary}
|
||||
aria-labelledby="exchange-summary-title"
|
||||
aria-describedby="exchange-summary-description"
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography component="h5" variant="h5">Exchange Summary</Typography>
|
||||
<List dense>
|
||||
<ListItem >
|
||||
<ListItemIcon size="small">
|
||||
<InventoryIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{fontSize: '14px'}}
|
||||
secondaryTypographyProps={{fontSize: '12px'}}
|
||||
primary={this.state.num_public_buy_orders}
|
||||
secondary="Public buy orders" />
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
|
||||
<ListItem >
|
||||
<ListItemIcon size="small">
|
||||
<SellIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{fontSize: '14px'}}
|
||||
secondaryTypographyProps={{fontSize: '12px'}}
|
||||
primary={this.state.num_public_sell_orders}
|
||||
secondary="Public sell orders" />
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
|
||||
<ListItem >
|
||||
<ListItemIcon size="small">
|
||||
<SmartToyIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{fontSize: '14px'}}
|
||||
secondaryTypographyProps={{fontSize: '12px'}}
|
||||
primary={this.state.active_robots_today}
|
||||
secondary="Today active robots" />
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
|
||||
<ListItem >
|
||||
<ListItemIcon size="small">
|
||||
<PriceChangeIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{fontSize: '14px'}}
|
||||
secondaryTypographyProps={{fontSize: '12px'}}
|
||||
primary={this.state.today_avg_nonkyc_btc_premium+"%"}
|
||||
secondary="Today non-KYC average premium" />
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
|
||||
<ListItem >
|
||||
<ListItemIcon size="small">
|
||||
<PercentIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{fontSize: '14px'}}
|
||||
secondaryTypographyProps={{fontSize: '12px'}}
|
||||
primary={this.state.fee*100+"%"}
|
||||
secondary="Trading fee" />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
bottomBarPhone =()=>{
|
||||
return(
|
||||
<Paper elevation={6} style={{height:40}}>
|
||||
<this.StatsDialog/>
|
||||
<this.CommunityDialog/>
|
||||
<this.exchangeSummaryDialogPhone/>
|
||||
<this.dialogProfile/>
|
||||
<Grid container xs={12}>
|
||||
|
||||
<Grid item xs={1.6}>
|
||||
<IconButton onClick={this.handleClickOpenProfile} sx={{margin: 0, top: -13, }} >
|
||||
<Badge badgeContent={(this.state.active_order_id >0 & !this.state.profileShown) ? "": null} color="primary">
|
||||
<Avatar className='flippedSmallAvatar'
|
||||
alt={this.props.nickname}
|
||||
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
|
||||
/>
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align="center">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.num_public_buy_orders} color="action">
|
||||
<InventoryIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align="center">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.num_public_sell_orders} color="action">
|
||||
<SellIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align="center">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.active_robots_today} color="action">
|
||||
<SmartToyIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.8} align="center">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.today_avg_nonkyc_btc_premium+"%"} color="action">
|
||||
<PriceChangeIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
<Grid container item xs={3.8}>
|
||||
<Grid item xs={6}>
|
||||
<Select
|
||||
size = 'small'
|
||||
defaultValue={1}
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}>
|
||||
<MenuItem value={1}>EN</MenuItem>
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<IconButton color="primary"
|
||||
aria-label="Stats for Nerds"
|
||||
onClick={this.handleClickOpenStatsForNerds} >
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Community"
|
||||
onClick={this.handleClickOpenCommunity} >
|
||||
<PeopleIcon />
|
||||
</IconButton>
|
||||
@ -255,6 +541,20 @@ export default class BottomBar extends Component {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<MediaQuery minWidth={1200}>
|
||||
<this.bottomBarDesktop/>
|
||||
</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={1199}>
|
||||
<this.bottomBarPhone/>
|
||||
</MediaQuery>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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 => <>
|
||||
<Card elevation={5} align="left" >
|
||||
{/* 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 ?
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar
|
||||
@ -113,6 +111,7 @@ export default class Chat extends Component {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
<FormHelperText>This chat has no memory. If you leave and come back the messages are lost.</FormHelperText>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
@ -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 (
|
||||
<Router >
|
||||
<Switch>
|
||||
<Route exact path='/' component={UserGenPage}/>
|
||||
<Route path='/home'><p>You are at the start page</p></Route>
|
||||
<Route path='/make' component={MakerPage}/>
|
||||
<Route path='/book' component={BookPage}/>
|
||||
<Route path="/order/:orderId" component={OrderPage}/>
|
||||
</Switch>
|
||||
<div className='appCenter'>
|
||||
<Switch>
|
||||
<Route exact path='/' render={(props) => <UserGenPage setAppState={this.setAppState}/>}/>
|
||||
<Route path='/make' component={MakerPage}/>
|
||||
<Route path='/book' component={BookPage}/>
|
||||
<Route path="/order/:orderId" component={OrderPage}/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className='bottomBar'>
|
||||
<BottomBar redirectTo={this.redirectTo} {...this.state} setAppState={this.setAppState} />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<DialogContent>
|
||||
<Typography component="h5" variant="h5">What is <i>RoboSats</i>?</Typography>
|
||||
|
||||
<MediaQuery minWidth={475}>
|
||||
<Grid container xs={12}>
|
||||
<Grid item xs={8}>
|
||||
<Typography component="h4" variant="h4">What is <i>RoboSats</i>?</Typography>
|
||||
<Typography component="body2" variant="body2">
|
||||
<p>It is a BTC/FIAT peer-to-peer exchange over lightning. <br/> It simplifies
|
||||
matchmaking and minimizes the need of trust. RoboSats focuses in privacy and speed.</p>
|
||||
|
||||
<p>RoboSats is an open source project <a
|
||||
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a>
|
||||
</p>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4} align="center">
|
||||
<Image className='newAvatar'
|
||||
disableError='true'
|
||||
cover='true'
|
||||
color='null'
|
||||
src={window.location.origin +'/static/assets/images/robosats_0.1.0.png'}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={474}>
|
||||
<Typography component="h4" variant="h4">What is <i>RoboSats</i>?</Typography>
|
||||
<Typography component="body2" variant="body2">
|
||||
<p>It is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies
|
||||
matchmaking and minimizes the trust needed to trade with a peer.</p>
|
||||
|
||||
matchmaking and minimizes the need of trust. RoboSats focuses in privacy and speed.</p>
|
||||
<img
|
||||
width='100%'
|
||||
src={window.location.origin +'/static/assets/images/robosats_0.1.0_banner.png'}
|
||||
/>
|
||||
<p>RoboSats is an open source project <a
|
||||
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a>
|
||||
</p>
|
||||
</Typography>
|
||||
|
||||
</MediaQuery>
|
||||
|
||||
<Typography component="h5" variant="h5">How does it work?</Typography>
|
||||
<Typography component="body2" variant="body2">
|
||||
<p>AdequateAlice01 wants to sell bitcoin. She posts a sell order.
|
||||
<p> 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. <i>RoboSats</i> locks the invoice until Bob confirms he sent
|
||||
the fiat to Alice. Once Alice confirms she received the fiat, she
|
||||
tells <i>RoboSats</i> to release the satoshis to Bob. Enjoy your satoshis,
|
||||
hold invoice. <i>RoboSats</i> locks the invoice until Alice confirms she
|
||||
received the fiat, then the satoshis are released to Bob. Enjoy your satoshis,
|
||||
Bob!</p>
|
||||
|
||||
<p>At no point, AdequateAlice01 and BafflingBob02 have to trust the
|
||||
bitcoin to each other. In case they have a conflict, <i>RoboSats</i> staff
|
||||
will help resolving the dispute.</p>
|
||||
<p>At no point, AnonymousAlice01 and BafflingBob02 have to trust the
|
||||
bitcoin funds to each other. In case they have a conflict, <i>RoboSats</i> staff
|
||||
will help resolving the dispute. You can find a step-by-step
|
||||
description of the trade pipeline in <a href='https://github.com/Reckless-Satoshi/robosats/blob/main/README.md#how-it-works'>How it works</a></p>
|
||||
</Typography>
|
||||
|
||||
<Typography component="h5" variant="h5">What payment methods are accepted?</Typography>
|
||||
@ -51,8 +84,9 @@ export default class InfoDialog extends Component {
|
||||
|
||||
<Typography component="h5" variant="h5">Is <i>RoboSats</i> private?</Typography>
|
||||
<Typography component="body2" variant="body2">
|
||||
<p> RoboSats will never ask you for your name, country or ID. For
|
||||
best anonymity use Tor Browser and access the .onion hidden service. </p>
|
||||
<p> 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. </p>
|
||||
|
||||
<p>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 {
|
||||
<Typography component="h5" variant="h5">What is the trust model?</Typography>
|
||||
<Typography component="body2" variant="body2">
|
||||
<p> The buyer and the seller never have to trust each other.
|
||||
Some trust on <i>RoboSats</i> staff is needed since linking
|
||||
the seller's hold invoice and buyer payment is not atomic (yet).
|
||||
Some trust on <i>RoboSats</i> is needed since linking the
|
||||
seller's hold invoice and buyer payment is not atomic (yet).
|
||||
In addition, disputes are solved by the <i>RoboSats</i> staff.
|
||||
</p>
|
||||
|
||||
<p> While trust requirements are minimized, <i>RoboSats</i> could
|
||||
run away with your satoshis. It could be argued that it is not
|
||||
worth it, as it would instantly destroy <i>RoboSats</i> reputation.
|
||||
<p> To be totally clear. Trust requirements are minimized. However, there is still
|
||||
one way <i>RoboSats </i> could run away with your satoshis: by not releasing
|
||||
the satoshis to the buyer. It could be argued that such move is not in <i>RoboSats' </i>
|
||||
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 <i>Bisq</i>
|
||||
</p>
|
||||
|
||||
<p> You can build more trust on <i>RoboSats</i> by <a href='https://github.com/reckless-satoshi/robosats'>
|
||||
inspecting the source code </a> </p>
|
||||
inspecting the source code. </a> </p>
|
||||
</Typography>
|
||||
|
||||
<Typography component="h5" variant="h5">What happens if <i>RoboSats</i> suddenly disapears?</Typography>
|
||||
<Typography component="body2" variant="body2">
|
||||
<p> Your sats will most likely return to you. Any hold invoice that is not
|
||||
<p> Your sats will return to you. Any hold invoice that is not
|
||||
settled would be automatically returned even if <i>RoboSats</i> 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
|
||||
<i> RoboSats</i> 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 <i>RoboSats</i> public channels.
|
||||
</p>
|
||||
</Typography>
|
||||
|
||||
@ -116,9 +154,10 @@ export default class InfoDialog extends Component {
|
||||
RoboSats</i> will definitely never ask for your robot token.
|
||||
</p>
|
||||
</Typography>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={this.props.handleCloseInfo}>Close</Button>
|
||||
</DialogActions>
|
||||
</DialogContent>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Paper, Alert, AlertTitle, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@mui/material"
|
||||
import { Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import getFlags from './getFlags'
|
||||
|
||||
function getCookie(name) {
|
||||
@ -37,7 +36,7 @@ export default class MakerPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
isExplicit: false,
|
||||
is_explicit: false,
|
||||
type: 0,
|
||||
currency: this.defaultCurrency,
|
||||
currencyCode: this.defaultCurrencyCode,
|
||||
@ -89,14 +88,14 @@ export default class MakerPage extends Component {
|
||||
}
|
||||
handleClickRelative=(e)=>{
|
||||
this.setState({
|
||||
isExplicit: false,
|
||||
is_explicit: false,
|
||||
satoshis: null,
|
||||
premium: 0,
|
||||
});
|
||||
}
|
||||
handleClickExplicit=(e)=>{
|
||||
this.setState({
|
||||
isExplicit: true,
|
||||
is_explicit: true,
|
||||
premium: null,
|
||||
});
|
||||
}
|
||||
@ -104,7 +103,6 @@ export default class MakerPage extends Component {
|
||||
handleCreateOfferButtonPressed=()=>{
|
||||
this.state.amount == null ? this.setState({amount: 0}) : null;
|
||||
|
||||
console.log(this.state)
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
||||
@ -113,7 +111,7 @@ export default class MakerPage extends Component {
|
||||
currency: this.state.currency,
|
||||
amount: this.state.amount,
|
||||
payment_method: this.state.payment_method,
|
||||
is_explicit: this.state.isExplicit,
|
||||
is_explicit: this.state.is_explicit,
|
||||
premium: this.state.premium,
|
||||
satoshis: this.state.satoshis,
|
||||
}),
|
||||
@ -241,7 +239,7 @@ export default class MakerPage extends Component {
|
||||
</FormControl>
|
||||
</Grid>
|
||||
{/* conditional shows either Premium % field or Satoshis field based on pricing method */}
|
||||
{ this.state.isExplicit
|
||||
{ this.state.is_explicit
|
||||
? <Grid item xs={12} align="center">
|
||||
<TextField
|
||||
label="Satoshis"
|
||||
@ -287,7 +285,7 @@ export default class MakerPage extends Component {
|
||||
<Typography component="subtitle2" variant="subtitle2">
|
||||
<div align='center'>
|
||||
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode}
|
||||
{this.state.isExplicit ? " of " + this.state.satoshis + " Satoshis" :
|
||||
{this.state.is_explicit ? " of " + this.state.satoshis + " Satoshis" :
|
||||
(this.state.premium == 0 ? " at market price" :
|
||||
(this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount")
|
||||
)
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
||||
import { Badge, Tab, Tabs, Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
||||
import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown';
|
||||
import MediaQuery from 'react-responsive'
|
||||
|
||||
import TradeBox from "./TradeBox";
|
||||
import getFlags from './getFlags'
|
||||
|
||||
@ -9,9 +11,7 @@ import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
||||
import PaymentsIcon from '@mui/icons-material/Payments';
|
||||
import MoneyIcon from '@mui/icons-material/Money';
|
||||
import ArticleIcon from '@mui/icons-material/Article';
|
||||
import ContentCopy from "@mui/icons-material/ContentCopy";
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
@ -39,86 +39,69 @@ export default class OrderPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isExplicit: false,
|
||||
is_explicit: false,
|
||||
delay: 60000, // Refresh every 60 seconds by default
|
||||
currencies_dict: {"1":"USD"},
|
||||
total_secs_expiry: 300,
|
||||
total_secs_exp: 300,
|
||||
loading: true,
|
||||
openCancel: false,
|
||||
openCollaborativeCancel: false,
|
||||
showContractBox: 1,
|
||||
};
|
||||
this.orderId = this.props.match.params.orderId;
|
||||
this.getCurrencyDict();
|
||||
this.getOrderDetails();
|
||||
|
||||
// Refresh delais according to Order status
|
||||
// Refresh delays according to Order status
|
||||
this.statusToDelay = {
|
||||
"0": 3000, //'Waiting for maker bond'
|
||||
"1": 30000, //'Public'
|
||||
"2": 9999999, //'Deleted'
|
||||
"3": 3000, //'Waiting for taker bond'
|
||||
"4": 9999999, //'Cancelled'
|
||||
"5": 999999, //'Expired'
|
||||
"6": 3000, //'Waiting for trade collateral and buyer invoice'
|
||||
"7": 3000, //'Waiting only for seller trade collateral'
|
||||
"8": 10000, //'Waiting only for buyer invoice'
|
||||
"9": 10000, //'Sending fiat - In chatroom'
|
||||
"10": 15000, //'Fiat sent - In chatroom'
|
||||
"11": 60000, //'In dispute'
|
||||
"12": 9999999,//'Collaboratively cancelled'
|
||||
"13": 3000, //'Sending satoshis to buyer'
|
||||
"14": 9999999,//'Sucessful trade'
|
||||
"15": 10000, //'Failed lightning network routing'
|
||||
"16": 9999999,//'Maker lost dispute'
|
||||
"17": 9999999,//'Taker lost dispute'
|
||||
"0": 2000, //'Waiting for maker bond'
|
||||
"1": 25000, //'Public'
|
||||
"2": 9999999, //'Deleted'
|
||||
"3": 2000, //'Waiting for taker bond'
|
||||
"4": 9999999, //'Cancelled'
|
||||
"5": 999999, //'Expired'
|
||||
"6": 3000, //'Waiting for trade collateral and buyer invoice'
|
||||
"7": 3000, //'Waiting only for seller trade collateral'
|
||||
"8": 8000, //'Waiting only for buyer invoice'
|
||||
"9": 10000, //'Sending fiat - In chatroom'
|
||||
"10": 10000, //'Fiat sent - In chatroom'
|
||||
"11": 30000, //'In dispute'
|
||||
"12": 9999999, //'Collaboratively cancelled'
|
||||
"13": 3000, //'Sending satoshis to buyer'
|
||||
"14": 9999999, //'Sucessful trade'
|
||||
"15": 10000, //'Failed lightning network routing'
|
||||
"16": 9999999, //'Maker lost dispute'
|
||||
"17": 9999999, //'Taker lost dispute'
|
||||
}
|
||||
}
|
||||
|
||||
completeSetState=(newStateVars)=>{
|
||||
|
||||
// In case the reply only has "bad_request"
|
||||
// Do not substitute these two for "undefined" as
|
||||
// otherStateVars will fail to assign values
|
||||
if (newStateVars.currency == null){
|
||||
newStateVars.currency = this.state.currency
|
||||
newStateVars.status = this.state.status
|
||||
}
|
||||
|
||||
var otherStateVars = {
|
||||
loading: false,
|
||||
delay: this.setDelay(newStateVars.status),
|
||||
currencyCode: this.getCurrencyCode(newStateVars.currency),
|
||||
penalty: newStateVars.penalty, // in case penalty time has finished, it goes back to null
|
||||
invoice_expired: newStateVars.invoice_expired // in case invoice had expired, it goes back to null when it is valid again
|
||||
};
|
||||
|
||||
var completeStateVars = Object.assign({}, newStateVars, otherStateVars);
|
||||
this.setState(completeStateVars);
|
||||
}
|
||||
|
||||
getOrderDetails() {
|
||||
this.setState(null)
|
||||
fetch('/api/order' + '?order_id=' + this.orderId)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {console.log(data) &
|
||||
this.setState({
|
||||
loading: false,
|
||||
delay: this.setDelay(data.status),
|
||||
id: data.id,
|
||||
statusCode: data.status,
|
||||
statusText: data.status_message,
|
||||
type: data.type,
|
||||
currency: data.currency,
|
||||
currencyCode: this.getCurrencyCode(data.currency),
|
||||
amount: data.amount,
|
||||
paymentMethod: data.payment_method,
|
||||
isExplicit: data.is_explicit,
|
||||
premium: data.premium,
|
||||
satoshis: data.satoshis,
|
||||
makerId: data.maker,
|
||||
isParticipant: data.is_participant,
|
||||
urNick: data.ur_nick,
|
||||
makerNick: data.maker_nick,
|
||||
takerId: data.taker,
|
||||
takerNick: data.taker_nick,
|
||||
isMaker: data.is_maker,
|
||||
isTaker: data.is_taker,
|
||||
isBuyer: data.is_buyer,
|
||||
isSeller: data.is_seller,
|
||||
penalty: data.penalty,
|
||||
expiresAt: data.expires_at,
|
||||
badRequest: data.bad_request,
|
||||
bondInvoice: data.bond_invoice,
|
||||
bondSatoshis: data.bond_satoshis,
|
||||
escrowInvoice: data.escrow_invoice,
|
||||
escrowSatoshis: data.escrow_satoshis,
|
||||
invoiceAmount: data.invoice_amount,
|
||||
total_secs_expiry: data.total_secs_exp,
|
||||
numSimilarOrders: data.num_similar_orders,
|
||||
priceNow: data.price_now,
|
||||
premiumNow: data.premium_now,
|
||||
robotsInBook: data.robots_in_book,
|
||||
premiumPercentile: data.premium_percentile,
|
||||
numSimilarOrders: data.num_similar_orders
|
||||
})
|
||||
});
|
||||
.then((data) => this.completeSetState(data));
|
||||
}
|
||||
|
||||
// These are used to refresh the data
|
||||
@ -137,11 +120,6 @@ export default class OrderPage extends Component {
|
||||
this.getOrderDetails();
|
||||
}
|
||||
|
||||
// Fix to use proper react props
|
||||
handleClickBackButton=()=>{
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
// Countdown Renderer callback with condition
|
||||
countdownRenderer = ({ total, hours, minutes, seconds, completed }) => {
|
||||
if (completed) {
|
||||
@ -150,7 +128,7 @@ export default class OrderPage extends Component {
|
||||
|
||||
} else {
|
||||
var col = 'black'
|
||||
var fraction_left = (total/1000) / this.state.total_secs_expiry
|
||||
var fraction_left = (total/1000) / this.state.total_secs_exp
|
||||
// Make orange at 25% of time left
|
||||
if (fraction_left < 0.25){col = 'orange'}
|
||||
// Make red at 10% of time left
|
||||
@ -163,14 +141,27 @@ export default class OrderPage extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
// Countdown Renderer callback with condition
|
||||
countdownPenaltyRenderer = ({ minutes, seconds, completed }) => {
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return (<span> Penalty lifted, good to go!</span>);
|
||||
|
||||
} else {
|
||||
return (
|
||||
<span> Wait {zeroPad(minutes)}m {zeroPad(seconds)}s </span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LinearDeterminate =()=> {
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setProgress((oldProgress) => {
|
||||
var left = calcTimeDelta( new Date(this.state.expiresAt)).total /1000;
|
||||
return (left / this.state.total_secs_expiry) * 100;
|
||||
var left = calcTimeDelta( new Date(this.state.expires_at)).total /1000;
|
||||
return (left / this.state.total_secs_exp) * 100;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
@ -187,7 +178,6 @@ export default class OrderPage extends Component {
|
||||
}
|
||||
|
||||
handleClickTakeOrderButton=()=>{
|
||||
console.log(this.state)
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
@ -197,9 +187,7 @@ export default class OrderPage extends Component {
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => (this.setState({badRequest:data.bad_request})
|
||||
& console.log(data)
|
||||
& this.getOrderDetails(data.id)));
|
||||
.then((data) => this.completeSetState(data));
|
||||
}
|
||||
getCurrencyDict() {
|
||||
fetch('/static/assets/currencies.json')
|
||||
@ -221,7 +209,6 @@ export default class OrderPage extends Component {
|
||||
}
|
||||
|
||||
handleClickConfirmCancelButton=()=>{
|
||||
console.log(this.state)
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
@ -231,7 +218,7 @@ export default class OrderPage extends Component {
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
|
||||
.then((data) => this.getOrderDetails(data.id));
|
||||
this.handleClickCloseConfirmCancelDialog();
|
||||
}
|
||||
|
||||
@ -266,11 +253,57 @@ export default class OrderPage extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
handleClickConfirmCollaborativeCancelButton=()=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action':'cancel',
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => this.getOrderDetails(data.id));
|
||||
this.handleClickCloseCollaborativeCancelDialog();
|
||||
}
|
||||
|
||||
handleClickOpenCollaborativeCancelDialog = () => {
|
||||
this.setState({openCollaborativeCancel: true});
|
||||
};
|
||||
handleClickCloseCollaborativeCancelDialog = () => {
|
||||
this.setState({openCollaborativeCancel: false});
|
||||
};
|
||||
|
||||
CollaborativeCancelDialog =() =>{
|
||||
return(
|
||||
<Dialog
|
||||
open={this.state.openCollaborativeCancel}
|
||||
onClose={this.handleClickCloseCollaborativeCancelDialog}
|
||||
aria-labelledby="collaborative-cancel-dialog-title"
|
||||
aria-describedby="collaborative-cancel-dialog-description"
|
||||
>
|
||||
<DialogTitle id="cancel-dialog-title">
|
||||
{"Collaborative cancel the order?"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="cancel-dialog-description">
|
||||
The trade escrow has been posted. The order can be cancelled only if both, maker and
|
||||
taker, agree to cancel.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleClickCloseCollaborativeCancelDialog} autoFocus>Go back</Button>
|
||||
<Button onClick={this.handleClickConfirmCollaborativeCancelButton}> Ask for Cancel </Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
CancelButton = () => {
|
||||
|
||||
// If maker and Waiting for Bond. Or if taker and Waiting for bond.
|
||||
// Simply allow to cancel without showing the cancel dialog.
|
||||
if ((this.state.isMaker & this.state.statusCode == 0) || this.state.isTaker & this.state.statusCode == 3){
|
||||
if ((this.state.is_maker & [0,1].includes(this.state.status)) || this.state.is_taker & this.state.status == 3){
|
||||
return(
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickConfirmCancelButton}>Cancel</Button>
|
||||
@ -278,51 +311,79 @@ export default class OrderPage extends Component {
|
||||
)}
|
||||
// If the order does not yet have an escrow deposited. Show dialog
|
||||
// to confirm forfeiting the bond
|
||||
if (this.state.statusCode < 8){
|
||||
if ([3,6,7].includes(this.state.status)){
|
||||
return(
|
||||
<Grid item xs={12} align="center">
|
||||
<this.CancelDialog/>
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickOpenConfirmCancelDialog}>Cancel</Button>
|
||||
</Grid>
|
||||
<div id="openDialogCancelButton">
|
||||
<Grid item xs={12} align="center">
|
||||
<this.CancelDialog/>
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickOpenConfirmCancelDialog}>Cancel</Button>
|
||||
</Grid>
|
||||
</div>
|
||||
)}
|
||||
|
||||
// TODO If the escrow is Locked, show the collaborative cancel button.
|
||||
|
||||
// If the escrow is Locked, show the collaborative cancel button.
|
||||
|
||||
if ([8,9].includes(this.state.status)){
|
||||
return(
|
||||
<Grid item xs={12} align="center">
|
||||
<this.CollaborativeCancelDialog/>
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickOpenCollaborativeCancelDialog}>Collaborative Cancel</Button>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
// If none of the above do not return a cancel button.
|
||||
return(null)
|
||||
}
|
||||
|
||||
// Colors for the status badges
|
||||
statusBadgeColor(status){
|
||||
if(status=='active'){
|
||||
return("success")
|
||||
}
|
||||
if(status=='seen_recently'){
|
||||
return("warning")
|
||||
}
|
||||
if(status=='inactive'){
|
||||
return('error')
|
||||
}
|
||||
}
|
||||
orderBox=()=>{
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid container spacing={1} >
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
Order Details
|
||||
</Typography>
|
||||
<MediaQuery minWidth={920}>
|
||||
<Typography component="h5" variant="h5">
|
||||
Order Details
|
||||
</Typography>
|
||||
</MediaQuery>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
<List dense="true">
|
||||
<ListItem >
|
||||
<ListItemAvatar sx={{ width: 56, height: 56 }}>
|
||||
<Avatar
|
||||
alt={this.state.makerNick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + this.state.makerNick + '.png'}
|
||||
/>
|
||||
<Badge variant="dot" badgeContent="" color={this.statusBadgeColor(this.state.maker_status)}>
|
||||
<Avatar className="flippedSmallAvatar"
|
||||
alt={this.state.maker_nick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + this.state.maker_nick + '.png'}
|
||||
/>
|
||||
</Badge>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={this.state.makerNick + (this.state.type ? " (Seller)" : " (Buyer)")} secondary="Order maker" align="right"/>
|
||||
<ListItemText primary={this.state.maker_nick + (this.state.type ? " (Seller)" : " (Buyer)")} secondary="Order maker" align="right"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
{this.state.isParticipant ?
|
||||
{this.state.is_participant ?
|
||||
<>
|
||||
{this.state.takerNick!='None' ?
|
||||
{this.state.taker_nick!='None' ?
|
||||
<>
|
||||
<ListItem align="left">
|
||||
<ListItemText primary={this.state.takerNick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/>
|
||||
<ListItemText primary={this.state.taker_nick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/>
|
||||
<ListItemAvatar >
|
||||
<Avatar
|
||||
alt={this.state.makerNick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + this.state.takerNick + '.png'}
|
||||
/>
|
||||
<Badge variant="dot" badgeContent="" color={this.statusBadgeColor(this.state.taker_status)}>
|
||||
<Avatar className="smallAvatar"
|
||||
alt={this.state.taker_nick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + this.state.taker_nick + '.png'}
|
||||
/>
|
||||
</Badge>
|
||||
</ListItemAvatar>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
@ -333,7 +394,7 @@ export default class OrderPage extends Component {
|
||||
<ListItemIcon>
|
||||
<ArticleIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={this.state.statusText} secondary="Order status"/>
|
||||
<ListItemText primary={this.state.status_message} secondary="Order status"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</>
|
||||
@ -352,7 +413,7 @@ export default class OrderPage extends Component {
|
||||
<ListItemIcon>
|
||||
<PaymentsIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={this.state.paymentMethod} secondary="Accepted payment methods"/>
|
||||
<ListItemText primary={this.state.payment_method} secondary="Accepted payment methods"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
@ -361,10 +422,10 @@ export default class OrderPage extends Component {
|
||||
<ListItemIcon>
|
||||
<PriceChangeIcon/>
|
||||
</ListItemIcon>
|
||||
{this.state.priceNow?
|
||||
<ListItemText primary={pn(this.state.priceNow)+" "+this.state.currencyCode+"/BTC - Premium: "+this.state.premiumNow+"%"} secondary="Price and Premium"/>
|
||||
{this.state.price_now?
|
||||
<ListItemText primary={pn(this.state.price_now)+" "+this.state.currencyCode+"/BTC - Premium: "+this.state.premium_now+"%"} secondary="Price and Premium"/>
|
||||
:
|
||||
(this.state.isExplicit ?
|
||||
(this.state.is_explicit ?
|
||||
<ListItemText primary={pn(this.state.satoshis)} secondary="Amount of Satoshis"/>
|
||||
:
|
||||
<ListItemText primary={parseFloat(parseFloat(this.state.premium).toFixed(2))+"%"} secondary="Premium over market price"/>
|
||||
@ -385,7 +446,7 @@ export default class OrderPage extends Component {
|
||||
<AccessTimeIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary="Expires in">
|
||||
<Countdown date={new Date(this.state.expiresAt)} renderer={this.countdownRenderer} />
|
||||
<Countdown date={new Date(this.state.expires_at)} renderer={this.countdownRenderer} />
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
<this.LinearDeterminate />
|
||||
@ -397,7 +458,31 @@ export default class OrderPage extends Component {
|
||||
<Divider />
|
||||
<Grid item xs={12} align="center">
|
||||
<Alert severity="warning" sx={{maxWidth:360}}>
|
||||
You cannot take an order yet! Wait {this.state.penalty} seconds
|
||||
You cannot take an order yet! <Countdown date={new Date(this.state.penalty)} renderer={this.countdownPenaltyRenderer} />
|
||||
</Alert>
|
||||
</Grid>
|
||||
</>
|
||||
: null}
|
||||
|
||||
{/* If the counterparty asked for collaborative cancel */}
|
||||
{this.state.pending_cancel ?
|
||||
<>
|
||||
<Divider />
|
||||
<Grid item xs={12} align="center">
|
||||
<Alert severity="warning" sx={{maxWidth:360}}>
|
||||
{this.state.is_maker ? this.state.taker_nick : this.state.maker_nick} is asking for a collaborative cancel
|
||||
</Alert>
|
||||
</Grid>
|
||||
</>
|
||||
: null}
|
||||
|
||||
{/* If the user has asked for a collaborative cancel */}
|
||||
{this.state.asked_for_cancel ?
|
||||
<>
|
||||
<Divider />
|
||||
<Grid item xs={12} align="center">
|
||||
<Alert severity="warning" sx={{maxWidth:360}}>
|
||||
You asked for a collaborative cancellation
|
||||
</Alert>
|
||||
</Grid>
|
||||
</>
|
||||
@ -405,46 +490,101 @@ export default class OrderPage extends Component {
|
||||
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Participants can see the "Cancel" Button, but cannot see the "Back" or "Take Order" buttons */}
|
||||
{this.state.isParticipant ?
|
||||
<this.CancelButton/>
|
||||
:
|
||||
<>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
{/* Participants can see the "Cancel" Button, but cannot see the "Back" or "Take Order" buttons */}
|
||||
{this.state.is_participant ?
|
||||
<this.CancelButton/>
|
||||
:
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.props.history.goBack}>Back</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
doubleOrderPageDesktop=()=>{
|
||||
return(
|
||||
<Grid container xs={12} align="center" spacing={2} >
|
||||
<Grid item xs={6} align="left" style={{ width:330}} >
|
||||
{this.orderBox()}
|
||||
</Grid>
|
||||
<Grid item xs={6} align="left">
|
||||
<TradeBox width={330} data={this.state} completeSetState={this.completeSetState} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
a11yProps(index) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
'aria-controls': `simple-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
doubleOrderPagePhone=()=>{
|
||||
|
||||
const [value, setValue] = React.useState(this.state.showContractBox);
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
this.setState({showContractBox:newValue})
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return(
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={value} onChange={handleChange} variant="fullWidth" >
|
||||
<Tab label="Order Details" {...this.a11yProps(0)} />
|
||||
<Tab label="Contract Box" {...this.a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item >
|
||||
<div style={{ width:330, display: this.state.showContractBox == 0 ? '':'none'}}>
|
||||
{this.orderBox()}
|
||||
</div>
|
||||
<div style={{display: this.state.showContractBox == 1 ? '':'none'}}>
|
||||
<TradeBox width={330} data={this.state} completeSetState={this.completeSetState} />
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
orderDetailsPage (){
|
||||
return(
|
||||
this.state.badRequest ?
|
||||
this.state.bad_request ?
|
||||
<div align='center'>
|
||||
<Typography component="subtitle2" variant="subtitle2" color="secondary" >
|
||||
{this.state.badRequest}<br/>
|
||||
{this.state.bad_request}<br/>
|
||||
</Typography>
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
|
||||
<Button variant='contained' color='secondary' onClick={this.props.history.goBack}>Back</Button>
|
||||
</div>
|
||||
:
|
||||
(this.state.isParticipant ?
|
||||
<Grid container xs={12} align="center" spacing={2}>
|
||||
<Grid item xs={6} align="left">
|
||||
{this.orderBox()}
|
||||
</Grid>
|
||||
<Grid item xs={6} align="left">
|
||||
<TradeBox data={this.state}/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
(this.state.is_participant ?
|
||||
<>
|
||||
{/* Desktop View */}
|
||||
<MediaQuery minWidth={920}>
|
||||
<this.doubleOrderPageDesktop/>
|
||||
</MediaQuery>
|
||||
|
||||
{/* SmarPhone View */}
|
||||
<MediaQuery maxWidth={919}>
|
||||
<this.doubleOrderPagePhone/>
|
||||
</MediaQuery>
|
||||
</>
|
||||
:
|
||||
<Grid item xs={12} align="center">
|
||||
<Grid item xs={12} align="center" style={{ width:330}}>
|
||||
{this.orderBox()}
|
||||
</Grid>)
|
||||
)
|
||||
|
@ -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"
|
||||
<audio autoPlay src={`/static/assets/sounds/${soundFileName}.mp3`} />
|
||||
)
|
||||
|
||||
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 {
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
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.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleClickCloseConfirmDispute} autoFocus>Disagree</Button>
|
||||
<Button onClick={this.handleClickAgreeDisputeButton}> Agree </Button>
|
||||
<Button onClick={this.handleClickAgreeDisputeButton}> Agree and open dispute </Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
@ -136,32 +148,32 @@ export default class TradeBox extends Component {
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2">
|
||||
Robosats show commitment to their peers
|
||||
Robots show commitment to their peers
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
{this.props.data.isMaker ?
|
||||
{this.props.data.is_maker ?
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||
<b>Lock {pn(this.props.data.bondSatoshis)} Sats to PUBLISH order </b>
|
||||
<b>Lock {pn(this.props.data.bond_satoshis)} Sats to PUBLISH order </b>
|
||||
</Typography>
|
||||
:
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||
<b>Lock {pn(this.props.data.bondSatoshis)} Sats to TAKE the order </b>
|
||||
<b>Lock {pn(this.props.data.bond_satoshis)} Sats to TAKE the order </b>
|
||||
</Typography>
|
||||
}
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<QRCode value={this.props.data.bondInvoice} size={305}/>
|
||||
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bondInvoice)}} align="center"> 📋Copy to clipboard</Button>
|
||||
<QRCode value={this.props.data.bond_invoice} size={305}/>
|
||||
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bond_invoice)}} align="center"> 📋Copy to clipboard</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
hiddenLabel
|
||||
variant="standard"
|
||||
size="small"
|
||||
defaultValue={this.props.data.bondInvoice}
|
||||
defaultValue={this.props.data.bond_invoice}
|
||||
disabled="true"
|
||||
helperText="This is a hold invoice. It will be charged only if you cancel or lose a dispute."
|
||||
helperText="This is a hold invoice, it will freeze in your wallet. It will be charged only if you cancel or lose a dispute."
|
||||
color = "secondary"
|
||||
/>
|
||||
</Grid>
|
||||
@ -173,7 +185,7 @@ export default class TradeBox extends Component {
|
||||
return (
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1" align="center">
|
||||
🔒 Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is locked
|
||||
🔒 Your {this.props.data.is_maker ? 'maker' : 'taker'} bond is locked
|
||||
</Typography>
|
||||
</Grid>
|
||||
);
|
||||
@ -182,23 +194,25 @@ export default class TradeBox extends Component {
|
||||
showEscrowQRInvoice=()=>{
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
{/* Make confirmation sound for HTLC received. */}
|
||||
<this.Sound soundFileName="locked-invoice"/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography color="green" component="subtitle1" variant="subtitle1">
|
||||
<b>Deposit {pn(this.props.data.escrowSatoshis)} Sats as trade collateral </b>
|
||||
<b>Deposit {pn(this.props.data.escrow_satoshis)} Sats as trade collateral </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<QRCode value={this.props.data.escrowInvoice} size={305}/>
|
||||
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrowInvoice)}} align="center"> 📋Copy to clipboard</Button>
|
||||
<QRCode value={this.props.data.escrow_invoice} size={305}/>
|
||||
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrow_invoice)}} align="center"> 📋Copy to clipboard</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
hiddenLabel
|
||||
variant="filled"
|
||||
size="small"
|
||||
defaultValue={this.props.data.escrowInvoice}
|
||||
defaultValue={this.props.data.escrow_invoice}
|
||||
disabled="true"
|
||||
helperText="This is a hold invoice. It will be charged once the buyer confirms he sent the fiat."
|
||||
helperText={"This is a hold invoice, it will freeze in your wallet. It will be released to the buyer once you confirm to have received the "+this.props.data.currencyCode+"."}
|
||||
color = "secondary"
|
||||
/>
|
||||
</Grid>
|
||||
@ -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 (
|
||||
<Grid container spacing={1}>
|
||||
{/* Make bell sound when taker is found */}
|
||||
<this.Sound soundFileName="taker-found"/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>A taker has been found! </b>
|
||||
@ -221,7 +234,9 @@ export default class TradeBox extends Component {
|
||||
<Divider/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2">
|
||||
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.
|
||||
</Typography>
|
||||
</Grid>
|
||||
{this.showBondIsLocked()}
|
||||
@ -232,6 +247,8 @@ export default class TradeBox extends Component {
|
||||
showMakerWait=()=>{
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
{/* Make confirmation sound for HTLC received. */}
|
||||
<this.Sound soundFileName="locked-invoice"/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b> Your order is public. Wait for a taker. </b>
|
||||
@ -251,20 +268,12 @@ export default class TradeBox extends Component {
|
||||
</Typography>
|
||||
</ListItem>
|
||||
{/* TODO API sends data for a more confortable wait */}
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SmartToyIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={'000 coming soon'} secondary="Robots looking at the book"/>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<BookIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={this.props.data.numSimilarOrders} secondary={"Public orders for " + this.props.data.currencyCode}/>
|
||||
<ListItemText primary={this.props.data.num_similar_orders} secondary={"Public orders for " + this.props.data.currencyCode}/>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
@ -272,7 +281,7 @@ export default class TradeBox extends Component {
|
||||
<ListItemIcon>
|
||||
<PercentIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={"Premium rank " + this.props.data.premiumPercentile*100+"%"}
|
||||
<ListItemText primary={"Premium rank " + this.props.data.premium_percentile*100+"%"}
|
||||
secondary={"Among public " + this.props.data.currencyCode + " orders (higher is cheaper)"} />
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
@ -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
|
||||
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||
<b> Submit a LN invoice for {pn(this.props.data.invoiceAmount)} Sats </b>
|
||||
<b> Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="left">
|
||||
<Typography component="body2" variant="body2">
|
||||
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.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</Grid>
|
||||
{this.state.qrscanner ?
|
||||
<Grid item xs={12} align="center">
|
||||
<QrReader
|
||||
delay={300}
|
||||
onError={this.handleError}
|
||||
onScan={this.handleScan}
|
||||
style={{ width: '75%' }}
|
||||
/>
|
||||
</Grid>
|
||||
: null }
|
||||
<Grid item xs={12} align="center">
|
||||
<IconButton><QrCodeScannerIcon onClick={this.handleQRbutton}/></IconButton>
|
||||
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button>
|
||||
</Grid>
|
||||
|
||||
@ -374,44 +411,80 @@ export default class TradeBox extends Component {
|
||||
}
|
||||
|
||||
// Asks the user for a dispute statement.
|
||||
showInDisputeStatement(){
|
||||
showInDisputeStatement=()=>{
|
||||
if(this.props.data.statement_submitted){
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||
<b> We have received your statement </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="left">
|
||||
<Typography component="body2" variant="body2">
|
||||
We are waiting for your trade counterparty statement.
|
||||
</Typography>
|
||||
</Grid>
|
||||
{this.showBondIsLocked()}
|
||||
</Grid>
|
||||
)
|
||||
}else{
|
||||
return (
|
||||
|
||||
// TODO Option to upload files
|
||||
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||
<b> A dispute has been opened </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="left">
|
||||
<Typography component="body2" variant="body2">
|
||||
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 <i>(aka humans)</i>, so be as helpful
|
||||
as possible to ensure a fair outcome. Max 5000 chars.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
error={this.state.badStatement}
|
||||
helperText={this.state.badStatement ? this.state.badStatement : "" }
|
||||
label={"Submit dispute statement"}
|
||||
required
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
multiline
|
||||
rows={4}
|
||||
onChange={this.handleInputDisputeChanged}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button onClick={this.handleClickSubmitStatementButton} variant='contained' color='primary'>Submit</Button>
|
||||
</Grid>
|
||||
|
||||
{this.showBondIsLocked()}
|
||||
</Grid>
|
||||
)}
|
||||
}
|
||||
|
||||
showWaitForDisputeResolution=()=>{
|
||||
return (
|
||||
|
||||
// TODO Option to upload files
|
||||
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||
<b> A dispute has been opened </b>
|
||||
<b> We have the statements </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="left">
|
||||
<Typography component="body2" variant="body2">
|
||||
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 <i>(aka humans)</i>, 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.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
error={this.state.badStatement}
|
||||
helperText={this.state.badStatement ? this.state.badStatement : "" }
|
||||
label={"Submit dispute statement"}
|
||||
required
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
multiline
|
||||
rows={4}
|
||||
onChange={this.handleInputDisputeChanged}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button onClick={this.handleClickSubmitStatementButton} variant='contained' color='primary'>Submit</Button>
|
||||
</Grid>
|
||||
|
||||
{this.showBondIsLocked()}
|
||||
</Grid>
|
||||
)
|
||||
@ -427,10 +500,9 @@ export default class TradeBox extends Component {
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="left">
|
||||
<p>We are waiting for the seller to deposit the full trade BTC amount
|
||||
into the escrow.</p>
|
||||
<p> Just hang on for a moment. If the seller does not deposit,
|
||||
you will get your bond back automatically.</p>
|
||||
<p>We are waiting for the seller lock the trade amount. </p>
|
||||
<p> Just hang on for a moment. If the seller does not deposit,
|
||||
you will get your bond back automatically.</p>
|
||||
</Typography>
|
||||
</Grid>
|
||||
{this.showBondIsLocked()}
|
||||
@ -441,6 +513,8 @@ export default class TradeBox extends Component {
|
||||
showWaitingForBuyerInvoice(){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
{/* Make confirmation sound for HTLC received. */}
|
||||
<this.Sound soundFileName="locked-invoice"/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>The trade collateral is locked! 🎉 </b>
|
||||
@ -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(
|
||||
<Grid container spacing={1}>
|
||||
{/* Make confirmation sound for Chat Open. */}
|
||||
<this.Sound soundFileName="chat-open"/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>Chatting with {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}</b>
|
||||
<b>Chatting with {this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
{this.props.data.isSeller ?
|
||||
{this.props.data.is_seller ?
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
Say hi! Be helpful and concise. Let them know how to send you {this.props.data.currencyCode}.
|
||||
</Typography>
|
||||
@ -547,12 +647,11 @@ handleRatingChange=(e)=>{
|
||||
<Divider/>
|
||||
</Grid>
|
||||
|
||||
<Chat orderId={this.props.data.id} urNick={this.props.data.urNick}/>
|
||||
|
||||
<Chat orderId={this.props.data.id} ur_nick={this.props.data.ur_nick}/>
|
||||
<Grid item xs={12} align="center">
|
||||
{openDisputeButton ? this.showOpenDisputeButton() : ""}
|
||||
{sendFiatButton ? this.showFiatSentButton() : ""}
|
||||
{receivedFiatButton ? this.showFiatReceivedButton() : ""}
|
||||
{showDisputeButton ? this.showOpenDisputeButton() : ""}
|
||||
{showSendButton ? this.showFiatSentButton() : ""}
|
||||
{showReveiceButton ? this.showFiatReceivedButton() : ""}
|
||||
</Grid>
|
||||
{this.showBondIsLocked()}
|
||||
</Grid>
|
||||
@ -562,6 +661,8 @@ handleRatingChange=(e)=>{
|
||||
showRateSelect(){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
{/* Make confirmation sound for Chat Open. */}
|
||||
<this.Sound soundFileName="successful"/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h6" variant="h6">
|
||||
🎉Trade finished!🥳
|
||||
@ -569,7 +670,7 @@ handleRatingChange=(e)=>{
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
What do you think of <b>{this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}</b>?
|
||||
What do you think of <b>{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>?
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
@ -582,49 +683,132 @@ handleRatingChange=(e)=>{
|
||||
)
|
||||
}
|
||||
|
||||
showSendingPayment(){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h6" variant="h6">
|
||||
Attempting Lightning Payment
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must
|
||||
be online in order to receive payments.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
showRoutingFailed=()=>{
|
||||
// TODO If it has failed 3 times, ask for a new invoice.
|
||||
if(this.props.data.invoice_expired){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h6" variant="h6">
|
||||
Lightning Routing Failed
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
Your invoice has expires or more than 3 payments have been attempted.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||
<b> Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
error={this.state.badInvoice}
|
||||
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
|
||||
label={"Payout Lightning Invoice"}
|
||||
required
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
multiline
|
||||
onChange={this.handleInputInvoiceChanged}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}else{
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h6" variant="h6">
|
||||
Lightning Routing Failed
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
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.
|
||||
</Typography>
|
||||
<List>
|
||||
<Divider/>
|
||||
<ListItemText secondary="Next attempt in">
|
||||
<Countdown date={new Date(this.props.data.next_retry_time)} renderer={this.countdownRenderer} />
|
||||
</ListItemText>
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid container spacing={1} style={{ width:330}}>
|
||||
<Grid container spacing={1} style={{ width:this.props.width}}>
|
||||
<this.ConfirmDisputeDialog/>
|
||||
<this.ConfirmFiatReceivedDialog/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
Contract Box
|
||||
</Typography>
|
||||
<MediaQuery minWidth={920}>
|
||||
<Typography component="h5" variant="h5">
|
||||
Contract Box
|
||||
</Typography>
|
||||
</MediaQuery>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
{/* 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() : ""}
|
||||
{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 - 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) : ""}
|
||||
{/* 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.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""}
|
||||
{this.props.data.is_buyer & this.props.data.status == 13 ? this.showSendingPayment() : ""}
|
||||
|
||||
{/* Trade Finished - Payment Routing Failed */}
|
||||
{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 */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
|
@ -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"
|
||||
>
|
||||
<InfoDialog/>
|
||||
<InfoDialog handleCloseInfo = {this.handleCloseInfo}/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
{this.state.showRobosat ?
|
||||
<Grid item xs={12} align="center" sx={{width:370, height:260}}>
|
||||
{!this.state.loadingRobot ?
|
||||
<div>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
@ -135,7 +147,7 @@ export default class UserGenPage extends Component {
|
||||
</div><br/>
|
||||
</Grid>
|
||||
</div>
|
||||
: <CircularProgress />}
|
||||
: <CircularProgress sx={{position: 'relative', top: 100, }}/>}
|
||||
</Grid>
|
||||
{
|
||||
this.state.found ?
|
||||
@ -149,14 +161,11 @@ export default class UserGenPage extends Component {
|
||||
}
|
||||
<Grid container align="center">
|
||||
<Grid item xs={12} align="center">
|
||||
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.token)}>
|
||||
<ContentCopy/>
|
||||
</IconButton>
|
||||
<TextField
|
||||
<TextField sx={{maxWidth: 280}}
|
||||
//sx={{ input: { color: 'purple' } }}
|
||||
InputLabelProps={{
|
||||
style: { color: 'green' },
|
||||
}}
|
||||
// InputLabelProps={{
|
||||
// style: { color: 'green' },
|
||||
// }}
|
||||
error={this.state.bad_request}
|
||||
label='Store your token safely'
|
||||
required='true'
|
||||
@ -166,18 +175,34 @@ export default class UserGenPage extends Component {
|
||||
size='small'
|
||||
// multiline = {true}
|
||||
onChange={this.handleChangeToken}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleClickSubmitToken();
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment:
|
||||
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.token)}>
|
||||
<ContentCopy color={this.state.tokenHasChanged ? 'inherit' : 'primary' } sx={{width:18, height:18}} />
|
||||
</IconButton>,
|
||||
endAdornment:
|
||||
<IconButton onClick={this.handleClickNewRandomToken}><CasinoIcon/></IconButton>,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button size='small' onClick={this.handleAnotherButtonPressed}>Generate Another Robosat</Button>
|
||||
<Button disabled={!this.state.tokenHasChanged} type="submit" size='small' onClick= {this.handleClickSubmitToken}>
|
||||
<SmartToyIcon sx={{width:18, height:18}} />
|
||||
<span> Generate Robot</span>
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<ButtonGroup variant="contained" aria-label="outlined primary button group">
|
||||
<Button color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||
<Button disabled={this.state.loadingRobot} color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||
<Button color='inherit' onClick={this.handleClickOpenInfo}>Info</Button>
|
||||
<this.InfoDialog/>
|
||||
<Button color='secondary' to='/book/' component={Link}>View Book</Button>
|
||||
<Button disabled={this.state.loadingRobot} color='secondary' to='/book/' component={Link}>View Book</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
|
BIN
frontend/static/assets/images/robosats_0.1.0.png
Executable file
After Width: | Height: | Size: 138 KiB |
BIN
frontend/static/assets/images/robosats_0.1.0_background.PNG
Executable file
After Width: | Height: | Size: 294 KiB |
BIN
frontend/static/assets/images/robosats_0.1.0_banner.png
Executable file
After Width: | Height: | Size: 132 KiB |
BIN
frontend/static/assets/images/robosats_0.1.0_jarring.png
Executable file
After Width: | Height: | Size: 345 KiB |
BIN
frontend/static/assets/images/robosats_0.1.0_white.png
Executable file
After Width: | Height: | Size: 126 KiB |
BIN
frontend/static/assets/images/robosats_0.1.0_white_squared.PNG
Executable file
After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 9.9 KiB |
BIN
frontend/static/assets/sounds/chat-open.mp3
Normal file
BIN
frontend/static/assets/sounds/locked-invoice.mp3
Normal file
BIN
frontend/static/assets/sounds/sucessful.mp3
Normal file
BIN
frontend/static/assets/sounds/taker-found.mp3
Normal file
@ -37,6 +37,11 @@ body {
|
||||
top: -14px;
|
||||
}
|
||||
|
||||
.profileNickname {
|
||||
margin: 0;
|
||||
left: -16px;
|
||||
}
|
||||
|
||||
.newAvatar {
|
||||
background-color:white;
|
||||
border-radius: 50%;
|
||||
@ -44,4 +49,21 @@ body {
|
||||
filter: drop-shadow(1px 1px 1px #000000);
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.profileAvatar {
|
||||
border: 0.5px solid #555;
|
||||
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
|
||||
left: 35px;
|
||||
}
|
||||
|
||||
.smallAvatar {
|
||||
border: 0.5px solid #555;
|
||||
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
|
||||
}
|
||||
|
||||
.flippedSmallAvatar {
|
||||
transform: scaleX(-1);
|
||||
border: 0.3px solid #555;
|
||||
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
|
||||
}
|
2
frontend/static/frontend/qr-decoding.worker.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!****************************************!*\
|
||||
!*** ./node_modules/jsqr/dist/jsQR.js ***!
|
||||
\****************************************/
|
||||
|
||||
/*!*************************************************************!*\
|
||||
!*** ./node_modules/react-webcam-qr-scanner/dist/Worker.js ***!
|
||||
\*************************************************************/
|
@ -0,0 +1,3 @@
|
||||
/*!****************************************!*\
|
||||
!*** ./node_modules/jsqr/dist/jsQR.js ***!
|
||||
\****************************************/
|
@ -31,9 +31,9 @@ app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler'
|
||||
|
||||
# Configure the periodic tasks
|
||||
app.conf.beat_schedule = {
|
||||
'users-cleansing': { # Cleans abandoned users every hour
|
||||
'users-cleansing': { # Cleans abandoned users every 6 hours
|
||||
'task': 'users_cleansing',
|
||||
'schedule': timedelta(hours=1),
|
||||
'schedule': timedelta(hours=6),
|
||||
},
|
||||
'cache-market-prices': { # Cache market prices every minutes for now.
|
||||
'task': 'cache_external_market_prices',
|
||||
|
79
setup.md
@ -126,9 +126,86 @@ npm install websocket
|
||||
npm install react-countdown
|
||||
npm install @mui/icons-material
|
||||
npm install @mui/x-data-grid
|
||||
npm install react-responsive
|
||||
npm install react-qr-reader
|
||||
```
|
||||
Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed)
|
||||
|
||||
### Launch the React render
|
||||
from frontend/ directory
|
||||
`npm run dev`
|
||||
`npm run dev`
|
||||
|
||||
## Robosats background threads.
|
||||
|
||||
There is 3 processes that run asynchronously: two admin commands and a celery beat scheduler.
|
||||
The celery worker will run the task of caching external API market prices and cleaning(deleting) the generated robots that were never used.
|
||||
`celery -A robosats worker --beat -l debug -S django`
|
||||
|
||||
The admin commands are used to keep an eye on the state of LND hold invoices and check whether orders have expired
|
||||
```
|
||||
python3 manage.py follow_invoices
|
||||
python3 manage.py clean_order
|
||||
```
|
||||
|
||||
It might be best to set up system services to continuously run these background processes.
|
||||
|
||||
### Follow invoices admin command as system service
|
||||
|
||||
Create `/etc/systemd/system/follow_invoices.service` and edit with:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=RoboSats Follow LND Invoices
|
||||
After=lnd.service
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/<USER>/robosats/
|
||||
StandardOutput=file:/home/<USER>/robosats/follow_invoices.log
|
||||
StandardError=file:/home/<USER>/robosats/follow_invoices.log
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
User=<USER>
|
||||
ExecStart=python3 manage.py follow_invoices
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Then launch it with
|
||||
|
||||
```
|
||||
systemctl start follow_invoices
|
||||
systemctl enable follow_invoices
|
||||
```
|
||||
### Clean orders admin command as system service
|
||||
|
||||
Create `/etc/systemd/system/clean_orders.service` and edit with (replace <USER> for your username):
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=RoboSats Clean Orders
|
||||
After=lnd.service
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/<USER>/robosats/
|
||||
StandardOutput=file:/home/<USER>/robosats/clean_orders.log
|
||||
StandardError=file:/home/<USER>/robosats/clean_orders.log
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
User=<USER>
|
||||
ExecStart=python3 manage.py clean_orders
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Then launch it with
|
||||
|
||||
```
|
||||
systemctl start clean_orders
|
||||
systemctl enable clean_orders
|
||||
```
|