Merge pull request #26 from Reckless-Satoshi/lightning-integration

Lightning integration first cycle. Just enough so it works. Fixes #24
This commit is contained in:
Reckless_Satoshi 2022-01-13 09:34:15 +00:00 committed by GitHub
commit fac9a62bcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1463 additions and 308 deletions

View File

@ -1,25 +1,40 @@
# base64 ~/.lnd/tls.cert | tr -d '\n'
LND_CERT_BASE64=''
# base64 ~/.lnd/data/chain/bitcoin/testnet/admin.macaroon | tr -d '\n'
LND_MACAROON_BASE64=''
LND_GRPC_HOST='127.0.0.1:10009'
# Market price public API
MARKET_PRICE_API = 'https://blockchain.info/ticker'
# Host e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
HOST_NAME = ''
# Trade fee in percentage %
FEE = 0.002
# Bond size in percentage %
BOND_SIZE = 0.01
# Time out penalty for canceling takers in MINUTES
PENALTY_TIMEOUT = 2
# Time out penalty for canceling takers in SECONDS
PENALTY_TIMEOUT = 60
# Trade limits in satoshis
MIN_TRADE = 10000
MAX_TRADE = 500000
# Expiration time for HODL invoices and returning collateral in HOURS
BOND_EXPIRY = 8
BOND_EXPIRY = 14
ESCROW_EXPIRY = 8
# Expiration time for locking collateral in MINUTES
# Expiration time for locking collateral in SECONDS
EXP_MAKER_BOND_INVOICE = 300
EXP_TAKER_BOND_INVOICE = 200
EXP_TRADE_ESCR_INVOICE = 200
# Time a order is public in the book HOURS
PUBLIC_ORDER_DURATION = 6
# Time to provide a valid invoice and the trade escrow MINUTES
INVOICE_AND_ESCROW_DURATION = 30
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
FIAT_EXCHANGE_DURATION = 4
# Username for HTLCs escrows
ESCROW_USERNAME = 'admin'

4
.gitignore vendored
View File

@ -641,3 +641,7 @@ frontend/static/frontend/main*
# robosats
frontend/static/assets/avatars*
api/lightning/lightning*
api/lightning/invoices*
api/lightning/router*
api/lightning/googleapis*

View File

