Skip to content

Commit

Permalink
Merge pull request #36 from Reckless-Satoshi/polish-and-debug-before-…
Browse files Browse the repository at this point in the history
…0.1.0-mvp

Merging debugging and polishing before v0.1.0-mvp
Fixes #22 #30 #32
  • Loading branch information
Reckless-Satoshi authored Jan 31, 2022
2 parents dcb5485 + d763da8 commit d3cbc89
Show file tree
Hide file tree
Showing 45 changed files with 1,841 additions and 709 deletions.
9 changes: 8 additions & 1 deletion .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ FEE = 0.002
BOND_SIZE = 0.01
# Time out penalty for canceling takers in SECONDS
PENALTY_TIMEOUT = 60
# Time between routing attempts of buyer invoice in MINUTES
RETRY_TIME = 5

# Trade limits in satoshis
MIN_TRADE = 10000
MAX_TRADE = 500000

# Expiration time for HODL invoices and returning collateral in HOURS
# Expiration (CLTV_expiry) time for HODL invoices in HOURS // 7 min/block assumed
BOND_EXPIRY = 14
ESCROW_EXPIRY = 8

Expand All @@ -41,5 +43,10 @@ INVOICE_AND_ESCROW_DURATION = 30
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
FIAT_EXCHANGE_DURATION = 4

# Proportional routing fee limit (fraction of total payout: % / 100)
PROPORTIONAL_ROUTING_FEE_LIMIT = 0.0002
# Base flat limit fee for routing in Sats (used only when proportional is lower than this)
MIN_FLAT_ROUTING_FEE_LIMIT = 10

