Work frontend trade pipeline

This commit is contained in:
Reckless_Satoshi 2022-01-09 12:05:19 -08:00
parent fb846c91d8
commit 8e5233267f
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
7 changed files with 166 additions and 57 deletions

View File

@ -1,3 +1,6 @@
# import codecs, grpc, os
# import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
from datetime import timedelta
from django.utils import timezone
@ -12,9 +15,16 @@ 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_hodl_invoice(num_satoshis, description, expiry):
'''Generates hodl invoice to publish an order'''
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
@ -22,12 +32,46 @@ class LNNode():
return invoice, payment_hash, expires_at
def validate_hodl_invoice_locked(payment_hash):
'''Generates hodl invoice to publish an order'''
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): # num_satoshis
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
@ -40,11 +84,11 @@ class LNNode():
'''Sends sats to buyer, or cancelinvoices'''
return True
def settle_hodl_htlcs(payment_hash):
'''Charges a LN hodl invoice'''
def settle_hold_htlcs(payment_hash):
'''Charges a LN hold invoice'''
return True
def return_hodl_htlcs(payment_hash):
def return_hold_htlcs(payment_hash):
'''Returns sats'''
return True

View File

@ -188,7 +188,7 @@ class Logics():
return False, {'bad_request':'You cannot cancel this order'}
@classmethod
def gen_maker_hodl_invoice(cls, order, user):
def gen_maker_hold_invoice(cls, order, user):
# Do not gen and cancel if order is more than 5 minutes old
if order.expires_at < timezone.now():
@ -206,12 +206,12 @@ class Logics():
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.'
# Gen HODL Invoice
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
# Gen hold Invoice
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
order.maker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.MAKEBOND,
type = LNPayment.Types.HODL,
type = LNPayment.Types.hold,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
@ -225,7 +225,7 @@ class Logics():
return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis}
@classmethod
def gen_taker_hodl_invoice(cls, order, user):
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
if order.taker_bond:
@ -245,12 +245,12 @@ class Logics():
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.'
# Gen HODL Invoice
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
# Gen hold Invoice
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
order.taker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.TAKEBOND,
type = LNPayment.Types.HODL,
type = LNPayment.Types.hold,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
@ -267,7 +267,7 @@ class Logics():
return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis}
@classmethod
def gen_escrow_hodl_invoice(cls, order, user):
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
if order.trade_escrow:
# Check if status is INVGEN and still not expired
@ -285,12 +285,12 @@ class Logics():
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.'
# Gen HODL Invoice
invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
# Gen hold Invoice
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
order.trade_escrow = LNPayment.objects.create(
concept = LNPayment.Concepts.TRESCROW,
type = LNPayment.Types.HODL,
type = LNPayment.Types.hold,
sender = user,
receiver = User.objects.get(username=ESCROW_USERNAME),
invoice = invoice,
@ -307,7 +307,7 @@ class Logics():
''' Settles the trade escrow HTLC'''
# TODO ERROR HANDLING
valid = LNNode.settle_hodl_htlcs(order.trade_escrow.payment_hash)
valid = LNNode.settle_hold_htlcs(order.trade_escrow.payment_hash)
return valid
def pay_buyer_invoice(order):
@ -338,7 +338,7 @@ 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.
@ -350,4 +350,4 @@ class Logics():
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
order.save()
return True, None
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-hodl)
HODL = 1, 'Hodl invoice'
NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hold)
hold = 1, 'hold invoice'
class Concepts(models.IntegerChoices):
MAKEBOND = 0, 'Maker bond'
@ -38,7 +38,7 @@ class LNPayment(models.Model):
FAILRO = 7, 'Failed routing'
# payment use details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL)
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)
@ -133,7 +133,7 @@ class Order(models.Model):
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}')
@receiver(pre_delete, sender=Order)
def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs):
def delete_HTLCs_at_order_deletion(sender, instance, **kwargs):
to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow)
for htlc in to_delete:
@ -193,7 +193,7 @@ class MarketTick(models.Model):
It is checked against current CEX price for useful
insight on the historical premium of Non-KYC BTC
Price is set when both taker bond is locked. Both
Price is set when taker bond is locked. Both
maker and taker are commited with bonds (contract
is finished and cancellation has a cost)
'''

View File

@ -136,17 +136,17 @@ class OrderView(viewsets.ViewSet):
elif data['is_buyer']:
data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount']
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice.
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice.
if order.status == Order.Status.WFB and data['is_maker']:
valid, context = Logics.gen_maker_hodl_invoice(order, request.user)
valid, context = Logics.gen_maker_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER HODL invoice.
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER hold invoice.
elif order.status == Order.Status.TAK and data['is_taker']:
valid, context = Logics.gen_taker_hodl_invoice(order, request.user)
valid, context = Logics.gen_taker_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
@ -155,9 +155,9 @@ class OrderView(viewsets.ViewSet):
# 7 a. ) If seller and status is 'WF2' or 'WFE'
elif data['is_seller'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFE):
# If the two bonds are locked, reply with an ESCROW HODL invoice.
# If the two bonds are locked, reply with an ESCROW hold invoice.
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
valid, context = Logics.gen_escrow_hodl_invoice(order, request.user)
valid, context = Logics.gen_escrow_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
@ -179,10 +179,7 @@ class OrderView(viewsets.ViewSet):
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
# 9) if buyer confirmed FIAT SENT
elif order.status == Order.Status.FSE:
data['buyer_confirmed']
return Response(data, status.HTTP_200_OK)

View File

@ -16,7 +16,6 @@ export default class BookPage extends Component {
this.state.currencyCode = this.getCurrencyCode(this.state.currency)
}
// Show message to be the first one to make an order
getOrderDetails(type,currency) {
fetch('/api/book' + '?currency=' + currency + "&type=" + type)
.then((response) => response.json())

View File

@ -283,9 +283,8 @@ export default class OrderPage extends Component {
)
}
render (){
return (
orderDetailsPage (){
return(
this.state.badRequest ?
<div align='center'>
<Typography component="subtitle2" variant="subtitle2" color="secondary" >
@ -307,6 +306,13 @@ export default class OrderPage extends Component {
<Grid item xs={12} align="center">
{this.orderBox()}
</Grid>)
)
}
render (){
return (
// Only so nothing shows while requesting the first batch of data
(this.state.statusCode == null & this.state.badRequest == null) ? "" : this.orderDetailsPage()
);
}
}

View File

@ -58,7 +58,7 @@ export default class TradeBox extends Component {
size="small"
defaultValue={this.props.data.bondInvoice}
disabled="true"
helperText="This is a HODL LN invoice. It will not be charged if the order succeeds or expires.
helperText="This is a hold invoice. It will not be charged if the order succeeds or expires.
It will be charged if the order is cancelled or you lose a dispute."
color = "secondary"
/>
@ -66,6 +66,7 @@ export default class TradeBox extends Component {
</Grid>
);
}
showEscrowQRInvoice=()=>{
return (
<Grid container spacing={1}>
@ -84,7 +85,7 @@ export default class TradeBox extends Component {
size="small"
defaultValue={this.props.data.escrowInvoice}
disabled="true"
helperText="This is a HODL LN invoice. It will be charged once the buyer confirms he sent the fiat."
helperText="This is a hold LN invoice. It will be charged once the buyer confirms he sent the fiat."
color = "secondary"
/>
</Grid>
@ -162,7 +163,7 @@ export default class TradeBox extends Component {
});
}
// Fix this, clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage.
// Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage.
handleClickSubmitInvoiceButton=()=>{
const requestOptions = {
@ -215,7 +216,6 @@ export default class TradeBox extends Component {
}
showWaitingForEscrow(){
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
@ -236,7 +236,6 @@ export default class TradeBox extends Component {
}
showWaitingForBuyerInvoice(){
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
@ -257,26 +256,90 @@ export default class TradeBox extends Component {
)
}
handleClickFiatConfirmButton=()=>{
handleClickConfirmButton=()=>{
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'confirm',
'invoice': this.state.invoice,
'action': "confirm",
}),
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => (this.props.data = data));
}
handleClickOpenDisputeButton=()=>{
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action': "dispute",
}),
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => (this.props.data = data));
}
showFiatSentButton(){
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Button variant='contained' color='primary' onClick={this.handleClickFiatConfirmButton}>Confirm {this.props.data.currencyCode} was sent. </Button>
<Button defaultValue="confirm" variant='contained' color='primary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} sent</Button>
</Grid>
</Grid>
)
}
showFiatReceivedButton(){
// 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>
</Grid>
</Grid>
)
}
showChat(sendFiatButton, receivedFiatButton, openDisputeButton){
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="subtitle1" variant="subtitle1">
<b>Chatting with {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}</b>
</Typography>
</Grid>
<Grid item xs={12} align="left">
{this.props.data.isSeller ?
<Typography component="body2" variant="body2">
Say hi to your peer robot! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}.
</Typography>
:
<Typography component="body2" variant="body2">
Say hi to your peer robot! Ask for payment details and click 'Confirm {this.props.data.currencyCode} sent' as soon as you send the payment.
</Typography>
}
</Grid>
<Grid item xs={12} style={{ width:330, height:360}}>
CHAT PLACEHOLDER
</Grid>
<Grid item xs={12} align="center">
{sendFiatButton ? this.showFiatSentButton() : ""}
{receivedFiatButton ? this.showFiatReceivedButton() : ""}
{openDisputeButton ? this.showOpenDisputeButton() : ""}
</Grid>
</Grid>
)
@ -316,11 +379,11 @@ export default class TradeBox extends Component {
{this.props.data.isBuyer & this.props.data.statusCode == 7 ? this.showWaitingForEscrow() : ""}
{this.props.data.isSeller & this.props.data.statusCode == 8 ? this.showWaitingForBuyerInvoice() : ""}
{/* In Chatroom */}
{this.props.data.isBuyer & this.props.data.statusCode == 9 ? this.showChat() & this.showFiatSentButton() : ""}
{this.props.data.isSeller & this.props.data.statusCode ==9 ? this.showChat() : ""}
{this.props.data.isBuyer & this.props.data.statusCode == 10 ? this.showChat() & this.showOpenDisputeButton() : ""}
{this.props.data.isSeller & this.props.data.statusCode == 10 ? this.showChat() & this.showFiatReceivedButton() & this.showOpenDisputeButton(): ""}
{/* In Chatroom - 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) : ""}
{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() : ""}