@ -26,7 +26,7 @@ class EUserAdmin(UserAdmin):
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('id','type','maker_link','taker_link','status','amount','currency','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_links = ('id','type')
change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow')
change_links = ('maker','taker','buyer_invoice','maker_bond','taker_bond','trade_escrow')
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
@admin.register(LNPayment)

View File

@ -1,110 +0,0 @@
# import codecs, grpc, os
# import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
from datetime import timedelta
from django.utils import timezone
import random
import string
#######
# Placeholder functions
# Should work with LND (maybe c-lightning in the future)
class LNNode():
'''
Place holder functions to interact with Lightning Node
'''
# macaroon = codecs.encode(open('LND_DIR/data/chain/bitcoin/simnet/admin.macaroon', 'rb').read(), 'hex')
# os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
# cert = open('LND_DIR/tls.cert', 'rb').read()
# ssl_creds = grpc.ssl_channel_credentials(cert)
# channel = grpc.secure_channel('localhost:10009', ssl_creds)
# stub = lightningstub.LightningStub(channel)
def gen_hold_invoice(num_satoshis, description, expiry):
'''Generates hold invoice to publish an order'''
# TODO
invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX
payment_hash = ''.join(random.choices(string.ascii_uppercase + string.digits, k=40)) #FIX
expires_at = timezone.now() + timedelta(hours=8) ##FIX
return invoice, payment_hash, expires_at
def validate_hold_invoice_locked(payment_hash):
'''Checks if hodl invoice is locked'''
# request = ln.InvoiceSubscription()
# When invoice is settled, return true. If time expires, return False.
# for invoice in stub.SubscribeInvoices(request):
# print(invoice)
return True
def validate_ln_invoice(invoice, num_satoshis):
'''Checks if the submited LN invoice is as expected'''
# request = lnrpc.PayReqString(pay_req=invoice)
# response = stub.DecodePayReq(request, metadata=[('macaroon', macaroon)])
# # {
# # "destination": <string>,
# # "payment_hash": <string>,
# # "num_satoshis": <int64>,
# # "timestamp": <int64>,
# # "expiry": <int64>,
# # "description": <string>,
# # "description_hash": <string>,
# # "fallback_addr": <string>,
# # "cltv_expiry": <int64>,
# # "route_hints": <array RouteHint>,
# # "payment_addr": <bytes>,
# # "num_msat": <int64>,
# # "features": <array FeaturesEntry>,
# # }
# if not response['num_satoshis'] == num_satoshis:
# return False, {'bad_invoice':f'The invoice provided is not for {num_satoshis}. '}, None, None, None
# description = response['description']
# payment_hash = response['payment_hash']
# expires_at = timezone(response['expiry'])
# if payment_hash and expires_at > timezone.now():
# return True, None, description, payment_hash, expires_at
valid = True
context = None
description = 'Placeholder desc' # TODO decrypt from LN invoice
payment_hash = '567&*GIHU126' # TODO decrypt
expires_at = timezone.now() # TODO decrypt
return valid, context, description, payment_hash, expires_at
def pay_invoice(invoice):
'''Sends sats to buyer, or cancelinvoices'''
return True
def check_if_hold_invoice_is_locked(payment_hash):
'''Every hodl invoice that is in state INVGEN
Has to be checked for payment received until
the window expires'''
return True
def settle_hold_htlcs(payment_hash):
'''Charges a LN hold invoice'''
return True
def return_hold_htlcs(payment_hash):
'''Returns sats'''
return True
def double_check_htlc_is_settled(payment_hash):
''' Just as it sounds. Better safe than sorry!'''
return True

220
api/lightning/node.py Normal file
View File

@ -0,0 +1,220 @@
import grpc, os, hashlib, secrets, json
from . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub
from . import router_pb2 as routerrpc, router_pb2_grpc as routerstub
from decouple import config
from base64 import b64decode
from datetime import timedelta, datetime
from django.utils import timezone
#######
# Should work with LND (c-lightning in the future if there are features that deserve the work)
#######
CERT = b64decode(config('LND_CERT_BASE64'))
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
LND_GRPC_HOST = config('LND_GRPC_HOST')
class LNNode():
os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA'
creds = grpc.ssl_channel_credentials(CERT)
channel = grpc.secure_channel(LND_GRPC_HOST, creds)
lightningstub = lightningstub.LightningStub(channel)
invoicesstub = invoicesstub.InvoicesStub(channel)
routerstub = routerstub.RouterStub(channel)
payment_failure_context = {
0: "Payment isn't failed (yet)",
1: "There are more routes to try, but the payment timeout was exceeded.",
2: "All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
3: "A non-recoverable error has occured.",
4: "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
5: "Insufficient local balance."}
@classmethod
def decode_payreq(cls, invoice):
'''Decodes a lightning payment request (invoice)'''
request = lnrpc.PayReqString(pay_req=invoice)
response = cls.lightningstub.DecodePayReq(request, metadata=[('macaroon', MACAROON.hex())])
return response
@classmethod
def cancel_return_hold_invoice(cls, payment_hash):
'''Cancels or returns a hold invoice'''
request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.CancelInvoice(request, metadata=[('macaroon', MACAROON.hex())])
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
return str(response) == "" # True if no response, false otherwise.
@classmethod
def settle_hold_invoice(cls, preimage):
'''settles a hold invoice'''
request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
response = cls.invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
# Fix this: tricky because settling sucessfully an invoice has None response. TODO
return str(response)=="" # True if no response, false otherwise.
@classmethod
def gen_hold_invoice(cls, num_satoshis, description, expiry):
'''Generates hold invoice'''
hold_payment = {}
# The preimage is a random hash of 256 bits entropy
preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest()
# Its hash is used to generate the hold invoice
r_hash = hashlib.sha256(preimage).digest()
request = invoicesrpc.AddHoldInvoiceRequest(
memo=description,
value=num_satoshis,
hash=r_hash,
expiry=expiry)
response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())])
hold_payment['invoice'] = response.payment_request
payreq_decoded = cls.decode_payreq(hold_payment['invoice'])
hold_payment['preimage'] = preimage.hex()
hold_payment['payment_hash'] = payreq_decoded.payment_hash
hold_payment['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
hold_payment['expires_at'] = hold_payment['created_at'] + timedelta(seconds=payreq_decoded.expiry)
return hold_payment
@classmethod
def validate_hold_invoice_locked(cls, payment_hash):
'''Checks if hold invoice is locked'''
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
print('status here')
print(response.state)
# TODO ERROR HANDLING
if response.state == 0: # OPEN
print('STATUS: OPEN')
pass
if response.state == 1: # SETTLED
pass
if response.state == 2: # CANCELLED
pass
if response.state == 3: # ACCEPTED (LOCKED)
print('STATUS: ACCEPTED')
return True
@classmethod
def check_until_invoice_locked(cls, payment_hash, expiration):
'''Checks until hold invoice is locked.
When invoice is locked, returns true.
If time expires, return False.'''
# Experimental, might need asyncio. Best if subscribing all invoices and running a background task
# Maybe best to pass LNpayment object and change status live.
request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
print(invoice)
if timezone.now > expiration:
break
if invoice.state == 3: # True if hold invoice is accepted.
return True
return False
@classmethod
def validate_ln_invoice(cls, invoice, num_satoshis):
'''Checks if the submited LN invoice comforms to expectations'''
buyer_invoice = {
'valid': False,
'context': None,
'description': None,
'payment_hash': None,
'created_at': None,
'expires_at': None,
}
try:
payreq_decoded = cls.decode_payreq(invoice)
print(payreq_decoded)
except:
buyer_invoice['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
return buyer_invoice
if payreq_decoded.num_satoshis == 0:
buyer_invoice['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
return buyer_invoice
if not payreq_decoded.num_satoshis == num_satoshis:
buyer_invoice['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
return buyer_invoice
buyer_invoice['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
buyer_invoice['expires_at'] = buyer_invoice['created_at'] + timedelta(seconds=payreq_decoded.expiry)
if buyer_invoice['expires_at'] < timezone.now():
buyer_invoice['context'] = {'bad_invoice':f'The invoice provided has already expired'}
return buyer_invoice
buyer_invoice['valid'] = True
buyer_invoice['description'] = payreq_decoded.description
buyer_invoice['payment_hash'] = payreq_decoded.payment_hash
return buyer_invoice
@classmethod
def pay_invoice(cls, invoice, num_satoshis):
'''Sends sats to buyer'''
fee_limit_sat = max(num_satoshis * 0.0002, 10) # 200 ppm or 10 sats
request = routerrpc.SendPaymentRequest(
payment_request=invoice,
fee_limit_sat=fee_limit_sat,
timeout_seconds=60)
for response in cls.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
print(response)
print(response.status)
# TODO ERROR HANDLING
if response.status == 0 : # Status 0 'UNKNOWN'
pass
if response.status == 1 : # Status 1 'IN_FLIGHT'
return True, 'In flight'
if response.status == 3 : # 4 'FAILED' ??
'''0 Payment isn't failed (yet).
1 There are more routes to try, but the payment timeout was exceeded.
2 All possible routes were tried and failed permanently. Or were no routes to the destination at all.
3 A non-recoverable error has occured.
4 Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)
5 Insufficient local balance.
'''
context = cls.payment_failure_context[response.failure_reason]
return False, context
if response.status == 2 : # STATUS 'SUCCEEDED'
return True, None
# How to catch the errors like:"grpc_message":"invoice is already paid","grpc_status":6}
# These are not in the response only printed to commandline
return False
@classmethod
def double_check_htlc_is_settled(cls, payment_hash):
''' Just as it sounds. Better safe than sorry!'''
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
return response.state == 1 # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled/returned

View File

@ -1,11 +1,13 @@
from datetime import timedelta
from datetime import time, timedelta
from django.utils import timezone
from .lightning import LNNode
from .lightning.node import LNNode
from .models import Order, LNPayment, MarketTick, User
from decouple import config
from .utils import get_exchange_rate
import math
FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE'))
MARKET_PRICE_API = config('MARKET_PRICE_API')
@ -17,11 +19,13 @@ MAX_TRADE = int(config('MAX_TRADE'))
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE'))
EXP_TRADE_ESCR_INVOICE = int(config('EXP_TRADE_ESCR_INVOICE'))
BOND_EXPIRY = int(config('BOND_EXPIRY'))
ESCROW_EXPIRY = int(config('ESCROW_EXPIRY'))
PUBLIC_ORDER_DURATION = int(config('PUBLIC_ORDER_DURATION'))
INVOICE_AND_ESCROW_DURATION = int(config('INVOICE_AND_ESCROW_DURATION'))
FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
class Logics():
@ -51,6 +55,7 @@ class Logics():
else:
order.taker = user
order.status = Order.Status.TAK
order.expires_at = timezone.now() + timedelta(minutes=EXP_TAKER_BOND_INVOICE)
order.save()
return True, None
@ -80,22 +85,43 @@ class Logics():
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
if not order.is_explicit:
premium = order.premium
price = exchange_rate
price = exchange_rate * (1+float(premium)/100)
else:
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
order_rate = float(order.amount) / (float(order.satoshis) / 100000000)
premium = order_rate / exchange_rate - 1
premium = int(premium*100) # 2 decimals left
price = order_rate
premium = int(premium*100) # 2 decimals left
significant_digits = 6
price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1)
return price, premium
def order_expires(order):
''' General case when time runs out. Only
used when the maker does not lock a publishing bond'''
order.status = Order.Status.EXP
order.maker = None
order.taker = None
order.save()
def kick_taker(order):
''' The taker did not lock the taker_bond. Now he has to go'''
# Add a time out to the taker
profile = order.taker.profile
profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
profile.save()
# Delete the taker_bond payment request, and make order public again
if LNNode.cancel_return_hold_invoice(order.taker_bond.payment_hash):
order.status = Order.Status.PUB
order.taker = None
order.taker_bond = None
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION) ## TO FIX. Restore the remaining order durantion, not all of it!
order.save()
return True
@classmethod
def buyer_invoice_amount(cls, order, user):
''' Computes buyer invoice amount. Uses order.last_satoshis,
@ -118,9 +144,10 @@ class Logics():
return False, {'bad_request':'You cannot a invoice while bonds are not posted.'}
num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount']
valid, context, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice, num_satoshis)
if not valid:
return False, context
buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis)
if not buyer_invoice['valid']:
return False, buyer_invoice['context']
order.buyer_invoice, _ = LNPayment.objects.update_or_create(
concept = LNPayment.Concepts.PAYBUYER,
@ -132,48 +159,44 @@ class Logics():
'invoice' : invoice,
'status' : LNPayment.Status.VALIDI,
'num_satoshis' : num_satoshis,
'description' : description,
'payment_hash' : payment_hash,
'expires_at' : expires_at}
'description' : buyer_invoice['description'],
'payment_hash' : buyer_invoice['payment_hash'],
'created_at' : buyer_invoice['created_at'],
'expires_at' : buyer_invoice['expires_at']}
)
# If the order status is 'Waiting for invoice'. Move forward to 'waiting for invoice'
if order.status == Order.Status.WFE: order.status = Order.Status.CHA
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
if order.status == Order.Status.WFI:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION)
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' or to 'chat'
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2:
print(order.trade_escrow)
if order.trade_escrow:
if order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA
# If the escrow is lock move to Chat.
if order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION)
else:
order.status = Order.Status.WFE
order.save()
return True, None
@classmethod
def rate_counterparty(cls, order, user, rating):
def add_profile_rating(profile, rating):
''' adds a new rating to a user profile'''
# If the trade is finished
if order.status > Order.Status.PAY:
profile.total_ratings = profile.total_ratings + 1
latest_ratings = profile.latest_ratings
if len(latest_ratings) <= 1:
profile.latest_ratings = [rating]
profile.avg_rating = rating
# if maker, rates taker
if order.maker == user:
order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1
last_ratings = list(order.taker.profile.last_ratings).append(rating)
order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
# if taker, rates maker
if order.taker == user:
order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1
last_ratings = list(order.maker.profile.last_ratings).append(rating)
order.maker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
else:
return False, {'bad_request':'You cannot rate your counterparty yet.'}
latest_ratings = list(latest_ratings).append(rating)
profile.latest_ratings = latest_ratings
profile.avg_rating = sum(latest_ratings) / len(latest_ratings)
order.save()
return True, None
profile.save()
def is_penalized(user):
''' Checks if a user that is not participant of orders
@ -200,11 +223,15 @@ class Logics():
return True, None
# 2) When maker cancels after bond
'''The order dissapears from book and goes to cancelled.
Maker is charged the bond to prevent DDOS
on the LN node and order book. TODO Only charge a small part
of the bond (requires maker submitting an invoice)'''
'''The order dissapears from book and goes to cancelled. Maker is charged the bond to prevent DDOS
on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)'''
elif order.status == Order.Status.PUB and order.maker == user:
#Settle the maker bond (Maker loses the bond for cancelling public order)
if cls.settle_maker_bond(order):
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
# 3) When taker cancels before bond
''' The order goes back to the book as public.
@ -212,7 +239,7 @@ class Logics():
elif order.status == Order.Status.TAK and order.taker == user:
# adds a timeout penalty
user.profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
user.save()
user.profile.save()
order.taker = None
order.status = Order.Status.PUB
@ -225,6 +252,29 @@ class Logics():
The order goes into the public book if taker cancels.
In both cases there is a small fee.'''
# 4.a) When maker cancel after bond (before escrow)
'''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.PUB and order.status < Order.Status.CHA and order.maker == user:
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_maker_bond(order)
if valid:
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
# 4.b) When taker cancel after bond (before escrow)
'''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.TAK and order.status < Order.Status.CHA and order.taker == user:
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_taker_bond(order)
if valid:
order.taker = None
order.status = Order.Status.PUB
# order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard.
order.save()
return True, None
# 5) When trade collateral has been posted (after escrow)
'''Always goes to cancelled status. Collaboration is needed.
When a user asks for cancel, 'order.is_pending_cancel' goes True.
@ -234,135 +284,211 @@ class Logics():
else:
return False, {'bad_request':'You cannot cancel this order'}
@classmethod
def is_maker_bond_locked(cls, order):
if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
order.maker_bond.status = LNPayment.Status.LOCKED
order.maker_bond.save()
order.status = Order.Status.PUB
# With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION)
order.save()
return True
return False
@classmethod
def gen_maker_hold_invoice(cls, order, user):
# Do not gen and cancel if order is more than 5 minutes old
# Do not gen and cancel if order is older than expiry time
if order.expires_at < timezone.now():
cls.order_expires(order)
return False, {'bad_request':'Invoice expired. You did not confirm publishing the order in time. Make a new order.'}
# Return the previous invoice if there was one and is still unpaid
if order.maker_bond:
if order.maker_bond.status == LNPayment.Status.INVGEN:
return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis}
else:
if cls.is_maker_bond_locked(order):
return False, None
elif order.maker_bond.status == LNPayment.Status.INVGEN:
return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis}
# If there was no maker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.'
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
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
order.maker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.MAKEBOND,
type = LNPayment.Types.hold,
type = LNPayment.Types.HOLD,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
invoice = hold_payment['invoice'],
preimage = hold_payment['preimage'],
status = LNPayment.Status.INVGEN,
num_satoshis = bond_satoshis,
description = description,
payment_hash = payment_hash,
expires_at = expires_at)
payment_hash = hold_payment['payment_hash'],
created_at = hold_payment['created_at'],
expires_at = hold_payment['expires_at'])
order.save()
return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis}
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
@classmethod
def is_taker_bond_locked(cls, order):
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
# (This is the last update to "last_satoshis", it becomes the escrow amount next!)
order.last_satoshis = cls.satoshis_now(order)
order.taker_bond.status = LNPayment.Status.LOCKED
order.taker_bond.save()
# Log a market tick
MarketTick.log_a_tick(order)
# With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(minutes=INVOICE_AND_ESCROW_DURATION)
order.status = Order.Status.WF2
order.save()
return True
return False
@classmethod
def gen_taker_hold_invoice(cls, order, user):
# Do not gen and cancel if a taker invoice is there and older than X minutes and unpaid still
# Do not gen and kick out the taker if order is older than expiry time
if order.expires_at < timezone.now():
cls.kick_taker(order)
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
# Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting.
if order.taker_bond:
# Check if status is INVGEN and still not expired
if order.taker_bond.status == LNPayment.Status.INVGEN:
if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)):
cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
# Return the previous invoice there was with INVGEN status
else:
return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
# Invoice exists, but was already locked or settled
else:
if cls.is_taker_bond_locked(order):
return False, None
elif order.taker_bond.status == LNPayment.Status.INVGEN:
return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE
# If there was no taker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat.'
description = f"RoboSats - Taking '{str(order)}' - 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
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
order.taker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.TAKEBOND,
type = LNPayment.Types.hold,
type = LNPayment.Types.HOLD,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
invoice = hold_payment['invoice'],
preimage = hold_payment['preimage'],
status = LNPayment.Status.INVGEN,
num_satoshis = bond_satoshis,
description = description,
payment_hash = payment_hash,
expires_at = expires_at)
payment_hash = hold_payment['payment_hash'],
created_at = hold_payment['created_at'],
expires_at = hold_payment['expires_at'])
# Extend expiry time to allow for escrow deposit
## Not here, on func for confirming taker collar. order.expires_at = timezone.now() + timedelta(minutes=EXP_TRADE_ESCR_INVOICE)
order.save()
return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis}
return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
@classmethod
def is_trade_escrow_locked(cls, order):
if LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
order.trade_escrow.status = LNPayment.Status.LOCKED
order.trade_escrow.save()
# If status is 'Waiting for both' move to Waiting for invoice
if order.status == Order.Status.WF2:
order.status = Order.Status.WFI
# If status is 'Waiting for invoice' move to Chat
elif order.status == Order.Status.WFE:
order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION)
order.save()
return True
return False
@classmethod
def gen_escrow_hold_invoice(cls, order, user):
# Do not generate and cancel if an invoice is there and older than X minutes and unpaid still
# Do not generate if escrow deposit time has expired
if order.expires_at < timezone.now():
cls.cancel_order(order,user)
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.
if order.trade_escrow:
# Check if status is INVGEN and still not expired
if order.trade_escrow.status == LNPayment.Status.INVGEN:
if order.trade_escrow.created_at > (timezone.now()+timedelta(minutes=EXP_TRADE_ESCR_INVOICE)): # Expired
cls.cancel_order(order, user, 4) # State 4, cancel order before trade escrow locked
return False, {'bad_request':'Invoice expired. You did not lock the trade escrow in time.'}
# Return the previous invoice there was with INVGEN status
else:
return True, {'escrow_invoice': order.trade_escrow.invoice, 'escrow_satoshis':order.trade_escrow.num_satoshis}
# Invoice exists, but was already locked or settled
else:
return False, None # Does not return any context of a healthy locked escrow
if cls.is_trade_escrow_locked(order):
return False, None
elif order.trade_escrow.status == LNPayment.Status.INVGEN:
return True, {'escrow_invoice':order.trade_escrow.invoice, 'escrow_satoshis':order.trade_escrow.num_satoshis}
escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis)
description = f'RoboSats - Escrow amount for {str(order)} - This escrow will be released to the buyer once you confirm you received the fiat.'
# If there was no taker_bond object yet, generates one
escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked
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
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
order.trade_escrow = LNPayment.objects.create(
concept = LNPayment.Concepts.TRESCROW,
type = LNPayment.Types.hold,
type = LNPayment.Types.HOLD,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
invoice = hold_payment['invoice'],
preimage = hold_payment['preimage'],
status = LNPayment.Status.INVGEN,
num_satoshis = escrow_satoshis,
description = description,
payment_hash = payment_hash,
expires_at = expires_at)
payment_hash = hold_payment['payment_hash'],
created_at = hold_payment['created_at'],
expires_at = hold_payment['expires_at'])
order.save()
return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis}
return True, {'escrow_invoice':hold_payment['invoice'],'escrow_satoshis': escrow_satoshis}
def settle_escrow(order):
''' Settles the trade escrow HTLC'''
''' Settles the trade escrow hold invoice'''
# TODO ERROR HANDLING
if LNNode.settle_hold_invoice(order.trade_escrow.preimage):
order.trade_escrow.status = LNPayment.Status.SETLED
order.trade_escrow.save()
return True
valid = LNNode.settle_hold_htlcs(order.trade_escrow.payment_hash)
return valid
def settle_maker_bond(order):
''' Settles the maker bond hold invoice'''
# TODO ERROR HANDLING
if LNNode.settle_hold_invoice(order.maker_bond.preimage):
order.maker_bond.status = LNPayment.Status.SETLED
order.maker_bond.save()
return True
def settle_taker_bond(order):
''' Settles the taker bond hold invoice'''
# TODO ERROR HANDLING
if LNNode.settle_hold_invoice(order.taker_bond.preimage):
order.taker_bond.status = LNPayment.Status.SETLED
order.taker_bond.save()
return True
def return_bond(bond):
'''returns a bond'''
if LNNode.cancel_return_hold_invoice(bond.payment_hash):
bond.status = LNPayment.Status.RETNED
return True
def pay_buyer_invoice(order):
''' Settles the trade escrow HTLC'''
''' Pay buyer invoice'''
# TODO ERROR HANDLING
valid = LNNode.pay_invoice(order.buyer_invoice.payment_hash)
return valid
if LNNode.pay_invoice(order.buyer_invoice.invoice, order.buyer_invoice.num_satoshis):
return True
@classmethod
def confirm_fiat(cls, order, user):
@ -385,16 +511,42 @@ class Logics():
return False, {'bad_request':'You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer.'}
# Make sure the trade escrow is at least as big as the buyer invoice
if order.trade_escrow.num_satoshis > order.buyer_invoice.num_satoshis:
if order.trade_escrow.num_satoshis <= order.buyer_invoice.num_satoshis:
return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'}
# Double check the escrow is settled.
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
if cls.pay_buyer_invoice(order): ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
order.status = Order.Status.PAY
order.buyer_invoice.status = LNPayment.Status.PAYING
is_payed, context = cls.pay_buyer_invoice(order) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
if is_payed:
order.status = Order.Status.SUC
order.buyer_invoice.status = LNPayment.Status.SUCCED
order.expires_at = timezone.now() + timedelta(days=1) # One day to rate / see this order.
order.save()
# RETURN THE BONDS
cls.return_bond(order.taker_bond)
cls.return_bond(order.maker_bond)
else:
# error handling here
pass
else:
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
order.save()
return True, None
@classmethod
def rate_counterparty(cls, order, user, rating):
# If the trade is finished
if order.status > Order.Status.PAY:
# if maker, rates taker
if order.maker == user:
cls.add_profile_rating(order.taker.profile, rating)
# if taker, rates maker
if order.taker == user:
cls.add_profile_rating(order.maker.profile, rating)
else:
return False, {'bad_request':'You cannot rate your counterparty yet.'}
return True, None

View File

@ -18,8 +18,8 @@ BOND_SIZE = float(config('BOND_SIZE'))
class LNPayment(models.Model):
class Types(models.IntegerChoices):
NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hold)
hold = 1, 'hold invoice'
NORM = 0, 'Regular invoice' # Only outgoing buyer payment will be a regular invoice (Non-hold)
HOLD = 1, 'hold invoice'
class Concepts(models.IntegerChoices):
MAKEBOND = 0, 'Maker bond'
@ -32,24 +32,27 @@ class LNPayment(models.Model):
LOCKED = 1, 'Locked'
SETLED = 2, 'Settled'
RETNED = 3, 'Returned'
MISSNG = 4, 'Missing'
EXPIRE = 4, 'Expired'
VALIDI = 5, 'Valid'
PAYING = 6, 'Paying ongoing'
FAILRO = 7, 'Failed routing'
FLIGHT = 6, 'In flight'
SUCCED = 7, 'Succeeded'
FAILRO = 8, 'Routing failed'
# 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)
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
# payment info
invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
payment_hash = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
description = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
invoice = models.CharField(max_length=1200, unique=True, null=True, default=None, blank=True) # Some invoices with lots of routing hints might be long
payment_hash = models.CharField(max_length=100, unique=True, null=True, default=None, blank=True)
preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True)
description = models.CharField(max_length=500, unique=False, null=True, default=None, blank=True)
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
created_at = models.DateTimeField()
expires_at = models.DateTimeField()
# involved parties
sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None)
@ -79,7 +82,7 @@ class Order(models.Model):
DIS = 11, 'In dispute'
CCA = 12, 'Collaboratively cancelled'
PAY = 13, 'Sending satoshis to buyer'
SUC = 14, 'Sucessfully settled'
SUC = 14, 'Sucessful trade'
FAI = 15, 'Failed lightning network routing'
MLD = 16, 'Maker lost dispute'
TLD = 17, 'Taker lost dispute'

View File

@ -12,6 +12,6 @@ class MakeOrderSerializer(serializers.ModelSerializer):
fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis')
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=300, allow_null=True, allow_blank=True, default=None)
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None)
action = serializers.ChoiceField(choices=('take','update_invoice','dispute','cancel','confirm','rate'), allow_null=False)
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)

View File

@ -1,11 +1,10 @@
import requests, ring, os
from decouple import config
import requests
import ring
storage = {}
market_cache = {}
@ring.dict(storage, expire=30) #keeps in cache for 30 seconds
@ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds
def get_exchange_rate(currency):
# TODO Add fallback Public APIs and error handling
# Think about polling price data in a different way (e.g. store locally every t seconds)
@ -13,4 +12,25 @@ def get_exchange_rate(currency):
market_prices = requests.get(config('MARKET_PRICE_API')).json()
exchange_rate = float(market_prices[currency]['last'])
return exchange_rate
return exchange_rate
lnd_v_cache = {}
@ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds
def get_lnd_version():
stream = os.popen('lnd --version')
lnd_version = stream.read()[:-1]
return lnd_version
robosats_commit_cache = {}
@ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats():
stream = os.popen('git log -n 1 --pretty=format:"%H"')
lnd_version = stream.read()
return lnd_version

View File

@ -1,4 +1,5 @@
from re import T
from django.db.models import query
from rest_framework import status, viewsets
from rest_framework.generics import CreateAPIView, ListAPIView
from rest_framework.views import APIView
@ -8,8 +9,9 @@ from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
from .models import LNPayment, Order
from .models import LNPayment, MarketTick, Order
from .logics import Logics
from .utils import get_lnd_version, get_commit_robosats
from .nick_generator.nick_generator import NickGenerator
from robohash import Robohash
@ -18,7 +20,7 @@ from math import log2
import numpy as np
import hashlib
from pathlib import Path
from datetime import timedelta
from datetime import timedelta, datetime
from django.utils import timezone
from decouple import config
@ -58,7 +60,7 @@ class MakerView(CreateAPIView):
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
expires_at=timezone.now()+timedelta(minutes=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
expires_at=timezone.now()+timedelta(seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
maker=request.user)
# TODO move to Order class method when new instance is created!
@ -93,11 +95,11 @@ class OrderView(viewsets.ViewSet):
# This is our order.
order = order[0]
# 1) If order expired
# 1) If order has expired
if order.status == Order.Status.EXP:
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
# 2) If order cancelled
# 2) If order has been cancelled
if order.status == Order.Status.UCA:
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
if order.status == Order.Status.CCA:
@ -105,7 +107,7 @@ class OrderView(viewsets.ViewSet):
data = ListOrderSerializer(order).data
# 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)
if is_penalized:
data['penalty'] = time_out
@ -123,7 +125,7 @@ class OrderView(viewsets.ViewSet):
elif not data['is_participant'] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK)
# For participants add position side, nicks and status as message
# For participants add positions, nicks and status as a message
data['is_buyer'] = Logics.is_buyer(order,request.user)
data['is_seller'] = Logics.is_seller(order,request.user)
data['maker_nick'] = str(order.maker)
@ -132,10 +134,10 @@ class OrderView(viewsets.ViewSet):
data['is_fiat_sent'] = order.is_fiat_sent
data['is_disputed'] = order.is_disputed
# If both bonds are locked, participants can see the trade in sats is also final.
# If both bonds are locked, participants can see the final trade amount in sats.
if order.taker_bond:
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
# Seller sees the amount he pays
# Seller sees the amount he sends
if data['is_seller']:
data['trade_satoshis'] = order.last_satoshis
# Buyer sees the amount he receives
@ -180,8 +182,10 @@ class OrderView(viewsets.ViewSet):
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 8) If status is 'CHA'or '' or '' and all HTLCS are in LOCKED
elif order.status == Order.Status.CHA: # TODO Add the other status
# 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
# If all bonds are locked.
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
# add whether a collaborative cancel is pending
data['pending_cancel'] = order.is_pending_cancel
@ -191,7 +195,7 @@ class OrderView(viewsets.ViewSet):
def take_update_confirm_dispute_cancel(self, request, format=None):
'''
Here take place all of the user updates to the order object.
Here takes place all of updatesto the order object.
That is: take, confim, cancel, dispute, update_invoice or rate.
'''
order_id = request.GET.get(self.lookup_url_kwarg)
@ -206,7 +210,7 @@ class OrderView(viewsets.ViewSet):
invoice = serializer.data.get('invoice')
rating = serializer.data.get('rating')
# 1) If action is take, it is be taker request!
# 1) If action is take, it is a taker request!
if action == 'take':
if order.status == Order.Status.PUB:
valid, context = Logics.validate_already_maker_or_taker(request.user)
@ -251,7 +255,7 @@ class OrderView(viewsets.ViewSet):
return Response(
{'bad_request':
'The Robotic Satoshis working in the warehouse did not understand you. ' +
'Please, fill a Bug Issue in Github https://github.com/Reckless-Satoshi/robosats/issues'},
'Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues'},
status.HTTP_501_NOT_IMPLEMENTED)
return self.get(request)
@ -275,6 +279,16 @@ class UserView(APIView):
- Creates login credentials (new User object)
Response with Avatar and Nickname.
'''
# if request.user.id:
# context = {}
# context['nickname'] = request.user.username
# participant = not Logics.validate_already_maker_or_taker(request.user)
# context['bad_request'] = f'You are already logged in as {request.user}'
# if participant:
# context['bad_request'] = f'You are already logged in as as {request.user} and have an active order'
# return Response(context,status.HTTP_200_OK)
token = request.GET.get(self.lookup_url_kwarg)
# Compute token entropy
@ -392,10 +406,34 @@ class InfoView(ListAPIView):
context = {}
context['num_public_buy_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB))
context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB))
context['last_day_avg_btc_premium'] = None # Todo
context['num_active_robots'] = None
context['total_volume'] = None
context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB))
# Number of active users (logged in in last 30 minutes)
active_user_time_range = (timezone.now() - timedelta(minutes=30), timezone.now())
context['num_active_robotsats'] = len(User.objects.filter(last_login__range=active_user_time_range))
# Compute average premium and volume of today
today = datetime.today()
queryset = MarketTick.objects.filter(timestamp__day=today.day)
if not len(queryset) == 0:
premiums = []
volumes = []
for tick in queryset:
premiums.append(tick.premium)
volumes.append(tick.volume)
avg_premium = sum(premiums) / len(premiums)
total_volume = sum(volumes)
else:
avg_premium = None
total_volume = None
context['today_avg_nonkyc_btc_premium'] = avg_premium
context['today_total_volume'] = total_volume
context['lnd_version'] = get_lnd_version()
context['robosats_running_commit_hash'] = get_commit_robosats()
context['fee'] = FEE
context['bond_size'] = float(config('BOND_SIZE'))
return Response(context, status.HTTP_200_OK)

