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
|
BOND_SIZE = 0.01
|
||||||
# Time out penalty for canceling takers in SECONDS
|
# Time out penalty for canceling takers in SECONDS
|
||||||
PENALTY_TIMEOUT = 60
|
PENALTY_TIMEOUT = 60
|
||||||
|
# Time between routing attempts of buyer invoice in MINUTES
|
||||||
|
RETRY_TIME = 5
|
||||||
|
|
||||||
# Trade limits in satoshis
|
# Trade limits in satoshis
|
||||||
MIN_TRADE = 10000
|
MIN_TRADE = 10000
|
||||||
MAX_TRADE = 500000
|
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
|
BOND_EXPIRY = 14
|
||||||
ESCROW_EXPIRY = 8
|
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
|
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
|
||||||
FIAT_EXCHANGE_DURATION = 4
|
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
|
# Username for HTLCs escrows
|
||||||
ESCROW_USERNAME = 'admin'
|
ESCROW_USERNAME = 'admin'
|
11
README.md
@ -1,11 +1,14 @@
|
|||||||
## RoboSats - Buy and sell Satoshis Privately.
|
## 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)
|
[![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)
|
[![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)
|
[![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.
|
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
|
## 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:**
|
**Bitcoin mainnet:**
|
||||||
- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Coming soon)
|
- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Coming soon)
|
||||||
@ -15,7 +18,7 @@ RoboSats is a simple and private way to exchange bitcoin for national currencies
|
|||||||
**Bitcoin testnet:**
|
**Bitcoin testnet:**
|
||||||
- Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion (Active - On Dev Node)
|
- Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion (Active - On Dev Node)
|
||||||
- Url: testnet.robosats.com (Coming soon)
|
- 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.*
|
*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)
|
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|
||||||
## Original idea
|
## 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
|
## License
|
||||||
|
|
||||||
|
14
api/admin.py
@ -19,23 +19,25 @@ class EUserAdmin(UserAdmin):
|
|||||||
inlines = [ProfileInline]
|
inlines = [ProfileInline]
|
||||||
list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
|
list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
|
||||||
list_display_links = ('id','username')
|
list_display_links = ('id','username')
|
||||||
|
ordering = ('-id',)
|
||||||
def avatar_tag(self, obj):
|
def avatar_tag(self, obj):
|
||||||
return obj.profile.avatar_tag()
|
return obj.profile.avatar_tag()
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Order)
|
@admin.register(Order)
|
||||||
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
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')
|
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')
|
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
|
||||||
|
|
||||||
@admin.register(LNPayment)
|
@admin.register(LNPayment)
|
||||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
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 = ('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','order_made','order_taken','order_escrow','order_paid')
|
list_display_links = ('hash','concept')
|
||||||
change_links = ('sender','receiver')
|
change_links = ('sender','receiver','order_made','order_taken','order_escrow','order_paid')
|
||||||
list_filter = ('type','concept','status')
|
list_filter = ('type','concept','status')
|
||||||
|
ordering = ('-expires_at',)
|
||||||
|
|
||||||
@admin.register(Profile)
|
@admin.register(Profile)
|
||||||
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
@ -49,9 +51,11 @@ class CurrencieAdmin(admin.ModelAdmin):
|
|||||||
list_display = ('id','currency','exchange_rate','timestamp')
|
list_display = ('id','currency','exchange_rate','timestamp')
|
||||||
list_display_links = ('id','currency')
|
list_display_links = ('id','currency')
|
||||||
readonly_fields = ('currency','exchange_rate','timestamp')
|
readonly_fields = ('currency','exchange_rate','timestamp')
|
||||||
|
ordering = ('id',)
|
||||||
|
|
||||||
@admin.register(MarketTick)
|
@admin.register(MarketTick)
|
||||||
class MarketTickAdmin(admin.ModelAdmin):
|
class MarketTickAdmin(admin.ModelAdmin):
|
||||||
list_display = ('timestamp','price','volume','premium','currency','fee')
|
list_display = ('timestamp','price','volume','premium','currency','fee')
|
||||||
readonly_fields = ('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 datetime import timedelta, datetime
|
||||||
from django.utils import timezone
|
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)
|
# 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.
|
return str(response)=="" # True if no response, false otherwise.
|
||||||
|
|
||||||
@classmethod
|
@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'''
|
'''Generates hold invoice'''
|
||||||
|
|
||||||
hold_payment = {}
|
hold_payment = {}
|
||||||
@ -74,11 +75,15 @@ class LNNode():
|
|||||||
# Its hash is used to generate the hold invoice
|
# Its hash is used to generate the hold invoice
|
||||||
r_hash = hashlib.sha256(preimage).digest()
|
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(
|
request = invoicesrpc.AddHoldInvoiceRequest(
|
||||||
memo=description,
|
memo=description,
|
||||||
value=num_satoshis,
|
value=num_satoshis,
|
||||||
hash=r_hash,
|
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())])
|
response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())])
|
||||||
|
|
||||||
hold_payment['invoice'] = response.payment_request
|
hold_payment['invoice'] = response.payment_request
|
||||||
@ -87,18 +92,22 @@ class LNNode():
|
|||||||
hold_payment['payment_hash'] = payreq_decoded.payment_hash
|
hold_payment['payment_hash'] = payreq_decoded.payment_hash
|
||||||
hold_payment['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
|
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['expires_at'] = hold_payment['created_at'] + timedelta(seconds=payreq_decoded.expiry)
|
||||||
|
hold_payment['cltv_expiry'] = cltv_expiry_blocks
|
||||||
|
|
||||||
return hold_payment
|
return hold_payment
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_hold_invoice_locked(cls, payment_hash):
|
def validate_hold_invoice_locked(cls, lnpayment):
|
||||||
'''Checks if hold invoice is locked'''
|
'''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())])
|
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||||
print('status here')
|
print('status here')
|
||||||
print(response.state)
|
print(response.state)
|
||||||
|
|
||||||
# TODO ERROR HANDLING
|
# 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
|
if response.state == 0: # OPEN
|
||||||
print('STATUS: OPEN')
|
print('STATUS: OPEN')
|
||||||
pass
|
pass
|
||||||
@ -108,31 +117,34 @@ class LNNode():
|
|||||||
pass
|
pass
|
||||||
if response.state == 3: # ACCEPTED (LOCKED)
|
if response.state == 3: # ACCEPTED (LOCKED)
|
||||||
print('STATUS: ACCEPTED')
|
print('STATUS: ACCEPTED')
|
||||||
|
lnpayment.expiry_height = response.htlcs[0].expiry_height
|
||||||
|
lnpayment.status = LNPayment.Status.LOCKED
|
||||||
|
lnpayment.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
# @classmethod
|
||||||
def check_until_invoice_locked(cls, payment_hash, expiration):
|
# def check_until_invoice_locked(cls, payment_hash, expiration):
|
||||||
'''Checks until hold invoice is locked.
|
# '''Checks until hold invoice is locked.
|
||||||
When invoice is locked, returns true.
|
# When invoice is locked, returns true.
|
||||||
If time expires, return False.'''
|
# If time expires, return False.'''
|
||||||
# Experimental, might need asyncio. Best if subscribing all invoices and running a background task
|
# # Experimental, might need asyncio. Best if subscribing all invoices and running a background task
|
||||||
# Maybe best to pass LNpayment object and change status live.
|
# # Maybe best to pass LNpayment object and change status live.
|
||||||
|
|
||||||
request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
|
# request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
|
||||||
for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
|
# for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
|
||||||
print(invoice)
|
# print(invoice)
|
||||||
if timezone.now > expiration:
|
# if timezone.now > expiration:
|
||||||
break
|
# break
|
||||||
if invoice.state == 3: # True if hold invoice is accepted.
|
# if invoice.state == 3: # True if hold invoice is accepted.
|
||||||
return True
|
# return True
|
||||||
return False
|
# return False
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_ln_invoice(cls, invoice, num_satoshis):
|
def validate_ln_invoice(cls, invoice, num_satoshis):
|
||||||
'''Checks if the submited LN invoice comforms to expectations'''
|
'''Checks if the submited LN invoice comforms to expectations'''
|
||||||
|
|
||||||
buyer_invoice = {
|
payout = {
|
||||||
'valid': False,
|
'valid': False,
|
||||||
'context': None,
|
'context': None,
|
||||||
'description': None,
|
'description': None,
|
||||||
@ -145,36 +157,36 @@ class LNNode():
|
|||||||
payreq_decoded = cls.decode_payreq(invoice)
|
payreq_decoded = cls.decode_payreq(invoice)
|
||||||
print(payreq_decoded)
|
print(payreq_decoded)
|
||||||
except:
|
except:
|
||||||
buyer_invoice['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
|
payout['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
|
||||||
return buyer_invoice
|
return payout
|
||||||
|
|
||||||
if payreq_decoded.num_satoshis == 0:
|
if payreq_decoded.num_satoshis == 0:
|
||||||
buyer_invoice['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
|
payout['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
|
||||||
return buyer_invoice
|
return payout
|
||||||
|
|
||||||
if not payreq_decoded.num_satoshis == num_satoshis:
|
if not payreq_decoded.num_satoshis == num_satoshis:
|
||||||
buyer_invoice['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
|
payout['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
|
||||||
return buyer_invoice
|
return payout
|
||||||
|
|
||||||
buyer_invoice['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
|
payout['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
|
||||||
buyer_invoice['expires_at'] = buyer_invoice['created_at'] + timedelta(seconds=payreq_decoded.expiry)
|
payout['expires_at'] = payout['created_at'] + timedelta(seconds=payreq_decoded.expiry)
|
||||||
|
|
||||||
if buyer_invoice['expires_at'] < timezone.now():
|
if payout['expires_at'] < timezone.now():
|
||||||
buyer_invoice['context'] = {'bad_invoice':f'The invoice provided has already expired'}
|
payout['context'] = {'bad_invoice':f'The invoice provided has already expired'}
|
||||||
return buyer_invoice
|
return payout
|
||||||
|
|
||||||
buyer_invoice['valid'] = True
|
payout['valid'] = True
|
||||||
buyer_invoice['description'] = payreq_decoded.description
|
payout['description'] = payreq_decoded.description
|
||||||
buyer_invoice['payment_hash'] = payreq_decoded.payment_hash
|
payout['payment_hash'] = payreq_decoded.payment_hash
|
||||||
|
|
||||||
|
|
||||||
return buyer_invoice
|
return payout
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pay_invoice(cls, invoice, num_satoshis):
|
def pay_invoice(cls, invoice, num_satoshis):
|
||||||
'''Sends sats to buyer'''
|
'''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(
|
request = routerrpc.SendPaymentRequest(
|
||||||
payment_request=invoice,
|
payment_request=invoice,
|
||||||
fee_limit_sat=fee_limit_sat,
|
fee_limit_sat=fee_limit_sat,
|
||||||
|
214
api/logics.py
@ -40,19 +40,19 @@ class Logics():
|
|||||||
'''Checks if the user is already partipant of an active order'''
|
'''Checks if the user is already partipant of an active order'''
|
||||||
queryset = Order.objects.filter(maker=user, status__in=active_order_status)
|
queryset = Order.objects.filter(maker=user, status__in=active_order_status)
|
||||||
if queryset.exists():
|
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)
|
queryset = Order.objects.filter(taker=user, status__in=active_order_status)
|
||||||
if queryset.exists():
|
if queryset.exists():
|
||||||
return False, {'bad_request':'You are already taker of an active order'}
|
return False, {'bad_request':'You are already taker of an active order'}, queryset[0]
|
||||||
return True, None
|
return True, None, None
|
||||||
|
|
||||||
def validate_order_size(order):
|
def validate_order_size(order):
|
||||||
'''Validates if order is withing limits in satoshis at t0'''
|
'''Validates if order is withing limits in satoshis at t0'''
|
||||||
if order.t0_satoshis > MAX_TRADE:
|
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:
|
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
|
return True, None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -89,7 +89,7 @@ class Logics():
|
|||||||
return int(satoshis_now)
|
return int(satoshis_now)
|
||||||
|
|
||||||
def price_and_premium_now(order):
|
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)
|
exchange_rate = float(order.currency.exchange_rate)
|
||||||
if not order.is_explicit:
|
if not order.is_explicit:
|
||||||
premium = order.premium
|
premium = order.premium
|
||||||
@ -111,14 +111,13 @@ class Logics():
|
|||||||
|
|
||||||
# Do not change order status if an order in any with
|
# Do not change order status if an order in any with
|
||||||
# any of these status is sent to expire here
|
# any of these status is sent to expire here
|
||||||
do_nothing = [Order.Status.DEL, Order.Status.UCA,
|
does_not_expire = [Order.Status.DEL, Order.Status.UCA,
|
||||||
Order.Status.EXP, Order.Status.FSE,
|
Order.Status.EXP, Order.Status.TLD,
|
||||||
Order.Status.DIS, Order.Status.CCA,
|
Order.Status.DIS, Order.Status.CCA,
|
||||||
Order.Status.PAY, Order.Status.SUC,
|
Order.Status.PAY, Order.Status.SUC,
|
||||||
Order.Status.FAI, Order.Status.MLD,
|
Order.Status.FAI, Order.Status.MLD]
|
||||||
Order.Status.TLD]
|
|
||||||
|
|
||||||
if order.status in do_nothing:
|
if order.status in does_not_expire:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif order.status == Order.Status.WFB:
|
elif order.status == Order.Status.WFB:
|
||||||
@ -196,8 +195,8 @@ class Logics():
|
|||||||
cls.publish_order(order)
|
cls.publish_order(order)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif order.status == Order.Status.CHA:
|
elif order.status in [Order.Status.CHA, Order.Status.FSE]:
|
||||||
# Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute
|
# 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
|
# was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
|
||||||
# sent", we assume this is a dispute case by default.
|
# sent", we assume this is a dispute case by default.
|
||||||
cls.open_dispute(order)
|
cls.open_dispute(order)
|
||||||
@ -221,7 +220,7 @@ class Logics():
|
|||||||
@classmethod
|
@classmethod
|
||||||
def open_dispute(cls, order, user=None):
|
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.
|
# Dispute winner will have to submit a new invoice.
|
||||||
|
|
||||||
if not order.trade_escrow.status == LNPayment.Status.SETLED:
|
if not order.trade_escrow.status == LNPayment.Status.SETLED:
|
||||||
@ -236,13 +235,17 @@ class Logics():
|
|||||||
if not user == None:
|
if not user == None:
|
||||||
profile = user.profile
|
profile = user.profile
|
||||||
profile.num_disputes = profile.num_disputes + 1
|
profile.num_disputes = profile.num_disputes + 1
|
||||||
|
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.orders_disputes_started = list(profile.orders_disputes_started).append(str(order.id))
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
def dispute_statement(order, user, statement):
|
def dispute_statement(order, user, statement):
|
||||||
''' Updates the dispute statements in DB'''
|
''' Updates the dispute statements'''
|
||||||
|
|
||||||
if not order.status == Order.Status.DIS:
|
if not order.status == Order.Status.DIS:
|
||||||
return False, {'bad_request':'Only orders in dispute accept a dispute statements'}
|
return False, {'bad_request':'Only orders in dispute accept a dispute statements'}
|
||||||
|
|
||||||
@ -263,7 +266,7 @@ class Logics():
|
|||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def buyer_invoice_amount(cls, order, user):
|
def payout_amount(cls, order, user):
|
||||||
''' Computes buyer invoice amount. Uses order.last_satoshis,
|
''' Computes buyer invoice amount. Uses order.last_satoshis,
|
||||||
that is the final trade amount set at Taker Bond time'''
|
that is the final trade amount set at Taker Bond time'''
|
||||||
|
|
||||||
@ -276,33 +279,35 @@ class Logics():
|
|||||||
def update_invoice(cls, order, user, invoice):
|
def update_invoice(cls, order, user, invoice):
|
||||||
|
|
||||||
# only the buyer can post a buyer invoice
|
# only the buyer can post a buyer invoice
|
||||||
|
|
||||||
if not cls.is_buyer(order, user):
|
if not cls.is_buyer(order, user):
|
||||||
return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
|
return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
|
||||||
if not order.taker_bond:
|
if not order.taker_bond:
|
||||||
return False, {'bad_request':'Wait for your order to be taken.'}
|
return False, {'bad_request':'Wait for your order to be taken.'}
|
||||||
if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED):
|
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']
|
num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount']
|
||||||
buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
||||||
|
|
||||||
if not buyer_invoice['valid']:
|
if not payout['valid']:
|
||||||
return False, buyer_invoice['context']
|
return False, payout['context']
|
||||||
|
|
||||||
order.buyer_invoice, _ = LNPayment.objects.update_or_create(
|
order.payout, _ = LNPayment.objects.update_or_create(
|
||||||
concept = LNPayment.Concepts.PAYBUYER,
|
concept = LNPayment.Concepts.PAYBUYER,
|
||||||
type = LNPayment.Types.NORM,
|
type = LNPayment.Types.NORM,
|
||||||
sender = User.objects.get(username=ESCROW_USERNAME),
|
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,
|
receiver= user,
|
||||||
# if there is a LNPayment matching these above, it updates that one with defaults below.
|
# if there is a LNPayment matching these above, it updates that one with defaults below.
|
||||||
defaults={
|
defaults={
|
||||||
'invoice' : invoice,
|
'invoice' : invoice,
|
||||||
'status' : LNPayment.Status.VALIDI,
|
'status' : LNPayment.Status.VALIDI,
|
||||||
'num_satoshis' : num_satoshis,
|
'num_satoshis' : num_satoshis,
|
||||||
'description' : buyer_invoice['description'],
|
'description' : payout['description'],
|
||||||
'payment_hash' : buyer_invoice['payment_hash'],
|
'payment_hash' : payout['payment_hash'],
|
||||||
'created_at' : buyer_invoice['created_at'],
|
'created_at' : payout['created_at'],
|
||||||
'expires_at' : buyer_invoice['expires_at']}
|
'expires_at' : payout['expires_at']}
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
|
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
|
||||||
@ -312,13 +317,22 @@ class Logics():
|
|||||||
|
|
||||||
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
|
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
|
||||||
if order.status == Order.Status.WF2:
|
if order.status == Order.Status.WF2:
|
||||||
# If the escrow is lock move to Chat.
|
# If the escrow does not exist, or is not locked move to WFE.
|
||||||
if order.trade_escrow.status == LNPayment.Status.LOCKED:
|
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.status = Order.Status.CHA
|
||||||
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
|
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
|
||||||
else:
|
else:
|
||||||
order.status = Order.Status.WFE
|
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()
|
order.save()
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@ -326,7 +340,7 @@ class Logics():
|
|||||||
''' adds a new rating to a user profile'''
|
''' adds a new rating to a user profile'''
|
||||||
|
|
||||||
# TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
|
# 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
|
latest_ratings = profile.latest_ratings
|
||||||
if latest_ratings == None:
|
if latest_ratings == None:
|
||||||
profile.latest_ratings = [rating]
|
profile.latest_ratings = [rating]
|
||||||
@ -355,20 +369,32 @@ class Logics():
|
|||||||
@classmethod
|
@classmethod
|
||||||
def cancel_order(cls, order, user, state=None):
|
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
|
# 1) When maker cancels before bond
|
||||||
'''The order never shows up on the book and order
|
'''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:
|
if order.status == Order.Status.WFB and order.maker == user:
|
||||||
order.status = Order.Status.UCA
|
order.status = Order.Status.UCA
|
||||||
order.save()
|
order.save()
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
# 2) When maker cancels after bond
|
# 2) When maker cancels after bond
|
||||||
'''The order dissapears from book and goes to cancelled. Maker is charged the bond to prevent DDOS
|
'''The order dissapears from book and goes to cancelled. If strict, maker is charged the bond
|
||||||
on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)'''
|
to prevent DDOS on the LN node and order book. If not strict, maker is returned
|
||||||
|
the bond (more user friendly).'''
|
||||||
elif order.status == Order.Status.PUB and order.maker == user:
|
elif order.status == Order.Status.PUB and order.maker == user:
|
||||||
#Settle the maker bond (Maker loses the bond for cancelling public order)
|
#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.status = Order.Status.UCA
|
||||||
order.save()
|
order.save()
|
||||||
return True, None
|
return True, None
|
||||||
@ -389,7 +415,7 @@ class Logics():
|
|||||||
|
|
||||||
# 4.a) When maker cancel after bond (before escrow)
|
# 4.a) When maker cancel after bond (before escrow)
|
||||||
'''The order into cancelled status if maker cancels.'''
|
'''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)
|
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
|
||||||
valid = cls.settle_bond(order.maker_bond)
|
valid = cls.settle_bond(order.maker_bond)
|
||||||
if valid:
|
if valid:
|
||||||
@ -408,14 +434,46 @@ class Logics():
|
|||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
# 5) When trade collateral has been posted (after escrow)
|
# 5) When trade collateral has been posted (after escrow)
|
||||||
'''Always goes to cancelled status. Collaboration is needed.
|
'''Always goes to CCA status. Collaboration is needed.
|
||||||
When a user asks for cancel, 'order.is_pending_cancel' goes True.
|
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.
|
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:
|
else:
|
||||||
return False, {'bad_request':'You cannot cancel this order'}
|
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):
|
def publish_order(order):
|
||||||
order.status = Order.Status.PUB
|
order.status = Order.Status.PUB
|
||||||
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[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):
|
def is_maker_bond_locked(cls, order):
|
||||||
if order.maker_bond.status == LNPayment.Status.LOCKED:
|
if order.maker_bond.status == LNPayment.Status.LOCKED:
|
||||||
return True
|
return True
|
||||||
elif LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
|
elif LNNode.validate_hold_invoice_locked(order.maker_bond):
|
||||||
order.maker_bond.status = LNPayment.Status.LOCKED
|
|
||||||
order.maker_bond.save()
|
|
||||||
cls.publish_order(order)
|
cls.publish_order(order)
|
||||||
return True
|
return True
|
||||||
return False
|
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."
|
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
|
# 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(
|
order.maker_bond = LNPayment.objects.create(
|
||||||
concept = LNPayment.Concepts.MAKEBOND,
|
concept = LNPayment.Concepts.MAKEBOND,
|
||||||
@ -469,7 +528,8 @@ class Logics():
|
|||||||
description = description,
|
description = description,
|
||||||
payment_hash = hold_payment['payment_hash'],
|
payment_hash = hold_payment['payment_hash'],
|
||||||
created_at = hold_payment['created_at'],
|
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()
|
order.save()
|
||||||
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
|
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
|
||||||
@ -504,7 +564,7 @@ class Logics():
|
|||||||
def is_taker_bond_locked(cls, order):
|
def is_taker_bond_locked(cls, order):
|
||||||
if order.taker_bond.status == LNPayment.Status.LOCKED:
|
if order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||||
return True
|
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)
|
cls.finalize_contract(order)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -514,8 +574,7 @@ class Logics():
|
|||||||
|
|
||||||
# Do not gen and kick out the taker if order is older than expiry time
|
# Do not gen and kick out the taker if order is older than expiry time
|
||||||
if order.expires_at < timezone.now():
|
if order.expires_at < timezone.now():
|
||||||
cls.cancel_bond(order.taker_bond)
|
cls.order_expires(order)
|
||||||
cls.kick_taker(order)
|
|
||||||
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
|
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.
|
# 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.")
|
+ " - 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
|
# 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(
|
order.taker_bond = LNPayment.objects.create(
|
||||||
concept = LNPayment.Concepts.TAKEBOND,
|
concept = LNPayment.Concepts.TAKEBOND,
|
||||||
@ -547,7 +609,8 @@ class Logics():
|
|||||||
description = description,
|
description = description,
|
||||||
payment_hash = hold_payment['payment_hash'],
|
payment_hash = hold_payment['payment_hash'],
|
||||||
created_at = hold_payment['created_at'],
|
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.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
|
||||||
order.save()
|
order.save()
|
||||||
@ -568,9 +631,7 @@ class Logics():
|
|||||||
def is_trade_escrow_locked(cls, order):
|
def is_trade_escrow_locked(cls, order):
|
||||||
if order.trade_escrow.status == LNPayment.Status.LOCKED:
|
if order.trade_escrow.status == LNPayment.Status.LOCKED:
|
||||||
return True
|
return True
|
||||||
elif LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
|
elif LNNode.validate_hold_invoice_locked(order.trade_escrow):
|
||||||
order.trade_escrow.status = LNPayment.Status.LOCKED
|
|
||||||
order.trade_escrow.save()
|
|
||||||
cls.trade_escrow_received(order)
|
cls.trade_escrow_received(order)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -580,7 +641,7 @@ class Logics():
|
|||||||
|
|
||||||
# Do not generate if escrow deposit time has expired
|
# Do not generate if escrow deposit time has expired
|
||||||
if order.expires_at < timezone.now():
|
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.'}
|
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.
|
# 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."
|
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
|
# 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(
|
order.trade_escrow = LNPayment.objects.create(
|
||||||
concept = LNPayment.Concepts.TRESCROW,
|
concept = LNPayment.Concepts.TRESCROW,
|
||||||
@ -610,7 +674,8 @@ class Logics():
|
|||||||
description = description,
|
description = description,
|
||||||
payment_hash = hold_payment['payment_hash'],
|
payment_hash = hold_payment['payment_hash'],
|
||||||
created_at = hold_payment['created_at'],
|
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()
|
order.save()
|
||||||
return True, {'escrow_invoice':hold_payment['invoice'],'escrow_satoshis': escrow_satoshis}
|
return True, {'escrow_invoice':hold_payment['invoice'],'escrow_satoshis': escrow_satoshis}
|
||||||
@ -635,6 +700,7 @@ class Logics():
|
|||||||
'''returns the trade escrow'''
|
'''returns the trade escrow'''
|
||||||
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
|
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
|
||||||
order.trade_escrow.status = LNPayment.Status.RETNED
|
order.trade_escrow.status = LNPayment.Status.RETNED
|
||||||
|
order.trade_escrow.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def cancel_escrow(order):
|
def cancel_escrow(order):
|
||||||
@ -642,6 +708,7 @@ class Logics():
|
|||||||
# Same as return escrow, but used when the invoice was never LOCKED
|
# Same as return escrow, but used when the invoice was never LOCKED
|
||||||
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
|
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
|
||||||
order.trade_escrow.status = LNPayment.Status.CANCEL
|
order.trade_escrow.status = LNPayment.Status.CANCEL
|
||||||
|
order.trade_escrow.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def return_bond(bond):
|
def return_bond(bond):
|
||||||
@ -651,10 +718,12 @@ class Logics():
|
|||||||
try:
|
try:
|
||||||
LNNode.cancel_return_hold_invoice(bond.payment_hash)
|
LNNode.cancel_return_hold_invoice(bond.payment_hash)
|
||||||
bond.status = LNPayment.Status.RETNED
|
bond.status = LNPayment.Status.RETNED
|
||||||
|
bond.save()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if 'invoice already settled' in str(e):
|
if 'invoice already settled' in str(e):
|
||||||
bond.status = LNPayment.Status.SETLED
|
bond.status = LNPayment.Status.SETLED
|
||||||
|
bond.save()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
@ -667,58 +736,53 @@ class Logics():
|
|||||||
try:
|
try:
|
||||||
LNNode.cancel_return_hold_invoice(bond.payment_hash)
|
LNNode.cancel_return_hold_invoice(bond.payment_hash)
|
||||||
bond.status = LNPayment.Status.CANCEL
|
bond.status = LNPayment.Status.CANCEL
|
||||||
|
bond.save()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if 'invoice already settled' in str(e):
|
if 'invoice already settled' in str(e):
|
||||||
bond.status = LNPayment.Status.SETLED
|
bond.status = LNPayment.Status.SETLED
|
||||||
|
bond.save()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def pay_buyer_invoice(order):
|
|
||||||
''' Pay buyer invoice'''
|
|
||||||
suceeded, context = follow_send_payment(order.buyer_invoice)
|
|
||||||
return suceeded, context
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def confirm_fiat(cls, order, user):
|
def confirm_fiat(cls, order, user):
|
||||||
''' If Order is in the CHAT states:
|
''' If Order is in the CHAT states:
|
||||||
If user is buyer: mark FIAT SENT and settle escrow!
|
If user is buyer: fiat_sent goes to true.
|
||||||
If User is the seller and FIAT is SENT: Pay buyer invoice!'''
|
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 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 buyer, settle escrow and mark fiat sent
|
||||||
if cls.is_buyer(order, user):
|
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.status = Order.Status.FSE
|
||||||
order.is_fiat_sent = True
|
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):
|
elif cls.is_seller(order, user):
|
||||||
if not order.is_fiat_sent:
|
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.'}
|
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
|
# 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.'}
|
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.
|
# Double check the escrow is settled.
|
||||||
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
|
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
|
||||||
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.taker_bond)
|
||||||
cls.return_bond(order.maker_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.save()
|
||||||
|
return True, context
|
||||||
else:
|
else:
|
||||||
# error handling here
|
# error handling here
|
||||||
pass
|
return False, context
|
||||||
else:
|
else:
|
||||||
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
|
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
|
||||||
|
|
||||||
@ -728,8 +792,10 @@ class Logics():
|
|||||||
@classmethod
|
@classmethod
|
||||||
def rate_counterparty(cls, order, user, rating):
|
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 the trade is finished
|
||||||
if order.status > Order.Status.PAY:
|
if order.status in rating_allowed_status:
|
||||||
# if maker, rates taker
|
# if maker, rates taker
|
||||||
if order.maker == user and order.maker_rated == False:
|
if order.maker == user and order.maker_rated == False:
|
||||||
cls.add_profile_rating(order.taker.profile, rating)
|
cls.add_profile_rating(order.taker.profile, rating)
|
||||||
|
@ -11,16 +11,18 @@ class Command(BaseCommand):
|
|||||||
# def add_arguments(self, parser):
|
# def add_arguments(self, parser):
|
||||||
# parser.add_argument('debug', nargs='+', type=boolean)
|
# 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
|
''' Continuously checks order expiration times for 1 hour. If order
|
||||||
has expires, it calls the logics module for expiration handling.'''
|
has expires, it calls the logics module for expiration handling.'''
|
||||||
|
|
||||||
|
# TODO handle 'database is locked'
|
||||||
|
|
||||||
do_nothing = [Order.Status.DEL, Order.Status.UCA,
|
do_nothing = [Order.Status.DEL, Order.Status.UCA,
|
||||||
Order.Status.EXP, Order.Status.FSE,
|
Order.Status.EXP, Order.Status.FSE,
|
||||||
Order.Status.DIS, Order.Status.CCA,
|
Order.Status.DIS, Order.Status.CCA,
|
||||||
Order.Status.PAY, Order.Status.SUC,
|
Order.Status.PAY, Order.Status.SUC,
|
||||||
Order.Status.FAI, Order.Status.MLD,
|
Order.Status.FAI, Order.Status.MLD,
|
||||||
Order.Status.TLD]
|
Order.Status.TLD, Order.Status.WFR]
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
@ -34,9 +36,30 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
for idx, order in enumerate(queryset):
|
for idx, order in enumerate(queryset):
|
||||||
context = str(order)+ " was "+ Order.Status(order.status).label
|
context = str(order)+ " was "+ Order.Status(order.status).label
|
||||||
|
try:
|
||||||
if Logics.order_expires(order): # Order send to expire here
|
if Logics.order_expires(order): # Order send to expire here
|
||||||
debug['expired_orders'].append({idx:context})
|
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:
|
if debug['num_expired_orders'] > 0:
|
||||||
self.stdout.write(str(timezone.now()))
|
self.stdout.write(str(timezone.now()))
|
||||||
self.stdout.write(str(debug))
|
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 django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
from api.lightning.node import LNNode
|
from api.lightning.node import LNNode
|
||||||
|
from api.tasks import follow_send_payment
|
||||||
from api.models import LNPayment, Order
|
from api.models import LNPayment, Order
|
||||||
from api.logics import Logics
|
from api.logics import Logics
|
||||||
|
|
||||||
@ -13,7 +14,30 @@ import time
|
|||||||
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
'''
|
|
||||||
|
help = 'Follows all active hold invoices'
|
||||||
|
rest = 5 # seconds between consecutive checks for invoice updates
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
''' 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.
|
Background: SubscribeInvoices stub iterator would be great to use here.
|
||||||
However, it only sends updates when the invoice is OPEN (new) or SETTLED.
|
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).
|
We are very interested on the other two states (CANCELLED and ACCEPTED).
|
||||||
@ -21,16 +45,6 @@ class Command(BaseCommand):
|
|||||||
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
|
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'''
|
|
||||||
|
|
||||||
lnd_state_to_lnpayment_status = {
|
lnd_state_to_lnpayment_status = {
|
||||||
0: LNPayment.Status.INVGEN, # OPEN
|
0: LNPayment.Status.INVGEN, # OPEN
|
||||||
1: LNPayment.Status.SETLED, # SETTLED
|
1: LNPayment.Status.SETLED, # SETTLED
|
||||||
@ -40,9 +54,6 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
stub = LNNode.invoicesstub
|
stub = LNNode.invoicesstub
|
||||||
|
|
||||||
while True:
|
|
||||||
time.sleep(self.rest)
|
|
||||||
|
|
||||||
# time it for debugging
|
# time it for debugging
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
|
queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
|
||||||
@ -54,20 +65,29 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
for idx, hold_lnpayment in enumerate(queryset):
|
for idx, hold_lnpayment in enumerate(queryset):
|
||||||
old_status = LNPayment.Status(hold_lnpayment.status).label
|
old_status = LNPayment.Status(hold_lnpayment.status).label
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# this is similar to LNNnode.validate_hold_invoice_locked
|
||||||
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
|
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
|
||||||
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||||
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
|
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:
|
except Exception as e:
|
||||||
# If it fails at finding the invoice it has been canceled.
|
# 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)
|
# In RoboSats DB we make a distinction between cancelled and returned (LND does not)
|
||||||
if 'unable to locate invoice' in str(e):
|
if 'unable to locate invoice' in str(e):
|
||||||
|
self.stdout.write(str(e))
|
||||||
hold_lnpayment.status = LNPayment.Status.CANCEL
|
hold_lnpayment.status = LNPayment.Status.CANCEL
|
||||||
|
|
||||||
# LND restarted.
|
# LND restarted.
|
||||||
if 'wallet locked, unlock it' in str(e):
|
if 'wallet locked, unlock it' in str(e):
|
||||||
self.stdout.write(str(timezone.now())+':: Wallet Locked')
|
self.stdout.write(str(timezone.now())+' :: Wallet Locked')
|
||||||
# Other write to logs
|
# Other write to logs
|
||||||
else:
|
else:
|
||||||
self.stdout.write(str(e))
|
self.stdout.write(str(e))
|
||||||
@ -78,8 +98,8 @@ class Command(BaseCommand):
|
|||||||
changed = not old_status==new_status
|
changed = not old_status==new_status
|
||||||
if changed:
|
if changed:
|
||||||
# self.handle_status_change(hold_lnpayment, old_status)
|
# self.handle_status_change(hold_lnpayment, old_status)
|
||||||
hold_lnpayment.save()
|
|
||||||
self.update_order_status(hold_lnpayment)
|
self.update_order_status(hold_lnpayment)
|
||||||
|
hold_lnpayment.save()
|
||||||
|
|
||||||
# Report for debugging
|
# Report for debugging
|
||||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||||
@ -97,6 +117,21 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(str(timezone.now()))
|
self.stdout.write(str(timezone.now()))
|
||||||
self.stdout.write(str(debug))
|
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):
|
def update_order_status(self, lnpayment):
|
||||||
''' Background process following LND hold invoices
|
''' Background process following LND hold invoices
|
||||||
@ -121,10 +156,27 @@ class Command(BaseCommand):
|
|||||||
elif hasattr(lnpayment, 'order_escrow' ):
|
elif hasattr(lnpayment, 'order_escrow' ):
|
||||||
Logics.trade_escrow_received(lnpayment.order_escrow)
|
Logics.trade_escrow_received(lnpayment.order_escrow)
|
||||||
return
|
return
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.stdout.write(str(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
|
# halt the order
|
||||||
if lnpayment.status == LNPayment.Status.LOCKED:
|
if lnpayment.status == LNPayment.Status.INVGEN:
|
||||||
pass
|
pass
|
@ -19,7 +19,7 @@ BOND_SIZE = float(config('BOND_SIZE'))
|
|||||||
|
|
||||||
class Currency(models.Model):
|
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_choices = [(int(val), label) for val, label in list(currency_dict.items())]
|
||||||
|
|
||||||
currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False, unique=True)
|
currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False, unique=True)
|
||||||
@ -37,7 +37,7 @@ class Currency(models.Model):
|
|||||||
class LNPayment(models.Model):
|
class LNPayment(models.Model):
|
||||||
|
|
||||||
class Types(models.IntegerChoices):
|
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'
|
HOLD = 1, 'hold invoice'
|
||||||
|
|
||||||
class Concepts(models.IntegerChoices):
|
class Concepts(models.IntegerChoices):
|
||||||
@ -58,12 +58,10 @@ class LNPayment(models.Model):
|
|||||||
SUCCED = 8, 'Succeeded'
|
SUCCED = 8, 'Succeeded'
|
||||||
FAILRO = 9, 'Routing failed'
|
FAILRO = 9, 'Routing failed'
|
||||||
|
|
||||||
|
|
||||||
# payment use details
|
# payment use details
|
||||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
|
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
|
||||||
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
||||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
|
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
|
||||||
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
|
|
||||||
|
|
||||||
# payment info
|
# payment info
|
||||||
payment_hash = models.CharField(max_length=100, unique=True, default=None, blank=True, primary_key=True)
|
payment_hash = models.CharField(max_length=100, unique=True, default=None, blank=True, primary_key=True)
|
||||||
@ -73,6 +71,12 @@ class LNPayment(models.Model):
|
|||||||
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
|
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
|
||||||
created_at = models.DateTimeField()
|
created_at = models.DateTimeField()
|
||||||
expires_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
|
# involved parties
|
||||||
sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None)
|
sender = models.ForeignKey(User, related_name='sender', 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...
|
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
|
# 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
|
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)
|
is_fiat_sent = models.BooleanField(default=False, null=False)
|
||||||
|
|
||||||
# in dispute
|
# 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)
|
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)
|
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)
|
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 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
|
# ratings
|
||||||
maker_rated = models.BooleanField(default=False, null=False)
|
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'
|
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'
|
9 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Sending fiat - In chatroom'
|
||||||
10 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Fiat sent - 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'
|
12 : 0, # 'Collaboratively cancelled'
|
||||||
13 : 24*60*60, # 'Sending satoshis to buyer'
|
13 : 24*60*60, # 'Sending satoshis to buyer'
|
||||||
14 : 24*60*60, # 'Sucessful trade'
|
14 : 24*60*60, # 'Sucessful trade'
|
||||||
15 : 24*60*60, # 'Failed lightning network routing'
|
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'
|
17 : 24*60*60, # 'Maker lost dispute'
|
||||||
18 : 24*60*60, # 'Taker lost dispute'
|
18 : 24*60*60, # 'Taker lost dispute'
|
||||||
}
|
}
|
||||||
@ -192,7 +198,7 @@ class Order(models.Model):
|
|||||||
|
|
||||||
@receiver(pre_delete, sender=Order)
|
@receiver(pre_delete, sender=Order)
|
||||||
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
|
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:
|
for lnpayment in to_delete:
|
||||||
try:
|
try:
|
||||||
@ -222,7 +228,7 @@ class Profile(models.Model):
|
|||||||
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True)
|
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 (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)
|
@receiver(post_save, sender=User)
|
||||||
def create_user_profile(sender, instance, created, **kwargs):
|
def create_user_profile(sender, instance, created, **kwargs):
|
||||||
|
@ -4773,6 +4773,7 @@ adjectives = [
|
|||||||
"Compound",
|
"Compound",
|
||||||
"Important",
|
"Important",
|
||||||
"Robotic",
|
"Robotic",
|
||||||
|
"Satoshi",
|
||||||
"Alltoocommon",
|
"Alltoocommon",
|
||||||
"Informative",
|
"Informative",
|
||||||
"Anxious",
|
"Anxious",
|
||||||
|
@ -6346,6 +6346,7 @@ nouns = [
|
|||||||
"Nair",
|
"Nair",
|
||||||
"Nairo",
|
"Nairo",
|
||||||
"Naivete",
|
"Naivete",
|
||||||
|
"Nakamoto",
|
||||||
"Name",
|
"Name",
|
||||||
"Namesake",
|
"Namesake",
|
||||||
"Nanometer",
|
"Nanometer",
|
||||||
@ -12229,6 +12230,7 @@ nouns = [
|
|||||||
"Sand",
|
"Sand",
|
||||||
"Sandwich",
|
"Sandwich",
|
||||||
"Satisfaction",
|
"Satisfaction",
|
||||||
|
"Satoshi",
|
||||||
"Save",
|
"Save",
|
||||||
"Savings",
|
"Savings",
|
||||||
"Scale",
|
"Scale",
|
||||||
|
26
api/tasks.py
@ -21,7 +21,7 @@ def users_cleansing():
|
|||||||
for user in queryset:
|
for user in queryset:
|
||||||
if not user.profile.total_contracts == 0:
|
if not user.profile.total_contracts == 0:
|
||||||
continue
|
continue
|
||||||
valid, _ = Logics.validate_already_maker_or_taker(user)
|
valid, _, _ = Logics.validate_already_maker_or_taker(user)
|
||||||
if valid:
|
if valid:
|
||||||
deleted_users.append(str(user))
|
deleted_users.append(str(user))
|
||||||
user.delete()
|
user.delete()
|
||||||
@ -38,6 +38,8 @@ def follow_send_payment(lnpayment):
|
|||||||
|
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from api.lightning.node import LNNode
|
from api.lightning.node import LNNode
|
||||||
from api.models import LNPayment, Order
|
from api.models import LNPayment, Order
|
||||||
@ -51,6 +53,7 @@ def follow_send_payment(lnpayment):
|
|||||||
timeout_seconds=60) # time out payment in 60 seconds
|
timeout_seconds=60) # time out payment in 60 seconds
|
||||||
|
|
||||||
order = lnpayment.order_paid
|
order = lnpayment.order_paid
|
||||||
|
try:
|
||||||
for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
|
for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
|
||||||
if response.status == 0 : # Status 0 'UNKNOWN'
|
if response.status == 0 : # Status 0 'UNKNOWN'
|
||||||
# Not sure when this status happens
|
# Not sure when this status happens
|
||||||
@ -66,11 +69,15 @@ def follow_send_payment(lnpayment):
|
|||||||
if response.status == 3 : # Status 3 'FAILED'
|
if response.status == 3 : # Status 3 'FAILED'
|
||||||
print('FAILED')
|
print('FAILED')
|
||||||
lnpayment.status = LNPayment.Status.FAILRO
|
lnpayment.status = LNPayment.Status.FAILRO
|
||||||
|
lnpayment.last_routing_time = timezone.now()
|
||||||
|
lnpayment.routing_attempts += 1
|
||||||
lnpayment.save()
|
lnpayment.save()
|
||||||
order.status = Order.Status.FAI
|
order.status = Order.Status.FAI
|
||||||
|
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
|
||||||
order.save()
|
order.save()
|
||||||
context = LNNode.payment_failure_context[response.failure_reason]
|
context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
|
||||||
# Call for a retry here
|
print(context)
|
||||||
|
# Call a retry in 5 mins here?
|
||||||
return False, context
|
return False, context
|
||||||
|
|
||||||
if response.status == 2 : # Status 2 'SUCCEEDED'
|
if response.status == 2 : # Status 2 'SUCCEEDED'
|
||||||
@ -78,9 +85,22 @@ def follow_send_payment(lnpayment):
|
|||||||
lnpayment.status = LNPayment.Status.SUCCED
|
lnpayment.status = LNPayment.Status.SUCCED
|
||||||
lnpayment.save()
|
lnpayment.save()
|
||||||
order.status = Order.Status.SUC
|
order.status = Order.Status.SUC
|
||||||
|
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
|
||||||
order.save()
|
order.save()
|
||||||
return True, None
|
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 = {'routing_failed':'The payout invoice has expired'}
|
||||||
|
return False, context
|
||||||
|
|
||||||
@shared_task(name="cache_external_market_prices", ignore_result=True)
|
@shared_task(name="cache_external_market_prices", ignore_result=True)
|
||||||
def cache_market():
|
def cache_market():
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ premium_percentile = {}
|
|||||||
@ring.dict(premium_percentile, expire=300)
|
@ring.dict(premium_percentile, expire=300)
|
||||||
def compute_premium_percentile(order):
|
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))
|
print(len(queryset))
|
||||||
if len(queryset) <= 1:
|
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'))
|
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
|
||||||
FEE = float(config('FEE'))
|
FEE = float(config('FEE'))
|
||||||
|
RETRY_TIME = int(config('RETRY_TIME'))
|
||||||
|
|
||||||
avatar_path = Path('frontend/static/assets/avatars')
|
avatar_path = Path('frontend/static/assets/avatars')
|
||||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
avatar_path.mkdir(parents=True, exist_ok=True)
|
||||||
@ -38,6 +39,9 @@ class MakerView(CreateAPIView):
|
|||||||
def post(self,request):
|
def post(self,request):
|
||||||
serializer = self.serializer_class(data=request.data)
|
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)
|
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
type = serializer.data.get('type')
|
type = serializer.data.get('type')
|
||||||
@ -48,7 +52,7 @@ class MakerView(CreateAPIView):
|
|||||||
satoshis = serializer.data.get('satoshis')
|
satoshis = serializer.data.get('satoshis')
|
||||||
is_explicit = serializer.data.get('is_explicit')
|
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)
|
if not valid: return Response(context, status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
# Creates a new order
|
# Creates a new order
|
||||||
@ -83,6 +87,9 @@ class OrderView(viewsets.ViewSet):
|
|||||||
'''
|
'''
|
||||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
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:
|
if order_id == None:
|
||||||
return Response({'bad_request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
|
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.
|
# if user is under a limit (penalty), inform him.
|
||||||
is_penalized, time_out = Logics.is_penalized(request.user)
|
is_penalized, time_out = Logics.is_penalized(request.user)
|
||||||
if is_penalized:
|
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
|
# Add booleans if user is maker, taker, partipant, buyer or seller
|
||||||
data['is_maker'] = order.maker == request.user
|
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:
|
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)
|
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
|
# 3.b If order is between public and WF2
|
||||||
if order.status >= Order.Status.PUB and order.status < Order.Status.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)
|
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
|
data['trade_satoshis'] = order.last_satoshis
|
||||||
# Buyer sees the amount he receives
|
# Buyer sees the amount he receives
|
||||||
elif data['is_buyer']:
|
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.
|
# 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']:
|
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'
|
# 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):
|
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 the two bonds are locked, reply with an ESCROW hold invoice.
|
||||||
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
|
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||||
valid, context = Logics.gen_escrow_hold_invoice(order, request.user)
|
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 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:
|
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:
|
if valid:
|
||||||
data = {**data, **context}
|
data = {**data, **context}
|
||||||
else:
|
else:
|
||||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED
|
# 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 all bonds are locked.
|
||||||
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
|
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
|
||||||
# add whether a collaborative cancel is pending
|
# add whether a collaborative cancel is pending or has been asked
|
||||||
data['pending_cancel'] = order.is_pending_cancel
|
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)
|
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!
|
# 1) If action is take, it is a taker request!
|
||||||
if action == 'take':
|
if action == 'take':
|
||||||
if order.status == Order.Status.PUB:
|
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)
|
if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
|
||||||
valid, context = Logics.take(order, request.user)
|
valid, context = Logics.take(order, request.user)
|
||||||
if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN)
|
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 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:
|
if request.user.is_authenticated:
|
||||||
context = {'nickname': request.user.username}
|
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
|
# Does not allow this 'mistake' if an active order
|
||||||
if not not_participant:
|
if not not_participant:
|
||||||
@ -386,23 +440,22 @@ class UserView(APIView):
|
|||||||
def delete(self,request):
|
def delete(self,request):
|
||||||
''' Pressing "give me another" deletes the logged in user '''
|
''' Pressing "give me another" deletes the logged in user '''
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user:
|
if not user.is_authenticated:
|
||||||
return Response(status.HTTP_403_FORBIDDEN)
|
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)):
|
if user.date_joined < (timezone.now() - timedelta(minutes=30)):
|
||||||
return Response(status.HTTP_400_BAD_REQUEST)
|
return Response(status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Check if it is not a maker or taker!
|
# 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)
|
return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
logout(request)
|
logout(request)
|
||||||
user.delete()
|
user.delete()
|
||||||
return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY)
|
return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BookView(ListAPIView):
|
class BookView(ListAPIView):
|
||||||
serializer_class = ListOrderSerializer
|
serializer_class = ListOrderSerializer
|
||||||
queryset = Order.objects.filter(status=Order.Status.PUB)
|
queryset = Order.objects.filter(status=Order.Status.PUB)
|
||||||
@ -424,7 +477,6 @@ class BookView(ListAPIView):
|
|||||||
if len(queryset)== 0:
|
if len(queryset)== 0:
|
||||||
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
|
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 = []
|
book_data = []
|
||||||
for order in queryset:
|
for order in queryset:
|
||||||
data = ListOrderSerializer(order).data
|
data = ListOrderSerializer(order).data
|
||||||
@ -468,12 +520,27 @@ class InfoView(ListAPIView):
|
|||||||
avg_premium = 0
|
avg_premium = 0
|
||||||
total_volume = 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_avg_nonkyc_btc_premium'] = round(avg_premium,2)
|
||||||
context['today_total_volume'] = total_volume
|
context['today_total_volume'] = total_volume
|
||||||
|
context['lifetime_satoshis_settled'] = lifetime_volume_settled
|
||||||
context['lnd_version'] = get_lnd_version()
|
context['lnd_version'] = get_lnd_version()
|
||||||
context['robosats_running_commit_hash'] = get_commit_robosats()
|
context['robosats_running_commit_hash'] = get_commit_robosats()
|
||||||
context['fee'] = FEE
|
context['fee'] = FEE
|
||||||
context['bond_size'] = float(config('BOND_SIZE'))
|
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)
|
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"
|
"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": {
|
"css-select": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
|
||||||
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
|
"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": {
|
"jss": {
|
||||||
"version": "10.9.0",
|
"version": "10.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/jss/-/jss-10.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/jss/-/jss-10.9.0.tgz",
|
||||||
@ -5040,6 +5050,14 @@
|
|||||||
"object-visit": "^1.0.0"
|
"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": {
|
"material-ui-image": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/material-ui-image/-/material-ui-image-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/material-ui-image/-/material-ui-image-3.3.2.tgz",
|
||||||
@ -6439,11 +6457,32 @@
|
|||||||
"qr.js": "0.0.0"
|
"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": {
|
"react-refresh": {
|
||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",
|
||||||
"integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA=="
|
"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": {
|
"react-router": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
||||||
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
|
"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": {
|
"safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@ -6965,6 +7012,11 @@
|
|||||||
"ajv-keywords": "^3.5.2"
|
"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": {
|
"semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
@ -7076,6 +7128,11 @@
|
|||||||
"kind-of": "^6.0.2"
|
"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": {
|
"shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@ -7858,6 +7915,15 @@
|
|||||||
"integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==",
|
"integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==",
|
||||||
"dev": true
|
"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": {
|
"websocket": {
|
||||||
"version": "1.0.34",
|
"version": "1.0.34",
|
||||||
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
|
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
|
||||||
|
@ -35,6 +35,8 @@
|
|||||||
"react-native": "^0.66.4",
|
"react-native": "^0.66.4",
|
||||||
"react-native-svg": "^12.1.1",
|
"react-native-svg": "^12.1.1",
|
||||||
"react-qr-code": "^2.0.3",
|
"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",
|
"react-router-dom": "^5.2.0",
|
||||||
"websocket": "^1.0.34"
|
"websocket": "^1.0.34"
|
||||||
}
|
}
|
||||||
|
@ -7,17 +7,20 @@ import BottomBar from "./BottomBar";
|
|||||||
export default class App extends Component {
|
export default class App extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
nickname: null,
|
||||||
|
token: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppState=(newState)=>{
|
||||||
|
this.setState(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='appCenter'>
|
<HomePage setAppState={this.setAppState}/>
|
||||||
<HomePage />
|
|
||||||
</div>
|
|
||||||
<div className='bottomBar'>
|
|
||||||
<BottomBar />
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 { 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 { Link } from 'react-router-dom'
|
||||||
import { DataGrid } from '@mui/x-data-grid';
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
|
import MediaQuery from 'react-responsive'
|
||||||
|
|
||||||
import getFlags from './getFlags'
|
import getFlags from './getFlags'
|
||||||
|
|
||||||
export default class BookPage extends Component {
|
export default class BookPage extends Component {
|
||||||
@ -10,7 +12,7 @@ export default class BookPage extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
orders: new Array({id:0,}),
|
orders: new Array({id:0,}),
|
||||||
currency: 0,
|
currency: 0,
|
||||||
type: 1,
|
type: 2,
|
||||||
currencies_dict: {"0":"ANY"},
|
currencies_dict: {"0":"ANY"},
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
@ -22,8 +24,7 @@ export default class BookPage extends Component {
|
|||||||
getOrderDetails(type, currency) {
|
getOrderDetails(type, currency) {
|
||||||
fetch('/api/book' + '?currency=' + currency + "&type=" + type)
|
fetch('/api/book' + '?currency=' + currency + "&type=" + type)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => console.log(data) &
|
.then((data) => this.setState({
|
||||||
this.setState({
|
|
||||||
orders: data,
|
orders: data,
|
||||||
not_found: data.not_found,
|
not_found: data.not_found,
|
||||||
loading: false,
|
loading: false,
|
||||||
@ -31,7 +32,6 @@ export default class BookPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleRowClick=(e)=>{
|
handleRowClick=(e)=>{
|
||||||
console.log(e)
|
|
||||||
this.props.history.push('/order/' + e);
|
this.props.history.push('/order/' + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export default class BookPage extends Component {
|
|||||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
}
|
}
|
||||||
|
|
||||||
bookListTable=()=>{
|
bookListTableDesktop=()=>{
|
||||||
return (
|
return (
|
||||||
<div style={{ height: 475, width: '100%' }}>
|
<div style={{ height: 475, width: '100%' }}>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
@ -93,7 +93,7 @@ export default class BookPage extends Component {
|
|||||||
renderCell: (params) => {return (
|
renderCell: (params) => {return (
|
||||||
<ListItemButton style={{ cursor: "pointer" }}>
|
<ListItemButton style={{ cursor: "pointer" }}>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar alt={params.row.robosat} src={params.row.avatar} />
|
<Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={params.row.robosat}/>
|
<ListItemText primary={params.row.robosat}/>
|
||||||
</ListItemButton>
|
</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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Grid className='orderBook' container spacing={1}>
|
<Grid className='orderBook' container spacing={1} sx={{minWidth:400}}>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="h2" variant="h2">
|
<Typography component="h2" variant="h2">
|
||||||
Order Book
|
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}
|
No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<br/>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
|
<Button variant="contained" color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Typography component="body1" variant="body1">
|
<Typography component="body1" variant="body1">
|
||||||
Be the first one to create an order
|
Be the first one to create an order
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>)
|
</Grid>)
|
||||||
:
|
:
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
|
{/* Desktop Book */}
|
||||||
|
<MediaQuery minWidth={920}>
|
||||||
<Paper elevation={0} style={{width: 910, maxHeight: 500, overflow: 'auto'}}>
|
<Paper elevation={0} style={{width: 910, maxHeight: 500, overflow: 'auto'}}>
|
||||||
|
{this.state.loading ? null : this.bookListTableDesktop()}
|
||||||
{this.state.loading ? null : this.bookListTable()}
|
|
||||||
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
</MediaQuery>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
}
|
}
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { Component } from 'react'
|
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
|
// Icons
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
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 EqualizerIcon from '@mui/icons-material/Equalizer';
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import PublicIcon from '@mui/icons-material/Public';
|
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 {
|
export default class BottomBar extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -21,12 +31,17 @@ export default class BottomBar extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
openStatsForNerds: false,
|
openStatsForNerds: false,
|
||||||
openCommuniy: false,
|
openCommuniy: false,
|
||||||
|
openExchangeSummary:false,
|
||||||
num_public_buy_orders: 0,
|
num_public_buy_orders: 0,
|
||||||
num_public_sell_orders: 0,
|
num_public_sell_orders: 0,
|
||||||
active_robots_today: 0,
|
active_robots_today: 0,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
today_avg_nonkyc_btc_premium: 0,
|
today_avg_nonkyc_btc_premium: 0,
|
||||||
today_total_volume: 0,
|
today_total_volume: 0,
|
||||||
|
lifetime_satoshis_settled: 0,
|
||||||
|
robosats_running_commit_hash: '000000000000000',
|
||||||
|
openProfile: false,
|
||||||
|
profileShown: false,
|
||||||
};
|
};
|
||||||
this.getInfo();
|
this.getInfo();
|
||||||
}
|
}
|
||||||
@ -39,19 +54,20 @@ export default class BottomBar extends Component {
|
|||||||
this.setState(null)
|
this.setState(null)
|
||||||
fetch('/api/info/')
|
fetch('/api/info/')
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {console.log(data) &
|
.then((data) => this.setState(data) &
|
||||||
this.setState(data)
|
this.props.setAppState({nickname:data.nickname}));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickOpenStatsForNerds = () => {
|
handleClickOpenStatsForNerds = () => {
|
||||||
this.setState({openStatsForNerds: true});
|
this.setState({openStatsForNerds: true});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClickCloseStatsForNerds = () => {
|
handleClickCloseStatsForNerds = () => {
|
||||||
this.setState({openStatsForNerds: false});
|
this.setState({openStatsForNerds: false});
|
||||||
};
|
};
|
||||||
|
|
||||||
StatsDialog =() =>{
|
StatsDialog =() =>{
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.openStatsForNerds}
|
open={this.state.openStatsForNerds}
|
||||||
@ -61,7 +77,7 @@ export default class BottomBar extends Component {
|
|||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Typography component="h5" variant="h5">Stats For Nerds</Typography>
|
<Typography component="h5" variant="h5">Stats For Nerds</Typography>
|
||||||
<List>
|
<List dense>
|
||||||
<Divider/>
|
<Divider/>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemIcon><BoltIcon/></ListItemIcon>
|
<ListItemIcon><BoltIcon/></ListItemIcon>
|
||||||
@ -71,9 +87,9 @@ export default class BottomBar extends Component {
|
|||||||
<Divider/>
|
<Divider/>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemIcon><GitHubIcon/></ListItemIcon>
|
<ListItemIcon><GitHubIcon/></ListItemIcon>
|
||||||
<ListItemText secondary="Currently running commit height">
|
<ListItemText secondary="Currently running commit hash">
|
||||||
<a href={"https://github.com/Reckless-Satoshi/robosats/tree/"
|
<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>
|
</a>
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -84,6 +100,12 @@ export default class BottomBar extends Component {
|
|||||||
<ListItemText primary={this.state.today_total_volume+" BTC"} secondary="Today traded volume"/>
|
<ListItemText primary={this.state.today_total_volume+" BTC"} secondary="Today traded volume"/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
<Divider/>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon><EqualizerIcon/></ListItemIcon>
|
||||||
|
<ListItemText primary={pn(this.state.lifetime_satoshis_settled)+" Sats"} secondary="Lifetime settled volume"/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<Divider/>
|
<Divider/>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemIcon><PublicIcon/></ListItemIcon>
|
<ListItemIcon><PublicIcon/></ListItemIcon>
|
||||||
@ -104,6 +126,7 @@ export default class BottomBar extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
CommunityDialog =() =>{
|
CommunityDialog =() =>{
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.openCommuniy}
|
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() {
|
dialogProfile =() =>{
|
||||||
return (
|
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}}>
|
<Paper elevation={6} style={{height:40}}>
|
||||||
<this.StatsDialog/>
|
<this.StatsDialog/>
|
||||||
<this.CommunityDialog/>
|
<this.CommunityDialog/>
|
||||||
|
<this.dialogProfile/>
|
||||||
<Grid container xs={12}>
|
<Grid container xs={12}>
|
||||||
|
|
||||||
<Grid item xs={1}>
|
<Grid item xs={2}>
|
||||||
<IconButton color="primary"
|
<ListItemButton onClick={this.handleClickOpenProfile} >
|
||||||
aria-label="Stats for Nerds"
|
<ListItemAvatar sx={{ width: 30, height: 30 }} >
|
||||||
onClick={this.handleClickOpenStatsForNerds} >
|
<Badge badgeContent={(this.state.active_order_id > 0 & !this.state.profileShown) ? "": null} color="primary">
|
||||||
<SettingsIcon />
|
<Avatar className='flippedSmallAvatar' sx={{margin: 0, top: -13}}
|
||||||
</IconButton>
|
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>
|
||||||
|
|
||||||
<Grid item xs={2}>
|
<Grid item xs={2}>
|
||||||
@ -214,11 +325,11 @@ export default class BottomBar extends Component {
|
|||||||
primaryTypographyProps={{fontSize: '14px'}}
|
primaryTypographyProps={{fontSize: '14px'}}
|
||||||
secondaryTypographyProps={{fontSize: '12px'}}
|
secondaryTypographyProps={{fontSize: '12px'}}
|
||||||
primary={this.state.today_avg_nonkyc_btc_premium+"%"}
|
primary={this.state.today_avg_nonkyc_btc_premium+"%"}
|
||||||
secondary="Today Non-KYC Avg Premium" />
|
secondary="Today Avg Premium" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={2}>
|
<Grid item xs={1}>
|
||||||
<ListItem className="bottomItem">
|
<ListItem className="bottomItem">
|
||||||
<ListItemIcon size="small">
|
<ListItemIcon size="small">
|
||||||
<PercentIcon/>
|
<PercentIcon/>
|
||||||
@ -227,12 +338,11 @@ export default class BottomBar extends Component {
|
|||||||
primaryTypographyProps={{fontSize: '14px'}}
|
primaryTypographyProps={{fontSize: '14px'}}
|
||||||
secondaryTypographyProps={{fontSize: '12px'}}
|
secondaryTypographyProps={{fontSize: '12px'}}
|
||||||
primary={this.state.fee*100}
|
primary={this.state.fee*100}
|
||||||
secondary="Trading Fee" />
|
secondary="Trade Fee" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid container item xs={1}>
|
<Grid container item xs={1}>
|
||||||
<Grid item xs={2}/>
|
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<Select
|
<Select
|
||||||
size = 'small'
|
size = 'small'
|
||||||
@ -243,10 +353,186 @@ export default class BottomBar extends Component {
|
|||||||
<MenuItem value={1}>EN</MenuItem>
|
<MenuItem value={1}>EN</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={3}>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
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} >
|
onClick={this.handleClickOpenCommunity} >
|
||||||
<PeopleIcon />
|
<PeopleIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -256,5 +542,19 @@ export default class BottomBar extends Component {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</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 React, { Component } from 'react';
|
||||||
import { w3cwebsocket as W3CWebSocket } from "websocket";
|
import { w3cwebsocket as W3CWebSocket } from "websocket";
|
||||||
import {Button, TextField, Link, Grid, Typography, Container, Card, CardHeader, Paper, Avatar} from "@mui/material";
|
import {Button, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, FormHelperText} from "@mui/material";
|
||||||
import { withStyles } from "@mui/material";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default class Chat extends Component {
|
export default class Chat extends Component {
|
||||||
@ -53,7 +51,7 @@ export default class Chat extends Component {
|
|||||||
this.client.send(JSON.stringify({
|
this.client.send(JSON.stringify({
|
||||||
type: "message",
|
type: "message",
|
||||||
message: this.state.value,
|
message: this.state.value,
|
||||||
nick: this.props.urNick,
|
nick: this.props.ur_nick,
|
||||||
}));
|
}));
|
||||||
this.state.value = ''
|
this.state.value = ''
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -66,7 +64,7 @@ export default class Chat extends Component {
|
|||||||
{this.state.messages.map(message => <>
|
{this.state.messages.map(message => <>
|
||||||
<Card elevation={5} align="left" >
|
<Card elevation={5} align="left" >
|
||||||
{/* If message sender is not our nick, gray color, if it is our nick, green color */}
|
{/* 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
|
<CardHeader
|
||||||
avatar={
|
avatar={
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -113,6 +111,7 @@ export default class Chat extends Component {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
|
<FormHelperText>This chat has no memory. If you leave and come back the messages are lost.</FormHelperText>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,43 @@
|
|||||||
import React, { Component } from "react";
|
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 UserGenPage from "./UserGenPage";
|
||||||
import MakerPage from "./MakerPage";
|
import MakerPage from "./MakerPage";
|
||||||
import BookPage from "./BookPage";
|
import BookPage from "./BookPage";
|
||||||
import OrderPage from "./OrderPage";
|
import OrderPage from "./OrderPage";
|
||||||
|
import BottomBar from "./BottomBar";
|
||||||
|
|
||||||
export default class HomePage extends Component {
|
export default class HomePage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
nickname: null,
|
||||||
|
token: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppState=(newState)=>{
|
||||||
|
this.setState(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectTo(location) {
|
||||||
|
this.props.history.push(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Router >
|
<Router >
|
||||||
|
<div className='appCenter'>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path='/' component={UserGenPage}/>
|
<Route exact path='/' render={(props) => <UserGenPage setAppState={this.setAppState}/>}/>
|
||||||
<Route path='/home'><p>You are at the start page</p></Route>
|
|
||||||
<Route path='/make' component={MakerPage}/>
|
<Route path='/make' component={MakerPage}/>
|
||||||
<Route path='/book' component={BookPage}/>
|
<Route path='/book' component={BookPage}/>
|
||||||
<Route path="/order/:orderId" component={OrderPage}/>
|
<Route path="/order/:orderId" component={OrderPage}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<div className='bottomBar'>
|
||||||
|
<BottomBar redirectTo={this.redirectTo} {...this.state} setAppState={this.setAppState} />
|
||||||
|
</div>
|
||||||
</Router>
|
</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 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 {
|
export default class InfoDialog extends Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<DialogContent>
|
<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">
|
<Typography component="body2" variant="body2">
|
||||||
<p>It is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies
|
<p>It is a BTC/FIAT peer-to-peer exchange over lightning. <br/> 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>
|
||||||
|
|
||||||
<p>RoboSats is an open source project <a
|
<p>RoboSats is an open source project <a
|
||||||
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a>
|
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a>
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</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 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="h5" variant="h5">How does it work?</Typography>
|
||||||
<Typography component="body2" variant="body2">
|
<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.
|
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
|
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
|
robots. Then, Alice posts the trade collateral also using a lightning
|
||||||
hold invoice. <i>RoboSats</i> locks the invoice until Bob confirms he sent
|
hold invoice. <i>RoboSats</i> locks the invoice until Alice confirms she
|
||||||
the fiat to Alice. Once Alice confirms she received the fiat, she
|
received the fiat, then the satoshis are released to Bob. Enjoy your satoshis,
|
||||||
tells <i>RoboSats</i> to release the satoshis to Bob. Enjoy your satoshis,
|
|
||||||
Bob!</p>
|
Bob!</p>
|
||||||
|
|
||||||
<p>At no point, AdequateAlice01 and BafflingBob02 have to trust the
|
<p>At no point, AnonymousAlice01 and BafflingBob02 have to trust the
|
||||||
bitcoin to each other. In case they have a conflict, <i>RoboSats</i> staff
|
bitcoin funds to each other. In case they have a conflict, <i>RoboSats</i> staff
|
||||||
will help resolving the dispute.</p>
|
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>
|
||||||
|
|
||||||
<Typography component="h5" variant="h5">What payment methods are accepted?</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="h5" variant="h5">Is <i>RoboSats</i> private?</Typography>
|
||||||
<Typography component="body2" variant="body2">
|
<Typography component="body2" variant="body2">
|
||||||
<p> RoboSats will never ask you for your name, country or ID. For
|
<p> RoboSats will never ask you for your name, country or ID. RoboSats does
|
||||||
best anonymity use Tor Browser and access the .onion hidden service. </p>
|
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
|
<p>Your trading peer is the only one who can potentially guess
|
||||||
anything about you. Keep your chat short and concise. Avoid
|
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="h5" variant="h5">What is the trust model?</Typography>
|
||||||
<Typography component="body2" variant="body2">
|
<Typography component="body2" variant="body2">
|
||||||
<p> The buyer and the seller never have to trust each other.
|
<p> The buyer and the seller never have to trust each other.
|
||||||
Some trust on <i>RoboSats</i> staff is needed since linking
|
Some trust on <i>RoboSats</i> is needed since linking the
|
||||||
the seller's hold invoice and buyer payment is not atomic (yet).
|
seller's hold invoice and buyer payment is not atomic (yet).
|
||||||
In addition, disputes are solved by the <i>RoboSats</i> staff.
|
In addition, disputes are solved by the <i>RoboSats</i> staff.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p> While trust requirements are minimized, <i>RoboSats</i> could
|
<p> To be totally clear. Trust requirements are minimized. However, there is still
|
||||||
run away with your satoshis. It could be argued that it is not
|
one way <i>RoboSats </i> could run away with your satoshis: by not releasing
|
||||||
worth it, as it would instantly destroy <i>RoboSats</i> reputation.
|
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
|
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>
|
time. For large amounts use an onchain escrow service such as <i>Bisq</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p> You can build more trust on <i>RoboSats</i> by <a href='https://github.com/reckless-satoshi/robosats'>
|
<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>
|
||||||
|
|
||||||
<Typography component="h5" variant="h5">What happens if <i>RoboSats</i> suddenly disapears?</Typography>
|
<Typography component="h5" variant="h5">What happens if <i>RoboSats</i> suddenly disapears?</Typography>
|
||||||
<Typography component="body2" variant="body2">
|
<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
|
settled would be automatically returned even if <i>RoboSats</i> goes down
|
||||||
forever. This is true for both, locked bonds and trading escrows. However,
|
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
|
there is a small window between the seller confirms FIAT RECEIVED and the moment
|
||||||
the seller releases the satoshis when the funds could be lost.
|
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>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
@ -116,9 +154,10 @@ export default class InfoDialog extends Component {
|
|||||||
RoboSats</i> will definitely never ask for your robot token.
|
RoboSats</i> will definitely never ask for your robot token.
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={this.props.handleCloseInfo}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
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 { Link } from 'react-router-dom'
|
||||||
|
|
||||||
import getFlags from './getFlags'
|
import getFlags from './getFlags'
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
@ -37,7 +36,7 @@ export default class MakerPage extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state={
|
this.state={
|
||||||
isExplicit: false,
|
is_explicit: false,
|
||||||
type: 0,
|
type: 0,
|
||||||
currency: this.defaultCurrency,
|
currency: this.defaultCurrency,
|
||||||
currencyCode: this.defaultCurrencyCode,
|
currencyCode: this.defaultCurrencyCode,
|
||||||
@ -89,14 +88,14 @@ export default class MakerPage extends Component {
|
|||||||
}
|
}
|
||||||
handleClickRelative=(e)=>{
|
handleClickRelative=(e)=>{
|
||||||
this.setState({
|
this.setState({
|
||||||
isExplicit: false,
|
is_explicit: false,
|
||||||
satoshis: null,
|
satoshis: null,
|
||||||
premium: 0,
|
premium: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleClickExplicit=(e)=>{
|
handleClickExplicit=(e)=>{
|
||||||
this.setState({
|
this.setState({
|
||||||
isExplicit: true,
|
is_explicit: true,
|
||||||
premium: null,
|
premium: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -104,7 +103,6 @@ export default class MakerPage extends Component {
|
|||||||
handleCreateOfferButtonPressed=()=>{
|
handleCreateOfferButtonPressed=()=>{
|
||||||
this.state.amount == null ? this.setState({amount: 0}) : null;
|
this.state.amount == null ? this.setState({amount: 0}) : null;
|
||||||
|
|
||||||
console.log(this.state)
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
||||||
@ -113,7 +111,7 @@ export default class MakerPage extends Component {
|
|||||||
currency: this.state.currency,
|
currency: this.state.currency,
|
||||||
amount: this.state.amount,
|
amount: this.state.amount,
|
||||||
payment_method: this.state.payment_method,
|
payment_method: this.state.payment_method,
|
||||||
is_explicit: this.state.isExplicit,
|
is_explicit: this.state.is_explicit,
|
||||||
premium: this.state.premium,
|
premium: this.state.premium,
|
||||||
satoshis: this.state.satoshis,
|
satoshis: this.state.satoshis,
|
||||||
}),
|
}),
|
||||||
@ -241,7 +239,7 @@ export default class MakerPage extends Component {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
{/* conditional shows either Premium % field or Satoshis field based on pricing method */}
|
{/* 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">
|
? <Grid item xs={12} align="center">
|
||||||
<TextField
|
<TextField
|
||||||
label="Satoshis"
|
label="Satoshis"
|
||||||
@ -287,7 +285,7 @@ export default class MakerPage extends Component {
|
|||||||
<Typography component="subtitle2" variant="subtitle2">
|
<Typography component="subtitle2" variant="subtitle2">
|
||||||
<div align='center'>
|
<div align='center'>
|
||||||
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode}
|
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 market price" :
|
||||||
(this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount")
|
(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 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 Countdown, { zeroPad, calcTimeDelta } from 'react-countdown';
|
||||||
|
import MediaQuery from 'react-responsive'
|
||||||
|
|
||||||
import TradeBox from "./TradeBox";
|
import TradeBox from "./TradeBox";
|
||||||
import getFlags from './getFlags'
|
import getFlags from './getFlags'
|
||||||
|
|
||||||
@ -9,9 +11,7 @@ import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
|||||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||||
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
||||||
import PaymentsIcon from '@mui/icons-material/Payments';
|
import PaymentsIcon from '@mui/icons-material/Payments';
|
||||||
import MoneyIcon from '@mui/icons-material/Money';
|
|
||||||
import ArticleIcon from '@mui/icons-material/Article';
|
import ArticleIcon from '@mui/icons-material/Article';
|
||||||
import ContentCopy from "@mui/icons-material/ContentCopy";
|
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
let cookieValue = null;
|
let cookieValue = null;
|
||||||
@ -39,86 +39,69 @@ export default class OrderPage extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isExplicit: false,
|
is_explicit: false,
|
||||||
delay: 60000, // Refresh every 60 seconds by default
|
delay: 60000, // Refresh every 60 seconds by default
|
||||||
currencies_dict: {"1":"USD"},
|
currencies_dict: {"1":"USD"},
|
||||||
total_secs_expiry: 300,
|
total_secs_exp: 300,
|
||||||
loading: true,
|
loading: true,
|
||||||
openCancel: false,
|
openCancel: false,
|
||||||
|
openCollaborativeCancel: false,
|
||||||
|
showContractBox: 1,
|
||||||
};
|
};
|
||||||
this.orderId = this.props.match.params.orderId;
|
this.orderId = this.props.match.params.orderId;
|
||||||
this.getCurrencyDict();
|
this.getCurrencyDict();
|
||||||
this.getOrderDetails();
|
this.getOrderDetails();
|
||||||
|
|
||||||
// Refresh delais according to Order status
|
// Refresh delays according to Order status
|
||||||
this.statusToDelay = {
|
this.statusToDelay = {
|
||||||
"0": 3000, //'Waiting for maker bond'
|
"0": 2000, //'Waiting for maker bond'
|
||||||
"1": 30000, //'Public'
|
"1": 25000, //'Public'
|
||||||
"2": 9999999, //'Deleted'
|
"2": 9999999, //'Deleted'
|
||||||
"3": 3000, //'Waiting for taker bond'
|
"3": 2000, //'Waiting for taker bond'
|
||||||
"4": 9999999, //'Cancelled'
|
"4": 9999999, //'Cancelled'
|
||||||
"5": 999999, //'Expired'
|
"5": 999999, //'Expired'
|
||||||
"6": 3000, //'Waiting for trade collateral and buyer invoice'
|
"6": 3000, //'Waiting for trade collateral and buyer invoice'
|
||||||
"7": 3000, //'Waiting only for seller trade collateral'
|
"7": 3000, //'Waiting only for seller trade collateral'
|
||||||
"8": 10000, //'Waiting only for buyer invoice'
|
"8": 8000, //'Waiting only for buyer invoice'
|
||||||
"9": 10000, //'Sending fiat - In chatroom'
|
"9": 10000, //'Sending fiat - In chatroom'
|
||||||
"10": 15000, //'Fiat sent - In chatroom'
|
"10": 10000, //'Fiat sent - In chatroom'
|
||||||
"11": 60000, //'In dispute'
|
"11": 30000, //'In dispute'
|
||||||
"12": 9999999,//'Collaboratively cancelled'
|
"12": 9999999, //'Collaboratively cancelled'
|
||||||
"13": 3000, //'Sending satoshis to buyer'
|
"13": 3000, //'Sending satoshis to buyer'
|
||||||
"14": 9999999,//'Sucessful trade'
|
"14": 9999999, //'Sucessful trade'
|
||||||
"15": 10000, //'Failed lightning network routing'
|
"15": 10000, //'Failed lightning network routing'
|
||||||
"16": 9999999,//'Maker lost dispute'
|
"16": 9999999, //'Maker lost dispute'
|
||||||
"17": 9999999,//'Taker 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() {
|
getOrderDetails() {
|
||||||
this.setState(null)
|
this.setState(null)
|
||||||
fetch('/api/order' + '?order_id=' + this.orderId)
|
fetch('/api/order' + '?order_id=' + this.orderId)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {console.log(data) &
|
.then((data) => this.completeSetState(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
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are used to refresh the data
|
// These are used to refresh the data
|
||||||
@ -137,11 +120,6 @@ export default class OrderPage extends Component {
|
|||||||
this.getOrderDetails();
|
this.getOrderDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix to use proper react props
|
|
||||||
handleClickBackButton=()=>{
|
|
||||||
window.history.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Countdown Renderer callback with condition
|
// Countdown Renderer callback with condition
|
||||||
countdownRenderer = ({ total, hours, minutes, seconds, completed }) => {
|
countdownRenderer = ({ total, hours, minutes, seconds, completed }) => {
|
||||||
if (completed) {
|
if (completed) {
|
||||||
@ -150,7 +128,7 @@ export default class OrderPage extends Component {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
var col = 'black'
|
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
|
// Make orange at 25% of time left
|
||||||
if (fraction_left < 0.25){col = 'orange'}
|
if (fraction_left < 0.25){col = 'orange'}
|
||||||
// Make red at 10% of time left
|
// 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 =()=> {
|
LinearDeterminate =()=> {
|
||||||
const [progress, setProgress] = React.useState(0);
|
const [progress, setProgress] = React.useState(0);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setProgress((oldProgress) => {
|
setProgress((oldProgress) => {
|
||||||
var left = calcTimeDelta( new Date(this.state.expiresAt)).total /1000;
|
var left = calcTimeDelta( new Date(this.state.expires_at)).total /1000;
|
||||||
return (left / this.state.total_secs_expiry) * 100;
|
return (left / this.state.total_secs_exp) * 100;
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
@ -187,7 +178,6 @@ export default class OrderPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClickTakeOrderButton=()=>{
|
handleClickTakeOrderButton=()=>{
|
||||||
console.log(this.state)
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
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)
|
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => (this.setState({badRequest:data.bad_request})
|
.then((data) => this.completeSetState(data));
|
||||||
& console.log(data)
|
|
||||||
& this.getOrderDetails(data.id)));
|
|
||||||
}
|
}
|
||||||
getCurrencyDict() {
|
getCurrencyDict() {
|
||||||
fetch('/static/assets/currencies.json')
|
fetch('/static/assets/currencies.json')
|
||||||
@ -221,7 +209,6 @@ export default class OrderPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClickConfirmCancelButton=()=>{
|
handleClickConfirmCancelButton=()=>{
|
||||||
console.log(this.state)
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
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)
|
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
|
.then((data) => this.getOrderDetails(data.id));
|
||||||
this.handleClickCloseConfirmCancelDialog();
|
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 = () => {
|
CancelButton = () => {
|
||||||
|
|
||||||
// If maker and Waiting for Bond. Or if taker and Waiting for bond.
|
// If maker and Waiting for Bond. Or if taker and Waiting for bond.
|
||||||
// Simply allow to cancel without showing the cancel dialog.
|
// 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(
|
return(
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Button variant='contained' color='secondary' onClick={this.handleClickConfirmCancelButton}>Cancel</Button>
|
<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
|
// If the order does not yet have an escrow deposited. Show dialog
|
||||||
// to confirm forfeiting the bond
|
// to confirm forfeiting the bond
|
||||||
if (this.state.statusCode < 8){
|
if ([3,6,7].includes(this.state.status)){
|
||||||
return(
|
return(
|
||||||
|
<div id="openDialogCancelButton">
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<this.CancelDialog/>
|
<this.CancelDialog/>
|
||||||
<Button variant='contained' color='secondary' onClick={this.handleClickOpenConfirmCancelDialog}>Cancel</Button>
|
<Button variant='contained' color='secondary' onClick={this.handleClickOpenConfirmCancelDialog}>Cancel</Button>
|
||||||
</Grid>
|
</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.
|
// If none of the above do not return a cancel button.
|
||||||
return(null)
|
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=()=>{
|
orderBox=()=>{
|
||||||
return(
|
return(
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1} >
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
|
<MediaQuery minWidth={920}>
|
||||||
<Typography component="h5" variant="h5">
|
<Typography component="h5" variant="h5">
|
||||||
Order Details
|
Order Details
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</MediaQuery>
|
||||||
<Paper elevation={12} style={{ padding: 8,}}>
|
<Paper elevation={12} style={{ padding: 8,}}>
|
||||||
<List dense="true">
|
<List dense="true">
|
||||||
<ListItem >
|
<ListItem >
|
||||||
<ListItemAvatar sx={{ width: 56, height: 56 }}>
|
<ListItemAvatar sx={{ width: 56, height: 56 }}>
|
||||||
<Avatar
|
<Badge variant="dot" badgeContent="" color={this.statusBadgeColor(this.state.maker_status)}>
|
||||||
alt={this.state.makerNick}
|
<Avatar className="flippedSmallAvatar"
|
||||||
src={window.location.origin +'/static/assets/avatars/' + this.state.makerNick + '.png'}
|
alt={this.state.maker_nick}
|
||||||
|
src={window.location.origin +'/static/assets/avatars/' + this.state.maker_nick + '.png'}
|
||||||
/>
|
/>
|
||||||
|
</Badge>
|
||||||
</ListItemAvatar>
|
</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>
|
</ListItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{this.state.isParticipant ?
|
{this.state.is_participant ?
|
||||||
<>
|
<>
|
||||||
{this.state.takerNick!='None' ?
|
{this.state.taker_nick!='None' ?
|
||||||
<>
|
<>
|
||||||
<ListItem align="left">
|
<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 >
|
<ListItemAvatar >
|
||||||
<Avatar
|
<Badge variant="dot" badgeContent="" color={this.statusBadgeColor(this.state.taker_status)}>
|
||||||
alt={this.state.makerNick}
|
<Avatar className="smallAvatar"
|
||||||
src={window.location.origin +'/static/assets/avatars/' + this.state.takerNick + '.png'}
|
alt={this.state.taker_nick}
|
||||||
|
src={window.location.origin +'/static/assets/avatars/' + this.state.taker_nick + '.png'}
|
||||||
/>
|
/>
|
||||||
|
</Badge>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -333,7 +394,7 @@ export default class OrderPage extends Component {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<ArticleIcon/>
|
<ArticleIcon/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={this.state.statusText} secondary="Order status"/>
|
<ListItemText primary={this.state.status_message} secondary="Order status"/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
</>
|
</>
|
||||||
@ -352,7 +413,7 @@ export default class OrderPage extends Component {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PaymentsIcon/>
|
<PaymentsIcon/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={this.state.paymentMethod} secondary="Accepted payment methods"/>
|
<ListItemText primary={this.state.payment_method} secondary="Accepted payment methods"/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
@ -361,10 +422,10 @@ export default class OrderPage extends Component {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PriceChangeIcon/>
|
<PriceChangeIcon/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
{this.state.priceNow?
|
{this.state.price_now?
|
||||||
<ListItemText primary={pn(this.state.priceNow)+" "+this.state.currencyCode+"/BTC - Premium: "+this.state.premiumNow+"%"} secondary="Price and Premium"/>
|
<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={pn(this.state.satoshis)} secondary="Amount of Satoshis"/>
|
||||||
:
|
:
|
||||||
<ListItemText primary={parseFloat(parseFloat(this.state.premium).toFixed(2))+"%"} secondary="Premium over market price"/>
|
<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/>
|
<AccessTimeIcon/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText secondary="Expires in">
|
<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>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<this.LinearDeterminate />
|
<this.LinearDeterminate />
|
||||||
@ -397,7 +458,31 @@ export default class OrderPage extends Component {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Alert severity="warning" sx={{maxWidth:360}}>
|
<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>
|
</Alert>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
@ -406,45 +491,100 @@ export default class OrderPage extends Component {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} align="center">
|
||||||
{/* Participants can see the "Cancel" Button, but cannot see the "Back" or "Take Order" buttons */}
|
{/* Participants can see the "Cancel" Button, but cannot see the "Back" or "Take Order" buttons */}
|
||||||
{this.state.isParticipant ?
|
{this.state.is_participant ?
|
||||||
<this.CancelButton/>
|
<this.CancelButton/>
|
||||||
:
|
:
|
||||||
<>
|
<Grid container spacing={1}>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
|
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
|
<Button variant='contained' color='secondary' onClick={this.props.history.goBack}>Back</Button>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
orderDetailsPage (){
|
doubleOrderPageDesktop=()=>{
|
||||||
return(
|
return(
|
||||||
this.state.badRequest ?
|
<Grid container xs={12} align="center" spacing={2} >
|
||||||
<div align='center'>
|
<Grid item xs={6} align="left" style={{ width:330}} >
|
||||||
<Typography component="subtitle2" variant="subtitle2" color="secondary" >
|
|
||||||
{this.state.badRequest}<br/>
|
|
||||||
</Typography>
|
|
||||||
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
(this.state.isParticipant ?
|
|
||||||
<Grid container xs={12} align="center" spacing={2}>
|
|
||||||
<Grid item xs={6} align="left">
|
|
||||||
{this.orderBox()}
|
{this.orderBox()}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6} align="left">
|
<Grid item xs={6} align="left">
|
||||||
<TradeBox data={this.state}/>
|
<TradeBox width={330} data={this.state} completeSetState={this.completeSetState} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</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.bad_request ?
|
||||||
|
<div align='center'>
|
||||||
|
<Typography component="subtitle2" variant="subtitle2" color="secondary" >
|
||||||
|
{this.state.bad_request}<br/>
|
||||||
|
</Typography>
|
||||||
|
<Button variant='contained' color='secondary' onClick={this.props.history.goBack}>Back</Button>
|
||||||
|
</div>
|
||||||
:
|
:
|
||||||
<Grid item xs={12} align="center">
|
(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" style={{ width:330}}>
|
||||||
{this.orderBox()}
|
{this.orderBox()}
|
||||||
</Grid>)
|
</Grid>)
|
||||||
)
|
)
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import React, { Component } from "react";
|
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 QRCode from "react-qr-code";
|
||||||
|
import Countdown from 'react-countdown';
|
||||||
import Chat from "./Chat"
|
import Chat from "./Chat"
|
||||||
|
import MediaQuery from 'react-responsive'
|
||||||
|
import QrReader from 'react-qr-reader'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
|
||||||
import PercentIcon from '@mui/icons-material/Percent';
|
import PercentIcon from '@mui/icons-material/Percent';
|
||||||
import BookIcon from '@mui/icons-material/Book';
|
import BookIcon from '@mui/icons-material/Book';
|
||||||
|
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
|
||||||
|
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
let cookieValue = null;
|
let cookieValue = null;
|
||||||
@ -41,9 +41,21 @@ export default class TradeBox extends Component {
|
|||||||
openConfirmDispute: false,
|
openConfirmDispute: false,
|
||||||
badInvoice: false,
|
badInvoice: false,
|
||||||
badStatement: 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 = () => {
|
handleClickOpenConfirmDispute = () => {
|
||||||
this.setState({openConfirmDispute: true});
|
this.setState({openConfirmDispute: true});
|
||||||
};
|
};
|
||||||
@ -61,7 +73,7 @@ export default class TradeBox extends Component {
|
|||||||
};
|
};
|
||||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => (this.props.data = data));
|
.then((data) => this.props.completeSetState(data));
|
||||||
this.handleClickCloseConfirmDispute();
|
this.handleClickCloseConfirmDispute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,15 +90,15 @@ export default class TradeBox extends Component {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText id="alert-dialog-description">
|
<DialogContentText id="alert-dialog-description">
|
||||||
The RoboSats staff will examine the statements and evidence provided by the participants.
|
The RoboSats staff will examine the statements and evidence provided. You need to build
|
||||||
It is best if you provide a burner contact method on your statement for the staff to contact you.
|
a complete case, as the staff cannot read the chat. It is best to provide a burner contact
|
||||||
The satoshis in the trade escrow will be sent to the dispute winner, while the dispute
|
method with your statement. The satoshis in the trade escrow will be sent to the dispute winner,
|
||||||
loser will lose the bond.
|
while the dispute loser will lose the bond.
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={this.handleClickCloseConfirmDispute} autoFocus>Disagree</Button>
|
<Button onClick={this.handleClickCloseConfirmDispute} autoFocus>Disagree</Button>
|
||||||
<Button onClick={this.handleClickAgreeDisputeButton}> Agree </Button>
|
<Button onClick={this.handleClickAgreeDisputeButton}> Agree and open dispute </Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
@ -136,32 +148,32 @@ export default class TradeBox extends Component {
|
|||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="body2" variant="body2">
|
<Typography component="body2" variant="body2">
|
||||||
Robosats show commitment to their peers
|
Robots show commitment to their peers
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
{this.props.data.isMaker ?
|
{this.props.data.is_maker ?
|
||||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
<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>
|
||||||
:
|
:
|
||||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
<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>
|
</Typography>
|
||||||
}
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<QRCode value={this.props.data.bondInvoice} size={305}/>
|
<QRCode value={this.props.data.bond_invoice} size={305}/>
|
||||||
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bondInvoice)}} align="center"> 📋Copy to clipboard</Button>
|
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bond_invoice)}} align="center"> 📋Copy to clipboard</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<TextField
|
<TextField
|
||||||
hiddenLabel
|
hiddenLabel
|
||||||
variant="standard"
|
variant="standard"
|
||||||
size="small"
|
size="small"
|
||||||
defaultValue={this.props.data.bondInvoice}
|
defaultValue={this.props.data.bond_invoice}
|
||||||
disabled="true"
|
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"
|
color = "secondary"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -173,7 +185,7 @@ export default class TradeBox extends Component {
|
|||||||
return (
|
return (
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography color="primary" component="subtitle1" variant="subtitle1" 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>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
@ -182,23 +194,25 @@ export default class TradeBox extends Component {
|
|||||||
showEscrowQRInvoice=()=>{
|
showEscrowQRInvoice=()=>{
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
|
{/* Make confirmation sound for HTLC received. */}
|
||||||
|
<this.Sound soundFileName="locked-invoice"/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography color="green" component="subtitle1" variant="subtitle1">
|
<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>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<QRCode value={this.props.data.escrowInvoice} size={305}/>
|
<QRCode value={this.props.data.escrow_invoice} size={305}/>
|
||||||
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrowInvoice)}} align="center"> 📋Copy to clipboard</Button>
|
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrow_invoice)}} align="center"> 📋Copy to clipboard</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<TextField
|
<TextField
|
||||||
hiddenLabel
|
hiddenLabel
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="small"
|
size="small"
|
||||||
defaultValue={this.props.data.escrowInvoice}
|
defaultValue={this.props.data.escrow_invoice}
|
||||||
disabled="true"
|
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"
|
color = "secondary"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -208,11 +222,10 @@ export default class TradeBox extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showTakerFound=()=>{
|
showTakerFound=()=>{
|
||||||
|
|
||||||
// TODO Make some sound here! The maker might have been waiting for long
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
|
{/* Make bell sound when taker is found */}
|
||||||
|
<this.Sound soundFileName="taker-found"/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="subtitle1" variant="subtitle1">
|
<Typography component="subtitle1" variant="subtitle1">
|
||||||
<b>A taker has been found! </b>
|
<b>A taker has been found! </b>
|
||||||
@ -221,7 +234,9 @@ export default class TradeBox extends Component {
|
|||||||
<Divider/>
|
<Divider/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="body2" variant="body2">
|
<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>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
{this.showBondIsLocked()}
|
{this.showBondIsLocked()}
|
||||||
@ -232,6 +247,8 @@ export default class TradeBox extends Component {
|
|||||||
showMakerWait=()=>{
|
showMakerWait=()=>{
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
|
{/* Make confirmation sound for HTLC received. */}
|
||||||
|
<this.Sound soundFileName="locked-invoice"/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="subtitle1" variant="subtitle1">
|
<Typography component="subtitle1" variant="subtitle1">
|
||||||
<b> Your order is public. Wait for a taker. </b>
|
<b> Your order is public. Wait for a taker. </b>
|
||||||
@ -251,20 +268,12 @@ export default class TradeBox extends Component {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{/* TODO API sends data for a more confortable wait */}
|
{/* 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/>
|
<Divider/>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<BookIcon/>
|
<BookIcon/>
|
||||||
</ListItemIcon>
|
</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>
|
</ListItem>
|
||||||
|
|
||||||
<Divider/>
|
<Divider/>
|
||||||
@ -272,7 +281,7 @@ export default class TradeBox extends Component {
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<PercentIcon/>
|
<PercentIcon/>
|
||||||
</ListItemIcon>
|
</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)"} />
|
secondary={"Among public " + this.props.data.currencyCode + " orders (higher is cheaper)"} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider/>
|
<Divider/>
|
||||||
@ -305,7 +314,7 @@ export default class TradeBox extends Component {
|
|||||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => this.setState({badInvoice:data.bad_invoice})
|
.then((data) => this.setState({badInvoice:data.bad_invoice})
|
||||||
& console.log(data));
|
& this.props.completeSetState(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputDisputeChanged=(e)=>{
|
handleInputDisputeChanged=(e)=>{
|
||||||
@ -329,25 +338,38 @@ export default class TradeBox extends Component {
|
|||||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => this.setState({badStatement:data.bad_statement})
|
.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(){
|
showInputInvoice(){
|
||||||
return (
|
return (
|
||||||
|
|
||||||
// TODO Option to upload files and images
|
|
||||||
|
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
<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>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="left">
|
<Grid item xs={12} align="left">
|
||||||
<Typography component="body2" variant="body2">
|
<Typography component="body2" variant="body2">
|
||||||
The taker is committed! Before letting you send {" "+ parseFloat(parseFloat(this.props.data.amount).toFixed(4))+
|
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
|
" "+ 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>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -357,14 +379,29 @@ export default class TradeBox extends Component {
|
|||||||
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
|
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
|
||||||
label={"Payout Lightning Invoice"}
|
label={"Payout Lightning Invoice"}
|
||||||
required
|
required
|
||||||
|
value={this.state.invoice}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
style: {textAlign:"center"}
|
style: {textAlign:"center"},
|
||||||
|
maxHeight: 200,
|
||||||
}}
|
}}
|
||||||
multiline
|
multiline
|
||||||
|
minRows={5}
|
||||||
|
maxRows={this.state.qrscanner ? 5 : 14}
|
||||||
onChange={this.handleInputInvoiceChanged}
|
onChange={this.handleInputInvoiceChanged}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{this.state.qrscanner ?
|
||||||
<Grid item xs={12} align="center">
|
<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>
|
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -374,7 +411,24 @@ export default class TradeBox extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Asks the user for a dispute statement.
|
// 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 (
|
return (
|
||||||
|
|
||||||
// TODO Option to upload files
|
// TODO Option to upload files
|
||||||
@ -412,6 +466,25 @@ export default class TradeBox extends Component {
|
|||||||
<Button onClick={this.handleClickSubmitStatementButton} variant='contained' color='primary'>Submit</Button>
|
<Button onClick={this.handleClickSubmitStatementButton} variant='contained' color='primary'>Submit</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{this.showBondIsLocked()}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
}
|
||||||
|
|
||||||
|
showWaitForDisputeResolution=()=>{
|
||||||
|
return (
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid item xs={12} align="center">
|
||||||
|
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||||
|
<b> We have the statements </b>
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} align="left">
|
||||||
|
<Typography component="body2" variant="body2">
|
||||||
|
Wait for the staff to resolve the dispute. The dispute winner
|
||||||
|
will be asked to submit a LN invoice.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
{this.showBondIsLocked()}
|
{this.showBondIsLocked()}
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
@ -427,8 +500,7 @@ export default class TradeBox extends Component {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="body2" variant="body2" align="left">
|
<Typography component="body2" variant="body2" align="left">
|
||||||
<p>We are waiting for the seller to deposit the full trade BTC amount
|
<p>We are waiting for the seller lock the trade amount. </p>
|
||||||
into the escrow.</p>
|
|
||||||
<p> Just hang on for a moment. If the seller does not deposit,
|
<p> Just hang on for a moment. If the seller does not deposit,
|
||||||
you will get your bond back automatically.</p>
|
you will get your bond back automatically.</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -441,6 +513,8 @@ export default class TradeBox extends Component {
|
|||||||
showWaitingForBuyerInvoice(){
|
showWaitingForBuyerInvoice(){
|
||||||
return(
|
return(
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
|
{/* Make confirmation sound for HTLC received. */}
|
||||||
|
<this.Sound soundFileName="locked-invoice"/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="subtitle1" variant="subtitle1">
|
<Typography component="subtitle1" variant="subtitle1">
|
||||||
<b>The trade collateral is locked! 🎉 </b>
|
<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)
|
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => (this.props.data = data));
|
.then((data) => this.props.completeSetState(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRatingChange=(e)=>{
|
handleRatingChange=(e)=>{
|
||||||
@ -484,7 +558,7 @@ handleRatingChange=(e)=>{
|
|||||||
};
|
};
|
||||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => (this.props.data = data));
|
.then((data) => this.props.completeSetState(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
showFiatSentButton(){
|
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(
|
return(
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
|
{/* Make confirmation sound for Chat Open. */}
|
||||||
|
<this.Sound soundFileName="chat-open"/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="subtitle1" variant="subtitle1">
|
<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>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
{this.props.data.isSeller ?
|
{this.props.data.is_seller ?
|
||||||
<Typography component="body2" variant="body2" align="center">
|
<Typography component="body2" variant="body2" align="center">
|
||||||
Say hi! Be helpful and concise. Let them know how to send you {this.props.data.currencyCode}.
|
Say hi! Be helpful and concise. Let them know how to send you {this.props.data.currencyCode}.
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -547,12 +647,11 @@ handleRatingChange=(e)=>{
|
|||||||
<Divider/>
|
<Divider/>
|
||||||
</Grid>
|
</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">
|
<Grid item xs={12} align="center">
|
||||||
{openDisputeButton ? this.showOpenDisputeButton() : ""}
|
{showDisputeButton ? this.showOpenDisputeButton() : ""}
|
||||||
{sendFiatButton ? this.showFiatSentButton() : ""}
|
{showSendButton ? this.showFiatSentButton() : ""}
|
||||||
{receivedFiatButton ? this.showFiatReceivedButton() : ""}
|
{showReveiceButton ? this.showFiatReceivedButton() : ""}
|
||||||
</Grid>
|
</Grid>
|
||||||
{this.showBondIsLocked()}
|
{this.showBondIsLocked()}
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -562,6 +661,8 @@ handleRatingChange=(e)=>{
|
|||||||
showRateSelect(){
|
showRateSelect(){
|
||||||
return(
|
return(
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
|
{/* Make confirmation sound for Chat Open. */}
|
||||||
|
<this.Sound soundFileName="successful"/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="h6" variant="h6">
|
<Typography component="h6" variant="h6">
|
||||||
🎉Trade finished!🥳
|
🎉Trade finished!🥳
|
||||||
@ -569,7 +670,7 @@ handleRatingChange=(e)=>{
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="body2" variant="body2" 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>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1} style={{ width:330}}>
|
<Grid container spacing={1} style={{ width:this.props.width}}>
|
||||||
<this.ConfirmDisputeDialog/>
|
<this.ConfirmDisputeDialog/>
|
||||||
<this.ConfirmFiatReceivedDialog/>
|
<this.ConfirmFiatReceivedDialog/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
|
<MediaQuery minWidth={920}>
|
||||||
<Typography component="h5" variant="h5">
|
<Typography component="h5" variant="h5">
|
||||||
Contract Box
|
Contract Box
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</MediaQuery>
|
||||||
<Paper elevation={12} style={{ padding: 8,}}>
|
<Paper elevation={12} style={{ padding: 8,}}>
|
||||||
{/* Maker and taker Bond request */}
|
{/* 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 */}
|
{/* Waiting for taker and taker bond request */}
|
||||||
{this.props.data.isMaker & this.props.data.statusCode == 1 ? this.showMakerWait() : ""}
|
{this.props.data.is_maker & this.props.data.status == 1 ? this.showMakerWait() : ""}
|
||||||
{this.props.data.isMaker & this.props.data.statusCode == 3 ? this.showTakerFound() : ""}
|
{this.props.data.is_maker & this.props.data.status == 3 ? this.showTakerFound() : ""}
|
||||||
|
|
||||||
{/* Send Invoice (buyer) and deposit collateral (seller) */}
|
{/* Send Invoice (buyer) and deposit collateral (seller) */}
|
||||||
{this.props.data.isSeller & this.props.data.escrowInvoice != null ? this.showEscrowQRInvoice() : ""}
|
{this.props.data.is_seller & (this.props.data.status == 6 || this.props.data.status == 7 ) ? this.showEscrowQRInvoice() : ""}
|
||||||
{this.props.data.isBuyer & this.props.data.invoiceAmount != null ? this.showInputInvoice() : ""}
|
{this.props.data.is_buyer & (this.props.data.status == 6 || this.props.data.status == 8 )? this.showInputInvoice() : ""}
|
||||||
{this.props.data.isBuyer & this.props.data.statusCode == 7 ? this.showWaitingForEscrow() : ""}
|
{this.props.data.is_buyer & this.props.data.status == 7 ? this.showWaitingForEscrow() : ""}
|
||||||
{this.props.data.isSeller & this.props.data.statusCode == 8 ? this.showWaitingForBuyerInvoice() : ""}
|
{this.props.data.is_seller & this.props.data.status == 8 ? this.showWaitingForBuyerInvoice() : ""}
|
||||||
|
|
||||||
{/* In Chatroom - No fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
|
{/* In Chatroom */}
|
||||||
{this.props.data.isBuyer & this.props.data.statusCode == 9 ? this.showChat(true,false,true) : ""}
|
{this.props.data.status == 9 || this.props.data.status == 10 ? this.showChat(): ""}
|
||||||
{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) : ""}
|
|
||||||
|
|
||||||
{/* Trade Finished */}
|
{/* Trade Finished */}
|
||||||
{(this.props.data.isSeller & this.props.data.statusCode > 12 & this.props.data.statusCode < 15) ? this.showRateSelect() : ""}
|
{(this.props.data.is_seller & [13,14,15].includes(this.props.data.status)) ? this.showRateSelect() : ""}
|
||||||
{(this.props.data.isBuyer & this.props.data.statusCode == 14) ? this.showRateSelect() : ""}
|
{(this.props.data.is_buyer & this.props.data.status == 14) ? this.showRateSelect() : ""}
|
||||||
|
|
||||||
{/* Trade Finished - Payment Routing Failed */}
|
{/* 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 */}
|
{/* 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 */}
|
{/* Order has expired */}
|
||||||
{this.props.data.statusCode == 5 ? this.showOrderExpired() : ""}
|
{this.props.data.status == 5 ? this.showOrderExpired() : ""}
|
||||||
{/* TODO */}
|
{/* TODO */}
|
||||||
{/* */}
|
{/* */}
|
||||||
{/* */}
|
{/* */}
|
||||||
|
@ -3,7 +3,8 @@ import { Button , Dialog, Grid, Typography, TextField, ButtonGroup, CircularProg
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import Image from 'material-ui-image'
|
import Image from 'material-ui-image'
|
||||||
import InfoDialog from './InfoDialog'
|
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";
|
import ContentCopy from "@mui/icons-material/ContentCopy";
|
||||||
|
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
@ -29,7 +30,8 @@ export default class UserGenPage extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
token: this.genBase62Token(34),
|
token: this.genBase62Token(34),
|
||||||
openInfo: false,
|
openInfo: false,
|
||||||
showRobosat: true,
|
loadingRobot: true,
|
||||||
|
tokenHasChanged: false,
|
||||||
};
|
};
|
||||||
this.getGeneratedUser(this.state.token);
|
this.getGeneratedUser(this.state.token);
|
||||||
}
|
}
|
||||||
@ -45,7 +47,7 @@ export default class UserGenPage extends Component {
|
|||||||
.substring(0, length);
|
.substring(0, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGeneratedUser(token) {
|
getGeneratedUser=(token)=>{
|
||||||
fetch('/api/user' + '?token=' + token)
|
fetch('/api/user' + '?token=' + token)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@ -56,8 +58,18 @@ export default class UserGenPage extends Component {
|
|||||||
shannon_entropy: data.token_shannon_entropy,
|
shannon_entropy: data.token_shannon_entropy,
|
||||||
bad_request: data.bad_request,
|
bad_request: data.bad_request,
|
||||||
found: data.found,
|
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));
|
.then((data) => console.log(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAnotherButtonPressed=(e)=>{
|
handleClickNewRandomToken=()=>{
|
||||||
this.delGeneratedUser()
|
this.setState({
|
||||||
// this.setState({
|
token: this.genBase62Token(34),
|
||||||
// showRobosat: false,
|
tokenHasChanged: true,
|
||||||
// token: this.genBase62Token(34),
|
});
|
||||||
// });
|
|
||||||
// this.getGeneratedUser(this.state.token);
|
|
||||||
window.location.reload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeToken=(e)=>{
|
handleChangeToken=(e)=>{
|
||||||
this.delGeneratedUser()
|
|
||||||
this.setState({
|
this.setState({
|
||||||
token: e.target.value,
|
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 = () => {
|
handleClickOpenInfo = () => {
|
||||||
@ -107,17 +120,16 @@ export default class UserGenPage extends Component {
|
|||||||
aria-describedby="info-dialog-description"
|
aria-describedby="info-dialog-description"
|
||||||
scroll="paper"
|
scroll="paper"
|
||||||
>
|
>
|
||||||
<InfoDialog/>
|
<InfoDialog handleCloseInfo = {this.handleCloseInfo}/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center" sx={{width:370, height:260}}>
|
||||||
{this.state.showRobosat ?
|
{!this.state.loadingRobot ?
|
||||||
<div>
|
<div>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="h5" variant="h5">
|
<Typography component="h5" variant="h5">
|
||||||
@ -135,7 +147,7 @@ export default class UserGenPage extends Component {
|
|||||||
</div><br/>
|
</div><br/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
: <CircularProgress />}
|
: <CircularProgress sx={{position: 'relative', top: 100, }}/>}
|
||||||
</Grid>
|
</Grid>
|
||||||
{
|
{
|
||||||
this.state.found ?
|
this.state.found ?
|
||||||
@ -149,14 +161,11 @@ export default class UserGenPage extends Component {
|
|||||||
}
|
}
|
||||||
<Grid container align="center">
|
<Grid container align="center">
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.token)}>
|
<TextField sx={{maxWidth: 280}}
|
||||||
<ContentCopy/>
|
|
||||||
</IconButton>
|
|
||||||
<TextField
|
|
||||||
//sx={{ input: { color: 'purple' } }}
|
//sx={{ input: { color: 'purple' } }}
|
||||||
InputLabelProps={{
|
// InputLabelProps={{
|
||||||
style: { color: 'green' },
|
// style: { color: 'green' },
|
||||||
}}
|
// }}
|
||||||
error={this.state.bad_request}
|
error={this.state.bad_request}
|
||||||
label='Store your token safely'
|
label='Store your token safely'
|
||||||
required='true'
|
required='true'
|
||||||
@ -166,18 +175,34 @@ export default class UserGenPage extends Component {
|
|||||||
size='small'
|
size='small'
|
||||||
// multiline = {true}
|
// multiline = {true}
|
||||||
onChange={this.handleChangeToken}
|
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>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<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>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<ButtonGroup variant="contained" aria-label="outlined primary button group">
|
<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>
|
<Button color='inherit' onClick={this.handleClickOpenInfo}>Info</Button>
|
||||||
<this.InfoDialog/>
|
<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>
|
</ButtonGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<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;
|
top: -14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profileNickname {
|
||||||
|
margin: 0;
|
||||||
|
left: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
.newAvatar {
|
.newAvatar {
|
||||||
background-color:white;
|
background-color:white;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -45,3 +50,20 @@ body {
|
|||||||
height: 200px;
|
height: 200px;
|
||||||
width: 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
|
# Configure the periodic tasks
|
||||||
app.conf.beat_schedule = {
|
app.conf.beat_schedule = {
|
||||||
'users-cleansing': { # Cleans abandoned users every hour
|
'users-cleansing': { # Cleans abandoned users every 6 hours
|
||||||
'task': 'users_cleansing',
|
'task': 'users_cleansing',
|
||||||
'schedule': timedelta(hours=1),
|
'schedule': timedelta(hours=6),
|
||||||
},
|
},
|
||||||
'cache-market-prices': { # Cache market prices every minutes for now.
|
'cache-market-prices': { # Cache market prices every minutes for now.
|
||||||
'task': 'cache_external_market_prices',
|
'task': 'cache_external_market_prices',
|
||||||
|
77
setup.md
@ -126,9 +126,86 @@ npm install websocket
|
|||||||
npm install react-countdown
|
npm install react-countdown
|
||||||
npm install @mui/icons-material
|
npm install @mui/icons-material
|
||||||
npm install @mui/x-data-grid
|
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)
|
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
|
### Launch the React render
|
||||||
from frontend/ directory
|
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
|
||||||
|
```
|