From 74c0838faf4f728037ace9010af958396bad2ba0 Mon Sep 17 00:00:00 2001 From: cryptosharks131 Date: Mon, 3 Jul 2023 10:47:46 -0400 Subject: [PATCH 01/64] Version bump v1.8.0 --- gui/templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/templates/base.html b/gui/templates/base.html index 33465be7..993a8d4b 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -90,7 +90,7 @@

My Lnd Overview

-
LNDg v1.7.0
+
LNDg v1.8.0
From 6df4bf851b2a476df197548267a4d5d804113d2e Mon Sep 17 00:00:00 2001 From: osito <74455114+blckbx@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:39:35 +0200 Subject: [PATCH 02/64] [bug] actions: add missing node pubkey (#303) --- gui/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/views.py b/gui/views.py index f5ade84d..a2e0ddd0 100644 --- a/gui/views.py +++ b/gui/views.py @@ -1139,6 +1139,7 @@ def actions(request): result = {} result['chan_id'] = channel.chan_id result['short_chan_id'] = channel.short_chan_id + result['remote_pubkey'] = channel.remote_pubkey result['alias'] = channel.alias result['capacity'] = channel.capacity result['local_balance'] = channel.local_balance + channel.pending_outbound From a2e16c7915f4bdbaa1ed543a2d2a33c080f9d77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio?= <43297242+proof-of-reality@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:46:29 +0100 Subject: [PATCH 03/64] [bug] Fix table rendering (#305) --- gui/static/helpers.js | 1 - gui/templates/base.html | 4 ++-- gui/templates/home.html | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gui/static/helpers.js b/gui/static/helpers.js index e3b16c90..e28ded7a 100644 --- a/gui/static/helpers.js +++ b/gui/static/helpers.js @@ -2,7 +2,6 @@ function byId(id){ return document.getElementById(id) } String.prototype.toInt = function(){ return parseInt(this.replace(/,/g,''))} String.prototype.toBool = function(if_false = 0){ return this && /^true$/i.test(this) ? 1 : if_false} -String.prototype.default = function(value){ return (this || '').length === 0 ? value : this} Number.prototype.intcomma = function(){ return parseInt(this).toLocaleString() } HTMLElement.prototype.defaultCloneNode = HTMLElement.prototype.cloneNode HTMLElement.prototype.cloneNode = function(attrs){ diff --git a/gui/templates/base.html b/gui/templates/base.html index 993a8d4b..45f117d3 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -21,8 +21,8 @@ "forward_date": f => ({innerHTML: formatDate(f.forward_date), title: adjustTZ(f.forward_date) }), "amt_in": f => ({innerHTML: f.amt_in > 1000 ? f.amt_in.intcomma() : f.amt_in}), "amt_out": f => ({innerHTML: f.amt_out > 1000 ? f.amt_out.intcomma() : f.amt_out}), - "chan_in_alias": f => ({innerHTML: f.chan_in_alias.default(f.chan_id_in)}), - "chan_out_alias": f => ({innerHTML: f.chan_out_alias.default(f.chan_id_out)}), + "chan_in_alias": f => ({innerHTML: f.chan_in_alias || f.chan_id_in }), + "chan_out_alias": f => ({innerHTML: f.chan_out_alias || f.chan_id_out }), "chan_id_in": f => ({innerHTML: `${(BigInt(f.chan_id_in)>>40n)+'x'+(BigInt(f.chan_id_in)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(f.chan_id_in) & BigInt('0xFFFF'))}`}), "chan_id_out": f => ({innerHTML: `${(BigInt(f.chan_id_out)>>40n)+'x'+(BigInt(f.chan_id_out)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(f.chan_id_out) & BigInt('0xFFFF'))}`}), "fee": f => ({innerHTML: parseFloat(f.fee.toFixed(3)).toLocaleString()}), diff --git a/gui/templates/home.html b/gui/templates/home.html index bbd56765..5dba4ba1 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -630,7 +630,7 @@

Sign a Message

}, {ppm}, { "status": p => ({innerHTML: ['Unknown', 'In-Flight', 'Succeeded', 'Failed'][p.status]}), "chan_out_alias": p => ({innerHTML: p.chan_out_alias||'---'}), - "chan_out": p => ({innerHTML: p.status == 2 && p.chan_out !== 'MPP' ? `${(BigInt(p.chan_out)>>40n)+'x'+(BigInt(p.chan_out)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(p.chan_out) & BigInt('0xFFFF'))}` : p.chan_out.default('---')}), + "chan_out": p => ({innerHTML: p.status == 2 && p.chan_out !== 'MPP' ? `${(BigInt(p.chan_out)>>40n)+'x'+(BigInt(p.chan_out)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(p.chan_out) & BigInt('0xFFFF'))}` : p.chan_out || '---' }), "payment_hash": p => ({innerHTML: `${p.payment_hash.substring(0,7)}`}), "rebal_chan": p => ({innerHTML: (p.rebal_chan || '').length > 0 ? 'Yes' : 'No'}), "keysend_preimage": p => ({innerHTML: p.keysend_preimage ? 'Yes' : 'No'}) From 4248287eb58d0eae8810a4c794f5ebbc570095a0 Mon Sep 17 00:00:00 2001 From: BhhagBoseDK <87854599+BhaagBoseDK@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:46:52 +0100 Subject: [PATCH 04/64] Show Inflight and Pending Rebalances separately (#309) --- gui/templates/rebalances.html | 2 ++ gui/templates/rebalances_table.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/templates/rebalances.html b/gui/templates/rebalances.html index d11e0403..6f364cf6 100644 --- a/gui/templates/rebalances.html +++ b/gui/templates/rebalances.html @@ -1,6 +1,8 @@ {% extends "base.html" %} {% block title %} {{ block.super }} - Rebalances{% endblock %} {% block content %} +{% include 'rebalances_table.html' with count=20 status=1 load_count=20 title='Rebalance Requests' %} {% include 'rebalances_table.html' with count=20 status=2 load_count=20 title='Rebalance Requests' %} +{% include 'rebalances_table.html' with count=20 status=0 load_count=20 title='Rebalance Requests' %} {% include 'rebalances_table.html' with count=50 load_count=50 title='Rebalance Requests' %} {% endblock %} diff --git a/gui/templates/rebalances_table.html b/gui/templates/rebalances_table.html index 57abffc9..7732ddc6 100644 --- a/gui/templates/rebalances_table.html +++ b/gui/templates/rebalances_table.html @@ -1,6 +1,6 @@ {% load humanize %}
-

{% if status %}Successful{% else %}Last{% endif %} {{title}}

+

{% if status == 2 %}Successful{% elif status == 0 %}Pending{% elif status == 1 %}In-Flight{% else %}Last{% endif %} {{title}}

From 99e7a023fba62fdcac40fef44067ae757da678fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio?= <43297242+proof-of-reality@users.noreply.github.com> Date: Fri, 28 Jul 2023 13:47:39 +0100 Subject: [PATCH 05/64] Add icons to better visualize the flow of HTLCs (#311) --- gui/templates/base.html | 8 +++++--- gui/templates/home.html | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/gui/templates/base.html b/gui/templates/base.html index 45f117d3..ab443f85 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -33,11 +33,13 @@ const failedHTLCs_template = { "timestamp": htlc => ({innerHTML: formatDate(htlc.timestamp), title: adjustTZ(htlc.timestamp) }), "chan_id_in": htlc => ({innerHTML: `${(BigInt(htlc.chan_id_in)>>40n)+'x'+(BigInt(htlc.chan_id_in)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(htlc.chan_id_in) & BigInt('0xFFFF'))}`}), - "chan_id_out": htlc => ({innerHTML: `${(BigInt(htlc.chan_id_out)>>40n)+'x'+(BigInt(htlc.chan_id_out)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(htlc.chan_id_out) & BigInt('0xFFFF'))}`}), "chan_in_alias": htlc => ({innerHTML: `${htlc.chan_in_alias || htlc.chan_id_in}`}), + "fw_amount": htlc => ({innerHTML: htlc.amount.intcomma() + ` +${htlc.missed_fee.intcomma()}`, style: {paddingRight: "4px", width: "160px"} }), + "symbolIn" : _ => ({innerHTML: "●━━━", style: {paddingRight: "0px", paddingLeft: "0px", textAlign: 'right'} }), + "symbolOut" : _ => ({innerHTML: "━━━▏", style: {paddingLeft: "0px", paddingRight: "0px", textAlign: 'left'} }), + "chan_out_liq": htlc => ({innerHTML: `${(htlc.chan_out_liq || 0).intcomma()} ${htlc.chan_out_pending}`, style: {paddingLeft: "0px", width: "160px"} }), "chan_out_alias": htlc => ({innerHTML: `${htlc.chan_out_alias || htlc.chan_id_out}`}), - "amount": htlc => ({innerHTML: htlc.amount.intcomma()}), - "chan_out_liq": htlc => ({innerHTML: `${(htlc.chan_out_liq || 0).intcomma()} ${htlc.chan_out_pending}`}), + "chan_id_out": htlc => ({innerHTML: `${(BigInt(htlc.chan_id_out)>>40n)+'x'+(BigInt(htlc.chan_id_out)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(htlc.chan_id_out) & BigInt('0xFFFF'))}`}), "missed_fee": htlc => ({innerHTML: parseFloat(htlc.missed_fee.toFixed(3)).toLocaleString()}), "wire_failure": htlc => ({innerHTML: htlc.wire_failure > wire_failures.length ? htlc.wire_failure : wire_failures[htlc.wire_failure]}), "failure_detail": htlc => ({innerHTML: htlc.failure_detail > failure_details.length ? htlc.failure_detail : failure_details[htlc.failure_detail]}), diff --git a/gui/templates/home.html b/gui/templates/home.html index 5dba4ba1..b3682a35 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -494,11 +494,11 @@

Channels Waiting To Close

- + + - - + From f4e775634539f294257d251077907f15a07e1874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio?= <43297242+proof-of-reality@users.noreply.github.com> Date: Fri, 28 Jul 2023 14:36:27 +0100 Subject: [PATCH 06/64] [Routed table] Add icons & split `in` and `out` (#310) --- gui/templates/base.html | 10 ++++++---- gui/templates/home.html | 8 ++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/gui/templates/base.html b/gui/templates/base.html index ab443f85..e681f648 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -19,11 +19,13 @@ } const routed_template = { "forward_date": f => ({innerHTML: formatDate(f.forward_date), title: adjustTZ(f.forward_date) }), - "amt_in": f => ({innerHTML: f.amt_in > 1000 ? f.amt_in.intcomma() : f.amt_in}), - "amt_out": f => ({innerHTML: f.amt_out > 1000 ? f.amt_out.intcomma() : f.amt_out}), - "chan_in_alias": f => ({innerHTML: f.chan_in_alias || f.chan_id_in }), - "chan_out_alias": f => ({innerHTML: f.chan_out_alias || f.chan_id_out }), "chan_id_in": f => ({innerHTML: `${(BigInt(f.chan_id_in)>>40n)+'x'+(BigInt(f.chan_id_in)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(f.chan_id_in) & BigInt('0xFFFF'))}`}), + "chan_in_alias": f => ({innerHTML: f.chan_in_alias || f.chan_id_in}), + "amt_in": f => ({innerHTML: f.amt_in > 1000 ? f.amt_in.intcomma() : f.amt_in, style: {paddingRight: "0px", width: "160px"} }), + "symbolIn": f => ({innerHTML: "●━━━", style: {paddingRight: "0px", paddingLeft: "0px", textAlign: 'right'} }), + "symbolOut": f => ({innerHTML: "━━━○", style: {paddingLeft: "0px", paddingRight: "0px", textAlign: 'left'} }), + "amt_out": f => ({innerHTML: (f.amt_out > 1000 ? f.amt_out.intcomma() : f.amt_out) + ` +${f.fee.intcomma().toLocaleString()}`, style: {paddingLeft: "0px", width: "160px"} }), + "chan_out_alias": f => ({innerHTML: f.chan_out_alias || f.chan_id_out}), "chan_id_out": f => ({innerHTML: `${(BigInt(f.chan_id_out)>>40n)+'x'+(BigInt(f.chan_id_out)>>16n & BigInt('0xFFFFFF'))+'x'+(BigInt(f.chan_id_out) & BigInt('0xFFFF'))}`}), "fee": f => ({innerHTML: parseFloat(f.fee.toFixed(3)).toLocaleString()}), "ppm": f => ({innerHTML: parseInt(f.ppm.toFixed(0)).toLocaleString()}), diff --git a/gui/templates/home.html b/gui/templates/home.html index b3682a35..77f9f960 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -429,11 +429,11 @@

Channels Waiting To Close

- - - - + + + + From ef4d58d89dc4d844f2ea00b52183be1975cac00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio?= <43297242+proof-of-reality@users.noreply.github.com> Date: Thu, 3 Aug 2023 19:41:03 +0100 Subject: [PATCH 07/64] Add Home auto refresh from API (#299) --- gui/templates/base.html | 8 + gui/templates/home.html | 460 ++++++++++++++-------------- gui/templates/rebalances_table.html | 12 +- gui/urls.py | 1 + gui/views.py | 237 +++++++------- 5 files changed, 382 insertions(+), 336 deletions(-) diff --git a/gui/templates/base.html b/gui/templates/base.html index e681f648..b8f5546f 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -127,6 +127,14 @@

My Lnd Overview

if (document.cookie.includes("privacy=soft")) document.body.classList.toggle("soft-encrypted") else if (document.cookie.includes("privacy=hard")) document.body.classList.toggle("encrypted") }) + + async function auto_refresh(async_callback) { + await sleep(21000) + while(document.hidden) { + await sleep(100) + } + await async_callback() + } //END: BASE CONFIG //------------------------------------------------------------------------------------------------------------------------- diff --git a/gui/templates/home.html b/gui/templates/home.html index 77f9f960..d61cb33c 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -25,14 +25,14 @@

{{ node_info.alias }} | {{ node_info.identity_pubkey }}

- LND - {% for info in node_info.chains %}{{ info }}{% endfor %} | {{ node_info.block_height }} #{{ node_info.block_hash }} + LND + ---
Public Capacity: 0 - Active Channels: {{node_info.num_active_channels}} / {{node_info.num_active_channels|add:node_info.num_inactive_channels}} - Peers: {{ node_info.num_peers }} - DB Size: {% if db_size > 0 %}{{ db_size }} GB{% else %}---{% endif %} + Active Channels: 0 / 0 + 0 + --- Total States Updates: --- Sign a message
@@ -84,12 +84,12 @@
Requested
Timestamp Chan In IDChan Out ID Chan In AliasForward AmountActual Outbound Chan Out AliasForward AmountActual OutboundChan Out ID Potential Fee HTLC Failure Failure Detail
TimestampAmount InAmount OutChannel In AliasChannel Out Alias Channel In IdChannel In AliasAmount InAmount OutChannel Out Alias Channel Out Id Fees Earned PPM Earned
- - - - - - + + + + + +
Balance: {{total_balance}}Onchain: {{ balances.total_balance|intcomma }}Confirmed: {{ balances.confirmed_balance|intcomma }}Unconfirmed: {{ balances.unconfirmed_balance|intcomma }}Limbo: {{ limbo_balance|intcomma }} Pending HTLCs: 0{{ pending_htlc_count }}Balance: 0Onchain: 0Confirmed: 0Unconfirmed: 0Limbo: 0Pending HTLCs: 0
{% csrf_token %} @@ -147,7 +147,7 @@
Batching Advanced Settings -
+ -
+ -
+ -{% if pending_open %} -
-

Pending Open Channels

+ -{% endif %} -{% if pending_closed %} -
-

Pending Close Channels

+ -{% endif %} -{% if pending_force_closed %} -
-

Pending Force Close Channels

+ -{% endif %} -{% if waiting_for_close %} -
-

Channels Waiting To Close

+ -{% endif %} -
- +
@@ -451,7 +323,7 @@

Channels Waiting To Close

Timestamp
{% include 'rebalances_table.html' with count=10 load_count=10 title='Rebalance Requests' %} -
+ -
+ -
+ + {% endif %} {% if not peers %}
diff --git a/gui/urls.py b/gui/urls.py index 55ab7f9d..5757a12f 100644 --- a/gui/urls.py +++ b/gui/urls.py @@ -72,6 +72,7 @@ path('api/', include(router.urls), name='api-root'), path('api-auth/', include('rest_framework.urls'), name='api-auth'), path('api/connectpeer/', views.connect_peer, name='connect-peer'), + path('api/disconnectpeer/', views.disconnect_peer, name='disconnect-peer'), path('api/rebalance_stats/', views.rebalance_stats, name='rebalance-stats'), path('api/openchannel/', views.open_channel, name='open-channel'), path('api/closechannel/', views.close_channel, name='close-channel'), diff --git a/gui/views.py b/gui/views.py index 9e5d4827..f4df9256 100644 --- a/gui/views.py +++ b/gui/views.py @@ -11,7 +11,7 @@ from rest_framework.permissions import IsAuthenticated from .forms import OpenChannelForm, CloseChannelForm, ConnectPeerForm, AddInvoiceForm, RebalancerForm, UpdateChannel, UpdateSetting, LocalSettingsForm, AddTowerForm, RemoveTowerForm, DeleteTowerForm, BatchOpenForm, UpdatePending, UpdateClosing, UpdateKeysend, AddAvoid, RemoveAvoid from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, Closures, Resolutions, PendingHTLCs, FailedHTLCs, Autopilot, Autofees, PendingChannels, AvoidNodes, PeerEvents -from .serializers import ConnectPeerSerializer, FailedHTLCSerializer, LocalSettingsSerializer, OpenChannelSerializer, CloseChannelSerializer, AddInvoiceSerializer, PaymentHopsSerializer, PaymentSerializer, InvoiceSerializer, ForwardSerializer, ChannelSerializer, PendingHTLCSerializer, RebalancerSerializer, UpdateAliasSerializer, PeerSerializer, OnchainSerializer, ClosuresSerializer, ResolutionsSerializer, BumpFeeSerializer, UpdateChanPolicy, NewAddressSerializer, BroadcastTXSerializer, PeerEventsSerializer, SignMessageSerializer, FeeLogSerializer +from .serializers import ConnectPeerSerializer, DisconnectPeerSerializer, FailedHTLCSerializer, LocalSettingsSerializer, OpenChannelSerializer, CloseChannelSerializer, AddInvoiceSerializer, PaymentHopsSerializer, PaymentSerializer, InvoiceSerializer, ForwardSerializer, ChannelSerializer, PendingHTLCSerializer, RebalancerSerializer, UpdateAliasSerializer, PeerSerializer, OnchainSerializer, ClosuresSerializer, ResolutionsSerializer, BumpFeeSerializer, UpdateChanPolicy, NewAddressSerializer, BroadcastTXSerializer, PeerEventsSerializer, SignMessageSerializer, FeeLogSerializer from gui.lnd_deps import lightning_pb2 as ln from gui.lnd_deps import lightning_pb2_grpc as lnrpc from gui.lnd_deps import router_pb2 as lnr @@ -2381,10 +2381,37 @@ def connect_peer(request): elif peer_id.count('@') == 1 and len(peer_id.split('@')[0]) == 66: peer_pubkey, host = peer_id.split('@') else: - raise Exception('Invalid peer pubkey or connection string.') + return Response({'error': 'Invalid peer pubkey or connection string.'}) ln_addr = ln.LightningAddress(pubkey=peer_pubkey, host=host) - response = stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr)) - return Response({'message': 'Connection successful!' + str(response)}) + stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln_addr)) + return Response({'message': 'Connection successful!'}) + except Exception as e: + error = str(e) + details_index = error.find('details =') + 11 + debug_error_index = error.find('debug_error_string =') - 3 + error_msg = error[details_index:debug_error_index] + return Response({'error': 'Connection request failed! Error: ' + error_msg}) + else: + return Response({'error': 'Invalid request!'}) + +@api_view(['POST']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) +def disconnect_peer(request): + serializer = DisconnectPeerSerializer(data=request.data) + if serializer.is_valid(): + try: + stub = lnrpc.LightningStub(lnd_connect()) + peer_id = serializer.validated_data['peer_id'] + if len(peer_id) == 66: + peer_pubkey = peer_id + else: + return Response({'error': 'Invalid peer pubkey.'}) + stub.DisconnectPeer(ln.DisconnectPeerRequest(pub_key=peer_pubkey)) + if Peers.objects.filter(pubkey=peer_id).exists(): + db_peer = Peers.objects.filter(pubkey=peer_id)[0] + db_peer.connected = False + db_peer.save() + return Response({'message': 'Disconnection successful!'}) except Exception as e: error = str(e) details_index = error.find('details =') + 11 From cce0b2db026b9cf1512907e33039100c603e1380 Mon Sep 17 00:00:00 2001 From: Impalor <101550606+Impa10r@users.noreply.github.com> Date: Sun, 19 Nov 2023 01:37:52 +0100 Subject: [PATCH 40/64] Increase maximum allowed fee bump to 1000 (#340) --- gui/templates/balances.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/templates/balances.html b/gui/templates/balances.html index d1211eb4..c18570e4 100644 --- a/gui/templates/balances.html +++ b/gui/templates/balances.html @@ -152,4 +152,4 @@

Transactions

byId('estimated').style.display = "block"; } -{% endblock %} \ No newline at end of file +{% endblock %} From 96c79a18fae736217f7631ac72c01b0fe5c0c07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio?= <43297242+proof-of-reality@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:39:47 +0000 Subject: [PATCH 41/64] Add command line args to keysend (#341) --- keysend.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/keysend.py b/keysend.py index febc9aeb..9c7a9ac0 100644 --- a/keysend.py +++ b/keysend.py @@ -1,3 +1,4 @@ +import argparse import secrets, time, struct from hashlib import sha256 from gui.lnd_deps import lightning_pb2 as ln @@ -52,25 +53,19 @@ def keysend(target_pubkey, msg, amount, fee_limit, timeout, sign): error_msg = error[details_index:debug_error_index] print('Error while sending keysend payment! Error: ' + error_msg) -def main(): - #Ask user for variables - try: - target_pubkey = input('Enter the pubkey of the node you want to send a keysend payment to: ') - amount = int(input('Enter an amount in sats to be sent with the keysend payment (defaults to 1 sat): ') or '1') - fee_limit = int(input('Enter an amount in sats to be used as a max fee limit for sending (defaults to 1 sat): ') or '1') - msg = input('Enter an optional message to be included (leave this blank for no message): ') - if len(msg) > 0: - sign = input('Self sign the message? (defaults to sending anonymously) [y/N]: ') - sign = True if sign.lower() == 'yes' or sign.lower() == 'y' else False - else: - sign = False - except: - print('Invalid data entered, please try again.') - timeout = 10 - print('Sending keysend payment of %s to: %s' % (amount, target_pubkey)) +def main(pubkey, amount, fee, msg, sign): + print('Sending a %s sats payment to: %s with %s sats max-fee' % (amount, pubkey, fee)) if len(msg) > 0: - print('Attaching this message to the keysend payment:', msg) - keysend(target_pubkey, msg, amount, fee_limit, timeout, sign) + print('MESSAGE: %s' % msg) + timeout = 10 + keysend(pubkey, msg, amount, fee, timeout, sign) if __name__ == '__main__': - main() \ No newline at end of file + argParser = argparse.ArgumentParser(prog="python keysend.py") + argParser.add_argument("-pk", "--pubkey", help='Target public key', required=True) + argParser.add_argument("-a", "--amount", help='Amount in sats (default: 1)', nargs='?', default=1, type=int) + argParser.add_argument("-f", "--fee", help='Max fee to send this keysend (default: 1)', nargs='?', default=1, type=int) + argParser.add_argument("-m", "--msg", help='Message to be sent (default: "")', nargs='?', default='', type=str) + argParser.add_argument("--sign", help='Sign this message (default: send anonymously) - if [MSG] is provided', action='store_true') + args = vars(argParser.parse_args()) + main(**args) \ No newline at end of file From f38d4193f0e60b5104d3cba9109b3a63e5873819 Mon Sep 17 00:00:00 2001 From: cryptosharks131 Date: Tue, 21 Nov 2023 16:47:58 -0500 Subject: [PATCH 42/64] Address public capacity issues --- gui/templates/home.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gui/templates/home.html b/gui/templates/home.html index 93ca658b..42e9114a 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -612,7 +612,7 @@

Sign a Message

return true } function build_active(channels, forwards_sum){ - let sum = {capacity: 0, inbound: 0, outbound: 0, unsettled: 0, earned:{d1:0, d7:0}, rOUTed:{d1:{amt:0, count:0}, d7:{amt:0, count:0} } } + let sum = {inbound: 0, outbound: 0, unsettled: 0, earned:{d1:0, d7:0}, rOUTed:{d1:{amt:0, count:0}, d7:{amt:0, count:0} } } if(!addTitle("Active Channels", channels)) return sum const [activeChannels, template] = [byId("active_channels"), use(detailed_ch_template)] @@ -623,7 +623,6 @@

Sign a Message

ch.fee_ratio = ch.local_fee_rate == 0 ? 100 : parseInt(ch.remote_fee_rate*100/ch.local_fee_rate) ch.fee_check = parseInt(ch.fee_ratio*100/ch.ar_max_cost) - sum.capacity += ch.capacity sum.inbound += ch.remote_balance sum.outbound += ch.local_balance sum.unsettled += ch.unsettled_balance @@ -652,7 +651,6 @@

Sign a Message

}).sort((ch1, ch2) => ch1.percent_outbound - ch2.percent_outbound) .forEach(ch => activeChannels.append(template.render(ch))) - byId('public_capacity').innerHTML = sum.capacity.intcomma() return update_liq('active_', sum) } function build_private(channels){ @@ -688,20 +686,18 @@

Sign a Message

return Object.assign({}, sum, {outbound: act.outbound + ina.outbound, inbound: act.inbound + ina.inbound}) } function build_inactive(channels){ - let sum = {capacity: 0, inbound: 0, outbound: 0, unsettled: 0} + let sum = {inbound: 0, outbound: 0, unsettled: 0} if(!addTitle("Inactive Channels", channels)) return sum - const table = byId('inactive_channels'), template = use(inactive_ch_template), public_capacity = byId('public_capacity') + const table = byId('inactive_channels'), template = use(inactive_ch_template) table.innerHTML = null channels.forEach(ch => { - sum.capacity += ch.capacity sum.inbound += ch.remote_balance sum.outbound += ch.local_balance sum.unsettled += ch.unsettled_balance table.append(template.render(ch)) }) - public_capacity.innerHTML = (public_capacity.innerHTML.toInt() + sum.capacity).intcomma() return update_liq('inactive_', sum) } function update_liq(status, sum){ @@ -954,7 +950,7 @@

Sign a Message

const invoices_task = GET('invoices', {data: {state__lt: 2, limit: 10} }) const failedHTLCs_task = GET('failedhtlcs', {data: {wire_failure__lt: 99, limit:10} }) - let [updates, pending_htlcs, active, inactive, private] = [0,0,[],[],[]] + let [updates, pending_htlcs, active, inactive, private, public_capacity] = [0,0,[],[],[],0] for (ch of (await GET('channels', {data: {is_open:true} })).results){ //basic setup updates += ch.num_updates pending_htlcs += ch.htlc_count @@ -964,11 +960,15 @@

Sign a Message

ch.percent_outbound = parseInt(ch.local_balance*100/ch.capacity) if(ch.private) private.push(ch) - else (ch.is_active ? active : inactive).push(ch) + else { + (ch.is_active ? active : inactive).push(ch) + public_capacity += ch.capacity + } } byId("updates").innerHTML = updates.intcomma() byId("pending_htlcs").innerHTML = pending_htlcs.intcomma() + byId('public_capacity').innerHTML = public_capacity.intcomma() let [forwardsSummary, payments7d] = [(await forwardsSummary_task).results, (await payments_task).results] let [forwards_sum, forwards_summary] = process_forwards(forwardsSummary) From c5593a34a6b4a6d5c5bf4d1600425d1276370d4f Mon Sep 17 00:00:00 2001 From: Florian Peter <4farlion@gmail.com> Date: Mon, 27 Nov 2023 16:08:13 +0000 Subject: [PATCH 43/64] docs: fix typo in README.md (#344) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8965d2d8..38d4d41b 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ AF Notes: ## Auto-Rebalancer - [Quick Start Guide](https://github.com/cryptosharks131/lndg/blob/master/quickstart.md) ### Here are some additional notes to help you better understand the Auto-Rebalancer (AR). -The objective of the Auto-Rebalancer is to "refill" the liquidity on the local side (i.e. OUTBOUND) of profitable and lucarative channels. So that, when a forward comes in from another node there is always enough liquidity to route the payment and in return collect the desired routing fees. +The objective of the Auto-Rebalancer is to "refill" the liquidity on the local side (i.e. OUTBOUND) of profitable and lucrative channels. So that, when a forward comes in from another node there is always enough liquidity to route the payment and in return collect the desired routing fees. 1. The AR variable `AR-Enabled` must be set to 1 (enabled) in order to start looking for new rebalance opportunities. (default=0) 2. The AR variable `AR-Target%` defines the % size of the channel capacity you would like to use for rebalance attempts. Example: If a channel size is 1M Sats and AR-Target% = 0.05 LNDg will select an amount of 5% of 1M = 50K for rebalancing. (default=5) From a3367037f01fb71ca6b2c5b45debfd4255c5cd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio?= <43297242+proof-of-reality@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:12:12 +0000 Subject: [PATCH 44/64] Add scroll to pending opens table (#342) --- gui/templates/home.html | 44 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/gui/templates/home.html b/gui/templates/home.html index 42e9114a..36998441 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -234,27 +234,29 @@
+ + + +{% endblock %} diff --git a/gui/urls.py b/gui/urls.py index 5757a12f..f00c171c 100644 --- a/gui/urls.py +++ b/gui/urls.py @@ -18,6 +18,7 @@ router.register(r'pendinghtlcs', views.PendingHTLCViewSet) router.register(r'failedhtlcs', views.FailedHTLCViewSet) router.register(r'peerevents', views.PeerEventsViewSet) +router.register(r'trades', views.TradeSalesViewSet) router.register(r'feelog', views.FeeLogViewSet) urlpatterns = [ @@ -29,6 +30,7 @@ path('closures', views.closures, name='closures'), path('towers', views.towers, name='towers'), path('batch', views.batch, name='batch'), + path('trades', views.trades, name='trades'), path('batchopen/', views.batch_open, name='batch-open'), path('resolutions', views.resolutions, name='resolutions'), path('channel', views.channel, name='channel'), @@ -88,6 +90,7 @@ path('api/chanpolicy/', views.chan_policy, name='chan-policy'), path('api/broadcast_tx/', views.broadcast_tx, name='broadcast-tx'), path('api/node_info/', views.node_info, name='node-info'), + path('api/createtrade/', views.create_trade, name='create-trade'), path('api/forwards_summary/', views.forwards_summary, name='forwards-summary'), path('api/sign_message/', views.sign_message, name='sign-message'), path('lndg-admin/', admin.site.urls), diff --git a/gui/views.py b/gui/views.py index f4df9256..009d0e87 100644 --- a/gui/views.py +++ b/gui/views.py @@ -9,9 +9,9 @@ from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated -from .forms import OpenChannelForm, CloseChannelForm, ConnectPeerForm, AddInvoiceForm, RebalancerForm, UpdateChannel, UpdateSetting, LocalSettingsForm, AddTowerForm, RemoveTowerForm, DeleteTowerForm, BatchOpenForm, UpdatePending, UpdateClosing, UpdateKeysend, AddAvoid, RemoveAvoid -from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, Closures, Resolutions, PendingHTLCs, FailedHTLCs, Autopilot, Autofees, PendingChannels, AvoidNodes, PeerEvents -from .serializers import ConnectPeerSerializer, DisconnectPeerSerializer, FailedHTLCSerializer, LocalSettingsSerializer, OpenChannelSerializer, CloseChannelSerializer, AddInvoiceSerializer, PaymentHopsSerializer, PaymentSerializer, InvoiceSerializer, ForwardSerializer, ChannelSerializer, PendingHTLCSerializer, RebalancerSerializer, UpdateAliasSerializer, PeerSerializer, OnchainSerializer, ClosuresSerializer, ResolutionsSerializer, BumpFeeSerializer, UpdateChanPolicy, NewAddressSerializer, BroadcastTXSerializer, PeerEventsSerializer, SignMessageSerializer, FeeLogSerializer +from .forms import * +from .serializers import * +from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, Closures, Resolutions, PendingHTLCs, FailedHTLCs, Autopilot, Autofees, PendingChannels, AvoidNodes, PeerEvents, TradeSales from gui.lnd_deps import lightning_pb2 as ln from gui.lnd_deps import lightning_pb2_grpc as lnrpc from gui.lnd_deps import router_pb2 as lnr @@ -25,7 +25,9 @@ from os import path from pandas import DataFrame, merge from requests import get -from . import af +from secrets import token_bytes +from trade import create_trade_details +import af def graph_links(): if LocalSettings.objects.filter(key='GUI-GraphLinks').exists(): @@ -1194,6 +1196,17 @@ def batch(request): else: return redirect(request.META.get('HTTP_REFERER')) +@is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) +def trades(request): + if request.method == 'GET': + stub = lnrpc.LightningStub(lnd_connect()) + context = { + 'trade_link': create_trade_details(stub) + } + return render(request, 'trades.html', context) + else: + return redirect(request.META.get('HTTP_REFERER')) + @is_login_required(login_required(login_url='/lndg-admin/login/?next=/'), settings.LOGIN_REQUIRED) def addresses(request): if request.method == 'GET': @@ -2133,6 +2146,19 @@ class PeerEventsViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = PeerEventsSerializer filterset_fields = {'chan_id': ['exact'], 'id': ['lt']} +class TradeSalesViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] + queryset = TradeSales.objects.all() + serializer_class = TradeSalesSerializer + + def update(self, request, pk): + rebalance = get_object_or_404(self.queryset, pk=pk) + serializer = self.get_serializer(rebalance, data=request.data, context={'request': request}, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors) + class FeeLogViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [IsAuthenticated] if settings.LOGIN_REQUIRED else [] queryset = Autofees.objects.all().order_by('-id') @@ -2817,7 +2843,7 @@ def broadcast_tx(request): if response.publish_error == '': return Response({'message': f'Successfully broadcast tx!'}) else: - return Response({'message': f'Error while broadcasting TX: {response.publish_error}'}) + return Response({'error': f'Error while broadcasting TX: {response.publish_error}'}) except Exception as e: error = str(e) details_index = error.find('details =') + 11 @@ -2825,4 +2851,26 @@ def broadcast_tx(request): error_msg = error[details_index:debug_error_index] return Response({'error': f'TX broadcast failed! Error: {error_msg}'}) else: - return Response({'error': 'Invalid request!'}) \ No newline at end of file + return Response({'error': 'Invalid request!'}) + +@api_view(['POST']) +@is_login_required(permission_classes([IsAuthenticated]), settings.LOGIN_REQUIRED) +def create_trade(request): + serializer = CreateTradeSerializer(data=request.data) + if serializer.is_valid(): + description = serializer.validated_data['description'] + price = serializer.validated_data['price'] + sale_type = serializer.validated_data['type'] + secret = serializer.validated_data['secret'] + expiry = serializer.validated_data['expiry'] + sale_limit = serializer.validated_data['sale_limit'] + trade_id = token_bytes(32).hex() + try: + new_trade = TradeSales(id=trade_id, description=description, price=price, secret=secret, expiry=expiry, sale_type=sale_type, sale_limit=sale_limit) + new_trade.save() + return Response({'message': f'Created trade: {description}', 'id': new_trade.id, 'description': new_trade.description, 'price': new_trade.price, 'expiry': new_trade.expiry, 'sale_type': new_trade.sale_type, 'secret': new_trade.secret, 'sale_count': new_trade.sale_count, 'sale_limit': new_trade.sale_limit}) + except Exception as e: + error = str(e) + return Response({'error': f'Error creating trade: {error}'}) + else: + return Response({'error': serializer.error_messages}) \ No newline at end of file diff --git a/jobs.py b/jobs.py index 59d12694..5fcb3143 100644 --- a/jobs.py +++ b/jobs.py @@ -14,7 +14,7 @@ environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings' django.setup() from gui.models import Payments, PaymentHops, Invoices, Forwards, Channels, Peers, Onchain, Closures, Resolutions, PendingHTLCs, LocalSettings, FailedHTLCs, Autofees, PendingChannels, HistFailedHTLC, PeerEvents -from gui import af +import af def update_payments(stub): self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey diff --git a/p2p.py b/p2p.py new file mode 100644 index 00000000..e09abe5e --- /dev/null +++ b/p2p.py @@ -0,0 +1,50 @@ +import django, multiprocessing +from datetime import datetime +from gui.lnd_deps import lightning_pb2_grpc as lnrpc +from gui.lnd_deps.lnd_connect import lnd_connect +from os import environ +from time import sleep +environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings' +django.setup() +from gui.models import LocalSettings +from trade import serve_trades + +def trade(): + stub = lnrpc.LightningStub(lnd_connect()) + serve_trades(stub) + +def check_setting(): + if LocalSettings.objects.filter(key='LND-ServeTrades').exists(): + return int(LocalSettings.objects.filter(key='LND-ServeTrades')[0].value) + else: + LocalSettings(key='LND-ServeTrades', value='0').save() + return 0 + +def main(): + while True: + current_value = None + try: + while True: + db_value = check_setting() + if current_value != db_value: + if db_value == 1: + print(f"{datetime.now().strftime('%c')} : [P2P] : Starting the p2p service...") + django.db.connections.close_all() + p2p_thread = multiprocessing.Process(target=trade) + p2p_thread.start() + else: + if 'p2p_thread' in locals() and p2p_thread.is_alive(): + print(f"{datetime.now().strftime('%c')} : [P2P] : Stopping the p2p service...") + p2p_thread.terminate() + current_value = db_value + sleep(2) # polling interval + except Exception as e: + print(f"{datetime.now().strftime('%c')} : [P2P] : Error running p2p service: {str(e)}") + finally: + if 'p2p_thread' in locals() and p2p_thread.is_alive(): + print(f"{datetime.now().strftime('%c')} : [P2P] : Removing any remaining processes...") + p2p_thread.terminate() + sleep(20) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0e077df7..4f5c5ca4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,6 @@ protobuf pytz pandas requests -asyncio \ No newline at end of file +asyncio +bech32 +cryptography \ No newline at end of file diff --git a/trade.py b/trade.py new file mode 100644 index 00000000..5ee64756 --- /dev/null +++ b/trade.py @@ -0,0 +1,1059 @@ +import django, base64, secrets, re, asyncio, json +from django.db.models import Sum, IntegerField, Count, F, Q +from django.db.models.functions import Round +from time import time +from bech32 import bech32_decode, bech32_encode, convertbits +from datetime import datetime, timedelta +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +from decimal import Decimal +from lndg import settings +from gui.lnd_deps import lightning_pb2 as ln +from gui.lnd_deps import lightning_pb2_grpc as lnrpc +from gui.lnd_deps import signer_pb2 as lns +from gui.lnd_deps import signer_pb2_grpc as lnsigner +from gui.lnd_deps import router_pb2 as lnr +from gui.lnd_deps import router_pb2_grpc as lnrouter +from gui.lnd_deps.lnd_connect import lnd_connect, async_lnd_connect +from os import environ +environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings' +django.setup() +from gui.models import TradeSales, Payments, PaymentHops, Forwards, Peers + +def is_hex(n): + return len(n) % 2 == 0 and all(c in '0123456789ABCDEFabcdef' for c in n) + +def hex_as_utf8(hex_str): + return bytes.fromhex(hex_str).decode('utf-8') + +def utf8_as_hex(utf8_str): + return bytes(utf8_str, 'utf-8').hex() + +def decode_basic_trade(records): + if not isinstance(records, list): + raise ValueError('ExpectedArrayOfRecordsToDecodeBasicTrade') + + description_record = next((record for record in records if record['type'] == '2'), None) + + if not description_record: + raise ValueError('ExpectedDescriptionRecordToDecodeBasicTrade') + + id_record = next((record for record in records if record['type'] == '1'), None) + + if not id_record: + raise ValueError('ExpectedIdRecordToDecodeBasicTradeDetails') + + return { + 'description': hex_as_utf8(description_record['value']), + 'id': id_record['value'] + } + +def encode_as_bigsize(number: int): + max_8_bit_number = 252 + max_16_bit_number = 65535 + max_32_bit_number = 4294967295 + + def tag_as_uint8(n): + return format(n, '02x') + + def tag_as_uint16(n): + return 'fd' + format(n, '04x') + + def tag_as_uint32(n): + return 'fe' + format(n, '08x') + + def tag_as_uint64(n): + return 'ff' + format(n, '016x') + + number = int(number) + + if number <= max_8_bit_number: + return tag_as_uint8(number) + elif number <= max_16_bit_number: + return tag_as_uint16(number) + elif number <= max_32_bit_number: + return tag_as_uint32(number) + else: + return tag_as_uint64(number) + +def decode_as_bigsize(encoded_string: str): + def read_uint8(encoded_string): + return int(encoded_string[:2], 16), encoded_string[2:] + + def read_uint16(encoded_string): + return int(encoded_string[2:6], 16), encoded_string[6:] + + def read_uint32(encoded_string): + return int(encoded_string[2:10], 16), encoded_string[10:] + + def read_uint64(encoded_string): + return int(encoded_string[2:18], 16), encoded_string[18:] + + if encoded_string.startswith('ff'): + value, remaining = read_uint64(encoded_string) + elif encoded_string.startswith('fe'): + value, remaining = read_uint32(encoded_string) + elif encoded_string.startswith('fd'): + value, remaining = read_uint16(encoded_string) + else: + value, remaining = read_uint8(encoded_string) + + return value + +def decode_big_size(encoded): + max_8bit_number = 0xfc + max_16bit_number = 0xffff + max_32bit_number = 0xffffffff + uint8_length = 1 + uint16 = 0xfd + uint16_length = 3 + uint32 = 0xfe + uint32_length = 5 + uint64_length = 9 + + if not bool(encoded) and is_hex(encoded): + raise ValueError('ExpectedHexEncodedBigSizeValueToDecode') + + bytes_data = bytes.fromhex(encoded) + + size = bytes_data[0] + + if size <= max_8bit_number: + return {'decoded': str(size), 'length': uint8_length} + + byte_length = len(bytes_data) + + if size == uint16: + if byte_length < uint16_length: + raise ValueError('ExpectedMoreBytesToDecodeUint16BigSize') + + uint16_number = int.from_bytes(bytes_data[1:3], 'big') + + if uint16_number <= max_8bit_number: + raise ValueError('ExpectedLargerNumberToDecodeUint16BigSize') + + return {'decoded': str(uint16_number), 'length': uint16_length} + elif size == uint32: + if byte_length < uint32_length: + raise ValueError('ExpectedMoreBytesToDecodeUint32BigSize') + + uint32_number = int.from_bytes(bytes_data[1:5], 'big') + + if uint32_number <= max_16bit_number: + raise ValueError('ExpectedLargerNumberToDecodeUint32BigSize') + + return {'decoded': str(uint32_number), 'length': uint32_length} + else: + if byte_length < uint64_length: + raise ValueError('ExpectedMoreBytesToDecodeUint64BigSize') + + uint64_number = int.from_bytes(bytes_data[1:9], 'big') + + if uint64_number <= max_32bit_number: + raise ValueError('ExpectedLargerNumberToDecodeUint64BigSize') + + return {'decoded': str(uint64_number), 'length': uint64_length} + +def decode_tlv_record(data): + def hex_len(byte_length): + return 0 if not byte_length else byte_length * 2 + + def read(from_pos, hex_str, to_pos=None): + if to_pos is None: + return hex_str[from_pos:] + else: + return hex_str[from_pos:to_pos + from_pos] + encoded = data['encoded'] + offset = data.get('offset', 0) + + start = hex_len(offset) + + type_record = decode_big_size(read(start, encoded)) + + size_start = start + hex_len(type_record['length']) + + bytes_record = decode_big_size(read(size_start, encoded)) + + meta_bytes = type_record['length'] + bytes_record['length'] + + if not int(bytes_record['decoded']): + return {'length': meta_bytes, 'type': type_record['decoded'], 'value': ''} + + value_start = start + hex_len(meta_bytes) + total_bytes = meta_bytes + int(bytes_record['decoded']) + + if start + hex_len(total_bytes) > len(encoded): + raise ValueError('ExpectedAdditionalValueBytesInTlvRecord') + + return { + 'length': total_bytes, + 'type': type_record['decoded'], + 'value': read(value_start, encoded, hex_len(int(bytes_record['decoded']))) + } + +def decode_tlv_stream(encoded): + + if not is_hex(encoded): + raise ValueError('ExpectedHexEncodedTlvStreamToDecode') + + if not encoded: + return [] + + total_bytes = len(encoded) // 2 + stream = {'offset': 0, 'records': []} + + while stream['offset'] < total_bytes: + stream['record'] = decode_tlv_record({'encoded': encoded, 'offset': stream['offset']}) + + stream['offset'] += stream['record']['length'] + stream['records'].append(stream['record']) + + return stream['records'] + +def parse_response_code(data): + encoded = data['encoded'] + + if not encoded: + raise ValueError('ExpectedResponseCodeValueToParseResponseCode') + + records = decode_tlv_stream(encoded) + code_record = next((record for record in records if record['type'] == '0'), None) + + if not code_record: + raise ValueError('ExpectedCodeRecordToParseResponseCode') + + code = int(decode_big_size(code_record['value'])['decoded']) + if code > 2 ** 53 - 1: + raise ValueError('UnexpectedlyLargeResponseCodeInResponse') + + if code < 100: + raise ValueError('UnexpectedlySmallResponseCodeInResponse') + + if not code > 400: + return {} + + message_record = next((record for record in records if record['type'] == '1'), None) or {'value': ''} + + return {'failure': [code, hex_as_utf8(message_record['value'])]} + +def parse_peer_request_message(message): + records = decode_tlv_stream(message[len('626f73ff'):]) + version = next((record for record in records if record['type'] == '0'), None) + + if version is not None: + raise ValueError('UnexpectedVersionNumberOfRequestMessage') + + id_record = next((record for record in records if record['type'] == '1'), None) + + if id_record is None or len(id_record['value']) != 64: + raise ValueError('ExpectedRequestIdInRequestMessage') + + records_record = next((record for record in records if record['type'] == '5'), None) or {'value':''} + + response_code_record = next((record for record in records if record['type'] == '2'), None) + type_record = next((record for record in records if record['type'] == '3'), None) + + if not type_record and not response_code_record: + raise ValueError('ExpectedEitherRequestParametersOrResponseCode') + + if response_code_record is not None: + response_code = parse_response_code({'encoded': response_code_record['value']}) + failure = response_code['failure'] if 'failure' in response_code else None + + return { + 'response': { + 'failure': failure, + 'id': id_record['value'], + 'records': [{'type': record['type'], 'value': record['value']} for record in decode_tlv_stream(records_record['value'])], + }, + } + else: + return { + 'request': { + 'id': id_record['value'], + 'records': [{'type': record['type'], 'value': record['value']} for record in decode_tlv_stream(records_record['value'])], + 'type': decode_big_size(type_record['value'])['decoded'], + }, + } + +def decode_basic_trade(records): + if not isinstance(records, list): + raise ValueError('ExpectedArrayOfRecordsToDecodeBasicTrade') + + description_record = next((record for record in records if record['type'] == '2'), None) + + if not description_record: + raise ValueError('ExpectedDescriptionRecordToDecodeBasicTrade') + + id_record = next((record for record in records if record['type'] == '1'), None) + + if not id_record: + raise ValueError('ExpectedIdRecordToDecodeBasicTradeDetails') + + return { + 'description': hex_as_utf8(description_record['value']), + 'id': id_record['value'] + } + +def decode_anchored_trade_data(encoded): + anchor_prefix = 'anchor-trade-secret:' + + if not encoded.startswith(anchor_prefix): + return {} + + encoded_data = encoded[len(anchor_prefix):] + + try: + decoded_data = decode_tlv_stream(base64.b64decode(encoded_data).hex()) + except Exception as e: + return {} + + records = decoded_data + channel_record = next((record for record in records if record['type'] == '3'), None) + description_record = next((record for record in records if record['type'] == '1'), None) + secret_record = next((record for record in records if record['type'] == '0'), None) + price_record = next((record for record in records if record['type'] == '2'), None) + + if channel_record: + try: + channel_value = int(decode_big_size({'encoded': channel_record['value']})['decoded']) + except ValueError: + return {} + + return { + 'channel': channel_value, + 'price': hex_as_utf8(price_record['value']) if price_record else None, + } + + if description_record and secret_record: + return { + 'description': hex_as_utf8(description_record['value']), + 'price': hex_as_utf8(price_record['value']) if price_record else None, + 'secret': hex_as_utf8(secret_record['value']), + } + return None + +def encode_tlv_record(data): + def byte_length_of(hex_string): + return len(hex_string) // 2 + + def encode(type, length, val): + return f"{type}{length}{val}" + + type_number = data["type"] + value_hex = data["value"] + + data_length = encode_as_bigsize(byte_length_of(value_hex)) + encoded_tlv_record = encode(encode_as_bigsize(type_number), data_length, value_hex) + + return encoded_tlv_record + +def encode_peer_request(data): + id = data['id'] + records = data.get('records') + request_type = data['type'] + if not id: + raise ValueError('ExpectedRequestIdHexStringToEncodePeerRequest') + + if records is not None and not isinstance(records, list): + raise ValueError('ExpectedRecordsArrayToEncodePeerRequest') + + if not request_type: + raise ValueError('ExpectedRequestTypeToEncodePeerRequest') + + peer_response = [{'type': '1', 'value': id},{'type': '3', 'value': encode_as_bigsize(request_type)},{'type': '5', 'value': records}] + + peer_response = [ + {'type': item['type'], 'value': item['value']} if not isinstance(item['value'], list) else {'type': item['type'], 'value': ''.join([encode_tlv_record(record) for record in item['value']])} + for item in peer_response + ] + + peer_response = [{'type': item['type'], 'value': str(item['value'])} for item in peer_response] + + return '626f73ff' + ''.join([encode_tlv_record(record) for record in peer_response]) + +def encode_response_code(data): + code_for_success = 200 + failure = data.get('failure') + + if not failure: + return ''.join([encode_tlv_record(record) for record in [{'type': '0', 'value': encode_as_bigsize(code_for_success)}]]) + + if not isinstance(failure, list): + raise ValueError('ExpectedFailureArrayToEncodeResponseCode') + + code, message = failure + + if not code: + raise ValueError('ExpectedErrorCodeToEncodeResponseCode') + + records = [{'value': encode_as_bigsize(code), 'type': '0'}, {'value': utf8_as_hex(message), 'type': '1'}] + + return ''.join([encode_tlv_record(record) for record in records]) + +def encode_peer_response(data): + failure = data.get('failure') + id = data['id'] + records = data.get('records') + + code = encode_response_code({'failure': failure}) + encoded = ''.join([encode_tlv_record(record) for record in records]) if records is not None else None + + peer_response = [{'type': '1', 'value': id}, {'type': '2', 'value': code}, {'type': '5', 'value': encoded}] + + return '626f73ff' + ''.join([encode_tlv_record(record) for record in peer_response]) + +def get_legacy_trades(stub): + trades = [] + for invoice in stub.ListInvoices(ln.ListInvoiceRequest(pending_only=True)).invoices: + if invoice.is_keysend == False: + trade = decode_anchored_trade_data(invoice.memo) + if trade: + trade['price'] = invoice.value + trade['id'] = invoice.r_hash.hex() + trade['expiry'] = invoice.expiry + trade['creation_date'] = invoice.creation_date + trades.append(trade) + return trades + +def get_trades(trade_id=None): + trades = TradeSales.objects.filter(Q(expiry__isnull=True) | Q(expiry__gt=datetime.now())).filter(Q(sale_limit__isnull=True) | Q(sale_count__lt=F('sale_limit'))) + if trade_id: + trades = trades.filter(id=trade_id) + return trades + +def decodePrefix(prefix): + bech32CurrencyCodes={"bc": "bitcoin","bcrt": "regtest","ltc": "litecoin","tb": "testnet","tbs": "signet","sb": "simnet"} + matches = re.compile(r'^ln(\S+?)(\d*)([a-zA-Z]?)$').match(prefix) + + if not matches or not matches.groups(): + raise ValueError('InvalidPaymentRequestPrefix') + + _, _, type = matches.groups() + + prefixElements = re.compile(r'^ln(\S+)$').match(prefix) if not type else matches + + currency, amount, units = prefixElements.groups() + + network = bech32CurrencyCodes.get(currency) + + if not network: + raise ValueError('UnknownCurrencyCodeInPaymentRequest') + + return amount, network, units + +def parseHumanReadableValue(data): + amountMultiplierPattern = r'^[^munp0-9]$' + divisibilityMarkerLen = 1 + divisibilityPattern = r'^[munp]$' + + amount = data.get('amount') + units = data.get('units') + + hrp = f"{amount}{units}" + + if re.match(divisibilityPattern, hrp[-divisibilityMarkerLen:]): + return { + 'divisor': hrp[-divisibilityMarkerLen:], + 'value': hrp[:-(divisibilityMarkerLen)], + } + + if re.match(amountMultiplierPattern, hrp[-divisibilityMarkerLen:]): + raise ValueError('InvalidAmountMultiplier') + + return {'value': hrp} + +def hrpAsMtokens(amount, units): + divisors ={"m": "1000","n": "1000000000","p": "1000000000000","u": "1000000"} + + if not amount: + return {} + + result = parseHumanReadableValue({'amount': amount, 'units': units}) + divisor = result['divisor'] + value = result['value'] + + if not bool(re.match(r'^\d+$', value)): + raise ValueError('ExpectedValidNumericAmountToParseHrpAsMtokens') + + val = Decimal(value) + + if not divisor: + return {'mtokens': str(int(val * Decimal(1e11)))} + + div = Decimal(divisors.get(divisor, 1)) + + return str(int(val * Decimal(1e11) / div)) + +def mtokensAsHrp(mtokens): + amount = int(mtokens) + hrp = None + + multipliers = { + 'n': 100, + 'u': 100000, + 'm': 100000000, + '': 100000000000, + } + + for letter, value in multipliers.items(): + value = int(value) + if amount % value == 0: + if letter == 'u': + hrp = f"{amount // value}u" + elif letter == 'm': + hrp = f"{amount // value}m" + elif letter == 'n': + hrp = f"{amount // value}n" + elif letter == '': + hrp = f"{amount // value}" + + if not hrp: + return str(amount * 10) + 'p' + return hrp + +def decodeBech32Words(words): + inBits = 5 + outBits = 8 + + bits = 0 + maxV = (1 << outBits) - 1 + result = [] + value = 0 + + for word in words: + value = (value << inBits) | word + bits += inBits + while bits >= outBits: + bits -= outBits + result.append((value >> bits) & maxV) + + if bits: + result.append((value << (outBits - bits)) & maxV) + + return bytes(result) + +def byteEncodeRequest(request): + if not request: + raise ValueError('ExpectedPaymentRequestToByteEncode') + + if request[:2].lower() != 'ln': + raise ValueError('ExpectedLnPrefixToByteEncodePaymentRequest') + + (prefix, words) = bech32_decode(request) + + (amount, network, units) = decodePrefix(prefix) + + mtokens = hrpAsMtokens(amount, units) + + encoded = decodeBech32Words(words).hex() + + return {'encoded': encoded, 'network': network, 'mtokens': mtokens, 'words': len(words)} + +def byteDecodeRequest(encoded, mtokens, network, words): + if not is_hex(encoded): + raise ValueError('ExpectedHexEncodedPaymentRequestDataToDecodeRequest') + if not network: + raise ValueError('ExpectedNetworkToDecodeByteEncodedRequest') + if not words: + raise ValueError('ExpectedWordsCountToDecodeByteEncodedRequest') + + if network == 'bitcoin': + prefix = 'bc' + elif network == 'testnet': + prefix = 'tb' + elif network == 'regtest': + prefix = 'bcrt' + elif network == 'signet': + prefix = 'tbs' + else: + raise ValueError('ExpectedKnownNetworkToDecodeByteEncodedRequest') + + prefix = 'ln' + prefix + mtokensAsHrp(mtokens) + five_bit = convertbits(bytes.fromhex(encoded), 8, 5)[:words] + return bech32_encode(prefix, five_bit) + +def encode_request_as_records(request): + if not request: + raise ValueError('ExpectedRequestToEncodeAsRequestRecords') + + records = [] + + result = byteEncodeRequest(request) + encoded = result['encoded'] + mtokens = result['mtokens'] + words = result['words'] + + records.append({'type': '1', 'value': encoded}) + records.append({'type': '0', 'value': encode_as_bigsize(words)}) + + if mtokens: + records.append({'type': '2', 'value': encode_as_bigsize(mtokens)}) + + return ''.join([encode_tlv_record(record) for record in records]), result['network'] + +def decode_records_as_request(encoded, network): + if not encoded: + raise ValueError('ExpectectedEncodedPaymentRequestRecordsToDecode') + + if not network: + raise ValueError('ExpectedNetworkNameToDeriveRequestFromRequestRecords') + + records = decode_tlv_stream(encoded) + word_count = next((record for record in records if record['type'] == '0'), None) + if not word_count: + raise ValueError('ExpectedWordCountRecordInPaymentTlvRecord') + try: + words = int(decode_as_bigsize(word_count['value'])) + except: + raise ValueError('ExpectedPaymentRequestWordCountInRequestRecords') + + details = next((record for record in records if record['type'] == '1'), None) + if not details: + raise ValueError('ExpectedEncodedPaymentDetailsInPaymentTlvRecord') + + amount = next((record for record in records if record['type'] == '2'), None) + if not amount: + raise ValueError('ExpectedPaymentRequestTokensInPaymentRecords') + mtokens = decode_as_bigsize(amount['value']) + + try: + request = byteDecodeRequest(details['value'], mtokens, network, words) + except: + raise ValueError('ExpectedValidPaymentRequestDetailsToDecodeRecords') + + return request + +def encode_final_trade(auth, payload, request): + if not request: + raise ValueError('ExpectedPaymentRequestToDeriveNetworkRecord') + + encoded_request, network = encode_request_as_records(request) + trade_records = [{'type': '2', 'value': encoded_request}] + + if settings.LND_NETWORK != 'mainnet': + network_value = '02' if network == 'regtest' else '01' + trade_records.append({'type': '1','value': network_value}) + + encryption_records = ''.join([encode_tlv_record(record) for record in [{'type': '0', 'value': payload}, {'type': '1', 'value': auth}]]) + details_records = ''.join([encode_tlv_record(record) for record in [{'type': '0', 'value': encryption_records}]]) + trade_records.append({'type': '3', 'value': details_records}) + + return '626f73ff' + ''.join([encode_tlv_record(record) for record in trade_records]) + +def decode_final_trade(network, request, details): + details = decode_tlv_stream(details['value']) + + encrypted = next((record for record in details if record['type'] == '0'), None) + if not encrypted: + raise ValueError('ExpectedEncryptedRecordToDecodeTrade') + encrypted_records = decode_tlv_stream(encrypted['value']) + + encrypted_data = next((record for record in encrypted_records if record['type'] == '0'), None) + if not encrypted_data: + raise ValueError('ExpectedEncryptedDataRecordToDecodeTrade') + + auth = next((record for record in encrypted_records if record['type'] == '1'), None) + if not auth: + raise ValueError('ExpectedAuthDataRecordToDecodeTrade') + + return decode_records_as_request(request['value'], network), auth['value'], encrypted_data['value'] + +def getSecret(stub, sale_type): + if sale_type == 1: # routing data + try: + filter_30day = datetime.now() - timedelta(days=30) + incoming_nodes = Forwards.objects.filter(forward_date__gte=filter_30day).values('chan_id_in').annotate(ppm=Round((Sum('fee')/Sum('amt_in_msat'))*1000000000, output_field=IntegerField()), score=Round((Round(Count('id')/1, output_field=IntegerField())+Round(Sum('amt_in_msat')/100000, output_field=IntegerField()))/10, output_field=IntegerField())).exclude(score=0).order_by('-score', '-ppm')[:5] + outgoing_nodes = Forwards.objects.filter(forward_date__gte=filter_30day).values('chan_id_out').annotate(ppm=Round((Sum('fee')/Sum('amt_out_msat'))*1000000000, output_field=IntegerField()), score=Round((Round(Count('id')/1, output_field=IntegerField())+Round(Sum('amt_out_msat')/100000, output_field=IntegerField()))/10, output_field=IntegerField())).exclude(score=0).order_by('-score', '-ppm')[:5] + secret = json.dumps({"incoming_nodes":list(incoming_nodes.values('chan_id_in', 'score', 'ppm')), "outgoing_nodes":list(outgoing_nodes.values('chan_id_out', 'score', 'ppm'))}) + except Exception as e: + print(f"{datetime.now().strftime('%c')} : [P2P] : Error getting secret: {str(e)}") + secret = None + finally: + return secret + elif sale_type == 2: # payment data + try: + self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey + filter_30day = datetime.now() - timedelta(days=30) + # exlcude_list = AvoidNodes.objects.values_list('pubkey') + payments_30day = Payments.objects.filter(creation_date__gte=filter_30day, status=2).values_list('payment_hash') + payment_nodes = PaymentHops.objects.filter(payment_hash__in=payments_30day).exclude(node_pubkey=self_pubkey).values('node_pubkey').annotate(ppm=Round((Sum('fee')/Sum('amt'))*1000000, output_field=IntegerField()), score=Round((Round(Count('id')/1, output_field=IntegerField())+Round(Sum('amt')/100000, output_field=IntegerField()))/10, output_field=IntegerField())).exclude(score=0).order_by('-score', 'ppm')[:10] + secret = json.dumps({"payment_nodes": list(payment_nodes.values('node_pubkey', 'score', 'ppm'))}) + except Exception as e: + print(f"{datetime.now().strftime('%c')} : [P2P] : Error getting secret: {str(e)}") + secret = None + finally: + return secret + else: + return None + +def serve_trades(stub): + print(f"{datetime.now().strftime('%c')} : [P2P] : Serving trades...") + for trade in get_trades(): + print(f"{datetime.now().strftime('%c')} : [P2P] : Serving trade: {trade.id}") + for response in stub.SubscribeCustomMessages(ln.SubscribeCustomMessagesRequest()): + if response.type == 32768: + from_peer = response.peer + msg_type = response.type + message = response.data.hex() + if msg_type == 32768 and message.lower().startswith('626f73ff'): + msg_response = parse_peer_request_message(message) + if 'request' in msg_response: + request = msg_response['request'] + if 'type' in request: + req_type = request['type'] + if req_type == '8050005': # request a seller to finalize a trade or give all open trades + print(f"{datetime.now().strftime('%c')} : [P2P] : SELLER ACTION", '|', 'ID:', request['id'], '|', 'Records:', request['records']) + select_trade = next((record for record in request['records'] if record['type'] == '0'), None) + request_trade = next((record for record in request['records'] if record['type'] == '1'), None) + if request_trade: + trades = get_trades() + for trade in trades: + trade_data = encode_peer_request({'id':secrets.token_bytes(32).hex(), 'type':'8050006', 'records':[{'type': '1', 'value': trade.id}, {'type': '2', 'value': utf8_as_hex(trade.description)}, {'type': '0', 'value': request_trade['value']}]}) + stub.SendCustomMessage(ln.SendCustomMessageRequest(peer=from_peer, type=32768, data=bytes.fromhex(trade_data))) + ack_data = encode_peer_response({'failure':None, 'id':request['id'], 'records':[]}) + for trade in trades: + stub.SendCustomMessage(ln.SendCustomMessageRequest(peer=from_peer, type=32768, data=bytes.fromhex(ack_data))) + if select_trade: + selected_trade = get_trades(trade.id) + if selected_trade: + trade_details = selected_trade[0] + if not trade_details.secret: + secret = getSecret(stub, trade_details.sale_type) + else: + secret = trade_details.secret + if not secret: + print(f"{datetime.now().strftime('%c')} : [P2P] : Failed to get secret for:", trade_details.id) + continue + signerstub = lnsigner.SignerStub(lnd_connect()) + shared_key = signerstub.DeriveSharedKey(lns.SharedKeyRequest(ephemeral_pubkey=from_peer)).shared_key + preimage = secrets.token_bytes(32) + shared_secret = bytes(x ^ y for x, y in zip(shared_key, preimage)) + cipher = Cipher(algorithms.AES(shared_secret), modes.GCM(bytes(16)), backend=default_backend()) + encryptor = cipher.encryptor() + ciphertext = (encryptor.update(secret.encode('utf-8')) + encryptor.finalize()).hex() + auth_tag = encryptor.tag.hex() + time_to_expiry = (trade_details.expiry-datetime.now()).seconds if trade_details.expiry else None + default_expiry = 30*60 + inv_expiry = default_expiry if time_to_expiry is None or time_to_expiry > default_expiry else time_to_expiry + if inv_expiry > 0: + final_invoice = stub.AddInvoice(ln.Invoice(memo=trade_details.description, value=trade_details.price, expiry=inv_expiry, r_preimage=preimage)) + trade_data = encode_peer_response({'failure': None, 'id':request['id'], 'records': [{'type':'1', 'value':encode_final_trade(auth_tag, ciphertext, final_invoice.payment_request)}]}) + trade_details.sale_count = F('sale_count') + 1 + trade_details.save() + stub.SendCustomMessage(ln.SendCustomMessageRequest(peer=from_peer, type=32768, data=bytes.fromhex(trade_data))) + else: + print(f"{datetime.now().strftime('%c')} : [P2P] : Expected request type in message:", request['id']) + if 'response' in msg_response: + request = msg_response['response'] + if 'failure' in request and request['failure'] != None: + # failure message returned + print(f"{datetime.now().strftime('%c')} : [P2P] : Failure:", request['failure']) + else: + if len(request['records']) == 0: + # message acknowledgements + print(f"{datetime.now().strftime('%c')} : [P2P] : ACK", '|', 'ID:', request['id']) + +async def get_open_trades(astub, results): + try: + async for response in astub.SubscribeCustomMessages(ln.SubscribeCustomMessagesRequest()): + if response.type == 32768: + # from_peer = response.peer + msg_type = response.type + message = response.data.hex() + if msg_type == 32768 and message.lower().startswith('626f73ff'): + msg_response = parse_peer_request_message(message) + if 'request' in msg_response: + request = msg_response['request'] + if 'type' in request: + req_type = request['type'] + if req_type == '8050006': # request a buyer to select the trade provided + # select a trade from the records + trade = decode_basic_trade(request['records']) + print('BUYER ACTION', '|', 'ID:', request['id'], '|', 'Trade:', trade) + results.append({'id': request['id'], 'trade': trade}) + else: + raise ValueError('ExpectedRequestTypeInRequestMessage') + if 'response' in msg_response: + request = msg_response['response'] + if 'failure' in request and request['failure'] != None: + # failure message returned + print('Failure:', request['failure']) + return + else: + if len(request['records']) == 0: + # message acknowledgements + print('ACK', '|', 'ID:', request['id']) + else: + # buyer to pay invoice and get secret their secret from preimage + print('BUYER FINALIZE', '|', 'ID:', request['id'], '|', 'Records:', request['records']) + return request['records'] + except asyncio.CancelledError: + pass + except Exception as e: + print('Error runnig task:', str(e)) + +def encode_nodes_data(data, network_value): + records = [{'type': '0', 'value': '01'}] + if network_value: + records.append({'type': '1', 'value': network_value}) + + node_records = [] + for idx, node in enumerate(data): + node_record = [{ 'type': '2', 'value': node['id'] }] + encoded_node_record = ''.join([encode_tlv_record(record) for record in node_record]) + node_records.append({'type': str(idx), 'value': encoded_node_record}) + records.append({'type': '4', 'value': ''.join([encode_tlv_record(record) for record in node_records])}) + nodes_encoded = '626f73ff' + ''.join([encode_tlv_record(record) for record in records]) + return nodes_encoded + +def decode_node_record(encoded): + channel_hex_length = 16 + if not encoded: + raise ValueError('ExpectedEncodedNodeRecordToGetNodePointer') + + records = decode_tlv_stream(encoded['value']) + high_key_record = next((n for n in records if n['type'] == '1'), None) + + if high_key_record and len(high_key_record['value']) != channel_hex_length: + raise ValueError('ExpectedChannelIdInHighKeyRecord') + if high_key_record: + return {'high_channel': high_key_record['value']} + + low_key_record = next((n for n in records if n['type'] == '0'), None) + if low_key_record and len(low_key_record['value']) != channel_hex_length: + raise ValueError('ExpectedChannelIdInLowKeyRecord') + if low_key_record: + return {'low_channel': low_key_record['value']} + + id_record = next((n for n in records if n['type'] == '2'), None) + if not id_record: + raise ValueError('ExpectedNodeIdRecordToMapNodeRecordToNodePointer') + + if not (bool(id_record['value']) and re.match(r'^0[2-3][0-9A-F]{64}$', id_record['value'], re.I)): + raise ValueError('ExpectedNodeIdPublicKeyToMapNodeRecordToNodePointer') + + return {'node': {'id': id_record['value']}} + +def decode_open_trade(network, records): + if not network: + raise ValueError('ExpectedNetworkNameToDecodeOpenTrade') + if not isinstance(records, list): + raise ValueError('ExpectedArrayOfRecordsToDecodeOpenTrade') + + nodes_record = next((n for n in records if n['type'] == '4'), None) + id_record = next((n for n in records if n['type'] == '5'), None) + if not nodes_record: + raise ValueError('ExpectedNodesRecordToDecodeOpenTradeDetails') + + try: + decode_tlv_stream(nodes_record['value']) + except: + raise ValueError('ExpectedValidNodesTlvStreamToDecodeOpenTradeDetails') + + node_records = decode_tlv_stream(nodes_record['value']) + if not node_records: + raise ValueError('ExpectedNodeRecordsForOpenTrade') + + return network, id_record['value'] if id_record else None, [decode_node_record(value) for value in node_records] + +def decode_trade_data(encoded): + if not encoded.lower().startswith('626f73ff'): + raise ValueError('UnexpectedFormatOfTradeToDecode') + try: + decoded_trade = decode_tlv_stream(encoded[8:]) + except: + raise ValueError('ExpectedValidTlvStreamForTradeData') + records = decoded_trade + + network_value = next((record for record in records if record['type'] == '1'), None) + if network_value: + if network_value['value'] == '01': + if settings.LND_NETWORK == 'testnet': + network = 'testnet' + else: + raise ValueError('TradeRequestForAnotherNetwork') + elif network_value['value'] =='02': + if settings.LND_NETWORK == 'regtest': + network = 'regtest' + else: + raise ValueError('TradeRequestForAnotherNetwork') + else: + raise ValueError('UnknownNetworkNameToDeriveNetworkRecordFor') + else: + if settings.LND_NETWORK == 'mainnet': + network = 'bitcoin' + else: + raise ValueError('TradeRequestForAnotherNetwork') + + request = next((record for record in records if record['type'] == '2'), None) + details = next((record for record in records if record['type'] == '3'), None) + swap = next((record for record in records if record['type'] == '6'), None) + + if request and details: + return {'secret': decode_final_trade(network, request, details)} + elif request: + pass # just a payment + elif swap: + pass # swap offer + else: + return {'connect': decode_open_trade(network, records)} + +def encode_trade(description, price, secret): + anchorPrefix = 'anchor-trade-secret:' + elements = [ + {'value': secret, 'type': '0'}, + {'value': description, 'type': '1'}, + {'value': price, 'type': '2'}, + ] + + records = [ + {'type': record['type'], 'value': bytes(record['value'], 'utf-8').hex()} + for record in elements if record + ] + encoded_data = ''.join([encode_tlv_record(record) for record in records]) + encoded = anchorPrefix + base64.b64encode(bytes.fromhex(encoded_data)).decode() + + return encoded + +def create_trade_details(stub): + nodes = [{'id':stub.GetInfo(ln.GetInfoRequest()).identity_pubkey}] + lnd_network = settings.LND_NETWORK + if lnd_network == 'mainnet': + network_value = None + elif lnd_network == 'testnet': + network_value = '01' + elif lnd_network == 'regtest': + network_value = '02' + else: + raise ValueError('UnsupportedNetworkForTrades') + return encode_nodes_data(nodes, network_value) + +def create_trade_anchor(stub, description, price, secret, expiry): + try: + encoded_trade = encode_trade(description, price, secret) + stub.AddInvoice(ln.Invoice(value=int(price), expiry=int(expiry)*60*60*24, memo=encoded_trade)) + except Exception as e: + print('Error creating trade:', str(e)) + print('Trade Anchor:', encoded_trade) + +async def request_trades(to_peer): + results = [] + start_time = time() + astub = lnrpc.LightningStub(async_lnd_connect()) + task = asyncio.create_task(get_open_trades(astub, results)) + asyncio.gather(task) + trade_data = encode_peer_request({'id':secrets.token_bytes(32).hex(), 'type':'8050005', 'records':[{'type': '1', 'value': secrets.token_bytes(32).hex()}]}) + astub.SendCustomMessage(ln.SendCustomMessageRequest(peer=bytes.fromhex(to_peer), type=32768, data=bytes.fromhex(trade_data))) + while len(results) == 0: + if (time() - start_time) < 30: + await asyncio.sleep(1) + else: + print('Timeout waiting for trade records from peer.') + task.cancel() + return None + await asyncio.sleep(1) + if len(results) == 1: + ack_data = encode_peer_response({'failure':None, 'id': results[0]['id'], 'records':[]}) + astub.SendCustomMessage(ln.SendCustomMessageRequest(peer=bytes.fromhex(to_peer), type=32768, data=bytes.fromhex(ack_data))) + choice = 1 + else: + print('Select a trade to buy:') + for idx, trade in enumerate(results, start=1): + ack_data = encode_peer_response({'failure':None, 'id': trade['id'], 'records':[]}) + astub.SendCustomMessage(ln.SendCustomMessageRequest(peer=bytes.fromhex(to_peer), type=32768, data=bytes.fromhex(ack_data))) + print(f"{idx}. {trade['trade']['description']}") + while True: + try: + choice = int(input("Enter the number of your choice: ")) + if 1 <= choice <= len(results): + break + else: + print("Invalid input. Please enter a valid option.") + except ValueError: + print("Invalid input. Please enter a number for your selection.") + selected_trade = results[choice - 1] + trade_data = encode_peer_request({'id':secrets.token_bytes(32).hex(), 'type':'8050005', 'records':[{'type': '0', 'value': selected_trade['trade']['id']}]}) + astub.SendCustomMessage(ln.SendCustomMessageRequest(peer=bytes.fromhex(to_peer), type=32768, data=bytes.fromhex(trade_data))) + return await task + +def decrypt_secret(stub, decoded_trade): + invoice, auth, payload = decoded_trade['secret'] + decoded_invoice = stub.DecodePayReq(ln.PayReqString(pay_req=invoice)) + print('Invoice for secret decoded.') + print('Destination:', decoded_invoice.destination) + print('Amount:', decoded_invoice.num_satoshis) + print('Description:', decoded_invoice.description) + ask_pay = input('Pay the invoice and decrypt the secret? [y/N]: ') + if ask_pay.lower() == 'y': + routerstub = lnrouter.RouterStub(lnd_connect()) + for response in routerstub.SendPaymentV2(lnr.SendPaymentRequest(payment_request=invoice, timeout_seconds=60)): + if response.status == 2: + print('Payment paid!') + preimage = bytes.fromhex(response.payment_preimage) + if response.status > 2: + print('Payment failed. Please try again.') + return + signerstub = lnsigner.SignerStub(lnd_connect()) + shared_key = signerstub.DeriveSharedKey(lns.SharedKeyRequest(ephemeral_pubkey=bytes.fromhex(decoded_invoice.destination))).shared_key + shared_secret = bytes(x ^ y for x, y in zip(shared_key, preimage)) + cipher = Cipher(algorithms.AES(shared_secret), modes.GCM(bytes(16), bytes.fromhex(auth)), backend=default_backend()) + decryptor = cipher.decryptor() + decrypted = decryptor.update(bytes.fromhex(payload)) + decryptor.finalize() + print('Successfully decrypted the secret:', decrypted.decode('utf-8')) + +def main(): + options = ["Buy A Trade", "Setup A Sale", "Serve Trades"] + print("Select an option:") + for idx, option in enumerate(options, start=1): + print(f"{idx}. {option}") + while True: + try: + choice = int(input("Enter the number of your choice: ")) + if 1 <= choice <= len(options): + break + else: + print("Invalid input. Please enter a valid option.") + except ValueError: + print("Invalid input. Please enter a number for your selection.") + selected_option = options[choice - 1] + stub = lnrpc.LightningStub(lnd_connect()) + if selected_option == 'Setup A Sale': + description = input('Enter a description for the trade: ') + price = input('Enter the price to charge in sats: ') + expiry = input('Enter desired days until trade expiry: ') + secret = input('Enter the secret you want to sell: ') + create_trade_anchor(stub, description, price, secret, expiry) + ask_serve = input('Start serving trades? y/N: ') + if ask_serve.lower() == 'y': + print('Generic Trade Link:', create_trade_details(stub)) + serve_trades(stub) + if selected_option == 'Serve Trades': + print('Generic Trade Link:', create_trade_details(stub)) + serve_trades(stub) + if selected_option == 'Buy A Trade': + trade = input('Enter an encoded trade: ') + decoded_trade = decode_trade_data(trade) + if 'secret' in decoded_trade: + decrypt_secret(stub, decoded_trade) + if 'connect' in decoded_trade: + network, id, connection = decoded_trade['connect'] + if 'node' in connection[0]: + try: + to_peer = connection[0]['node']['id'] + if not (Peers.objects.filter(pubkey=to_peer).exists() and Peers.objects.filter(pubkey=to_peer)[0].connected == True): + host = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=to_peer, include_channels=False)).node.addresses[0].addr + stub.ConnectPeer(ln.ConnectPeerRequest(addr=ln.LightningAddress(pubkey=to_peer, host=host), timeout=60)) + except: + raise ValueError('PeerConnectionError') + else: + raise ValueError('NoPeerFoundInConnectionData') + trade = asyncio.run(request_trades(to_peer)) + if trade: + trade_data = next((record for record in trade if record['type'] == '1'), None) + if trade_data: + decoded_trade = decode_trade_data(trade_data['value']) + decrypt_secret(stub, decoded_trade) + +if __name__ == '__main__': + main() \ No newline at end of file From 404ac65696f051d633c60d66068b43243eae3a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio?= <43297242+proof-of-reality@users.noreply.github.com> Date: Thu, 7 Dec 2023 03:43:13 +0000 Subject: [PATCH 46/64] Remove max base fee limit (#343) --- gui/templates/advanced.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/templates/advanced.html b/gui/templates/advanced.html index 8ec53ee4..6f982834 100644 --- a/gui/templates/advanced.html +++ b/gui/templates/advanced.html @@ -19,7 +19,7 @@

Advanced Channel Settings

{% csrf_token %} - +
@@ -131,7 +131,7 @@

Advanced Channel Settings

{% csrf_token %} - +
From 750fc2515077707588db77dc8888343feeabebbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio?= <43297242+proof-of-reality@users.noreply.github.com> Date: Thu, 7 Dec 2023 03:54:48 +0000 Subject: [PATCH 47/64] Add routing stats highlight (#347) --- gui/templates/base.html | 4 +++- gui/templates/home.html | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/gui/templates/base.html b/gui/templates/base.html index fbb830d3..b54ba093 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -120,11 +120,13 @@

My Lnd Overview