View File

@ -2106,6 +2106,14 @@
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
"requires": {
"@types/ms": "*"
}
},
"@types/eslint": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz",
@ -2140,6 +2148,14 @@
"@types/node": "*"
}
},
"@types/hast": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
"integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
"requires": {
"@types/unist": "*"
}
},
"@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@ -2167,6 +2183,24 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
"integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==",
"requires": {
"@types/unist": "*"
}
},
"@types/mdurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA=="
},
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
},
"@types/node": {
"version": "17.0.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.6.tgz",
@ -2220,6 +2254,11 @@
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"@types/unist": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
},
"@types/yargs": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@ -2711,6 +2750,11 @@
"babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0"
}
},
"bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
"integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2946,6 +2990,11 @@
}
}
},
"character-entities": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.1.tgz",
"integrity": "sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ=="
},
"chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@ -3069,6 +3118,11 @@
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
},
"comma-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz",
"integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg=="
},
"command-exists": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
@ -3283,6 +3337,14 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decode-named-character-reference": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz",
"integrity": "sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w==",
"requires": {
"character-entities": "^2.0.0"
}
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@ -3356,11 +3418,21 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"diff": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
},
"dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@ -3634,6 +3706,11 @@
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extend-shallow": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@ -3974,6 +4051,11 @@
}
}
},
"hast-util-whitespace": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz",
"integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg=="
},
"hermes-engine": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.9.0.tgz",
@ -4100,6 +4182,11 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"inline-style-parser": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
},
"interpret": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
@ -4223,6 +4310,11 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"is-plain-obj": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz",
"integrity": "sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw=="
},
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -4963,11 +5055,78 @@
"prop-types": "^15.5.8"
}
},
"mdast-util-definitions": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz",
"integrity": "sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ==",
"requires": {
"@types/mdast": "^3.0.0",
"@types/unist": "^2.0.0",
"unist-util-visit": "^3.0.0"
},
"dependencies": {
"unist-util-visit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz",
"integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0",
"unist-util-visit-parents": "^4.0.0"
}
}
}
},
"mdast-util-from-markdown": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz",
"integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==",
"requires": {
"@types/mdast": "^3.0.0",
"@types/unist": "^2.0.0",
"decode-named-character-reference": "^1.0.0",
"mdast-util-to-string": "^3.1.0",
"micromark": "^3.0.0",
"micromark-util-decode-numeric-character-reference": "^1.0.0",
"micromark-util-decode-string": "^1.0.0",
"micromark-util-normalize-identifier": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0",
"unist-util-stringify-position": "^3.0.0",
"uvu": "^0.5.0"
}
},
"mdast-util-to-hast": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-11.3.0.tgz",
"integrity": "sha512-4o3Cli3hXPmm1LhB+6rqhfsIUBjnKFlIUZvudaermXB+4/KONdd/W4saWWkC+LBLbPMqhFSSTSRgafHsT5fVJw==",
"requires": {
"@types/hast": "^2.0.0",
"@types/mdast": "^3.0.0",
"@types/mdurl": "^1.0.0",
"mdast-util-definitions": "^5.0.0",
"mdurl": "^1.0.0",
"unist-builder": "^3.0.0",
"unist-util-generated": "^2.0.0",
"unist-util-position": "^4.0.0",
"unist-util-visit": "^4.0.0"
}
},
"mdast-util-to-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz",
"integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA=="
},
"mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -5436,6 +5595,218 @@
"nullthrows": "^1.1.1"
}
},
"micromark": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.10.tgz",
"integrity": "sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==",
"requires": {
"@types/debug": "^4.0.0",
"debug": "^4.0.0",
"decode-named-character-reference": "^1.0.0",
"micromark-core-commonmark": "^1.0.1",
"micromark-factory-space": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-chunked": "^1.0.0",
"micromark-util-combine-extensions": "^1.0.0",
"micromark-util-decode-numeric-character-reference": "^1.0.0",
"micromark-util-encode": "^1.0.0",
"micromark-util-normalize-identifier": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0",
"micromark-util-sanitize-uri": "^1.0.0",
"micromark-util-subtokenize": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.1",
"uvu": "^0.5.0"
}
},
"micromark-core-commonmark": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz",
"integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==",
"requires": {
"decode-named-character-reference": "^1.0.0",
"micromark-factory-destination": "^1.0.0",
"micromark-factory-label": "^1.0.0",
"micromark-factory-space": "^1.0.0",
"micromark-factory-title": "^1.0.0",
"micromark-factory-whitespace": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-chunked": "^1.0.0",
"micromark-util-classify-character": "^1.0.0",
"micromark-util-html-tag-name": "^1.0.0",
"micromark-util-normalize-identifier": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0",
"micromark-util-subtokenize": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.1",
"uvu": "^0.5.0"
}
},
"micromark-factory-destination": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz",
"integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-factory-label": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz",
"integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0",
"uvu": "^0.5.0"
}
},
"micromark-factory-space": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz",
"integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-factory-title": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz",
"integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==",
"requires": {
"micromark-factory-space": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0",
"uvu": "^0.5.0"
}
},
"micromark-factory-whitespace": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz",
"integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==",
"requires": {
"micromark-factory-space": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-character": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz",
"integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==",
"requires": {
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-chunked": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz",
"integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==",
"requires": {
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-classify-character": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz",
"integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-combine-extensions": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz",
"integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==",
"requires": {
"micromark-util-chunked": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-decode-numeric-character-reference": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz",
"integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==",
"requires": {
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-decode-string": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz",
"integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==",
"requires": {
"decode-named-character-reference": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-decode-numeric-character-reference": "^1.0.0",
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-encode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz",
"integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA=="
},
"micromark-util-html-tag-name": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz",
"integrity": "sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g=="
},
"micromark-util-normalize-identifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz",
"integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==",
"requires": {
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-resolve-all": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz",
"integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==",
"requires": {
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-sanitize-uri": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz",
"integrity": "sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-encode": "^1.0.0",
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-subtokenize": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz",
"integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==",
"requires": {
"micromark-util-chunked": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0",
"uvu": "^0.5.0"
}
},
"micromark-util-symbol": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz",
"integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ=="
},
"micromark-util-types": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz",
"integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w=="
},
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@ -5518,6 +5889,11 @@
"minimist": "^1.2.5"
}
},
"mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -6002,6 +6378,11 @@
}
}
},
"property-information": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
"integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -6078,6 +6459,27 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"react-markdown": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-7.1.2.tgz",
"integrity": "sha512-ibMcc0EbfmbwApqJD8AUr0yls8BSrKzIbHaUsPidQljxToCqFh34nwtu3CXNEItcVJNzpjDHrhK8A+MAh2JW3A==",
"requires": {
"@types/hast": "^2.0.0",
"@types/unist": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^2.0.0",
"prop-types": "^15.0.0",
"property-information": "^6.0.0",
"react-is": "^17.0.0",
"remark-parse": "^10.0.0",
"remark-rehype": "^9.0.0",
"space-separated-tokens": "^2.0.0",
"style-to-object": "^0.3.0",
"unified": "^10.0.0",
"unist-util-visit": "^4.0.0",
"vfile": "^5.0.0"
}
},
"react-native": {
"version": "0.66.4",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.66.4.tgz",
@ -6509,6 +6911,27 @@
}
}
},
"remark-parse": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz",
"integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==",
"requires": {
"@types/mdast": "^3.0.0",
"mdast-util-from-markdown": "^1.0.0",
"unified": "^10.0.0"
}
},
"remark-rehype": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-9.1.0.tgz",
"integrity": "sha512-oLa6YmgAYg19zb0ZrBACh40hpBLteYROaPLhBXzLgjqyHQrN+gVP9N/FJvfzuNNuzCutktkroXEZBrxAxKhh7Q==",
"requires": {
"@types/hast": "^2.0.0",
"@types/mdast": "^3.0.0",
"mdast-util-to-hast": "^11.0.0",
"unified": "^10.0.0"
}
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -6610,6 +7033,14 @@
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
},
"sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"requires": {
"mri": "^1.1.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -7175,6 +7606,11 @@
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="
},
"space-separated-tokens": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz",
"integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw=="
},
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -7294,6 +7730,14 @@
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true
},
"style-to-object": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
"integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
"requires": {
"inline-style-parser": "0.1.1"
}
},
"stylis": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz",
@ -7448,6 +7892,11 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
},
"trough": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.0.2.tgz",
"integrity": "sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w=="
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@ -7503,6 +7952,27 @@
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz",
"integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ=="
},
"unified": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.1.tgz",
"integrity": "sha512-v4ky1+6BN9X3pQrOdkFIPWAaeDsHPE1svRDxq7YpTc2plkIqFMwukfqM+l0ewpP9EfwARlt9pPFAeWYhHm8X9w==",
"requires": {
"@types/unist": "^2.0.0",
"bail": "^2.0.0",
"extend": "^3.0.0",
"is-buffer": "^2.0.0",
"is-plain-obj": "^4.0.0",
"trough": "^2.0.0",
"vfile": "^5.0.0"
},
"dependencies": {
"is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
}
}
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -7514,6 +7984,67 @@
"set-value": "^2.0.1"
}
},
"unist-builder": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz",
"integrity": "sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==",
"requires": {
"@types/unist": "^2.0.0"
}
},
"unist-util-generated": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz",
"integrity": "sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw=="
},
"unist-util-is": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz",
"integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ=="
},
"unist-util-position": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.1.tgz",
"integrity": "sha512-mgy/zI9fQ2HlbOtTdr2w9lhVaiFUHWQnZrFF2EUoVOqtAUdzqMtNiD99qA5a1IcjWVR8O6aVYE9u7Z2z1v0SQA=="
},
"unist-util-stringify-position": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.0.tgz",
"integrity": "sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA==",
"requires": {
"@types/unist": "^2.0.0"
}
},
"unist-util-visit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz",
"integrity": "sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0",
"unist-util-visit-parents": "^5.0.0"
},
"dependencies": {
"unist-util-visit-parents": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz",
"integrity": "sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
}
}
}
},
"unist-util-visit-parents": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz",
"integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@ -7607,6 +8138,24 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"uvu": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz",
"integrity": "sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==",
"requires": {
"dequal": "^2.0.0",
"diff": "^5.0.0",
"kleur": "^4.0.3",
"sade": "^1.7.3"
},
"dependencies": {
"kleur": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA=="
}
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
@ -7617,6 +8166,33 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vfile": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.0.tgz",
"integrity": "sha512-Tj44nY/48OQvarrE4FAjUfrv7GZOYzPbl5OD65HxVKwLJKMPU7zmfV8cCgCnzKWnSfYG2f3pxu+ALqs7j22xQQ==",
"requires": {
"@types/unist": "^2.0.0",
"is-buffer": "^2.0.0",
"unist-util-stringify-position": "^3.0.0",
"vfile-message": "^3.0.0"
},
"dependencies": {
"is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
}
}
},
"vfile-message": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.0.tgz",
"integrity": "sha512-4QJbBk+DkPEhBXq3f260xSaWtjE4gPKOfulzfMFF8ZNwaPZieWsg3iVlcmF04+eebzpcpeXOOFMfrYzJHVYg+g==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-stringify-position": "^3.0.0"
}
},
"vlq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz",