# Username for HTLCs escrows
ESCROW_USERNAME = 'admin'
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
## RoboSats - Buy and sell Satoshis Privately.
[![release](https://img.shields.io/badge/release-v0.1.0%20MVP-orange)](https://github.com/Reckless-Satoshi/robosats/releases)
## RoboSats - Buy and sell Satoshis Privately
[![release](https://img.shields.io/badge/release-v0.1.0%20MVP-red)](https://github.com/Reckless-Satoshi/robosats/releases)
[![AGPL-3.0 license](https://img.shields.io/badge/license-AGPL--3.0-blue)](https://github.com/Reckless-Satoshi/robosats/blob/main/LICENSE)
[![Telegram](https://img.shields.io/badge/chat-telegram-brightgreen)](https://t.me/robosats)

RoboSats is a simple and private way to exchange bitcoin for national currencies. Robosats simplifies the peer-to-peer user experience and uses lightning hold invoices to minimize custody and trust requirements. The deterministically generated avatars help users stick to best privacy practices.

## Try it out
<div align="center">
<img width="75%" src="https://raw.githubusercontent.com/Reckless-Satoshi/robosats/frontend/static/assets/images/robosats_0.1.0_banner.png">
</div>

**Bitcoin mainnet:**
- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Coming soon)
Expand All @@ -15,7 +18,7 @@ RoboSats is a simple and private way to exchange bitcoin for national currencies
**Bitcoin testnet:**
- Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion (Active - On Dev Node)
- Url: testnet.robosats.com (Coming soon)
- Commit height: Latest commit.
- Latest commit.

*Always use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.*

Expand All @@ -39,7 +42,7 @@ Alice wants to buy satoshis privately:
See [CONTRIBUTING.md](CONTRIBUTING.md)

## Original idea
The concept of a simple custody-minimized lightning exchange using hold invoices is heavily inspired by [P2PLNBOT](https://github.com/grunch/p2plnbot) by @grunch
The concept of a simple custody-minimized lightning exchange using hold invoices is heavily inspired in [P2PLNBOT](https://github.com/grunch/p2plnbot) by @grunch

## License

Expand Down
16 changes: 10 additions & 6 deletions api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,25 @@ class EUserAdmin(UserAdmin):
inlines = [ProfileInline]
list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
list_display_links = ('id','username')
ordering = ('-id',)
def avatar_tag(self, obj):
return obj.profile.avatar_tag()


@admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link')
list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'payout_link','maker_bond_link','taker_bond_link','trade_escrow_link')
list_display_links = ('id','type')
change_links = ('maker','taker','currency','buyer_invoice','maker_bond','taker_bond','trade_escrow')
change_links = ('maker','taker','currency','payout','maker_bond','taker_bond','trade_escrow')
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')

@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('hash','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link','order_made','order_taken','order_escrow','order_paid')
list_display_links = ('hash','concept','order_made','order_taken','order_escrow','order_paid')
change_links = ('sender','receiver')
list_display = ('hash','concept','status','num_satoshis','type','expires_at','expiry_height','sender_link','receiver_link','order_made_link','order_taken_link','order_escrow_link','order_paid_link')
list_display_links = ('hash','concept')
change_links = ('sender','receiver','order_made','order_taken','order_escrow','order_paid')
list_filter = ('type','concept','status')
ordering = ('-expires_at',)

@admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
Expand All @@ -49,9 +51,11 @@ class CurrencieAdmin(admin.ModelAdmin):
list_display = ('id','currency','exchange_rate','timestamp')
list_display_links = ('id','currency')
readonly_fields = ('currency','exchange_rate','timestamp')
ordering = ('id',)

@admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin):
list_display = ('timestamp','price','volume','premium','currency','fee')
readonly_fields = ('timestamp','price','volume','premium','currency','fee')
list_filter = ['currency']
list_filter = ['currency']
ordering = ('-timestamp',)
88 changes: 50 additions & 38 deletions api/lightning/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from datetime import timedelta, datetime
from django.utils import timezone

from api.models import LNPayment
#######
# Should work with LND (c-lightning in the future if there are features that deserve the work)
#######
Expand Down Expand Up @@ -64,7 +65,7 @@ def settle_hold_invoice(cls, preimage):
return str(response)=="" # True if no response, false otherwise.

@classmethod
def gen_hold_invoice(cls, num_satoshis, description, expiry):
def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry, cltv_expiry_secs):
'''Generates hold invoice'''

hold_payment = {}
Expand All @@ -73,12 +74,16 @@ def gen_hold_invoice(cls, num_satoshis, description, expiry):

# Its hash is used to generate the hold invoice
r_hash = hashlib.sha256(preimage).digest()


# timelock expiry for the last hop, computed based on a 10 minutes block with 30% padding (~7 min block)
cltv_expiry_blocks = int(cltv_expiry_secs / (7*60))
request = invoicesrpc.AddHoldInvoiceRequest(
memo=description,
value=num_satoshis,
hash=r_hash,
expiry=expiry)
expiry=int(invoice_expiry*1.5), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
cltv_expiry=cltv_expiry_blocks,
)
response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())])

hold_payment['invoice'] = response.payment_request
Expand All @@ -87,18 +92,22 @@ def gen_hold_invoice(cls, num_satoshis, description, expiry):
hold_payment['payment_hash'] = payreq_decoded.payment_hash
hold_payment['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
hold_payment['expires_at'] = hold_payment['created_at'] + timedelta(seconds=payreq_decoded.expiry)
hold_payment['cltv_expiry'] = cltv_expiry_blocks

return hold_payment

@classmethod
def validate_hold_invoice_locked(cls, payment_hash):
def validate_hold_invoice_locked(cls, lnpayment):
'''Checks if hold invoice is locked'''
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(lnpayment.payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
print('status here')
print(response.state)

# TODO ERROR HANDLING
# Will fail if 'unable to locate invoice'. Happens if invoice expiry
# time has passed (but these are 15% padded at the moment). Should catch it
# and report back that the invoice has expired (better robustness)
if response.state == 0: # OPEN
print('STATUS: OPEN')
pass
Expand All @@ -108,31 +117,34 @@ def validate_hold_invoice_locked(cls, payment_hash):
pass
if response.state == 3: # ACCEPTED (LOCKED)
print('STATUS: ACCEPTED')
lnpayment.expiry_height = response.htlcs[0].expiry_height
lnpayment.status = LNPayment.Status.LOCKED
lnpayment.save()
return True

@classmethod
def check_until_invoice_locked(cls, payment_hash, expiration):
'''Checks until hold invoice is locked.
When invoice is locked, returns true.
If time expires, return False.'''
# Experimental, might need asyncio. Best if subscribing all invoices and running a background task
# Maybe best to pass LNpayment object and change status live.

request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
print(invoice)
if timezone.now > expiration:
break
if invoice.state == 3: # True if hold invoice is accepted.
return True
return False
# @classmethod
# def check_until_invoice_locked(cls, payment_hash, expiration):
# '''Checks until hold invoice is locked.
# When invoice is locked, returns true.
# If time expires, return False.'''
# # Experimental, might need asyncio. Best if subscribing all invoices and running a background task
# # Maybe best to pass LNpayment object and change status live.

# request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
# for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
# print(invoice)
# if timezone.now > expiration:
# break
# if invoice.state == 3: # True if hold invoice is accepted.
# return True
# return False


@classmethod
def validate_ln_invoice(cls, invoice, num_satoshis):
'''Checks if the submited LN invoice comforms to expectations'''

buyer_invoice = {
payout = {
'valid': False,
'context': None,
'description': None,
Expand All @@ -145,36 +157,36 @@ def validate_ln_invoice(cls, invoice, num_satoshis):
payreq_decoded = cls.decode_payreq(invoice)
print(payreq_decoded)
except:
buyer_invoice['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
return buyer_invoice
payout['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
return payout

if payreq_decoded.num_satoshis == 0:
buyer_invoice['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
return buyer_invoice
payout['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
return payout

if not payreq_decoded.num_satoshis == num_satoshis:
buyer_invoice['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
return buyer_invoice
payout['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
return payout

buyer_invoice['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
buyer_invoice['expires_at'] = buyer_invoice['created_at'] + timedelta(seconds=payreq_decoded.expiry)
payout['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
payout['expires_at'] = payout['created_at'] + timedelta(seconds=payreq_decoded.expiry)

if buyer_invoice['expires_at'] < timezone.now():
buyer_invoice['context'] = {'bad_invoice':f'The invoice provided has already expired'}
return buyer_invoice
if payout['expires_at'] < timezone.now():
payout['context'] = {'bad_invoice':f'The invoice provided has already expired'}
return payout

buyer_invoice['valid'] = True
buyer_invoice['description'] = payreq_decoded.description
buyer_invoice['payment_hash'] = payreq_decoded.payment_hash
payout['valid'] = True
payout['description'] = payreq_decoded.description
payout['payment_hash'] = payreq_decoded.payment_hash


return buyer_invoice
return payout

@classmethod
def pay_invoice(cls, invoice, num_satoshis):
'''Sends sats to buyer'''

fee_limit_sat = max(num_satoshis * 0.0002, 10) # 200 ppm or 10 sats
fee_limit_sat = int(max(num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
request = routerrpc.SendPaymentRequest(
payment_request=invoice,
fee_limit_sat=fee_limit_sat,
Expand Down
Loading

0 comments on commit d3cbc89

Please sign in to comment.