Skip to content

Commit 0414e45

Browse files
v1.7.0 (#293)
2 parents 360967c + 52bb742 commit 0414e45

37 files changed

+2270
-1522
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ lndg/settings.py
44
frontend
55
node_modules
66
lndg-admin.txt
7-
.vscode
7+
.vscode
8+
.vs
9+
gui/static/admin
10+
gui/static/rest_framework
11+
data/

gui/af.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import django
2+
from django.db.models import Sum
3+
from datetime import datetime, timedelta
4+
from os import environ
5+
from pandas import DataFrame, concat
6+
environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings'
7+
django.setup()
8+
from gui.models import Forwards, Channels, LocalSettings, FailedHTLCs
9+
10+
def main(channels):
11+
channels_df = DataFrame.from_records(channels.values())
12+
filter_1day = datetime.now() - timedelta(days=1)
13+
filter_7day = datetime.now() - timedelta(days=7)
14+
if channels_df.shape[0] > 0:
15+
if LocalSettings.objects.filter(key='AF-MaxRate').exists():
16+
max_rate = int(LocalSettings.objects.filter(key='AF-MaxRate')[0].value)
17+
else:
18+
LocalSettings(key='AF-MaxRate', value='2500').save()
19+
max_rate = 2500
20+
if LocalSettings.objects.filter(key='AF-MinRate').exists():
21+
min_rate = int(LocalSettings.objects.filter(key='AF-MinRate')[0].value)
22+
else:
23+
LocalSettings(key='AF-MinRate', value='0').save()
24+
min_rate = 0
25+
if LocalSettings.objects.filter(key='AF-Increment').exists():
26+
increment = int(LocalSettings.objects.filter(key='AF-Increment')[0].value)
27+
else:
28+
LocalSettings(key='AF-Increment', value='5').save()
29+
increment = 5
30+
if LocalSettings.objects.filter(key='AF-Multiplier').exists():
31+
multiplier = int(LocalSettings.objects.filter(key='AF-Multiplier')[0].value)
32+
else:
33+
LocalSettings(key='AF-Multiplier', value='5').save()
34+
multiplier = 5
35+
if LocalSettings.objects.filter(key='AF-FailedHTLCs').exists():
36+
failed_htlc_limit = int(LocalSettings.objects.filter(key='AF-FailedHTLCs')[0].value)
37+
else:
38+
LocalSettings(key='AF-FailedHTLCs', value='25').save()
39+
failed_htlc_limit = 25
40+
if LocalSettings.objects.filter(key='AF-UpdateHours').exists():
41+
update_hours = int(LocalSettings.objects.filter(key='AF-UpdateHours').get().value)
42+
else:
43+
LocalSettings(key='AF-UpdateHours', value='24').save()
44+
update_hours = 24
45+
if LocalSettings.objects.filter(key='AF-LowLiqLimit').exists():
46+
lowliq_limit = int(LocalSettings.objects.filter(key='AF-LowLiqLimit').get().value)
47+
else:
48+
LocalSettings(key='AF-LowLiqLimit', value='5').save()
49+
lowliq_limit = 15
50+
if LocalSettings.objects.filter(key='AF-ExcessLimit').exists():
51+
excess_limit = int(LocalSettings.objects.filter(key='AF-ExcessLimit').get().value)
52+
else:
53+
LocalSettings(key='AF-ExcessLimit', value='95').save()
54+
excess_limit = 90
55+
if lowliq_limit >= excess_limit:
56+
print('Invalid thresholds detected, using defaults...')
57+
lowliq_limit = 5
58+
excess_limit = 95
59+
forwards = Forwards.objects.filter(forward_date__gte=filter_7day, amt_out_msat__gte=1000000)
60+
if forwards.exists():
61+
forwards_df_in_7d_sum = DataFrame.from_records(forwards.values('chan_id_in').annotate(amt_out_msat=Sum('amt_out_msat'), fee=Sum('fee')), 'chan_id_in')
62+
forwards_df_out_7d_sum = DataFrame.from_records(forwards.values('chan_id_out').annotate(amt_out_msat=Sum('amt_out_msat'), fee=Sum('fee')), 'chan_id_out')
63+
else:
64+
forwards_df_in_7d_sum = DataFrame()
65+
forwards_df_out_7d_sum = DataFrame()
66+
channels_df['amt_routed_in_7day'] = channels_df.apply(lambda row: int(forwards_df_in_7d_sum.loc[row.chan_id].amt_out_msat/1000) if (forwards_df_in_7d_sum.index == row.chan_id).any() else 0, axis=1)
67+
channels_df['amt_routed_out_7day'] = channels_df.apply(lambda row: int(forwards_df_out_7d_sum.loc[row.chan_id].amt_out_msat/1000) if (forwards_df_out_7d_sum.index == row.chan_id).any() else 0, axis=1)
68+
channels_df['net_routed_7day'] = channels_df.apply(lambda row: round((row['amt_routed_out_7day']-row['amt_routed_in_7day'])/row['capacity'], 1), axis=1)
69+
channels_df['local_balance'] = channels_df.apply(lambda row: row.local_balance + row.pending_outbound, axis=1)
70+
channels_df['remote_balance'] = channels_df.apply(lambda row: row.remote_balance + row.pending_inbound, axis=1)
71+
channels_df['in_percent'] = channels_df.apply(lambda row: int(round((row['remote_balance']/row['capacity'])*100, 0)), axis=1)
72+
channels_df['out_percent'] = channels_df.apply(lambda row: int(round((row['local_balance']/row['capacity'])*100, 0)), axis=1)
73+
channels_df['eligible'] = channels_df.apply(lambda row: (datetime.now()-row['fees_updated']).total_seconds() > (update_hours*3600), axis=1)
74+
75+
# Low Liquidity
76+
lowliq_df = channels_df[channels_df['out_percent'] <= lowliq_limit].copy()
77+
failed_htlc_df = DataFrame.from_records(FailedHTLCs.objects.exclude(wire_failure=99).filter(timestamp__gte=filter_1day).order_by('-id').values())
78+
if failed_htlc_df.shape[0] > 0:
79+
failed_htlc_df = failed_htlc_df[(failed_htlc_df['wire_failure']==15) & (failed_htlc_df['failure_detail']==6) & (failed_htlc_df['amount']>failed_htlc_df['chan_out_liq']+failed_htlc_df['chan_out_pending'])]
80+
lowliq_df['failed_out_1day'] = 0 if failed_htlc_df.empty else lowliq_df.apply(lambda row: len(failed_htlc_df[failed_htlc_df['chan_id_out']==row.chan_id]), axis=1)
81+
# INCREASE IF (failed htlc > threshhold) && (flow in == 0)
82+
lowliq_df['new_rate'] = lowliq_df.apply(lambda row: row['local_fee_rate']+(5*multiplier) if row['failed_out_1day']>failed_htlc_limit and row['amt_routed_in_7day'] == 0 else row['local_fee_rate'], axis=1)
83+
84+
# Balanced Liquidity
85+
balanced_df = channels_df[(channels_df['out_percent'] > lowliq_limit) & (channels_df['out_percent'] < excess_limit)].copy()
86+
# IF NO FLOW THEN DECREASE FEE AND IF HIGH FLOW THEN SLOWLY INCREASE FEE
87+
balanced_df['new_rate'] = balanced_df.apply(lambda row: row['local_fee_rate']+((2*multiplier)*(1+(row['net_routed_7day']/row['capacity']))) if row['net_routed_7day'] > row['capacity'] else row['local_fee_rate'], axis=1)
88+
balanced_df['new_rate'] = balanced_df.apply(lambda row: row['local_fee_rate']-(3*multiplier) if (row['amt_routed_in_7day']+row['amt_routed_out_7day']) == 0 else row['local_fee_rate'], axis=1)
89+
90+
# Excess Liquidity
91+
excess_df = channels_df[channels_df['out_percent'] >= excess_limit].copy()
92+
excess_df['revenue_7day'] = excess_df.apply(lambda row: int(forwards_df_out_7d_sum.loc[row.chan_id].fee) if forwards_df_out_7d_sum.empty == False and (forwards_df_out_7d_sum.index == row.chan_id).any() else 0, axis=1)
93+
excess_df['revenue_assist_7day'] = excess_df.apply(lambda row: int(forwards_df_in_7d_sum.loc[row.chan_id].fee) if forwards_df_in_7d_sum.empty == False and (forwards_df_in_7d_sum.index == row.chan_id).any() else 0, axis=1)
94+
# DECREASE IF (assisting channel or stagnant liq)
95+
excess_df['new_rate'] = excess_df.apply(lambda row: row['local_fee_rate']-(5*multiplier) if row['net_routed_7day'] < 0 and row['revenue_assist_7day'] > (row['revenue_7day']*10) else row['local_fee_rate'], axis=1)
96+
excess_df['new_rate'] = excess_df.apply(lambda row: row['local_fee_rate']-(5*multiplier) if (row['amt_routed_in_7day']+row['amt_routed_out_7day']) == 0 else row['local_fee_rate'], axis=1)
97+
98+
#Merge back results
99+
result_df = concat([lowliq_df, balanced_df, excess_df])
100+
result_df['new_rate'] = result_df.apply(lambda row: int(round(row['new_rate']/increment, 0)*increment), axis=1)
101+
result_df['new_rate'] = result_df.apply(lambda row: max_rate if max_rate < row['new_rate'] else row['new_rate'], axis=1)
102+
result_df['new_rate'] = result_df.apply(lambda row: min_rate if min_rate > row['new_rate'] else row['new_rate'], axis=1)
103+
result_df['adjustment'] = result_df.apply(lambda row: int(row['new_rate']-row['local_fee_rate']), axis=1)
104+
return result_df
105+
else:
106+
return DataFrame()
107+
108+
109+
if __name__ == '__main__':
110+
print(main(Channels.objects.filter(is_open=True)))

gui/forms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class AutoFeesForm(AutoRebalanceForm):
8686
af_multiplier = forms.IntegerField(label='af_multiplier', required=False)
8787
af_failedHTLCs = forms.IntegerField(label='af_failedHTLCs', required=False)
8888
af_updateHours = forms.IntegerField(label='af_updateHours', required=False)
89+
af_lowliq = forms.IntegerField(label='af_lowliq', required=False)
90+
af_excess = forms.IntegerField(label='af_excess', required=False)
8991

9092
class GUIForm(AutoFeesForm):
9193
gui_graphLinks = forms.CharField(label='gui_graphLinks', required=False)

gui/migrations/0036_peers.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from django.db import migrations, models
2+
from gui.lnd_deps import lightning_pb2 as ln
3+
from gui.lnd_deps import lightning_pb2_grpc as lnrpc
4+
from gui.lnd_deps.lnd_connect import lnd_connect
5+
6+
def migrate_update_channels(apps, schedma_editor):
7+
try:
8+
stub = lnrpc.LightningStub(lnd_connect())
9+
lnd_channels = stub.ListChannels(ln.ListChannelsRequest()).channels
10+
db_channels = apps.get_model('gui', 'channels')
11+
for channel in lnd_channels:
12+
if db_channels.objects.filter(chan_id=channel.chan_id).exists():
13+
db_channel = db_channels.objects.filter(chan_id=channel.chan_id)[0]
14+
db_channel.push_amt = channel.push_amount_sat
15+
db_channel.close_address = channel.close_address
16+
db_channel.save()
17+
except Exception as e:
18+
print('Unable to get current channel data:', str(e))
19+
20+
def revert_update_channels(apps, schedma_editor):
21+
pass
22+
23+
class Migration(migrations.Migration):
24+
25+
dependencies = [
26+
('gui', '0035_histfailedhtlc'),
27+
]
28+
29+
operations = [
30+
migrations.AddField(
31+
model_name='peers',
32+
name='ping_time',
33+
field=models.BigIntegerField(default=0),
34+
),
35+
migrations.AddField(
36+
model_name='channels',
37+
name='notes',
38+
field=models.TextField(blank=True, default=''),
39+
),
40+
migrations.AddField(
41+
model_name='channels',
42+
name='close_address',
43+
field=models.CharField(default='', max_length=100),
44+
preserve_default=False,
45+
),
46+
migrations.AddField(
47+
model_name='channels',
48+
name='push_amt',
49+
field=models.BigIntegerField(default=0),
50+
preserve_default=False,
51+
),
52+
migrations.RunPython(migrate_update_channels, revert_update_channels),
53+
]

gui/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ class Channels(models.Model):
9696
remote_cltv = models.IntegerField()
9797
remote_min_htlc_msat = models.BigIntegerField()
9898
remote_max_htlc_msat = models.BigIntegerField()
99+
push_amt = models.BigIntegerField()
100+
close_address = models.CharField(max_length=100)
99101
is_active = models.BooleanField()
100102
is_open = models.BooleanField()
101103
last_update = models.DateTimeField()
@@ -106,6 +108,7 @@ class Channels(models.Model):
106108
ar_max_cost = models.IntegerField()
107109
fees_updated = models.DateTimeField(default=timezone.now)
108110
auto_fees = models.BooleanField()
111+
notes = models.TextField(default='', blank=True)
109112

110113
def save(self, *args, **kwargs):
111114
if self.auto_fees is None:
@@ -157,6 +160,7 @@ class Peers(models.Model):
157160
inbound = models.BooleanField()
158161
connected = models.BooleanField()
159162
last_reconnected = models.DateTimeField(null=True, default=None)
163+
ping_time = models.BigIntegerField(default=0)
160164
class Meta:
161165
app_label = 'gui'
162166

gui/serializers.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from rest_framework import serializers
22
from rest_framework.relations import PrimaryKeyRelatedField
3-
from .models import LocalSettings, Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, Peers, Onchain, PendingHTLCs, FailedHTLCs, Closures, Resolutions
3+
from .models import LocalSettings, Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, Peers, Onchain, PendingHTLCs, FailedHTLCs, Closures, Resolutions, PeerEvents
44

55
##FUTURE UPDATE 'exclude' TO 'fields'
66

@@ -71,6 +71,9 @@ class ChannelSerializer(serializers.HyperlinkedModelSerializer):
7171
remote_max_htlc_msat = serializers.ReadOnlyField()
7272
alias = serializers.ReadOnlyField()
7373
fees_updated = serializers.ReadOnlyField()
74+
push_amt = serializers.ReadOnlyField()
75+
close_address = serializers.ReadOnlyField()
76+
opened_in = serializers.SerializerMethodField()
7477
ar_max_cost = serializers.IntegerField(required=False)
7578
ar_amt_target = serializers.IntegerField(required=False)
7679
ar_out_target = serializers.IntegerField(required=False)
@@ -80,6 +83,9 @@ class Meta:
8083
model = Channels
8184
exclude = []
8285

86+
def get_opened_in(self, obj):
87+
return int(obj.short_chan_id.split('x')[0])
88+
8389
class RebalancerSerializer(serializers.HyperlinkedModelSerializer):
8490
id = serializers.ReadOnlyField()
8591
requested = serializers.ReadOnlyField()
@@ -110,12 +116,27 @@ class BumpFeeSerializer(serializers.Serializer):
110116
target_fee = serializers.IntegerField(label='target_fee')
111117
force = serializers.BooleanField(default=False)
112118

119+
class BroadcastTXSerializer(serializers.Serializer):
120+
raw_tx = serializers.CharField(label='raw_tx')
121+
113122
class AddInvoiceSerializer(serializers.Serializer):
114123
value = serializers.IntegerField(label='value')
115124

116125
class UpdateAliasSerializer(serializers.Serializer):
117126
peer_pubkey = serializers.CharField(label='peer_pubkey', max_length=66)
118127

128+
class UpdateChanPolicy(serializers.Serializer):
129+
chan_id = serializers.CharField(max_length=20)
130+
base_fee = serializers.IntegerField(required=False, default=None)
131+
fee_rate = serializers.IntegerField(required=False, default=None)
132+
disabled = serializers.IntegerField(required=False, default=None)
133+
cltv = serializers.IntegerField(required=False, default=None)
134+
min_htlc = serializers.FloatField(required=False, default=None)
135+
max_htlc = serializers.FloatField(required=False, default=None)
136+
137+
class NewAddressSerializer(serializers.Serializer):
138+
legacy = serializers.BooleanField(required=False, default=False)
139+
119140
class PeerSerializer(serializers.HyperlinkedModelSerializer):
120141
pubkey = serializers.ReadOnlyField()
121142
class Meta:
@@ -158,6 +179,17 @@ class Meta:
158179
model = PendingHTLCs
159180
exclude = []
160181

182+
class PeerEventsSerializer(serializers.HyperlinkedModelSerializer):
183+
id = serializers.ReadOnlyField()
184+
out_liq_percent = serializers.SerializerMethodField()
185+
class Meta:
186+
model = PeerEvents
187+
exclude = []
188+
189+
def get_out_liq_percent(self, obj):
190+
capacity = Channels.objects.filter(chan_id=obj.chan_id).get().capacity
191+
return int(round((obj.out_liq/capacity)*100, 1))
192+
161193
class FailedHTLCSerializer(serializers.HyperlinkedModelSerializer):
162194
id = serializers.ReadOnlyField()
163195
class Meta:

gui/static/api.js

Lines changed: 4 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
async function GET(url, {method = 'GET', data = null}){
1+
async function GET(url, {method = 'GET', data} = {}){
22
if(!data.limit) data.limit = 100000
33
if(!data.format) data.format = 'json'
44
return call({url, method, data})
@@ -16,7 +16,7 @@ async function PATCH(url, {method = 'PATCH', body}){
1616
return call({url, method, body})
1717
}
1818

19-
async function DELETE(url, {method = 'DELETE'}){
19+
async function DELETE(url, {method = 'DELETE'} = {}){
2020
return call({url, method})
2121
}
2222

@@ -31,89 +31,7 @@ class Sync{
3131
static PUT(url, {method = 'PUT', body}, callback){
3232
call({url, method, body}).then(res => callback(res))
3333
}
34-
}
35-
36-
function showBannerMsg(h1Msg, result){
37-
document.getElementById('content').insertAdjacentHTML("beforebegin", `<div style="top:5px" class="w3-panel w3-orange w3-display-container"><span onclick="this.parentElement.style.display='none'" class="w3-button w3-hover-red w3-display-topright">X</span><h1 style="word-wrap: break-word">${h1Msg} updated to: ${result}</h1></div>`);
38-
window.scrollTo(0, 0);
39-
}
40-
41-
function flash(element, response){
42-
if (response != element.value) {
43-
element.value = response
44-
return
45-
}
46-
var rgb = window.getComputedStyle(element).backgroundColor;
47-
rgb = rgb.substring(4, rgb.length-1).replace(/ /g, '').split(',');
48-
var r = rgb[0], g = rgb[1], bOrigin = rgb[2], b = bOrigin;
49-
var add = false;
50-
var complete = false;
51-
const increment = 15;
52-
var flashOn = setInterval(() => {
53-
if(add){
54-
if(b < 255 && (b+increment) <= 255){
55-
b += increment;
56-
}else{
57-
add = false;
58-
complete = true;
59-
}
60-
}else{
61-
if(complete == true && b < bOrigin){
62-
b = bOrigin;
63-
clearInterval(flashOn);
64-
}
65-
else if(b > 0 && (b-increment) >= 0){
66-
b -= increment;
67-
}else{
68-
add = true;
69-
}
70-
}
71-
element.style.backgroundColor = 'RGB('+r+','+g+','+b+')';
72-
if(b == bOrigin) element.style.removeProperty("background-color");
73-
}, 50);
74-
}
75-
76-
function formatDate(start, end = new Date().getTime() + new Date().getTimezoneOffset()*60000){
77-
if (end == null) return '---'
78-
end = new Date(end)
79-
if (start == null) return '---'
80-
difference = (end - new Date(start))/1000
81-
if (difference < 0) return 'Just now'
82-
if (difference < 60) {
83-
if (Math.floor(difference) == 1){
84-
return `a second ago`;
85-
}else{
86-
return `${Math.floor(difference)} seconds ago`;
87-
}
88-
} else if (difference < 3600) {
89-
if (Math.floor(difference / 60) == 1){
90-
return `a minute ago`;
91-
}else{
92-
return `${Math.floor(difference / 60)} minutes ago`;
93-
}
94-
} else if (difference < 86400) {
95-
if (Math.floor(difference / 3600) == 1){
96-
return `an hour ago`;
97-
}else{
98-
return `${Math.floor(difference / 3600)} hours ago`;
99-
}
100-
} else if (difference < 2620800) {
101-
if (Math.floor(difference / 86400) == 1){
102-
return `a day ago`;
103-
}else{
104-
return `${Math.floor(difference / 86400)} days ago`;
105-
}
106-
} else if (difference < 31449600) {
107-
if (Math.floor(difference / 2620800) == 1){
108-
return `a month ago`;
109-
}else{
110-
return `${Math.floor(difference / 2620800)} months ago`;
111-
}
112-
} else {
113-
if (Math.floor(difference / 31449600) == 1){
114-
return `a year ago`;
115-
}else{
116-
return `${Math.floor(difference / 31449600)} years ago`;
117-
}
34+
static POST(url, {method = 'POST', body}, callback){
35+
call({url, method, body}).then(res => callback(res))
11836
}
11937
}

0 commit comments

Comments
 (0)