View File

@ -29,6 +29,7 @@
"@mui/material": "^5.2.7",
"@mui/system": "^5.2.6",
"material-ui-image": "^3.3.2",
"react-markdown": "^7.1.2",
"react-native": "^0.66.4",
"react-native-svg": "^12.1.1",
"react-qr-code": "^2.0.3",

View File

@ -196,7 +196,7 @@ export default class BookPage extends Component {
</Grid>)
:
<Grid item xs={12} align="center">
<Paper elevation={0} style={{width: 1100, maxHeight: 600, overflow: 'auto'}}>
<Paper elevation={0} style={{width: 900, maxHeight: 500, overflow: 'auto'}}>
<List >
{this.bookListItems()}
</List>

View File

@ -5,6 +5,7 @@ import UserGenPage from "./UserGenPage";
import MakerPage from "./MakerPage";
import BookPage from "./BookPage";
import OrderPage from "./OrderPage";
import InfoPage from "./InfoPage";
export default class HomePage extends Component {
constructor(props) {
@ -17,6 +18,7 @@ export default class HomePage extends Component {
<Switch>
<Route exact path='/' component={UserGenPage}/>
<Route path='/home'><p>You are at the start page</p></Route>
<Route path='/info' component={InfoPage}/>
<Route path='/make' component={MakerPage}/>
<Route path='/book' component={BookPage}/>
<Route path="/order/:orderId" component={OrderPage}/>

View File

@ -0,0 +1,38 @@
import ReactMarkdown from 'react-markdown'
import {Paper, Grid, CircularProgress, Button, Link} from "@mui/material"
import React, { Component } from 'react'
export default class InfoPage extends Component {
constructor(props) {
super(props);
this.state = {
info: null,
loading: true,
};
this.getInfoMarkdown()
}
getInfoMarkdown() {
fetch('/static/assets/info.md')
.then((response) => response.text())
.then((data) => this.setState({info:data, loading:false}));
}
render() {
return (
<Grid container spacing={1}>
{this.state.loading ? <Grid item xs={12} align="center">
<CircularProgress />
</Grid> : ""}
<Paper elevation={12} style={{ padding: 10, width: 900, maxHeight: 500, overflow: 'auto'}}>
<ReactMarkdown children={this.state.info} />
</Paper>
<Grid item xs={12} align="center">
<Button color="secondary" variant="contained" to="/" component={Link}>
Back
</Button>
</Grid>
</Grid>
)
}
}

View File

@ -181,7 +181,6 @@ export default class MakerPage extends Component {
onChange={this.handleAmountChange}
/>
<Select
label="Select Payment Currency"
required="true"
defaultValue={this.defaultCurrency}
inputProps={{

View File

@ -67,7 +67,7 @@ export default class OrderPage extends Component {
super(props);
this.state = {
isExplicit: false,
delay: 10000, // Refresh every 10 seconds
delay: 2000, // Refresh every 2 seconds by default
currencies_dict: {"1":"USD"}
};
this.orderId = this.props.match.params.orderId;
@ -109,7 +109,7 @@ export default class OrderPage extends Component {
escrowInvoice: data.escrow_invoice,
escrowSatoshis: data.escrow_satoshis,
invoiceAmount: data.invoice_amount,
});
})
});
}
@ -129,9 +129,6 @@ export default class OrderPage extends Component {
tick = () => {
this.getOrderDetails();
}
handleDelayChange = (e) => {
this.setState({ delay: Number(e.target.value) });
}
// Fix to use proper react props
handleClickBackButton=()=>{
@ -149,7 +146,9 @@ export default class OrderPage extends Component {
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json())
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
.then((data) => (this.setState({badRequest:data.bad_request})
& console.log(data)
& this.getOrderDetails(data.id)));
}
getCurrencyDict() {
fetch('/static/assets/currencies.json')
@ -278,13 +277,19 @@ export default class OrderPage extends Component {
</>
}
{/* Makers can cancel before commiting the bond (status 0)*/}
{this.state.isMaker & this.state.statusCode == 0 ?
{/* Makers can cancel before trade escrow deposited (status <9)*/}
{/* Only free cancel before bond locked (status 0)*/}
{this.state.isMaker & this.state.statusCode < 9 ?
<Grid item xs={12} align="center">
<Button variant='contained' color='secondary' onClick={this.handleClickCancelOrderButton}>Cancel</Button>
</Grid>
:""}
{this.state.isMaker & this.state.statusCode > 0 & this.state.statusCode < 9 ?
<Grid item xs={12} align="center">
<Typography color="secondary" variant="subtitle2" component="subtitle2">Cancelling now forfeits the maker bond</Typography>
</Grid>
:""}
{/* Takers can cancel before commiting the bond (status 3)*/}
{this.state.isTaker & this.state.statusCode == 3 ?
<Grid item xs={12} align="center">

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Paper, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material"
import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material"
import QRCode from "react-qr-code";
function getCookie(name) {
@ -27,6 +27,9 @@ function pn(x) {
export default class TradeBox extends Component {
constructor(props) {
super(props);
this.state = {
badInvoice: false,
}
}
showQRInvoice=()=>{
@ -34,16 +37,16 @@ export default class TradeBox extends Component {
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2">
Robots around here usually show commitment
Robosats show commitment to their peers
</Typography>
</Grid>
<Grid item xs={12} align="center">
{this.props.data.isMaker ?
<Typography component="subtitle1" variant="subtitle1">
<Typography color="primary" component="subtitle1" variant="subtitle1">
<b>Lock {pn(this.props.data.bondSatoshis)} Sats to PUBLISH order </b>
</Typography>
:
<Typography component="subtitle1" variant="subtitle1">
<Typography color="primary" component="subtitle1" variant="subtitle1">
<b>Lock {pn(this.props.data.bondSatoshis)} Sats to TAKE the order </b>
</Typography>
}
@ -67,11 +70,21 @@ export default class TradeBox extends Component {
);
}
showBondIsLocked=()=>{
return (
<Grid item xs={12} align="center">
<Typography color="primary" component="subtitle1" variant="subtitle1" align="center">
🔒 Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is safely locked
</Typography>
</Grid>
);
}
showEscrowQRInvoice=()=>{
return (
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="subtitle1" variant="subtitle1">
<Typography color="primary" component="subtitle1" variant="subtitle1">
<b>Deposit {pn(this.props.data.escrowSatoshis)} Sats as trade collateral </b>
</Typography>
</Grid>
@ -89,6 +102,7 @@ export default class TradeBox extends Component {
color = "secondary"
/>
</Grid>
{this.showBondIsLocked()}
</Grid>
);
}
@ -110,6 +124,7 @@ export default class TradeBox extends Component {
Please wait for the taker to confirm his commitment by locking a bond.
</Typography>
</Grid>
{this.showBondIsLocked()}
</Grid>
);
}
@ -119,7 +134,7 @@ export default class TradeBox extends Component {
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<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>
</Typography>
</Grid>
<Grid item xs={12} align="center">
@ -135,7 +150,6 @@ export default class TradeBox extends Component {
return to you (no action needed).</p>
</Typography>
</ListItem>
{/* TODO API sends data for a more confortable wait */}
<Divider/>
<ListItem>
@ -151,21 +165,27 @@ export default class TradeBox extends Component {
<ListItem>
<ListItemText primary="33%" secondary="Premium percentile" />
</ListItem>
<Divider/>
</List>
</Grid>
{this.showBondIsLocked()}
</Grid>
)
}
handleInputInvoiceChanged=(e)=>{
this.setState({
invoice: e.target.value,
invoice: e.target.value,
badInvoice: false,
});
}
// Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage.
handleClickSubmitInvoiceButton=()=>{
this.setState({badInvoice:false});
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
@ -176,7 +196,8 @@ export default class TradeBox extends Component {
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => (this.props.data = data));
.then((data) => this.setState({badInvoice:data.bad_invoice})
& console.log(data));
}
showInputInvoice(){
@ -186,7 +207,7 @@ export default class TradeBox extends Component {
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="subtitle1" variant="subtitle1">
<Typography color="primary" component="subtitle1" variant="subtitle1">
<b> Submit a LN invoice for {pn(this.props.data.invoiceAmount)} Sats </b>
</Typography>
</Grid>
@ -199,6 +220,8 @@ export default class TradeBox extends Component {
</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={{
@ -211,6 +234,7 @@ export default class TradeBox extends Component {
<Grid item xs={12} align="center">
<Button variant='contained' color='primary' onClick={this.handleClickSubmitInvoiceButton}>Submit</Button>
</Grid>
{this.showBondIsLocked()}
</Grid>
)
}
@ -231,6 +255,7 @@ export default class TradeBox extends Component {
you will get your bond back automatically.</p>
</Typography>
</Grid>
{this.showBondIsLocked()}
</Grid>
)
}
@ -252,6 +277,7 @@ export default class TradeBox extends Component {
you will get back the trade collateral and your bond automatically.</p>
</Typography>
</Grid>
{this.showBondIsLocked()}
</Grid>
)
}
@ -280,6 +306,19 @@ handleClickOpenDisputeButton=()=>{
.then((response) => response.json())
.then((data) => (this.props.data = data));
}
handleRatingChange=(e)=>{
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action': "rate",
'rating': e.target.value,
}),
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => (this.props.data = data));
}
showFiatSentButton(){
return(
@ -295,22 +334,18 @@ handleClickOpenDisputeButton=()=>{
// TODO, show alert and ask for double confirmation (Have you check you received the fiat? Confirming fiat received settles the trade.)
// Ask for double confirmation.
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Button defaultValue="confirm" variant='contained' color='primary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} received</Button>
</Grid>
</Grid>
)
}
showOpenDisputeButton(){
// TODO, show alert about how opening a dispute might involve giving away personal data and might mean losing the bond. Ask for double confirmation.
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Button defaultValue="dispute" variant='contained' onClick={this.handleClickOpenDisputeButton}>Open Dispute</Button>
<Button color="inherit" onClick={this.handleClickOpenDisputeButton}>Open Dispute</Button>
</Grid>
</Grid>
)
}
@ -341,21 +376,33 @@ handleClickOpenDisputeButton=()=>{
{receivedFiatButton ? this.showFiatReceivedButton() : ""}
{openDisputeButton ? this.showOpenDisputeButton() : ""}
</Grid>
{this.showBondIsLocked()}
</Grid>
)
}
// showFiatReceivedButton(){
// }
// showOpenDisputeButton(){
// }
// showRateSelect(){
// }
showRateSelect(){
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="h6" variant="h6">
🎉Trade finished!🥳
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
What do you think of <b>{this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}</b>?
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} />
</Grid>
<Grid item xs={12} align="center">
<Button color='primary' to='/' component={Link}>Start Again</Button>
</Grid>
</Grid>
)
}
render() {
@ -379,14 +426,25 @@ handleClickOpenDisputeButton=()=>{
{this.props.data.isBuyer & this.props.data.statusCode == 7 ? this.showWaitingForEscrow() : ""}
{this.props.data.isSeller & this.props.data.statusCode == 8 ? this.showWaitingForBuyerInvoice() : ""}
{/* In Chatroom - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
{/* In Chatroom - No fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
{this.props.data.isBuyer & this.props.data.statusCode == 9 ? this.showChat(true,false,true) : ""}
{this.props.data.isSeller & this.props.data.statusCode == 9 ? this.showChat(false,false,true) : ""}
{/* In Chatroom - Fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
{this.props.data.isBuyer & this.props.data.statusCode == 10 ? this.showChat(false,false,true) : ""}
{this.props.data.isSeller & this.props.data.statusCode == 10 ? this.showChat(false,true,true) : ""}
{/* Trade Finished */}
{this.props.data.isSeller & this.props.data.statusCode > 12 & this.props.data.statusCode < 15 ? this.showRateSelect() : ""}
{(this.props.data.isSeller & this.props.data.statusCode > 12 & this.props.data.statusCode < 15) ? this.showRateSelect() : ""}
{(this.props.data.isBuyer & this.props.data.statusCode == 14) ? this.showRateSelect() : ""}
{/* Trade Finished - Payment Routing Failed */}
{this.props.data.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""}
{/* Trade Finished - Payment Routing Failed - TODO Needs more planning */}
{this.props.data.statusCode == 11 ? this.showInDispute() : ""}
{/* TODO */}
{/* */}
{/* */}

View File

@ -137,7 +137,7 @@ export default class UserGenPage extends Component {
<Grid item xs={12} align="center">
<ButtonGroup variant="contained" aria-label="outlined primary button group">
<Button color='primary' to='/make/' component={Link}>Make Order</Button>
<Button color='inherit' to='/home' component={Link}>INFO</Button>
<Button color='inherit' to='/info' component={Link}>INFO</Button>
<Button color='secondary' to='/book/' component={Link}>View Book</Button>
</ButtonGroup>
</Grid>

View File

@ -1,8 +1,31 @@
{
"1":"USD",
"2":"EUR",
"3":"ETH",
"4":"AUD",
"5":"BRL",
"6":"CAD"
}
"3":"JPY",
"4":"GBP",
"5":"AUD",
"6":"CAD",
"7":"CHF",
"8":"CNY",
"9":"HKD",
"10":"NZD",
"11":"SEK",
"12":"KRW",
"13":"SGD",
"14":"NOK",
"15":"MXN",
"16":"KRW",
"17":"RUB",
"18":"ZAR",
"19":"TRY",
"20":"BRL",
"21": "CLP",
"22": "CZK",
"23": "DKK",
"24": "HKR",
"25": "HUF",
"26": "INR",
"27": "ISK",
"28": "PLN",
"29": "RON"
}

View File

@ -0,0 +1,81 @@
# 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.
## Thats 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 sellers 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} doesnt know anything about you and doesnt 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.

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Robosats</title>
<title>RoboSats - Simple and Private Bitcoin Exchange</title>
{% load static %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<link

View File

@ -3,7 +3,7 @@ from .views import index
urlpatterns = [
path('', index),
path('home/', index),
path('info/', index),
path('login/', index),
path('make/', index),
path('book/', index),

View File

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/4.0/ref/settings/
"""
from pathlib import Path
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -25,7 +26,7 @@ SECRET_KEY = 'django-insecure-6^&6uw$b5^en%(cu2kc7_o)(mgpazx#j_znwlym0vxfamn2uo-
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = [config('HOST_NAME'),'127.0.0.1']
# Application definition

View File

@ -6,8 +6,6 @@
### Install virtual environments
```
pip install virtualenvwrapper
pip install python-decouple
pip install ring
```
### Add to .bashrc
@ -45,11 +43,41 @@ python3 manage.py migrate
python3 manage.py runserver
```
### Install python dependencies
### Install other python dependencies
```
pip install robohash
pip install python-decouple
pip install ring
```
### Install LND python dependencies
```
cd api/lightning
pip install grpcio grpcio-tools googleapis-common-protos
git clone https://github.com/googleapis/googleapis.git
curl -o lightning.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/lightning.proto
python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. lightning.proto
```
We also use the *Invoices* and *Router* subservices for invoice validation and payment routing.
```
curl -o invoices.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/invoicesrpc/invoices.proto
python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. invoices.proto
curl -o router.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/routerrpc/router.proto
python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. router.proto
```
Relative imports are not working at the moment, so some editing is needed in
`api/lightning` files `lightning_pb2_grpc.py`, `invoices_pb2_grpc.py`, `invoices_pb2.py`, `router_pb2_grpc.py` and `router_pb2.py`.
For example in `lightning_pb2_grpc.py` , add "from . " :
`import lightning_pb2 as lightning__pb2`
to
`from . import lightning_pb2 as lightning__pb2`
Same for every other file
## React development environment
### Install npm
`sudo apt install npm`
@ -71,8 +99,9 @@ npm install react-native
npm install react-native-svg
npm install react-qr-code
npm install @mui/material
npm install react-markdown
```
Note we are using mostly MaterialUI V5, but Image loading from V4 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
from frontend/ directory