diff --git a/gui/lnd_deps/signer_pb2.py b/gui/lnd_deps/signer_pb2.py new file mode 100644 index 00000000..08a170f1 --- /dev/null +++ b/gui/lnd_deps/signer_pb2.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: signer.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0csigner.proto\x12\x07signrpc\"3\n\nKeyLocator\x12\x12\n\nkey_family\x18\x01 \x01(\x05\x12\x11\n\tkey_index\x18\x02 \x01(\x05\"L\n\rKeyDescriptor\x12\x15\n\rraw_key_bytes\x18\x01 \x01(\x0c\x12$\n\x07key_loc\x18\x02 \x01(\x0b\x32\x13.signrpc.KeyLocator\")\n\x05TxOut\x12\r\n\x05value\x18\x01 \x01(\x03\x12\x11\n\tpk_script\x18\x02 \x01(\x0c\"\xc4\x01\n\x0eSignDescriptor\x12(\n\x08key_desc\x18\x01 \x01(\x0b\x32\x16.signrpc.KeyDescriptor\x12\x14\n\x0csingle_tweak\x18\x02 \x01(\x0c\x12\x14\n\x0c\x64ouble_tweak\x18\x03 \x01(\x0c\x12\x16\n\x0ewitness_script\x18\x04 \x01(\x0c\x12\x1e\n\x06output\x18\x05 \x01(\x0b\x32\x0e.signrpc.TxOut\x12\x0f\n\x07sighash\x18\x07 \x01(\r\x12\x13\n\x0binput_index\x18\x08 \x01(\x05\"L\n\x07SignReq\x12\x14\n\x0craw_tx_bytes\x18\x01 \x01(\x0c\x12+\n\nsign_descs\x18\x02 \x03(\x0b\x32\x17.signrpc.SignDescriptor\"\x1c\n\x08SignResp\x12\x10\n\x08raw_sigs\x18\x01 \x03(\x0c\"2\n\x0bInputScript\x12\x0f\n\x07witness\x18\x01 \x03(\x0c\x12\x12\n\nsig_script\x18\x02 \x01(\x0c\">\n\x0fInputScriptResp\x12+\n\rinput_scripts\x18\x01 \x03(\x0b\x32\x14.signrpc.InputScript\"m\n\x0eSignMessageReq\x12\x0b\n\x03msg\x18\x01 \x01(\x0c\x12$\n\x07key_loc\x18\x02 \x01(\x0b\x32\x13.signrpc.KeyLocator\x12\x13\n\x0b\x64ouble_hash\x18\x03 \x01(\x08\x12\x13\n\x0b\x63ompact_sig\x18\x04 \x01(\x08\"$\n\x0fSignMessageResp\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"B\n\x10VerifyMessageReq\x12\x0b\n\x03msg\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0e\n\x06pubkey\x18\x03 \x01(\x0c\"\"\n\x11VerifyMessageResp\x12\r\n\x05valid\x18\x01 \x01(\x08\"\x80\x01\n\x10SharedKeyRequest\x12\x18\n\x10\x65phemeral_pubkey\x18\x01 \x01(\x0c\x12(\n\x07key_loc\x18\x02 \x01(\x0b\x32\x13.signrpc.KeyLocatorB\x02\x18\x01\x12(\n\x08key_desc\x18\x03 \x01(\x0b\x32\x16.signrpc.KeyDescriptor\"\'\n\x11SharedKeyResponse\x12\x12\n\nshared_key\x18\x01 \x01(\x0c\x32\xd4\x02\n\x06Signer\x12\x34\n\rSignOutputRaw\x12\x10.signrpc.SignReq\x1a\x11.signrpc.SignResp\x12@\n\x12\x43omputeInputScript\x12\x10.signrpc.SignReq\x1a\x18.signrpc.InputScriptResp\x12@\n\x0bSignMessage\x12\x17.signrpc.SignMessageReq\x1a\x18.signrpc.SignMessageResp\x12\x46\n\rVerifyMessage\x12\x19.signrpc.VerifyMessageReq\x1a\x1a.signrpc.VerifyMessageResp\x12H\n\x0f\x44\x65riveSharedKey\x12\x19.signrpc.SharedKeyRequest\x1a\x1a.signrpc.SharedKeyResponseB/Z-github.com/lightningnetwork/lnd/lnrpc/signrpcb\x06proto3') + + + +_KEYLOCATOR = DESCRIPTOR.message_types_by_name['KeyLocator'] +_KEYDESCRIPTOR = DESCRIPTOR.message_types_by_name['KeyDescriptor'] +_TXOUT = DESCRIPTOR.message_types_by_name['TxOut'] +_SIGNDESCRIPTOR = DESCRIPTOR.message_types_by_name['SignDescriptor'] +_SIGNREQ = DESCRIPTOR.message_types_by_name['SignReq'] +_SIGNRESP = DESCRIPTOR.message_types_by_name['SignResp'] +_INPUTSCRIPT = DESCRIPTOR.message_types_by_name['InputScript'] +_INPUTSCRIPTRESP = DESCRIPTOR.message_types_by_name['InputScriptResp'] +_SIGNMESSAGEREQ = DESCRIPTOR.message_types_by_name['SignMessageReq'] +_SIGNMESSAGERESP = DESCRIPTOR.message_types_by_name['SignMessageResp'] +_VERIFYMESSAGEREQ = DESCRIPTOR.message_types_by_name['VerifyMessageReq'] +_VERIFYMESSAGERESP = DESCRIPTOR.message_types_by_name['VerifyMessageResp'] +_SHAREDKEYREQUEST = DESCRIPTOR.message_types_by_name['SharedKeyRequest'] +_SHAREDKEYRESPONSE = DESCRIPTOR.message_types_by_name['SharedKeyResponse'] +KeyLocator = _reflection.GeneratedProtocolMessageType('KeyLocator', (_message.Message,), { + 'DESCRIPTOR' : _KEYLOCATOR, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.KeyLocator) + }) +_sym_db.RegisterMessage(KeyLocator) + +KeyDescriptor = _reflection.GeneratedProtocolMessageType('KeyDescriptor', (_message.Message,), { + 'DESCRIPTOR' : _KEYDESCRIPTOR, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.KeyDescriptor) + }) +_sym_db.RegisterMessage(KeyDescriptor) + +TxOut = _reflection.GeneratedProtocolMessageType('TxOut', (_message.Message,), { + 'DESCRIPTOR' : _TXOUT, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.TxOut) + }) +_sym_db.RegisterMessage(TxOut) + +SignDescriptor = _reflection.GeneratedProtocolMessageType('SignDescriptor', (_message.Message,), { + 'DESCRIPTOR' : _SIGNDESCRIPTOR, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.SignDescriptor) + }) +_sym_db.RegisterMessage(SignDescriptor) + +SignReq = _reflection.GeneratedProtocolMessageType('SignReq', (_message.Message,), { + 'DESCRIPTOR' : _SIGNREQ, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.SignReq) + }) +_sym_db.RegisterMessage(SignReq) + +SignResp = _reflection.GeneratedProtocolMessageType('SignResp', (_message.Message,), { + 'DESCRIPTOR' : _SIGNRESP, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.SignResp) + }) +_sym_db.RegisterMessage(SignResp) + +InputScript = _reflection.GeneratedProtocolMessageType('InputScript', (_message.Message,), { + 'DESCRIPTOR' : _INPUTSCRIPT, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.InputScript) + }) +_sym_db.RegisterMessage(InputScript) + +InputScriptResp = _reflection.GeneratedProtocolMessageType('InputScriptResp', (_message.Message,), { + 'DESCRIPTOR' : _INPUTSCRIPTRESP, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.InputScriptResp) + }) +_sym_db.RegisterMessage(InputScriptResp) + +SignMessageReq = _reflection.GeneratedProtocolMessageType('SignMessageReq', (_message.Message,), { + 'DESCRIPTOR' : _SIGNMESSAGEREQ, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.SignMessageReq) + }) +_sym_db.RegisterMessage(SignMessageReq) + +SignMessageResp = _reflection.GeneratedProtocolMessageType('SignMessageResp', (_message.Message,), { + 'DESCRIPTOR' : _SIGNMESSAGERESP, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.SignMessageResp) + }) +_sym_db.RegisterMessage(SignMessageResp) + +VerifyMessageReq = _reflection.GeneratedProtocolMessageType('VerifyMessageReq', (_message.Message,), { + 'DESCRIPTOR' : _VERIFYMESSAGEREQ, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.VerifyMessageReq) + }) +_sym_db.RegisterMessage(VerifyMessageReq) + +VerifyMessageResp = _reflection.GeneratedProtocolMessageType('VerifyMessageResp', (_message.Message,), { + 'DESCRIPTOR' : _VERIFYMESSAGERESP, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.VerifyMessageResp) + }) +_sym_db.RegisterMessage(VerifyMessageResp) + +SharedKeyRequest = _reflection.GeneratedProtocolMessageType('SharedKeyRequest', (_message.Message,), { + 'DESCRIPTOR' : _SHAREDKEYREQUEST, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.SharedKeyRequest) + }) +_sym_db.RegisterMessage(SharedKeyRequest) + +SharedKeyResponse = _reflection.GeneratedProtocolMessageType('SharedKeyResponse', (_message.Message,), { + 'DESCRIPTOR' : _SHAREDKEYRESPONSE, + '__module__' : 'signer_pb2' + # @@protoc_insertion_point(class_scope:signrpc.SharedKeyResponse) + }) +_sym_db.RegisterMessage(SharedKeyResponse) + +_SIGNER = DESCRIPTOR.services_by_name['Signer'] +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'Z-github.com/lightningnetwork/lnd/lnrpc/signrpc' + _SHAREDKEYREQUEST.fields_by_name['key_loc']._options = None + _SHAREDKEYREQUEST.fields_by_name['key_loc']._serialized_options = b'\030\001' + _KEYLOCATOR._serialized_start=25 + _KEYLOCATOR._serialized_end=76 + _KEYDESCRIPTOR._serialized_start=78 + _KEYDESCRIPTOR._serialized_end=154 + _TXOUT._serialized_start=156 + _TXOUT._serialized_end=197 + _SIGNDESCRIPTOR._serialized_start=200 + _SIGNDESCRIPTOR._serialized_end=396 + _SIGNREQ._serialized_start=398 + _SIGNREQ._serialized_end=474 + _SIGNRESP._serialized_start=476 + _SIGNRESP._serialized_end=504 + _INPUTSCRIPT._serialized_start=506 + _INPUTSCRIPT._serialized_end=556 + _INPUTSCRIPTRESP._serialized_start=558 + _INPUTSCRIPTRESP._serialized_end=620 + _SIGNMESSAGEREQ._serialized_start=622 + _SIGNMESSAGEREQ._serialized_end=731 + _SIGNMESSAGERESP._serialized_start=733 + _SIGNMESSAGERESP._serialized_end=769 + _VERIFYMESSAGEREQ._serialized_start=771 + _VERIFYMESSAGEREQ._serialized_end=837 + _VERIFYMESSAGERESP._serialized_start=839 + _VERIFYMESSAGERESP._serialized_end=873 + _SHAREDKEYREQUEST._serialized_start=876 + _SHAREDKEYREQUEST._serialized_end=1004 + _SHAREDKEYRESPONSE._serialized_start=1006 + _SHAREDKEYRESPONSE._serialized_end=1045 + _SIGNER._serialized_start=1048 + _SIGNER._serialized_end=1388 +# @@protoc_insertion_point(module_scope) diff --git a/gui/lnd_deps/signer_pb2_grpc.py b/gui/lnd_deps/signer_pb2_grpc.py new file mode 100644 index 00000000..42a136db --- /dev/null +++ b/gui/lnd_deps/signer_pb2_grpc.py @@ -0,0 +1,245 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from . import signer_pb2 as signer__pb2 + + +class SignerStub(object): + """Signer is a service that gives access to the signing functionality of the + daemon's wallet. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.SignOutputRaw = channel.unary_unary( + '/signrpc.Signer/SignOutputRaw', + request_serializer=signer__pb2.SignReq.SerializeToString, + response_deserializer=signer__pb2.SignResp.FromString, + ) + self.ComputeInputScript = channel.unary_unary( + '/signrpc.Signer/ComputeInputScript', + request_serializer=signer__pb2.SignReq.SerializeToString, + response_deserializer=signer__pb2.InputScriptResp.FromString, + ) + self.SignMessage = channel.unary_unary( + '/signrpc.Signer/SignMessage', + request_serializer=signer__pb2.SignMessageReq.SerializeToString, + response_deserializer=signer__pb2.SignMessageResp.FromString, + ) + self.VerifyMessage = channel.unary_unary( + '/signrpc.Signer/VerifyMessage', + request_serializer=signer__pb2.VerifyMessageReq.SerializeToString, + response_deserializer=signer__pb2.VerifyMessageResp.FromString, + ) + self.DeriveSharedKey = channel.unary_unary( + '/signrpc.Signer/DeriveSharedKey', + request_serializer=signer__pb2.SharedKeyRequest.SerializeToString, + response_deserializer=signer__pb2.SharedKeyResponse.FromString, + ) + + +class SignerServicer(object): + """Signer is a service that gives access to the signing functionality of the + daemon's wallet. + """ + + def SignOutputRaw(self, request, context): + """ + SignOutputRaw is a method that can be used to generated a signature for a + set of inputs/outputs to a transaction. Each request specifies details + concerning how the outputs should be signed, which keys they should be + signed with, and also any optional tweaks. The return value is a fixed + 64-byte signature (the same format as we use on the wire in Lightning). + + If we are unable to sign using the specified keys, then an error will be + returned. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ComputeInputScript(self, request, context): + """ + ComputeInputScript generates a complete InputIndex for the passed + transaction with the signature as defined within the passed SignDescriptor. + This method should be capable of generating the proper input script for + both regular p2wkh output and p2wkh outputs nested within a regular p2sh + output. + + Note that when using this method to sign inputs belonging to the wallet, + the only items of the SignDescriptor that need to be populated are pkScript + in the TxOut field, the value in that same field, and finally the input + index. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SignMessage(self, request, context): + """ + SignMessage signs a message with the key specified in the key locator. The + returned signature is fixed-size LN wire format encoded. + + The main difference to SignMessage in the main RPC is that a specific key is + used to sign the message instead of the node identity private key. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def VerifyMessage(self, request, context): + """ + VerifyMessage verifies a signature over a message using the public key + provided. The signature must be fixed-size LN wire format encoded. + + The main difference to VerifyMessage in the main RPC is that the public key + used to sign the message does not have to be a node known to the network. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeriveSharedKey(self, request, context): + """ + DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key + derivation between the ephemeral public key in the request and the node's + key specified in the key_desc parameter. Either a key locator or a raw + public key is expected in the key_desc, if neither is supplied, defaults to + the node's identity private key: + P_shared = privKeyNode * ephemeralPubkey + The resulting shared public key is serialized in the compressed format and + hashed with sha256, resulting in the final key length of 256bit. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_SignerServicer_to_server(servicer, server): + rpc_method_handlers = { + 'SignOutputRaw': grpc.unary_unary_rpc_method_handler( + servicer.SignOutputRaw, + request_deserializer=signer__pb2.SignReq.FromString, + response_serializer=signer__pb2.SignResp.SerializeToString, + ), + 'ComputeInputScript': grpc.unary_unary_rpc_method_handler( + servicer.ComputeInputScript, + request_deserializer=signer__pb2.SignReq.FromString, + response_serializer=signer__pb2.InputScriptResp.SerializeToString, + ), + 'SignMessage': grpc.unary_unary_rpc_method_handler( + servicer.SignMessage, + request_deserializer=signer__pb2.SignMessageReq.FromString, + response_serializer=signer__pb2.SignMessageResp.SerializeToString, + ), + 'VerifyMessage': grpc.unary_unary_rpc_method_handler( + servicer.VerifyMessage, + request_deserializer=signer__pb2.VerifyMessageReq.FromString, + response_serializer=signer__pb2.VerifyMessageResp.SerializeToString, + ), + 'DeriveSharedKey': grpc.unary_unary_rpc_method_handler( + servicer.DeriveSharedKey, + request_deserializer=signer__pb2.SharedKeyRequest.FromString, + response_serializer=signer__pb2.SharedKeyResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'signrpc.Signer', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class Signer(object): + """Signer is a service that gives access to the signing functionality of the + daemon's wallet. + """ + + @staticmethod + def SignOutputRaw(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/signrpc.Signer/SignOutputRaw', + signer__pb2.SignReq.SerializeToString, + signer__pb2.SignResp.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ComputeInputScript(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/signrpc.Signer/ComputeInputScript', + signer__pb2.SignReq.SerializeToString, + signer__pb2.InputScriptResp.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def SignMessage(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/signrpc.Signer/SignMessage', + signer__pb2.SignMessageReq.SerializeToString, + signer__pb2.SignMessageResp.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def VerifyMessage(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/signrpc.Signer/VerifyMessage', + signer__pb2.VerifyMessageReq.SerializeToString, + signer__pb2.VerifyMessageResp.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeriveSharedKey(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/signrpc.Signer/DeriveSharedKey', + signer__pb2.SharedKeyRequest.SerializeToString, + signer__pb2.SharedKeyResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/gui/migrations/0021_auto_20220221_1309.py b/gui/migrations/0021_auto_20220221_1309.py new file mode 100644 index 00000000..7395eeff --- /dev/null +++ b/gui/migrations/0021_auto_20220221_1309.py @@ -0,0 +1,216 @@ +# Generated by Django 3.2.7 on 2022-02-23 09:31 + +from django.db import migrations, models +import django.db.models.deletion +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.lnd_connect import lnd_connect +from lndg import settings + +def update_messages(apps, schedma_editor): + invoices = apps.get_model('gui', 'invoices') + try: + messages = invoices.objects.exclude(message=None) + if len(messages) > 0: + stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + signerstub = lnsigner.SignerStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey + for message in messages: + records = stub.LookupInvoice(ln.PaymentHash(r_hash=bytes.fromhex(message.r_hash))).htlcs[0].custom_records + if 34349337 in records and 34349339 in records and 34349343 in records and 34349334 in records: + try: + valid = signerstub.VerifyMessage(lns.VerifyMessageReq(msg=(records[34349339]+bytes.fromhex(self_pubkey)+records[34349343]+records[34349334]), signature=records[34349337], pubkey=records[34349339])).valid + except: + print('Unable to validate signature on invoice: ' + message.r_hash) + valid = False + sender = records[34349339].hex() if valid == True else None + else: + sender = None + try: + alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=sender, include_channels=False)).node.alias if sender != None else None + except: + alias = None + message.message = records[34349334].decode('utf-8', errors='ignore')[:1000] + message.sender = sender + message.sender_alias = alias + message.save() + except Exception as e: + print('Migration step failed:', str(e)) + +def revert_messages(apps, schedma_editor): + pass + +def update_rebal_channel(apps, schedma_editor): + payments = apps.get_model('gui', 'payments') + hops = apps.get_model('gui', 'paymenthops') + try: + stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey + for payment in payments.objects.filter(status=2).iterator(): + last_hop = hops.objects.filter(payment_hash=payment.payment_hash).order_by('-step')[0] if hops.objects.filter(payment_hash=payment.payment_hash).exists() else None + if last_hop != None and last_hop.node_pubkey == self_pubkey: + payment.rebal_chan = last_hop.chan_id + payment.save() + except Exception as e: + print('Migration step failed:', str(e)) + +def revert_rebal_channel(apps, schedma_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0020_auto_20220126_2113'), + ] + + operations = [ + migrations.CreateModel( + name='Closures', + fields=[ + ('chan_id', models.CharField(max_length=20, primary_key=True, serialize=False)), + ('closing_tx', models.CharField(max_length=64)), + ('remote_pubkey', models.CharField(max_length=66)), + ('capacity', models.BigIntegerField()), + ('close_height', models.IntegerField()), + ('settled_balance', models.BigIntegerField()), + ('time_locked_balance', models.BigIntegerField()), + ('close_type', models.IntegerField()), + ('open_initiator', models.IntegerField()), + ('close_initiator', models.IntegerField()), + ('resolution_count', models.IntegerField()), + ], + ), + migrations.AddField( + model_name='channels', + name='htlc_count', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='channels', + name='pending_inbound', + field=models.BigIntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='channels', + name='pending_outbound', + field=models.BigIntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='channels', + name='private', + field=models.BooleanField(default=False), + preserve_default=False, + ), + migrations.AddField( + model_name='channels', + name='total_received', + field=models.BigIntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='channels', + name='total_sent', + field=models.BigIntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='invoices', + name='sender', + field=models.CharField(max_length=66, null=True), + ), + migrations.AddField( + model_name='invoices', + name='sender_alias', + field=models.CharField(max_length=32, null=True), + ), + migrations.AddField( + model_name='payments', + name='rebal_chan', + field=models.CharField(max_length=20, null=True), + ), + migrations.AddField( + model_name='peers', + name='alias', + field=models.CharField(max_length=32, null=True), + ), + migrations.AlterField( + model_name='autopilot', + name='chan_id', + field=models.CharField(max_length=20), + ), + migrations.AlterField( + model_name='channels', + name='chan_id', + field=models.CharField(max_length=20, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='failedhtlcs', + name='chan_id_in', + field=models.CharField(max_length=20), + ), + migrations.AlterField( + model_name='failedhtlcs', + name='chan_id_out', + field=models.CharField(max_length=20), + ), + migrations.AlterField( + model_name='forwards', + name='chan_id_in', + field=models.CharField(max_length=20), + ), + migrations.AlterField( + model_name='forwards', + name='chan_id_out', + field=models.CharField(max_length=20), + ), + migrations.AlterField( + model_name='invoices', + name='chan_in', + field=models.CharField(max_length=20, null=True), + ), + migrations.AlterField( + model_name='invoices', + name='message', + field=models.CharField(max_length=1000, null=True), + ), + migrations.AlterField( + model_name='paymenthops', + name='chan_id', + field=models.CharField(max_length=20), + ), + migrations.AlterField( + model_name='payments', + name='chan_out', + field=models.CharField(max_length=20, null=True), + ), + migrations.AlterField( + model_name='payments', + name='message', + field=models.CharField(max_length=1000, null=True), + ), + migrations.AlterField( + model_name='pendinghtlcs', + name='chan_id', + field=models.CharField(max_length=20), + ), + migrations.CreateModel( + name='Resolutions', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('resolution_type', models.IntegerField()), + ('outcome', models.IntegerField()), + ('outpoint_tx', models.CharField(max_length=64)), + ('outpoint_index', models.IntegerField()), + ('amount_sat', models.BigIntegerField()), + ('sweep_txid', models.CharField(max_length=64)), + ('chan_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gui.closures')), + ], + ), + migrations.RunPython(update_rebal_channel, revert_rebal_channel), + migrations.RunPython(update_messages, revert_messages), + ] diff --git a/gui/migrations/0022_auto_20220227_2235.py b/gui/migrations/0022_auto_20220227_2235.py new file mode 100644 index 00000000..e3fa9e35 --- /dev/null +++ b/gui/migrations/0022_auto_20220227_2235.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.7 on 2022-02-27 22:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gui', '0021_auto_20220221_1309'), + ] + + operations = [ + migrations.AddField( + model_name='failedhtlcs', + name='chan_out_liq', + field=models.BigIntegerField(null=True), + ), + migrations.AddField( + model_name='failedhtlcs', + name='chan_out_pending', + field=models.BigIntegerField(null=True), + ), + ] diff --git a/gui/models.py b/gui/models.py index 0a5ce8c6..d6e5b703 100644 --- a/gui/models.py +++ b/gui/models.py @@ -9,11 +9,12 @@ class Payments(models.Model): fee = models.FloatField() status = models.IntegerField() index = models.IntegerField() - chan_out = models.IntegerField(null=True) + chan_out = models.CharField(max_length=20, null=True) chan_out_alias = models.CharField(null=True, max_length=32) keysend_preimage = models.CharField(null=True, max_length=64) - message = models.CharField(null=True, max_length=255) + message = models.CharField(null=True, max_length=1000) cleaned = models.BooleanField(default=False) + rebal_chan = models.CharField(max_length=20, null=True) class Meta: app_label = 'gui' @@ -21,7 +22,7 @@ class PaymentHops(models.Model): payment_hash = models.ForeignKey('Payments', on_delete=models.CASCADE) attempt_id = models.IntegerField() step = models.IntegerField() - chan_id = models.IntegerField() + chan_id = models.CharField(max_length=20) alias = models.CharField(max_length=32) chan_capacity = models.BigIntegerField() node_pubkey = models.CharField(max_length=66) @@ -39,18 +40,20 @@ class Invoices(models.Model): value = models.FloatField() amt_paid = models.BigIntegerField() state = models.IntegerField() - chan_in = models.IntegerField(null=True) + chan_in = models.CharField(max_length=20, null=True) chan_in_alias = models.CharField(null=True, max_length=32) keysend_preimage = models.CharField(null=True, max_length=64) - message = models.CharField(null=True, max_length=500) + message = models.CharField(null=True, max_length=1000) + sender = models.CharField(null=True, max_length=66) + sender_alias = models.CharField(null=True, max_length=32) index = models.IntegerField() class Meta: app_label = 'gui' class Forwards(models.Model): forward_date = models.DateTimeField() - chan_id_in = models.IntegerField() - chan_id_out = models.IntegerField() + chan_id_in = models.CharField(max_length=20) + chan_id_out = models.CharField(max_length=20) chan_in_alias = models.CharField(null=True, max_length=32) chan_out_alias = models.CharField(null=True, max_length=32) amt_in_msat = models.BigIntegerField() @@ -61,7 +64,7 @@ class Meta: class Channels(models.Model): remote_pubkey = models.CharField(max_length=66) - chan_id = models.IntegerField(primary_key=True) + chan_id = models.CharField(max_length=20, primary_key=True) funding_txid = models.CharField(max_length=64) output_index = models.IntegerField() capacity = models.BigIntegerField() @@ -73,6 +76,12 @@ class Channels(models.Model): num_updates = models.IntegerField() initiator = models.BooleanField() alias = models.CharField(max_length=32) + total_sent = models.BigIntegerField() + total_received = models.BigIntegerField() + private = models.BooleanField() + pending_outbound = models.BigIntegerField() + pending_inbound = models.BigIntegerField() + htlc_count = models.IntegerField() local_base_fee = models.IntegerField() local_fee_rate = models.IntegerField() local_disabled = models.BooleanField() @@ -117,6 +126,7 @@ class Meta: class Peers(models.Model): pubkey = models.CharField(max_length=66, primary_key=True) + alias = models.CharField(null=True, max_length=32) address = models.CharField(max_length=100) sat_sent = models.BigIntegerField() sat_recv = models.BigIntegerField() @@ -158,8 +168,34 @@ class Onchain(models.Model): class Meta: app_label = 'gui' +class Closures(models.Model): + chan_id = models.CharField(max_length=20, primary_key=True) + closing_tx = models.CharField(max_length=64) + remote_pubkey = models.CharField(max_length=66) + capacity = models.BigIntegerField() + close_height = models.IntegerField() + settled_balance = models.BigIntegerField() + time_locked_balance = models.BigIntegerField() + close_type = models.IntegerField() + open_initiator = models.IntegerField() + close_initiator = models.IntegerField() + resolution_count = models.IntegerField() + class Meta: + app_label = 'gui' + +class Resolutions(models.Model): + chan_id = models.ForeignKey('Closures', on_delete=models.CASCADE) + resolution_type = models.IntegerField() + outcome = models.IntegerField() + outpoint_tx = models.CharField(max_length=64) + outpoint_index = models.IntegerField() + amount_sat = models.BigIntegerField() + sweep_txid = models.CharField(max_length=64) + class Meta: + app_label = 'gui' + class PendingHTLCs(models.Model): - chan_id = models.IntegerField() + chan_id = models.CharField(max_length=20) alias = models.CharField(max_length=32) incoming = models.BooleanField() amount = models.BigIntegerField() @@ -173,10 +209,12 @@ class Meta: class FailedHTLCs(models.Model): timestamp = models.DateTimeField(default=timezone.now) amount = models.IntegerField() - chan_id_in = models.IntegerField() - chan_id_out = models.IntegerField() + chan_id_in = models.CharField(max_length=20) + chan_id_out = models.CharField(max_length=20) chan_in_alias = models.CharField(null=True, max_length=32) chan_out_alias = models.CharField(null=True, max_length=32) + chan_out_liq = models.BigIntegerField(null=True) + chan_out_pending = models.BigIntegerField(null=True) wire_failure = models.IntegerField() failure_detail = models.IntegerField() missed_fee = models.FloatField() @@ -185,7 +223,7 @@ class Meta: class Autopilot(models.Model): timestamp = models.DateTimeField(default=timezone.now) - chan_id = models.IntegerField() + chan_id = models.CharField(max_length=20) peer_alias = models.CharField(max_length=32) setting = models.CharField(max_length=20) old_value = models.IntegerField() diff --git a/gui/templates/action_list.html b/gui/templates/action_list.html index 2c1d481c..4ad2a453 100644 --- a/gui/templates/action_list.html +++ b/gui/templates/action_list.html @@ -25,8 +25,8 @@

Suggested Action List

{% for channel in action_list %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} ({{ channel.outbound_percent }}%)
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
diff --git a/gui/templates/advanced.html b/gui/templates/advanced.html index c3ffa55a..7b235639 100644 --- a/gui/templates/advanced.html +++ b/gui/templates/advanced.html @@ -28,14 +28,14 @@

Advanced Channel Settings

{% for channel in channels %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {% if channel.private == False %}{% endif %}{% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}{% if channel.private == False %}{% endif %} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} ({{ channel.out_percent }}%)
{% if channel.in_percent == 0 %}
{% elif channel.out_percent == 0 %}
{% else %}
{% endif %}
{{ channel.remote_balance|intcomma }} ({{ channel.in_percent }}%) - - {% if channel.is_active == True %} + + {% if channel.is_active == True and channel.private == False %}
{% csrf_token %} @@ -47,7 +47,7 @@

Advanced Channel Settings

--- {% endif %} - + {% csrf_token %} @@ -55,7 +55,7 @@

Advanced Channel Settings

- +
{% csrf_token %} diff --git a/gui/templates/autopilot.html b/gui/templates/autopilot.html index 67b3ccc5..a6b20cec 100644 --- a/gui/templates/autopilot.html +++ b/gui/templates/autopilot.html @@ -16,9 +16,9 @@

Autopilot Logs

{% for log in autopilot %} - {{ log.timestamp|naturaltime }} - {{ log.chan_id }} - {{ log.peer_alias }} + {{ log.timestamp|naturaltime }} + {{ log.chan_id }} + {% if log.peer_alias == '' %}---{% else %}{{ log.peer_alias }}{% endif %} {{ log.setting }} {{ log.old_value }} {{ log.new_value }} diff --git a/gui/templates/base.html b/gui/templates/base.html index 75d7f1ad..05c3f196 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -28,7 +28,7 @@

My Lnd Overview

diff --git a/gui/templates/channel.html b/gui/templates/channel.html new file mode 100644 index 00000000..679fa2f9 --- /dev/null +++ b/gui/templates/channel.html @@ -0,0 +1,349 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Channel Card{% endblock %} +{% block content %} +{% load humanize %} +{% if channel %} +
+

Details For Channel: {{ chan_id }} | Peer: {% if channel.alias != "" %}{{ channel.alias }} ({{ channel.remote_pubkey }}){% else %}{{ channel.remote_pubkey }}{% endif %}

+

Capacity: {{ channel.capacity|intcomma }} | Is Open: {{ channel.is_open }} | Is Active: {{ channel.is_active }} | Is Private: {{ channel.private }} | Channel Updates: {{ channel.num_updates|intcomma }}

+

Channel Point: {{ channel.funding_txid }}:{{ channel.output_index }} | Opened In: {{ channel.open_block|intcomma }} | Opener: {% if channel.initiator == True %}Local{% else %}Remote{% endif %}

+

Outbound Liquidity: {{ channel.local_balance|intcomma }} ({{ channel.out_percent }}%) | Inbound Liquidity: {{ channel.remote_balance|intcomma }} ({{ channel.in_percent }}%)

+
+
+

Routing Events

+

1-Day Routed Out: {{ channel.amt_routed_out_1day|intcomma }} ({{ channel.routed_out_1day|intcomma }}) | Routed In: {{ channel.amt_routed_in_1day|intcomma }} ({{ channel.routed_in_1day|intcomma }}) | Average Out: {{ channel.average_out_1day|intcomma }} | Average In: {{ channel.average_in_1day|intcomma }}

+

7-Day Routed Out: {{ channel.amt_routed_out_7day|intcomma }} ({{ channel.routed_out_7day|intcomma }}) | Routed In: {{ channel.amt_routed_in_7day|intcomma }} ({{ channel.routed_in_7day|intcomma }}) | Average Out: {{ channel.average_out_7day|intcomma }} | Average In: {{ channel.average_in_7day|intcomma }}

+

30-Day Routed Out: {{ channel.amt_routed_out_30day|intcomma }} ({{ channel.routed_out_30day|intcomma }}) | Routed In: {{ channel.amt_routed_in_30day|intcomma }} ({{ channel.routed_in_30day|intcomma }}) | Average Out: {{ channel.average_out_30day|intcomma }} | Average In: {{ channel.average_in_30day|intcomma }}

+

Lifetime Routed Out: {{ channel.amt_routed_out|intcomma }} ({{ channel.routed_out|intcomma }}) | Routed In: {{ channel.amt_routed_in|intcomma }} ({{ channel.routed_in|intcomma }}) | Average Out: {{ channel.average_out|intcomma }} | Average In: {{ channel.average_in|intcomma }}

+
+
+

Rebalancing Events

+

1-Day Rebalanced Out: {{ channel.amt_rebal_out_1day|intcomma }} ({{ channel.rebal_out_1day|intcomma }}) | Rebalanced In: {{ channel.amt_rebal_in_1day|intcomma }} ({{ channel.rebal_in_1day|intcomma }}) | Success Rate: {{ channel.success_rate_1day }}% ({{ channel.success_1day }}/{{ channel.attempts_1day }})

+

7-Day Rebalanced Out: {{ channel.amt_rebal_out_7day|intcomma }} ({{ channel.rebal_out_7day|intcomma }}) | Rebalanced In: {{ channel.amt_rebal_in_7day|intcomma }} ({{ channel.rebal_in_7day|intcomma }}) | Success Rate: {{ channel.success_rate_7day }}% ({{ channel.success_7day }}/{{ channel.attempts_7day }})

+

30-Day Rebalanced Out: {{ channel.amt_rebal_out_30day|intcomma }} ({{ channel.rebal_out_30day|intcomma }}) | Rebalanced In: {{ channel.amt_rebal_in_30day|intcomma }} ({{ channel.rebal_in_30day|intcomma }}) | Success Rate: {{ channel.success_rate_30day }}% ({{ channel.success_30day }}/{{ channel.attempts_30day }})

+

Lifetime Rebalanced Out: {{ channel.amt_rebal_out|intcomma }} ({{ channel.rebal_out|intcomma }}) | Rebalanced In: {{ channel.amt_rebal_in|intcomma }} ({{ channel.rebal_in|intcomma }}) | Success Rate: {{ channel.success_rate }}% ({{ channel.success }}/{{ channel.attempts }})

+
+
+

Profitability

+

1-Day Revenue: {{ channel.revenue_1day|intcomma }} | Costs: {{ channel.costs_1day|intcomma }} | Profits: {{ channel.profits_1day|intcomma }} [{{ channel.profits_vol_1day }}] | Assisted Revenues: {{ channel.revenue_assist_1day|intcomma }} | APY: {{ channel.apy_1day }}%

+

7-Day Revenue: {{ channel.revenue_7day|intcomma }} | Costs: {{ channel.costs_7day|intcomma }} | Profits: {{ channel.profits_7day|intcomma }} [{{ channel.profits_vol_7day }}] | Assisted Revenues: {{ channel.revenue_assist_7day|intcomma }} | APY: {{ channel.apy_7day }}%

+

30-Day Revenue: {{ channel.revenue_30day|intcomma }} | Costs: {{ channel.costs_30day|intcomma }} | Profits: {{ channel.profits_30day|intcomma }} [{{ channel.profits_vol_30day }}] | Assisted Revenues: {{ channel.revenue_assist_30day|intcomma }} | APY: {{ channel.apy_30day }}%

+

Lifetime Revenue: {{ channel.revenue|intcomma }} | Costs: {{ channel.costs|intcomma }} | Profits: {{ channel.profits|intcomma }} [{{ channel.profits_vol }}] | Assisted Revenues: {{ channel.revenue_assist|intcomma }} | APY: {{ channel.apy }}%

+
+
+

Channel Settings

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
7Day FlowOut RateRebal RateAssisted RatioAdjustmentSuggested RateChannel StateoRateoBaseiRateiBaseTarget AmtMax Cost %oTarget%iTarget%ARRebal In?Active
{% if channel.net_routed_7day > 0 %}OUT{% elif channel.net_routed_7day < 0 %}IN{% else %}---{% endif %}{% if channel.net_routed_7day != 0 %} | {{ channel.net_routed_7day }}{% endif %}{{ channel.out_rate }}{{ channel.rebal_ppm }}{{ channel.assisted_ratio }}{{ channel.adjustment }}{{ channel.new_rate|intcomma }} + {% if channel.is_active == True %} + + {% csrf_token %} + + + + + + {% else %} + --- + {% endif %} + +
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
{{ channel.remote_fee_rate|intcomma }}{{ channel.remote_base_fee|intcomma }} +
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + + +
+
= 1 and channel.fee_check < 100 and channel.auto_rebalance == True %}style="background-color: #a6dce2">True ({{ channel.steps }}){% else %}style="background-color: #fadbd5">False{% endif %}True{% else %}style="background-color: #fadbd5">False{% endif %}
+
+{% if outgoing_htlcs %} +
+

Outgoing HTLCs

+ + + + + + + + + {% for htlc in outgoing_htlcs %} + + + + + + + + {% endfor %} +
Forwarding ChannelForwarding AliasAmountExpiration HeightHash Lock
{{ htlc.forwarding_channel }}{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %}{{ htlc.amount|intcomma }}{{ htlc.expiration_height|intcomma }}{{ htlc.hash_lock }}
+
+{% endif %} +{% if incoming_htlcs %} +
+

Incoming HTLCs

+ + + + + + + + + {% for htlc in incoming_htlcs %} + + + + + + + + {% endfor %} +
Forwarding ChannelForwarding AliasAmountExpiration HeightHash Lock
{{ htlc.forwarding_channel }}{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %}{{ htlc.amount|intcomma }}{{ htlc.expiration_height|intcomma }}{{ htlc.hash_lock }}
+
+{% endif %} +{% if forwards %} +
+

Last 5 Payments Routed

+ + + + + + + + + + + + + {% for forward in forwards %} + + + + + + + + + + + + {% endfor %} +
TimestampAmount InAmount OutChannel In AliasChannel Out AliasChannel In IdChannel Out IdFees EarnedPPM Earned
{{ forward.forward_date|naturaltime }}{{ forward.amt_in|intcomma }}{{ forward.amt_out|intcomma }}{% if forward.chan_in_alias == '' %}---{% else %}{{ forward.chan_in_alias }}{% endif %}{% if forward.chan_out_alias == '' %}---{% else %}{{ forward.chan_out_alias }}{% endif %}{{ forward.chan_id_in }}{{ forward.chan_id_out }}{{ forward.fee }}{{ forward.ppm|intcomma }}
+
+{% endif %} +{% if rebalances %} +
+

Last 5 Rebalance Requests

+ + + + + + + + + + + + + {% for rebalance in rebalances %} + + + + + + + + + + + + {% endfor %} +
RequestedStartStopScheduled DurationValueFee LimitTarget PPMLast Hop AliasStatus
{{ rebalance.requested|naturaltime }}N/A{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %} 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>N/A{% endif %}{{ rebalance.duration }} minutes{{ rebalance.value|intcomma }}{{ rebalance.fee_limit|intcomma }}{{ rebalance.ppm|intcomma }}{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}Successful{% elif rebalance.status == 3 %}Timeout{% elif rebalance.status == 4 %}No Route{% elif rebalance.status == 5 %}Error{% elif rebalance.status == 6 %}Incorrect Payment Details{% elif rebalance.status == 7 %}Insufficient Balance{% elif rebalance.status == 400 %}Rebalancer Request Failed{% elif rebalance.status == 408 %}Rebalancer Request Timeout{% else %}{{ rebalance.status }}{% endif %}
+
+{% endif %} +{% if payments %} +
+

Last 5 Payments Sent

+ + + + + + + + + + + + + {% for payment in payments %} + + + + + + + + + + + + {% endfor %} +
TimestampHashValueFee PaidStatusChan Out AliasChan Out IDRouteKeysend
{{ payment.creation_date|naturaltime }}{{ payment.payment_hash }}{{ payment.value|add:"0"|intcomma }}{{ payment.fee|intcomma }}{% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %}{% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %}{% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %}{% if payment.status == 2 %}Open{% else %}N/A{% endif %}{% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %}
+
+{% endif %} +{% if invoices %} +
+

Last 5 Payments Received

+ + + + + + + + + + + + + {% for invoice in invoices %} + + + + + + + + + + + + {% endfor %} +
CreatedSettledPayment HashValueAmount PaidStateChannel In AliasChannel InKeysend
{{ invoice.creation_date|naturaltime }}{% if invoice.state == 1 %}{{ invoice.settle_date|naturaltime }}{% else %}N/A{% endif %}{{ invoice.r_hash }}{{ invoice.value|add:"0"|intcomma }}{% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}N/A{% endif %}{% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %}{% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}N/A{% endif %}{% if invoice.state == 1 and invoice.chan_in != None %}{{ invoice.chan_in }}{% else %}N/A{% endif %}{% if invoice.keysend_preimage != None %}Yes{% else %}No{% endif %}
+
+{% endif %} +{% if failed_htlcs %} +
+

Last 5 Failed HTLCs

+ + + + + + + + + + + + + {% for failed_htlc in failed_htlcs %} + + + + + + + + + + + + {% endfor %} +
TimestampChan In IDChan Out IDChan In AliasChan Out AliasForward AmountPotential FeeHTLC FailureFailure Detail
{{ failed_htlc.timestamp|naturaltime }}{{ failed_htlc.chan_id_in }}{{ failed_htlc.chan_id_out }}{% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %}{% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %}{{ failed_htlc.amount|intcomma }}{{ failed_htlc.missed_fee|intcomma }}{% if failed_htlc.wire_failure == 15 %}Temporary Channel Failure{% elif failed_htlc.wire_failure == 18 %}Unknown Next Peer{% elif failed_htlc.wire_failure == 12 %}Fee Insufficient{% else %}{{ failed_htlc.wire_failure }}{% endif %}{% if failed_htlc.failure_detail == 1 %}---{% elif failed_htlc.failure_detail == 5 %}HTLC Exceeds Max{% elif failed_htlc.failure_detail == 6 %}Insufficient Balance{% elif failed_htlc.failure_detail == 20 %}Invalid Keysend{% elif failed_htlc.failure_detail == 22 %}Circular Route{% else %}{{ failed_htlc.failure_detail }}{% endif %}
+

Failed Outgoing HTLC Events Due To Insufficient Balance

+

1-Day: {{ channel.failed_out_1day }} | 7-Day: {{ channel.failed_out_7day }} | 30-Day: {{ channel.failed_out_30day }} | Lifetime: {{ channel.failed_out }}

+
+{% endif %} +{% endif %} +{% if not channel %} +
+

No data found for this channel!

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/channels.html b/gui/templates/channels.html index 486fe444..753575ea 100644 --- a/gui/templates/channels.html +++ b/gui/templates/channels.html @@ -11,46 +11,40 @@

Channel Performance

- 7-Day Activity And Revenue | {{ apy_7day }}% APY - 30-Day Activity And Revenue | {{ apy_30day }}% APY - Local Fees - Peer Fees + 7-Day Activity And Revenue | {{ apy_7day }}% APY + 30-Day Activity And Revenue | {{ apy_30day }}% APY Channel Health Channel ID Peer Alias Capacity - Outbound Flow - Out [Profit] | In - Inbound Flow - Outbound Flow - Out [Profit] | In - Inbound Flow - Rate - Base - Rate - Base + Outbound Flow + APY + Out [Profit] | In + Inbound Flow + Outbound Flow + APY + Out [Profit] | In + Inbound Flow Updates Opener {% for channel in channels %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} {{ channel.capacity }} M {{ channel.amt_routed_out_7day|intcomma }} M ({{ channel.routed_out_7day }}) | {{ channel.amt_rebal_in_7day|intcomma }} M ({{ channel.rebal_in_7day }}) + {{ channel.apy_7day }}% {{ channel.revenue_7day|intcomma }} [{{ channel.profits_7day|intcomma }}] | {{ channel.revenue_assist_7day|intcomma }} {{ channel.amt_routed_in_7day|intcomma }} M ({{ channel.routed_in_7day }}) | {{ channel.amt_rebal_out_7day|intcomma }} M ({{ channel.rebal_out_7day }}) {{ channel.amt_routed_out_30day|intcomma }} M ({{ channel.routed_out_30day }}) | {{ channel.amt_rebal_in_30day|intcomma }} M ({{ channel.rebal_in_30day }}) + {{ channel.apy_30day }}% {{ channel.revenue_30day|intcomma }} [{{ channel.profits_30day|intcomma }}] | {{ channel.revenue_assist_30day|intcomma }} {{ channel.amt_routed_in_30day|intcomma }} M ({{ channel.routed_in_30day }}) | {{ channel.amt_rebal_out_30day|intcomma }} M ({{ channel.rebal_out_30day }}) - {{ channel.local_fee_rate|intcomma }} - {{ channel.local_base_fee|intcomma }} - {{ channel.remote_fee_rate|intcomma }} - {{ channel.remote_base_fee|intcomma }} {{ channel.updates }}% - {{ channel.initiator }} + {% if channel.initiator == True %}Local{% else %}Remote{% endif %} {% endfor %} diff --git a/gui/templates/closures.html b/gui/templates/closures.html index 5ad26ab2..adec9a8c 100644 --- a/gui/templates/closures.html +++ b/gui/templates/closures.html @@ -8,6 +8,7 @@

Closures

+ @@ -16,18 +17,21 @@

Closures

+ {% for closure in closures %} - + + - + + {% endfor %}
Channel IDAlias Capacity Closing TXID Settled Balance Close Type Opener CloserResolutions
{{ closure.chan_id }}{% if closure.chan_id == '0' %}---{% else %}{{ closure.chan_id }}{% endif %}{% if closure.alias == '' %}{{ closure.remote_pubkey|slice:":12" }}{% else %}{{ closure.alias }}{% endif %} {{ closure.capacity|intcomma }}{{ closure.closing_tx_hash }}{% if closure.closing_tx == '0000000000000000000000000000000000000000000000000000000000000000' %}---{% else %}{{ closure.closing_tx }}{% endif %} {{ closure.settled_balance|intcomma }} {{ closure.time_locked_balance|intcomma }} {{ closure.close_height|intcomma }} {% if closure.close_type == 0 %}Cooperative{% elif closure.close_type == 1 %}Local Force{% elif closure.close_type == 2 %}Remote Force{% elif closure.close_type == 3 %}Breach{% elif closure.close_type == 4 %}Funding Cancelled{% elif closure.close_type == 5 %}Abandoned{% else %}{{ closure.close_type }}{% endif %} {% if closure.open_initiator == 0 %}Unknown{% elif closure.open_initiator == 1 %}Local{% elif closure.open_initiator == 2 %}Remote{% elif closure.open_initiator == 3 %}Both{% else %}{{ closure.open_initiator }}{% endif %} {% if closure.close_initiator == 0 %}Unknown{% elif closure.close_initiator == 1 %}Local{% elif closure.close_initiator == 2 %}Remote{% elif closure.close_initiator == 3 %}Both{% else %}{{ closure.close_initiator }}{% endif %}{% if closure.resolution_count > 0 %}Details{% else %}---{% endif %}
diff --git a/gui/templates/failed_htlcs.html b/gui/templates/failed_htlcs.html index a294ac14..ccc4a50a 100644 --- a/gui/templates/failed_htlcs.html +++ b/gui/templates/failed_htlcs.html @@ -13,6 +13,7 @@

Last 150 Failed HTLCs

Chan In Alias Chan Out Alias Forward Amount + Actual Outbound Potential Fee HTLC Failure Failure Detail @@ -22,9 +23,10 @@

Last 150 Failed HTLCs

{{ failed_htlc.timestamp|naturaltime }} {{ failed_htlc.chan_id_in }} {{ failed_htlc.chan_id_out }} - {{ failed_htlc.chan_in_alias }} - {{ failed_htlc.chan_out_alias }} + {% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %} + {% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %} {{ failed_htlc.amount|intcomma }} + {{ failed_htlc.chan_out_liq|intcomma }} ({{ failed_htlc.chan_out_pending|intcomma }}) {{ failed_htlc.missed_fee|intcomma }} {% if failed_htlc.wire_failure == 15 %}Temporary Channel Failure{% elif failed_htlc.wire_failure == 18 %}Unknown Next Peer{% elif failed_htlc.wire_failure == 12 %}Fee Insufficient{% else %}{{ failed_htlc.wire_failure }}{% endif %} {% if failed_htlc.failure_detail == 1 %}---{% elif failed_htlc.failure_detail == 5 %}HTLC Exceeds Max{% elif failed_htlc.failure_detail == 6 %}Insufficient Balance{% elif failed_htlc.failure_detail == 20 %}Invalid Keysend{% elif failed_htlc.failure_detail == 22 %}Circular Route{% else %}{{ failed_htlc.failure_detail }}{% endif %} diff --git a/gui/templates/fee_rates.html b/gui/templates/fee_rates.html index 3d1a469c..fa2b9801 100644 --- a/gui/templates/fee_rates.html +++ b/gui/templates/fee_rates.html @@ -28,8 +28,8 @@

Suggested Fee Rates

{% for channel in channels %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {% if channel.private == False %}{% endif %}{% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}{% if channel.private == False %}{% endif %} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} ({{ channel.out_percent }}%)
{% if channel.in_percent == 0 %}
{% elif channel.out_percent == 0 %}
{% else %}
{% endif %}
diff --git a/gui/templates/forwards.html b/gui/templates/forwards.html index c3ee1b3e..33f3cf3d 100644 --- a/gui/templates/forwards.html +++ b/gui/templates/forwards.html @@ -22,10 +22,10 @@

Last 150 Forwards

{{ forward.forward_date|naturaltime }} {{ forward.amt_in|intcomma }} {{ forward.amt_out|intcomma }} - {{ forward.chan_in_alias }} - {{ forward.chan_out_alias }} - {{ forward.chan_id_in }} - {{ forward.chan_id_out }} + {% if forward.chan_in_alias == '' %}---{% else %}{{ forward.chan_in_alias }}{% endif %} + {% if forward.chan_out_alias == '' %}---{% else %}{{ forward.chan_out_alias }}{% endif %} + {{ forward.chan_id_in }} + {{ forward.chan_id_out }} {{ forward.fee }} {{ forward.ppm|intcomma }} diff --git a/gui/templates/home.html b/gui/templates/home.html index ac9f2ce7..a3d6ecc9 100644 --- a/gui/templates/home.html +++ b/gui/templates/home.html @@ -24,7 +24,7 @@

7-Day Routed: {{ routed_7day|intcomma }} | Value: {{ routed_7day_amt|intcomm

Inbound Liquidity: {{ inbound|intcomma }} | Outbound Liquidity: {{ outbound|intcomma }} | Liquidity Ratio: {{ liq_ratio }}%

Balance In Limbo: {{ limbo_balance|intcomma }} | Unsettled Liquidity: {{ unsettled|intcomma }} | Pending HTLCs: {{ pending_htlc_count }}

-

Closures | New Peers | AR Actions | Fee Rates | Autopilot Logs | Channel Performance | Advanced Settings

+

Closures | New Peers | AR Actions | Fee Rates | Autopilot Logs | Channel Performance | Keysends | Rebalancing | Advanced Settings

{% if active_channels %}
@@ -52,8 +52,8 @@

Active Channels

{% for channel in active_channels %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }} ({{ channel.outbound_percent }}%)
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
@@ -113,14 +113,14 @@

Inactive Channels

Local Commit Local Reserve Downtime - Initiated By Me + Opener iTarget% AR {% for channel in inactive_channels %} - {{ channel.chan_id }} - {{ channel.alias }} + {{ channel.chan_id }} + {% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %} {{ channel.capacity|intcomma }} {{ channel.local_balance|intcomma }}
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
@@ -133,7 +133,7 @@

Inactive Channels

{{ channel.local_commit|intcomma }} {{ channel.local_chan_reserve|intcomma }} {{ channel.last_update|naturaltime|slice:":-4" }} - {{ channel.initiator }} + {% if channel.initiator == True %}Local{% else %}Remote{% endif %} {% if channel.auto_rebalance == True %}
@@ -160,6 +160,49 @@

Inactive Channels

{% endif %} +{% if private_channels %} +
+

Private Channels

+ + + + + + + + + + + + + + + + + + + {% for channel in private_channels %} + + + + + + + + + + + + + + + + + + {% endfor %} +
Channel IDPeer AliasCapacityOutbound LiquidityInbound LiquidityUnsettledoRateoBaseoLifeiLifeLocal CommitLocal ReserveOpenerUp/Down
{{ channel.chan_id }}{% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }}
{% if channel.inbound_percent == 0 %}
{% elif channel.outbound_percent == 0 %}
{% else %}
{% endif %}
{{ channel.remote_balance|intcomma }}{{ channel.unsettled_balance|intcomma }}{{ channel.local_fee_rate|intcomma }}{{ channel.local_base_fee|intcomma }}{{ channel.total_sent|intcomma }}{{ channel.total_received|intcomma }}{{ channel.local_commit|intcomma }}{{ channel.local_chan_reserve|intcomma }}{% if channel.initiator == True %}Local{% else %}Remote{% endif %}{{ channel.last_update|naturaltime|slice:":-4" }}
+
+{% endif %} {% if pending_open %}

Pending Open Channels

@@ -296,10 +339,10 @@

Last 10 Payments Routed

{{ forward.forward_date|naturaltime }} {{ forward.amt_in|intcomma }} {{ forward.amt_out|intcomma }} - {{ forward.chan_in_alias }} - {{ forward.chan_out_alias }} - {{ forward.chan_id_in }} - {{ forward.chan_id_out }} + {% if forward.chan_in_alias == '' %}---{% else %}{{ forward.chan_in_alias }}{% endif %} + {% if forward.chan_out_alias == '' %}---{% else %}{{ forward.chan_out_alias }}{% endif %} + {{ forward.chan_id_in }} + {{ forward.chan_id_out }} {{ forward.fee }} {{ forward.ppm|intcomma }} @@ -309,7 +352,7 @@

Last 10 Payments Routed

{% endif %} {% if rebalances %}
-

Last 10 Rebalance Requests (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing)

+

Last 10 Rebalance Requests (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing)

@@ -318,6 +361,7 @@

Last 10 Rebalance Requests (currently scheduling {{ eligible_count }} of {{

+ @@ -329,8 +373,9 @@

Last 10 Rebalance Requests (currently scheduling {{ eligible_count }} of {{

+ - + {% endfor %}
RequestedScheduled Duration Value Fee LimitTarget PPM Last Hop Alias Status
{{ rebalance.duration }} minutes {{ rebalance.value|intcomma }} {{ rebalance.fee_limit|intcomma }}{{ rebalance.ppm|intcomma }} {% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}Successful{% elif rebalance.status == 3 %}Timeout{% elif rebalance.status == 4 %}No Route{% elif rebalance.status == 5 %}Error{% elif rebalance.status == 6 %}Incorrect Payment Details{% elif rebalance.status == 7 %}Insufficient Balance{% elif rebalance.status == 400 %}Rebalancer Request Failed{% elif rebalance.status == 408 %}Rebalancer Request Timeout{% else %}{{ rebalance.status }}{% endif %}{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}Successful{% elif rebalance.status == 3 %}Timeout{% elif rebalance.status == 4 %}No Route{% elif rebalance.status == 5 %}Error{% elif rebalance.status == 6 %}Incorrect Payment Details{% elif rebalance.status == 7 %}Insufficient Balance{% elif rebalance.status == 400 %}Rebalancer Request Failed{% elif rebalance.status == 408 %}Rebalancer Request Timeout{% else %}{{ rebalance.status }}{% endif %}
@@ -358,8 +403,8 @@

Last 5 Payments Sent

{{ payment.value|add:"0"|intcomma }} {{ payment.fee|intcomma }} {% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %} - {% if payment.status == 2 %}{{ payment.chan_out_alias }}{% else %}N/A{% endif %} - {% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %} + {% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %} + {% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %} {% if payment.status == 2 %}Open{% else %}N/A{% endif %} {% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %} @@ -390,8 +435,8 @@

Last 5 Payments Received

{{ invoice.value|add:"0"|intcomma }} {% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}N/A{% endif %} {% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %} - {{ invoice.chan_in_alias }} - {{ invoice.chan_in }} + {% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}N/A{% endif %} + {% if invoice.state == 1 and invoice.chan_in != None %}{{ invoice.chan_in }}{% else %}N/A{% endif %} {% if invoice.keysend_preimage != None %}Yes{% else %}No{% endif %} {% endfor %} @@ -409,6 +454,7 @@

Last 10 Failed HTLCs

Chan In Alias Chan Out Alias Forward Amount + Actual Outbound Potential Fee HTLC Failure Failure Detail @@ -416,11 +462,12 @@

Last 10 Failed HTLCs

{% for failed_htlc in failed_htlcs %} {{ failed_htlc.timestamp|naturaltime }} - {{ failed_htlc.chan_id_in }} - {{ failed_htlc.chan_id_out }} - {{ failed_htlc.chan_in_alias }} - {{ failed_htlc.chan_out_alias }} + {{ failed_htlc.chan_id_in }} + {{ failed_htlc.chan_id_out }} + {% if failed_htlc.chan_in_alias == '' %}---{% else %}{{ failed_htlc.chan_in_alias }}{% endif %} + {% if failed_htlc.chan_out_alias == '' %}---{% else %}{{ failed_htlc.chan_out_alias }}{% endif %} {{ failed_htlc.amount|intcomma }} + {{ failed_htlc.chan_out_liq|intcomma }} ({{ failed_htlc.chan_out_pending|intcomma }}) {{ failed_htlc.missed_fee|intcomma }} {% if failed_htlc.wire_failure == 15 %}Temporary Channel Failure{% elif failed_htlc.wire_failure == 18 %}Unknown Next Peer{% elif failed_htlc.wire_failure == 12 %}Fee Insufficient{% else %}{{ failed_htlc.wire_failure }}{% endif %} {% if failed_htlc.failure_detail == 1 %}---{% elif failed_htlc.failure_detail == 5 %}HTLC Exceeds Max{% elif failed_htlc.failure_detail == 6 %}Insufficient Balance{% elif failed_htlc.failure_detail == 20 %}Invalid Keysend{% elif failed_htlc.failure_detail == 22 %}Circular Route{% else %}{{ failed_htlc.failure_detail }}{% endif %} @@ -431,7 +478,7 @@

Last 10 Failed HTLCs

{% endif %} {% if local_settings %}
-

Auto-Rebalancer Settings

+

Auto-Rebalancer Settings

@@ -469,29 +516,6 @@

Update Auto Rebalancer Settings

-
-

Manual Rebalancer Request

-
-
- {% csrf_token %} - - - - - - - - - -
    -
  • - {% for channel in rebalancer_form.outgoing_chan_ids %} -
  • {{ channel }}
  • - {% endfor %} -
- -
-

Connect To A Peer

@@ -544,6 +568,17 @@

Create Invoice

+
+

Update Peer Alias

+
+
+ {% csrf_token %} + + + + +
+

Update Channel Policy

@@ -565,15 +600,4 @@

Update Channel Policy

-
-

Update Peer Alias

-
-
- {% csrf_token %} - - - - -
-
{% endblock %} diff --git a/gui/templates/invoices.html b/gui/templates/invoices.html index b46fa047..0a306efd 100644 --- a/gui/templates/invoices.html +++ b/gui/templates/invoices.html @@ -25,8 +25,8 @@

Last 150 Invoices

- - + + {% endfor %} diff --git a/gui/templates/keysends.html b/gui/templates/keysends.html index 3104ab99..9f229362 100644 --- a/gui/templates/keysends.html +++ b/gui/templates/keysends.html @@ -5,19 +5,19 @@ {% if keysends %}

Received Keysends

-
Key {{ invoice.value|add:"0"|intcomma }} {% if invoice.state == 1 %}{{ invoice.amt_paid|intcomma }}{% else %}N/A{% endif %} {% if invoice.state == 0 %}Open{% elif invoice.state == 1 %}Settled{% elif invoice.state == 2 %}Canceled{% else %}{{ invoice.state }}{% endif %}{{ invoice.chan_in_alias }}{{ invoice.chan_in }}{% if invoice.state == 1 %}{% if invoice.chan_in_alias == '' %}---{% else %}{{ invoice.chan_in_alias }}{% endif %}{% else %}N/A{% endif %}{% if invoice.state == 1 and invoice.chan_in != None %}{{ invoice.chan_in }}{% else %}N/A{% endif %} {% if invoice.keysend_preimage != None %}Yes{% else %}No{% endif %}
+
- - - + + + {% for keysend in keysends %} - + - + {% endfor %}
Settle DateChannel In AliasAmountSettle DateChannel In AliasAmount Message
{{ keysend.settle_date|naturaltime }}{{ keysend.chan_in_alias }}{% if keysend.chan_in_alias == '' %}---{% else %}{{ keysend.chan_in_alias }}{% endif %} {{ keysend.amt_paid|intcomma }}{{ keysend.message }}{{ keysend.message }}{% if keysend.sender != None %} | Signed By: {% if keysend.sender_alias != None %}{{ keysend.sender_alias }}{% else %}{{ keysend.sender }}{% endif %}{% endif %}
diff --git a/gui/templates/open_list.html b/gui/templates/open_list.html index 70dfcd34..0f24d3ed 100644 --- a/gui/templates/open_list.html +++ b/gui/templates/open_list.html @@ -20,7 +20,7 @@

Suggested Open List

{% for node in open_list %} {{ node.node_pubkey }} - {{ node.alias }} + {% if node.alias == '' %}---{% else %}{{ node.alias }}{% endif %} {{ node.count }} {{ node.amount|add:"0"|intcomma }} {{ node.fees|add:"0"|intcomma }} diff --git a/gui/templates/payments.html b/gui/templates/payments.html index 48dc8173..11fd5d60 100644 --- a/gui/templates/payments.html +++ b/gui/templates/payments.html @@ -24,8 +24,8 @@

Last 150 Payments

{{ payment.value|add:"0"|intcomma }} {{ payment.fee|intcomma }} {% if payment.status == 1 %}In-Flight{% elif payment.status == 2 %}Succeeded{% elif payment.status == 3 %}Failed{% else %}{{ payment.status }}{% endif %} - {% if payment.status == 2 %}{{ payment.chan_out_alias }}{% else %}N/A{% endif %} - {% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %} + {% if payment.status == 2 %}{% if payment.chan_out_alias == '' %}---{% else %}{{ payment.chan_out_alias }}{% endif %}{% else %}N/A{% endif %} + {% if payment.status == 2 %}{{ payment.chan_out }}{% else %}N/A{% endif %} {% if payment.status == 2 %}Open{% else %}N/A{% endif %} {% if payment.keysend_preimage != None %}Yes{% else %}No{% endif %} diff --git a/gui/templates/peers.html b/gui/templates/peers.html index b6e797fa..92a2e7cd 100644 --- a/gui/templates/peers.html +++ b/gui/templates/peers.html @@ -4,10 +4,11 @@ {% load humanize %} {% if peers %}
-

Peers List

+

Peers List ({{ num_peers }})

+ @@ -16,6 +17,7 @@

Peers List

{% for peer in peers %} + diff --git a/gui/templates/pending_htlcs.html b/gui/templates/pending_htlcs.html index ab591981..8ccf498b 100644 --- a/gui/templates/pending_htlcs.html +++ b/gui/templates/pending_htlcs.html @@ -18,9 +18,9 @@

Outgoing HTLCs

{% for htlc in outgoing_htlcs %} - + - + @@ -45,9 +45,9 @@

Incoming HTLCs

{% for htlc in incoming_htlcs %} - + - + diff --git a/gui/templates/rebalancing.html b/gui/templates/rebalancing.html new file mode 100644 index 00000000..b7b7df1e --- /dev/null +++ b/gui/templates/rebalancing.html @@ -0,0 +1,196 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Rebalancing{% endblock %} +{% block content %} +{% load humanize %} +{% if channels %} +
+

Channel Rebalancing (currently scheduling {{ eligible_count }} of {{ enabled_count }} enabled channels for rebalancing)

+
+
Peer PubKeyPeer Alias Network Address Inbound Sats Sent
{{ peer.pubkey }}{% if peer.alias != '' %}{{ peer.alias }}{% else %}---{% endif %} {{ peer.address }} {{ peer.inbound }} {{ peer.sat_sent|intcomma }}
{{ htlc.chan_id }}{{ htlc.alias }}{% if htlc.alias == '' %}---{% else %}{{ htlc.alias }}{% endif %} {{ htlc.forwarding_channel }}{{ htlc.forwarding_alias }}{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %} {{ htlc.amount|intcomma }} {{ htlc.expiration_height|intcomma }} {{ htlc.hash_lock }}
{{ htlc.chan_id }}{{ htlc.alias }}{% if htlc.alias == '' %}---{% else %}{{ htlc.alias }}{% endif %} {{ htlc.forwarding_channel }}{{ htlc.forwarding_alias }}{% if htlc.forwarding_alias == '' %}---{% else %}{{ htlc.forwarding_alias }}{% endif %} {{ htlc.amount|intcomma }} {{ htlc.expiration_height|intcomma }} {{ htlc.hash_lock }}
+ + + + + + + + + + + + + + + + + + + + {% for channel in channels %} + + + + + + + + + + + + + + + + + + + + {% endfor %} +
Channel IDPeer AliasCapacityOutbound LiquidityInbound LiquidityRebal Out?Enabled?Fee RatioRebal In?Target AmtMax Cost %oTarget%iTarget%AR7-Day RateActive
{{ channel.chan_id }}{% if channel.alias == '' %}{{ channel.remote_pubkey|slice:":12" }}{% else %}{{ channel.alias }}{% endif %}{{ channel.capacity|intcomma }}{{ channel.local_balance|intcomma }} ({{ channel.percent_outbound }}%)
{% if channel.percent_inbound == 0 %}
{% elif channel.percent_outbound == 0 %}
{% else %}
{% endif %}
{{ channel.remote_balance|intcomma }} ({{ channel.percent_inbound }}%)= channel.ar_out_target and channel.auto_rebalance == False %}style="background-color: #a6dce2">True{% else %}style="background-color: #fadbd5">False{% endif %}True{% else %}style="background-color: #fadbd5">False{% endif %}{{ channel.fee_ratio }}%{% else %}style="background-color: #fadbd5">{{ channel.fee_ratio }}%{% endif %}= 1 and channel.fee_check < 100 and channel.auto_rebalance == True %}style="background-color: #a6dce2">True ({{ channel.steps }}){% else %}style="background-color: #fadbd5">False{% endif %} +
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + +
+
+
+ {% csrf_token %} + + + + +
+
{{ channel.success_rate }}% ({{ channel.success }}/{{ channel.attempts }})True{% else %}style="background-color: #fadbd5">False{% endif %}
+
+
+{% endif %} +{% if not channels %} +
+

You dont have any channels to rebalance yet.

+
+{% endif %} +{% if rebalancer %} +
+

Last 20 Rebalance Requests

+ + + + + + + + + + + + + {% for rebalance in rebalancer %} + + + + + + + + + + + + {% endfor %} +
RequestedStartStopScheduled DurationValueFee LimitTarget PPMLast Hop AliasStatus
{{ rebalance.requested|naturaltime }}N/A{% else %}title="{{ rebalance.start }}">{{ rebalance.start|naturaltime }}{% endif %} 1 %}title="{{ rebalance.stop }}">{{ rebalance.stop|naturaltime }}{% else %}>N/A{% endif %}{{ rebalance.duration }} minutes{{ rebalance.value|intcomma }}{{ rebalance.fee_limit|intcomma }}{{ rebalance.ppm|intcomma }}{% if rebalance.target_alias == '' %}None Specified{% else %}{{ rebalance.target_alias }}{% endif %}{% if rebalance.status == 0 %}Pending{% elif rebalance.status == 1 %}In-Flight{% elif rebalance.status == 2 %}Successful{% elif rebalance.status == 3 %}Timeout{% elif rebalance.status == 4 %}No Route{% elif rebalance.status == 5 %}Error{% elif rebalance.status == 6 %}Incorrect Payment Details{% elif rebalance.status == 7 %}Insufficient Balance{% elif rebalance.status == 400 %}Rebalancer Request Failed{% elif rebalance.status == 408 %}Rebalancer Request Timeout{% else %}{{ rebalance.status }}{% endif %}
+
+{% endif %} +{% if not rebalancer %} +
+

You dont have any rebalancer requests yet.

+
+{% endif %} +{% if local_settings %} +
+

Auto-Rebalancer Settings

+ + + + + + {% for settings in local_settings %} + + + + + {% endfor %} +
KeyValue
{{ settings.key }}{{ settings.value|intcomma }}
+
+{% endif %} +
+

Update Auto Rebalancer Settings

+
+
+ {% csrf_token %} + + + + + + + + + + + + + + + +
+
+
+{% if channels %} +
+

Manual Rebalancer Request

+
+
+ {% csrf_token %} + + + + + + + + + +
    +
  • + {% for channel in rebalancer_form.outgoing_chan_ids %} +
  • {{ channel }}
  • + {% endfor %} +
+
+
+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/resolutions.html b/gui/templates/resolutions.html new file mode 100644 index 00000000..2a6afdbc --- /dev/null +++ b/gui/templates/resolutions.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block title %} {{ block.super }} - Resolutions{% endblock %} +{% block content %} +{% load humanize %} +{% if resolutions %} +
+

Resolutions For Channel: {{ chan_id }}

+ + + + + + + + + + {% for resolution in resolutions %} + + + + + + + + + {% endfor %} +
Channel IDResolution TypeOutcomeOutpointAmountSweep TX
{{ resolution.chan_id.chan_id }}{% if resolution.resolution_type == 0 %}Unknown{% elif resolution.resolution_type == 1 %}Anchor{% elif resolution.resolution_type == 2 %}Incoming HTLC{% elif resolution.resolution_type == 3 %}Outgoing HTLC{% elif resolution.resolution_type == 4 %}Commit{% else %}{{ resolution.resolution_type }}{% endif %}{% if resolution.outcome == 0 %}Unknown{% elif resolution.outcome == 1 %}Claimed{% elif resolution.outcome == 2 %}Unclaimed{% elif resolution.outcome == 3 %}Abandoned{% elif resolution.outcome == 4 %}First Stage{% elif resolution.outcome == 5 %}Timeout{% else %}{{ resolution.outcome }}{% endif %}{{ resolution.outpoint_tx }}:{{ resolution.outpoint_index }}{{ resolution.amount_sat|intcomma }}{{ resolution.sweep_txid }}
+
+{% endif %} +{% if not resolutions %} +
+

No resolutions were found for this channel!

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/gui/templates/route.html b/gui/templates/route.html index dce393c5..e584834d 100644 --- a/gui/templates/route.html +++ b/gui/templates/route.html @@ -19,11 +19,11 @@

Route For Payment: {{ payment_hash }}

{% for hop in route %} {{ hop.step }} - {{ hop.amt }} - {{ hop.fee }} - {{ hop.ppm }} - {{ hop.cost_to }} - {{ hop.alias }} + {{ hop.amt|intcomma }} + {{ hop.fee|intcomma }} + {{ hop.ppm|intcomma }} + {{ hop.cost_to|intcomma }} + {% if hop.alias == '' %}---{% else %}{{ hop.alias }}{% endif %} {{ hop.chan_id }} {{ hop.chan_capacity|intcomma }} diff --git a/gui/urls.py b/gui/urls.py index 714725c0..3d57b4e6 100644 --- a/gui/urls.py +++ b/gui/urls.py @@ -22,11 +22,14 @@ path('peers', views.peers, name='peers'), path('balances', views.balances, name='balances'), path('closures', views.closures, name='closures'), + path('resolutions', views.resolutions, name='resolutions'), + path('channel', views.channel, name='channel'), path('pending_htlcs', views.pending_htlcs, name='pending-htlcs'), path('failed_htlcs', views.failed_htlcs, name='failed-htlcs'), path('payments', views.payments, name='payments'), path('invoices', views.invoices, name='invoices'), path('forwards', views.forwards, name='forwards'), + path('rebalancing', views.rebalancing, name='rebalancing'), path('openchannel/', views.open_channel_form, name='open-channel-form'), path('closechannel/', views.close_channel_form, name='close-channel-form'), path('connectpeer/', views.connect_peer_form, name='connect-peer-form'), diff --git a/gui/views.py b/gui/views.py index c1d8046d..15caa40f 100644 --- a/gui/views.py +++ b/gui/views.py @@ -1,6 +1,6 @@ from django.contrib import messages from django.shortcuts import get_object_or_404, render, redirect -from django.db.models import Sum, IntegerField, Count, F +from django.db.models import Sum, IntegerField, Count, F, Q from django.db.models.functions import Round from django.contrib.auth.decorators import login_required from django.conf import settings @@ -9,8 +9,8 @@ from rest_framework.response import Response from rest_framework.decorators import api_view from .forms import OpenChannelForm, CloseChannelForm, ConnectPeerForm, AddInvoiceForm, RebalancerForm, ChanPolicyForm, UpdateChannel, UpdateSetting, AutoRebalanceForm -from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, PendingHTLCs, FailedHTLCs, Autopilot -from .serializers import ConnectPeerSerializer, FailedHTLCSerializer, LocalSettingsSerializer, OpenChannelSerializer, CloseChannelSerializer, AddInvoiceSerializer, PaymentHopsSerializer, PaymentSerializer, InvoiceSerializer, ForwardSerializer, ChannelSerializer, PendingHTLCSerializer, RebalancerSerializer, UpdateAliasSerializer, PeerSerializer, OnchainSerializer, PendingHTLCs, FailedHTLCs +from .models import Payments, PaymentHops, Invoices, Forwards, Channels, Rebalancer, LocalSettings, Peers, Onchain, Closures, Resolutions, PendingHTLCs, FailedHTLCs, Autopilot +from .serializers import ConnectPeerSerializer, FailedHTLCSerializer, LocalSettingsSerializer, OpenChannelSerializer, CloseChannelSerializer, AddInvoiceSerializer, PaymentHopsSerializer, PaymentSerializer, InvoiceSerializer, ForwardSerializer, ChannelSerializer, PendingHTLCSerializer, RebalancerSerializer, UpdateAliasSerializer, PeerSerializer, OnchainSerializer from .lnd_deps import lightning_pb2 as ln from .lnd_deps import lightning_pb2_grpc as lnrpc from gui.lnd_deps import router_pb2 as lnr @@ -18,7 +18,7 @@ from .lnd_deps.lnd_connect import lnd_connect from lndg.settings import LND_NETWORK, LND_DIR_PATH from os import path -from pandas import DataFrame +from pandas import DataFrame, merge def graph_links(): if LocalSettings.objects.filter(key='GUI-GraphLinks').exists(): @@ -69,7 +69,7 @@ def home(request): forwards_df_in_count = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_in', as_index=True).count() forwards_df_out_count = DataFrame() if forwards_df.empty else forwards_df.groupby('chan_id_out', as_index=True).count() #Get current active channels - active_channels = Channels.objects.filter(is_active=True, is_open=True).annotate(outbound_percent=(Sum('local_balance')*1000)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*1000)/Sum('capacity')).order_by('outbound_percent') + active_channels = Channels.objects.filter(is_active=True, is_open=True, private=False).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*1000)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*1000)/Sum('capacity')).order_by('outbound_percent') total_capacity = 0 if active_channels.count() == 0 else active_channels.aggregate(Sum('capacity'))['capacity__sum'] total_inbound = 0 if total_capacity == 0 else active_channels.aggregate(Sum('remote_balance'))['remote_balance__sum'] total_outbound = 0 if total_capacity == 0 else active_channels.aggregate(Sum('local_balance'))['local_balance__sum'] @@ -86,17 +86,16 @@ def home(request): payments_7day = payments.filter(status=2).filter(creation_date__gte=filter_7day) payments_7day_amt = 0 if payments_7day.count() == 0 else payments_7day.aggregate(Sum('value'))['value__sum'] total_7day_fees = 0 if payments_7day.count() == 0 else payments_7day.aggregate(Sum('fee'))['fee__sum'] - pending_htlcs = PendingHTLCs.objects.all() - pending_htlc_count = pending_htlcs.count() - pending_outbound = 0 if pending_htlcs.filter(incoming=False).count() == 0 else pending_htlcs.filter(incoming=False).aggregate(Sum('amount'))['amount__sum'] + pending_htlc_count = Channels.objects.filter(is_open=True).aggregate(Sum('htlc_count'))['htlc_count__sum'] if Channels.objects.filter(is_open=True).exists() else 0 + pending_outbound = Channels.objects.filter(is_open=True).aggregate(Sum('pending_outbound'))['pending_outbound__sum'] if Channels.objects.filter(is_open=True).exists() else 0 detailed_active_channels = [] for channel in active_channels: detailed_channel = {} detailed_channel['remote_pubkey'] = channel.remote_pubkey detailed_channel['chan_id'] = channel.chan_id detailed_channel['capacity'] = channel.capacity - detailed_channel['local_balance'] = channel.local_balance - detailed_channel['remote_balance'] = channel.remote_balance + detailed_channel['local_balance'] = channel.local_balance + channel.pending_outbound + detailed_channel['remote_balance'] = channel.remote_balance + channel.pending_inbound detailed_channel['unsettled_balance'] = channel.unsettled_balance detailed_channel['initiator'] = channel.initiator detailed_channel['alias'] = channel.alias @@ -119,21 +118,23 @@ def home(request): detailed_channel['routed_out_7day'] = forwards_df_out_7d_count.loc[channel.chan_id].amt_out_msat if (forwards_df_out_7d_count.index == channel.chan_id).any() else 0 detailed_channel['amt_routed_in_7day'] = int(forwards_df_in_7d_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_in_7d_sum.index == channel.chan_id).any() else 0 detailed_channel['amt_routed_out_7day'] = int(forwards_df_out_7d_sum.loc[channel.chan_id].amt_out_msat//10000000)/100 if (forwards_df_out_7d_sum.index == channel.chan_id).any() else 0 - detailed_channel['htlc_count'] = pending_htlcs.filter(chan_id=channel.chan_id).count() + detailed_channel['htlc_count'] = channel.htlc_count detailed_channel['auto_rebalance'] = channel.auto_rebalance detailed_channel['ar_in_target'] = channel.ar_in_target detailed_active_channels.append(detailed_channel) #Get current inactive channels - inactive_channels = Channels.objects.filter(is_active=False, is_open=True).annotate(outbound_percent=(Sum('local_balance')*100)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*100)/Sum('capacity')).order_by('outbound_percent') + inactive_channels = Channels.objects.filter(is_active=False, is_open=True, private=False).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity')).order_by('outbound_percent') + private_channels = Channels.objects.filter(is_open=True, private=True).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity')).order_by('outbound_percent') inactive_outbound = 0 if inactive_channels.count() == 0 else inactive_channels.aggregate(Sum('local_balance'))['local_balance__sum'] - sum_outbound = total_outbound + pending_outbound + inactive_outbound + private_outbound = 0 if private_channels.count() == 0 else private_channels.aggregate(Sum('local_balance'))['local_balance__sum'] + sum_outbound = total_outbound + pending_outbound + inactive_outbound + private_outbound onchain_txs = Onchain.objects.all() onchain_costs = 0 if onchain_txs.count() == 0 else onchain_txs.aggregate(Sum('fee'))['fee__sum'] onchain_costs_7day = 0 if onchain_txs.filter(time_stamp__gte=filter_7day).count() == 0 else onchain_txs.filter(time_stamp__gte=filter_7day).aggregate(Sum('fee'))['fee__sum'] total_costs = total_fees + onchain_costs total_costs_7day = total_7day_fees + onchain_costs_7day #Get list of recent rebalance requests - rebalances = Rebalancer.objects.all().order_by('-requested') + rebalances = Rebalancer.objects.all().annotate(ppm=(Sum('fee_limit')*1000000)/Sum('value')).order_by('-id') total_channels = node_info.num_active_channels + node_info.num_inactive_channels local_settings = LocalSettings.objects.filter(key__contains='AR-') try: @@ -174,22 +175,22 @@ def home(request): 'unsettled': total_unsettled, 'limbo_balance': limbo_balance, 'inactive_channels': inactive_channels, + 'private_channels': private_channels, 'pending_open': pending_open, 'pending_closed': pending_closed, 'pending_force_closed': pending_force_closed, 'waiting_for_close': waiting_for_close, 'rebalances': rebalances[:12], - 'rebalancer_form': RebalancerForm, 'chan_policy_form': ChanPolicyForm, 'local_settings': local_settings, 'pending_htlc_count': pending_htlc_count, - 'failed_htlcs': FailedHTLCs.objects.all().order_by('-timestamp')[:10], + 'failed_htlcs': FailedHTLCs.objects.all().order_by('-id')[:10], 'payments_ppm': 0 if total_sent == 0 else int((total_fees/total_sent)*1000000), 'routed_ppm': 0 if total_value_forwards == 0 else int((total_earned/total_value_forwards)*1000000), '7day_routed_ppm': 0 if routed_7day_amt == 0 else int((total_earned_7day/routed_7day_amt)*1000000), '7day_payments_ppm': 0 if payments_7day_amt == 0 else int((total_7day_fees/payments_7day_amt)*1000000), 'liq_ratio': 0 if total_outbound == 0 else int((total_inbound/sum_outbound)*100), - 'eligible_count': Channels.objects.filter(is_active=True, is_open=True, auto_rebalance=True).annotate(inbound_can=(Sum('remote_balance')*100)/Sum('capacity')).annotate(fee_ratio=(Sum('remote_fee_rate')*100)/Sum('local_fee_rate')).filter(inbound_can__gte=F('ar_in_target'), fee_ratio__lte=F('ar_max_cost')).count(), + 'eligible_count': Channels.objects.filter(is_active=True, is_open=True, private=False, auto_rebalance=True).annotate(inbound_can=((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity')).annotate(fee_ratio=(Sum('remote_fee_rate')*100)/Sum('local_fee_rate')).filter(inbound_can__gte=F('ar_in_target'), fee_ratio__lte=F('ar_max_cost')).count(), 'enabled_count': Channels.objects.filter(is_open=True, auto_rebalance=True).count(), 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', 'graph_links': graph_links(), @@ -206,9 +207,9 @@ def channels(request): filter_7day = datetime.now() - timedelta(days=7) filter_30day = datetime.now() - timedelta(days=30) forwards = Forwards.objects.filter(forward_date__gte=filter_30day) - payments = Payments.objects.filter(status=2).filter(creation_date__gte=filter_30day).filter(payment_hash__in=Invoices.objects.filter(state=1).filter(settle_date__gte=filter_30day).values_list('r_hash')) + payments = Payments.objects.filter(status=2).filter(creation_date__gte=filter_30day).filter(rebal_chan__isnull=False) invoices = Invoices.objects.filter(state=1).filter(settle_date__gte=filter_30day).filter(r_hash__in=payments.values_list('payment_hash')) - channels = Channels.objects.filter(is_open=True) + channels = Channels.objects.filter(is_open=True, private=False) channels_df = DataFrame.from_records(channels.values()) if channels_df.shape[0] > 0: forwards_df_30d = DataFrame.from_records(forwards.values()) @@ -236,6 +237,8 @@ def channels(request): invoice_hashes_7d = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True)['r_hash'].apply(list) invoice_hashes_30d = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True)['r_hash'].apply(list) channels_df['capacity'] = channels_df.apply(lambda row: round(row.capacity/1000000, 1), axis=1) + channels_df['local_balance'] = channels_df.apply(lambda row: row.local_balance + row.pending_outbound, axis=1) + channels_df['remote_balance'] = channels_df.apply(lambda row: row.remote_balance + row.pending_inbound, axis=1) channels_df['routed_in_7day'] = channels_df.apply(lambda row: forwards_df_in_7d_count.loc[row.chan_id].amt_out_msat if (forwards_df_in_7d_count.index == row.chan_id).any() else 0, axis=1) channels_df['routed_out_7day'] = channels_df.apply(lambda row: forwards_df_out_7d_count.loc[row.chan_id].amt_out_msat if (forwards_df_out_7d_count.index == row.chan_id).any() else 0, axis=1) channels_df['routed_in_30day'] = channels_df.apply(lambda row: forwards_df_in_30d_count.loc[row.chan_id].amt_out_msat if (forwards_df_in_30d_count.index == row.chan_id).any() else 0, axis=1) @@ -260,11 +263,24 @@ def channels(request): channels_df['costs_30day'] = channels_df.apply(lambda row: 0 if row['rebal_in_30day'] == 0 else int(payments_df_30d.set_index('payment_hash', inplace=False).loc[invoice_hashes_30d[row.chan_id] if invoice_hashes_30d.empty == False and (invoice_hashes_30d.index == row.chan_id).any() else []]['fee'].sum()), axis=1) channels_df['profits_7day'] = channels_df.apply(lambda row: row['revenue_7day'] - row['costs_7day'], axis=1) channels_df['profits_30day'] = channels_df.apply(lambda row: row['revenue_30day'] - row['costs_30day'], axis=1) - channels_df['open_block'] = channels_df.apply(lambda row: row.chan_id>>40, axis=1) + channels_df['open_block'] = channels_df.apply(lambda row: int(row.chan_id)>>40, axis=1) apy_7day = round((channels_df['profits_7day'].sum()*5214.2857)/channels_df['local_balance'].sum(), 2) apy_30day = round((channels_df['profits_30day'].sum()*1216.6667)/channels_df['local_balance'].sum(), 2) active_updates = channels_df['num_updates'].sum() channels_df['updates'] = channels_df.apply(lambda row: 0 if active_updates == 0 else int(round((row['num_updates']/active_updates)*100, 0)), axis=1) + node_outbound = channels_df['local_balance'].sum() + node_capacity = channels_df['capacity'].sum() + if node_capacity > 0: + outbound_ratio = node_outbound/node_capacity + channels_df['apy_7day'] = channels_df.apply(lambda row: round((row['profits_7day']*5214.2857)/(row['capacity']*outbound_ratio), 2), axis=1) + channels_df['apy_30day'] = channels_df.apply(lambda row: round((row['profits_30day']*1216.6667)/(row['capacity']*outbound_ratio), 2), axis=1) + else: + channels_df['attempts_30day'] = 0 + channels_df['success_30day'] = 0 + channels_df['success_rate_30day'] = 0 + channels_df['attempts_7day'] = 0 + channels_df['success_7day'] = 0 + channels_df['success_rate_7day'] = 0 context = { 'channels': channels_df.to_dict(orient='records'), 'apy_7day': apy_7day, @@ -281,13 +297,18 @@ def channels(request): def fees(request): if request.method == 'GET': filter_7day = datetime.now() - timedelta(days=7) - forwards = Forwards.objects.filter(forward_date__gte=filter_7day, amt_out_msat__gte=1000000) channels = Channels.objects.filter(is_open=True) channels_df = DataFrame.from_records(channels.values()) if channels_df.shape[0] > 0: + failed_htlc_df = DataFrame.from_records(FailedHTLCs.objects.filter(timestamp__gte=filter_7day).order_by('-id').values()) + if failed_htlc_df.shape[0] > 0: + 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']] + forwards = Forwards.objects.filter(forward_date__gte=filter_7day, amt_out_msat__gte=1000000) forwards_df_7d = DataFrame.from_records(forwards.values()) forwards_df_in_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_in', as_index=True).sum() forwards_df_out_7d_sum = DataFrame() if forwards_df_7d.empty else forwards_df_7d.groupby('chan_id_out', as_index=True).sum() + channels_df['local_balance'] = channels_df.apply(lambda row: row.local_balance + row.pending_outbound, axis=1) + channels_df['remote_balance'] = channels_df.apply(lambda row: row.remote_balance + row.pending_inbound, axis=1) channels_df['in_percent'] = channels_df.apply(lambda row: int(round((row['remote_balance']/row['capacity'])*100, 0)), axis=1) channels_df['out_percent'] = channels_df.apply(lambda row: int(round((row['local_balance']/row['capacity'])*100, 0)), axis=1) 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) @@ -296,7 +317,8 @@ def fees(request): channels_df['revenue_7day'] = channels_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) channels_df['revenue_assist_7day'] = channels_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) channels_df['out_rate'] = channels_df.apply(lambda row: int((row['revenue_7day']/row['amt_routed_out_7day'])*1000000) if row['amt_routed_out_7day'] > 0 else 0, axis=1) - payments = Payments.objects.filter(status=2).filter(creation_date__gte=filter_7day).filter(payment_hash__in=Invoices.objects.filter(state=1).filter(settle_date__gte=filter_7day).values_list('r_hash')) + channels_df['failed_out_7day'] = 0 if failed_htlc_df.empty else channels_df.apply(lambda row: len(failed_htlc_df[failed_htlc_df['chan_id_out']==row.chan_id]), axis=1) + payments = Payments.objects.filter(status=2).filter(creation_date__gte=filter_7day).filter(rebal_chan__isnull=False) invoices = Invoices.objects.filter(state=1).filter(settle_date__gte=filter_7day).filter(r_hash__in=payments.values_list('payment_hash')) payments_df_7d = DataFrame.from_records(payments.filter(creation_date__gte=filter_7day).values()) invoices_df_7d = DataFrame.from_records(invoices.filter(settle_date__gte=filter_7day).values()) @@ -306,7 +328,9 @@ def fees(request): channels_df['costs_7day'] = channels_df.apply(lambda row: 0 if row['amt_rebal_in_7day'] == 0 else int(payments_df_7d.set_index('payment_hash', inplace=False).loc[invoice_hashes_7d[row.chan_id] if invoice_hashes_7d.empty == False and (invoice_hashes_7d.index == row.chan_id).any() else []]['fee'].sum()), axis=1) channels_df['rebal_ppm'] = channels_df.apply(lambda row: int((row['costs_7day']/row['amt_rebal_in_7day'])*1000000) if row['amt_rebal_in_7day'] > 0 else 0, axis=1) channels_df['max_suggestion'] = channels_df.apply(lambda row: int((row['out_rate'] if row['out_rate'] > 0 else row['local_fee_rate'])*1.15) if row['in_percent'] > 25 else int(row['local_fee_rate']), axis=1) + channels_df['max_suggestion'] = channels_df.apply(lambda row: row['local_fee_rate']+25 if row['max_suggestion'] > (row['local_fee_rate']+25) else row['max_suggestion'], axis=1) channels_df['min_suggestion'] = channels_df.apply(lambda row: int((row['out_rate'] if row['out_rate'] > 0 else row['local_fee_rate'])*0.75) if row['out_percent'] > 25 else int(row['local_fee_rate']), axis=1) + channels_df['min_suggestion'] = channels_df.apply(lambda row: row['local_fee_rate']-50 if row['min_suggestion'] < (row['local_fee_rate']-50) else row['min_suggestion'], axis=1) channels_df['assisted_ratio'] = channels_df.apply(lambda row: round((row['revenue_assist_7day'] if row['revenue_7day'] == 0 else row['revenue_assist_7day']/row['revenue_7day']), 2), axis=1) channels_df['profit_margin'] = channels_df.apply(lambda row: row['out_rate']*((100-row['ar_max_cost'])/100), axis=1) channels_df['adjusted_out_rate'] = channels_df.apply(lambda row: int(row['out_rate']+row['net_routed_7day']*row['assisted_ratio']), axis=1) @@ -332,12 +356,14 @@ def fees(request): @login_required(login_url='/lndg-admin/login/?next=/') def advanced(request): if request.method == 'GET': - channels = Channels.objects.filter(is_open=True).annotate(outbound_percent=(Sum('local_balance')*1000)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*1000)/Sum('capacity')).order_by('-is_active', 'outbound_percent') + channels = Channels.objects.filter(is_open=True).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*1000)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*1000)/Sum('capacity')).order_by('-is_active', 'outbound_percent') channels_df = DataFrame.from_records(channels.values()) if channels_df.shape[0] > 0: channels_df['out_percent'] = channels_df.apply(lambda row: int(round(row['outbound_percent']/10, 0)), axis=1) channels_df['in_percent'] = channels_df.apply(lambda row: int(round(row['inbound_percent']/10, 0)), axis=1) - channels_df['fee_ratio'] = channels_df.apply(lambda row: 0 if row['local_fee_rate'] == 0 else int(round(((row['remote_fee_rate']/row['local_fee_rate'])*1000)/10, 0)), axis=1) + channels_df['local_balance'] = channels_df.apply(lambda row: row.local_balance + row.pending_outbound, axis=1) + channels_df['remote_balance'] = channels_df.apply(lambda row: row.remote_balance + row.pending_inbound, axis=1) + channels_df['fee_ratio'] = channels_df.apply(lambda row: 100 if row['local_fee_rate'] == 0 else int(round(((row['remote_fee_rate']/row['local_fee_rate'])*1000)/10, 0)), axis=1) context = { 'channels': channels_df.to_dict(orient='records'), 'local_settings': LocalSettings.objects.all(), @@ -364,8 +390,10 @@ def route(request): @login_required(login_url='/lndg-admin/login/?next=/') def peers(request): if request.method == 'GET': + peers = Peers.objects.filter(connected=True) context = { - 'peers': Peers.objects.filter(connected=True), + 'peers': peers, + 'num_peers': len(peers), 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', 'graph_links': graph_links() } @@ -390,16 +418,351 @@ def balances(request): @login_required(login_url='/lndg-admin/login/?next=/') def closures(request): if request.method == 'GET': - stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + closures_df = DataFrame.from_records(Closures.objects.all().values()) + channels_df = DataFrame.from_records(Channels.objects.all().values('chan_id', 'alias')) + merged = merge(closures_df, channels_df, on='chan_id', how='left') + merged['alias'] = merged['alias'].fillna('') context = { - 'closures': stub.ClosedChannels(ln.ClosedChannelsRequest()).channels[::-1], + 'closures': merged.sort_values(by=['close_height'], ascending=False).to_dict(orient='records'), 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', - 'network_links': network_links() + 'network_links': network_links(), + 'graph_links': graph_links() } return render(request, 'closures.html', context) else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') +def resolutions(request): + if request.method == 'GET': + chan_id = request.GET.urlencode()[1:] + context = { + 'chan_id': chan_id, + 'resolutions': Resolutions.objects.filter(chan_id=chan_id), + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'network_links': network_links() + } + return render(request, 'resolutions.html', context) + else: + return redirect('home') + +@login_required(login_url='/lndg-admin/login/?next=/') +def channel(request): + if request.method == 'GET': + chan_id = request.GET.urlencode()[1:] + if Channels.objects.filter(chan_id=chan_id).exists(): + filter_1day = datetime.now() - timedelta(days=1) + filter_7day = datetime.now() - timedelta(days=7) + filter_30day = datetime.now() - timedelta(days=30) + forwards_df = DataFrame.from_records(Forwards.objects.filter(Q(chan_id_in=chan_id) | Q(chan_id_out=chan_id)).values()) + payments_df = DataFrame.from_records(Payments.objects.filter(status=2).filter(chan_out=chan_id).filter(rebal_chan__isnull=False).values()) + invoices_df = DataFrame.from_records(Invoices.objects.filter(state=1).filter(chan_in=chan_id).filter(r_hash__in=Payments.objects.filter(status=2).filter(rebal_chan=chan_id)).values()) + channels_df = DataFrame.from_records(Channels.objects.filter(is_open=True).values()) + node_outbound = channels_df['local_balance'].sum() + node_capacity = channels_df['capacity'].sum() + channels_df = DataFrame.from_records(Channels.objects.filter(chan_id=chan_id).values()) + rebalancer_df = DataFrame.from_records(Rebalancer.objects.filter(last_hop_pubkey=channels_df['remote_pubkey'][0]).annotate(ppm=(Sum('fee_limit')*1000000)/Sum('value')).order_by('-id').values()) + failed_htlc_df = DataFrame.from_records(FailedHTLCs.objects.filter(Q(chan_id_in=chan_id) | Q(chan_id_out=chan_id)).order_by('-id').values()) + channels_df['local_balance'] = channels_df['local_balance'] + channels_df['pending_outbound'] + channels_df['remote_balance'] = channels_df['remote_balance'] + channels_df['pending_inbound'] + channels_df['in_percent'] = int(round((channels_df['remote_balance']/channels_df['capacity'])*100, 0)) + channels_df['out_percent'] = int(round((channels_df['local_balance']/channels_df['capacity'])*100, 0)) + channels_df['open_block'] = int(channels_df.chan_id)>>40 + channels_df['routed_in'] = 0 + channels_df['routed_in_30day'] = 0 + channels_df['routed_in_7day'] = 0 + channels_df['routed_in_1day'] = 0 + channels_df['routed_out'] = 0 + channels_df['routed_out_30day'] = 0 + channels_df['routed_out_7day'] = 0 + channels_df['routed_out_1day'] = 0 + channels_df['amt_routed_in'] = 0 + channels_df['amt_routed_in_30day'] = 0 + channels_df['amt_routed_in_7day'] = 0 + channels_df['amt_routed_in_1day'] = 0 + channels_df['amt_routed_out'] = 0 + channels_df['amt_routed_out_30day'] = 0 + channels_df['amt_routed_out_7day'] = 0 + channels_df['amt_routed_out_1day'] = 0 + channels_df['average_in'] = 0 + channels_df['average_in_30day'] = 0 + channels_df['average_in_7day'] = 0 + channels_df['average_in_1day'] = 0 + channels_df['average_out'] = 0 + channels_df['average_out_30day'] = 0 + channels_df['average_out_7day'] = 0 + channels_df['average_out_1day'] = 0 + channels_df['revenue'] = 0 + channels_df['revenue_30day'] = 0 + channels_df['revenue_7day'] = 0 + channels_df['revenue_1day'] = 0 + channels_df['revenue_assist'] = 0 + channels_df['revenue_assist_30day'] = 0 + channels_df['revenue_assist_7day'] = 0 + channels_df['revenue_assist_1day'] = 0 + channels_df['rebal_out'] = 0 + channels_df['rebal_out_30day'] = 0 + channels_df['rebal_out_7day'] = 0 + channels_df['rebal_out_1day'] = 0 + channels_df['amt_rebal_out'] = 0 + channels_df['amt_rebal_out_30day'] = 0 + channels_df['amt_rebal_out_7day'] = 0 + channels_df['amt_rebal_out_1day'] = 0 + channels_df['rebal_in'] = 0 + channels_df['rebal_in_30day'] = 0 + channels_df['rebal_in_7day'] = 0 + channels_df['rebal_in_1day'] = 0 + channels_df['amt_rebal_in'] = 0 + channels_df['amt_rebal_in_30day'] = 0 + channels_df['amt_rebal_in_7day'] = 0 + channels_df['amt_rebal_in_1day'] = 0 + channels_df['costs'] = 0 + channels_df['costs_30day'] = 0 + channels_df['costs_7day'] = 0 + channels_df['costs_1day'] = 0 + channels_df['attempts'] = 0 + channels_df['attempts_30day'] = 0 + channels_df['attempts_7day'] = 0 + channels_df['attempts_1day'] = 0 + channels_df['success'] = 0 + channels_df['success_30day'] = 0 + channels_df['success_7day'] = 0 + channels_df['success_1day'] = 0 + channels_df['success_rate'] = 0 + channels_df['success_rate_30day'] = 0 + channels_df['success_rate_7day'] = 0 + channels_df['success_rate_1day'] = 0 + channels_df['failed_out'] = 0 + channels_df['failed_out_30day'] = 0 + channels_df['failed_out_7day'] = 0 + channels_df['failed_out_1day'] = 0 + start_date = None + if failed_htlc_df.shape[0]> 0: + channels_df['failed_out'] = len(failed_htlc_df[failed_htlc_df['chan_id_out']==chan_id][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']]) + failed_htlc_df_30d = failed_htlc_df.loc[failed_htlc_df['timestamp'] >= filter_30day] + if failed_htlc_df_30d.shape[0]> 0: + channels_df['failed_out_30day'] = len(failed_htlc_df_30d[failed_htlc_df_30d['chan_id_out']==chan_id][failed_htlc_df_30d['wire_failure']==15][failed_htlc_df_30d['failure_detail']==6][failed_htlc_df_30d['amount']>failed_htlc_df_30d['chan_out_liq']+failed_htlc_df_30d['chan_out_pending']]) + failed_htlc_df_7d = failed_htlc_df_30d.loc[failed_htlc_df_30d['timestamp'] >= filter_7day] + if failed_htlc_df_7d.shape[0]> 0: + channels_df['failed_out_7day'] = len(failed_htlc_df_7d[failed_htlc_df_7d['chan_id_out']==chan_id][failed_htlc_df_7d['wire_failure']==15][failed_htlc_df_7d['failure_detail']==6][failed_htlc_df_7d['amount']>failed_htlc_df_7d['chan_out_liq']+failed_htlc_df_7d['chan_out_pending']]) + failed_htlc_df_1d = failed_htlc_df_7d.loc[failed_htlc_df_7d['timestamp'] >= filter_1day] + if failed_htlc_df_1d.shape[0]> 0: + channels_df['failed_out_1day'] = len(failed_htlc_df_1d[failed_htlc_df_1d['chan_id_out']==chan_id][failed_htlc_df_1d['wire_failure']==15][failed_htlc_df_1d['failure_detail']==6][failed_htlc_df_1d['amount']>failed_htlc_df_1d['chan_out_liq']+failed_htlc_df_1d['chan_out_pending']]) + if rebalancer_df.shape[0]> 0: + channels_df['attempts'] = len(rebalancer_df[rebalancer_df['status']>=2][rebalancer_df['status']<400]) + channels_df['success'] = len(rebalancer_df[rebalancer_df['status']==2]) + channels_df['success_rate'] = 0 if channels_df['attempts'][0] == 0 else int((channels_df['success']/channels_df['attempts'])*100) + rebalancer_df_30d = rebalancer_df.loc[rebalancer_df['stop'] >= filter_30day] + if rebalancer_df_30d.shape[0]> 0: + channels_df['attempts_30day'] = len(rebalancer_df_30d[rebalancer_df_30d['status']>=2][rebalancer_df_30d['status']<400]) + channels_df['success_30day'] = len(rebalancer_df_30d[rebalancer_df_30d['status']==2]) + channels_df['success_rate_30day'] = 0 if channels_df['attempts_30day'][0] == 0 else int((channels_df['success_30day']/channels_df['attempts_30day'])*100) + rebalancer_df_7d = rebalancer_df_30d.loc[rebalancer_df_30d['stop'] >= filter_7day] + if rebalancer_df_7d.shape[0]> 0: + channels_df['attempts_7day'] = len(rebalancer_df_7d[rebalancer_df_7d['status']>=2][rebalancer_df_7d['status']<400]) + channels_df['success_7day'] = len(rebalancer_df_7d[rebalancer_df_7d['status']==2]) + channels_df['success_rate_7day'] = 0 if channels_df['attempts_7day'][0] == 0 else int((channels_df['success_7day']/channels_df['attempts_7day'])*100) + rebalancer_df_1d = rebalancer_df_7d.loc[rebalancer_df_7d['stop'] >= filter_1day] + if rebalancer_df_1d.shape[0]> 0: + channels_df['attempts_1day'] = len(rebalancer_df_1d[rebalancer_df_1d['status']>=2][rebalancer_df_1d['status']<400]) + channels_df['success_1day'] = len(rebalancer_df_1d[rebalancer_df_1d['status']==2]) + channels_df['success_rate_1day'] = 0 if channels_df['attempts_1day'][0] == 0 else int((channels_df['success_1day']/channels_df['attempts_1day'])*100) + if forwards_df.shape[0]> 0: + forwards_in_df = forwards_df[forwards_df['chan_id_in'] == chan_id] + forwards_out_df = forwards_df[forwards_df['chan_id_out'] == chan_id] + forwards_df['amt_in'] = (forwards_df['amt_in_msat']/1000).astype(int) + forwards_df['amt_out'] = (forwards_df['amt_out_msat']/1000).astype(int) + forwards_df['ppm'] = (forwards_df['fee']/(forwards_df['amt_out']/1000000)).astype(int) + else: + forwards_in_df = DataFrame() + forwards_out_df = DataFrame() + if forwards_in_df.shape[0]> 0: + forwards_in_df_count = forwards_in_df.groupby('chan_id_in', as_index=True).count() + forwards_in_df_sum = forwards_in_df.groupby('chan_id_in', as_index=True).sum() + channels_df['routed_in'] = forwards_in_df_count.loc[chan_id].amt_out_msat + channels_df['amt_routed_in'] = int(forwards_in_df_sum.loc[chan_id].amt_out_msat/1000) + channels_df['average_in'] = 0 if channels_df['routed_in'][0] == 0 else int(channels_df['amt_routed_in']/channels_df['routed_in']) + channels_df['revenue_assist'] = int(forwards_in_df_sum.loc[chan_id].fee) if forwards_in_df_sum.empty == False else 0 + forwards_in_df_30d = forwards_in_df.loc[forwards_in_df['forward_date'] >= filter_30day] + if forwards_in_df_30d.shape[0] > 0: + forwards_in_df_30d_count = forwards_in_df_30d.groupby('chan_id_in', as_index=True).count() + forwards_in_df_30d_sum = forwards_in_df_30d.groupby('chan_id_in', as_index=True).sum() + channels_df['routed_in_30day'] = forwards_in_df_30d_count.loc[chan_id].amt_out_msat + channels_df['amt_routed_in_30day'] = int(forwards_in_df_30d_sum.loc[chan_id].amt_out_msat/1000) + channels_df['average_in_30day'] = 0 if channels_df['routed_in_30day'][0] == 0 else int(channels_df['amt_routed_in_30day']/channels_df['routed_in_30day']) + channels_df['revenue_assist_30day'] = int(forwards_in_df_30d_sum.loc[chan_id].fee) if forwards_in_df_30d_sum.empty == False else 0 + forwards_in_df_7d = forwards_in_df_30d.loc[forwards_in_df_30d['forward_date'] >= filter_7day] + if forwards_in_df_7d.shape[0] > 0: + forwards_in_df_7d_count = forwards_in_df_7d.groupby('chan_id_in', as_index=True).count() + forwards_in_df_7d_sum = forwards_in_df_7d.groupby('chan_id_in', as_index=True).sum() + channels_df['routed_in_7day'] = forwards_in_df_7d_count.loc[chan_id].amt_out_msat + channels_df['amt_routed_in_7day'] = int(forwards_in_df_7d_sum.loc[chan_id].amt_out_msat/1000) + channels_df['average_in_7day'] = 0 if channels_df['routed_in_7day'][0] == 0 else int(channels_df['amt_routed_in_7day']/channels_df['routed_in_7day']) + channels_df['revenue_assist_7day'] = int(forwards_in_df_7d_sum.loc[chan_id].fee) + forwards_in_df_1d = forwards_in_df_7d.loc[forwards_in_df_7d['forward_date'] >= filter_1day] + if forwards_in_df_1d.shape[0] > 0: + forwards_in_df_1d_count = forwards_in_df_1d.groupby('chan_id_in', as_index=True).count() + forwards_in_df_1d_sum = forwards_in_df_1d.groupby('chan_id_in', as_index=True).sum() + channels_df['routed_in_1day'] = forwards_in_df_1d_count.loc[chan_id].amt_out_msat + channels_df['amt_routed_in_1day'] = int(forwards_in_df_1d_sum.loc[chan_id].amt_out_msat/1000) + channels_df['average_in_1day'] = 0 if channels_df['routed_in_1day'][0] == 0 else int(channels_df['amt_routed_in_1day']/channels_df['routed_in_1day']) + channels_df['revenue_assist_1day'] = int(forwards_in_df_1d_sum.loc[chan_id].fee) + if forwards_out_df.shape[0]> 0: + start_date = forwards_out_df['forward_date'].min() + forwards_out_df_out_count = forwards_out_df.groupby('chan_id_out', as_index=True).count() + forwards_out_df_out_sum = forwards_out_df.groupby('chan_id_out', as_index=True).sum() + channels_df['routed_out'] = forwards_out_df_out_count.loc[chan_id].amt_out_msat + channels_df['amt_routed_out'] = int(forwards_out_df_out_sum.loc[chan_id].amt_out_msat/1000) + channels_df['average_out'] = 0 if channels_df['routed_out'][0] == 0 else int(channels_df['amt_routed_out']/channels_df['routed_out']) + channels_df['revenue'] = int(forwards_out_df_out_sum.loc[chan_id].fee) if forwards_out_df_out_sum.empty == False else 0 + forwards_out_df_30d = forwards_out_df.loc[forwards_out_df['forward_date'] >= filter_30day] + if forwards_out_df_30d.shape[0] > 0: + forwards_out_df_out_30d_count = forwards_out_df_30d.groupby('chan_id_out', as_index=True).count() + forwards_out_df_out_30d_sum = forwards_out_df_30d.groupby('chan_id_out', as_index=True).sum() + channels_df['routed_out_30day'] = forwards_out_df_out_30d_count.loc[chan_id].amt_out_msat + channels_df['amt_routed_out_30day'] = int(forwards_out_df_out_30d_sum.loc[chan_id].amt_out_msat/1000) + channels_df['average_out_30day'] = 0 if channels_df['routed_out_30day'][0] == 0 else int(channels_df['amt_routed_out_30day']/channels_df['routed_out_30day']) + channels_df['revenue_30day'] = int(forwards_out_df_out_30d_sum.loc[chan_id].fee) if forwards_out_df_out_30d_sum.empty == False else 0 + forwards_out_df_7d = forwards_out_df_30d.loc[forwards_out_df_30d['forward_date'] >= filter_7day] + if forwards_out_df_7d.shape[0] > 0: + forwards_out_df_out_7d_count = forwards_out_df_7d.groupby('chan_id_out', as_index=True).count() + forwards_out_df_out_7d_sum = forwards_out_df_7d.groupby('chan_id_out', as_index=True).sum() + channels_df['routed_out_7day'] = forwards_out_df_out_7d_count.loc[chan_id].amt_out_msat + channels_df['amt_routed_out_7day'] = int(forwards_out_df_out_7d_sum.loc[chan_id].amt_out_msat/1000) + channels_df['average_out_7day'] = 0 if channels_df['routed_out_7day'][0] == 0 else int(channels_df['amt_routed_out_7day']/channels_df['routed_out_7day']) + channels_df['revenue_7day'] = int(forwards_out_df_out_7d_sum.loc[chan_id].fee) + forwards_out_df_1d = forwards_out_df_7d.loc[forwards_out_df_7d['forward_date'] >= filter_1day] + if forwards_out_df_1d.shape[0] > 0: + forwards_out_df_out_1d_count = forwards_out_df_1d.groupby('chan_id_out', as_index=True).count() + forwards_out_df_out_1d_sum = forwards_out_df_1d.groupby('chan_id_out', as_index=True).sum() + channels_df['routed_out_1day'] = forwards_out_df_out_1d_count.loc[chan_id].amt_out_msat + channels_df['amt_routed_out_1day'] = int(forwards_out_df_out_1d_sum.loc[chan_id].amt_out_msat/1000) + channels_df['average_out_1day'] = 0 if channels_df['routed_out_1day'][0] == 0 else int(channels_df['amt_routed_out_1day']/channels_df['routed_out_1day']) + channels_df['revenue_1day'] = int(forwards_out_df_out_1d_sum.loc[chan_id].fee) + if payments_df.shape[0] > 0: + payments_df_count = payments_df.groupby('chan_out', as_index=True).count() + payments_df_sum = payments_df.groupby('chan_out', as_index=True).sum() + channels_df['rebal_out'] = payments_df_count.loc[chan_id].value + channels_df['amt_rebal_out'] = int(payments_df_sum.loc[chan_id].value) + payments_df_30d = payments_df.loc[payments_df['creation_date'] >= filter_30day] + if payments_df_30d.shape[0] > 0: + payments_df_30d_count = payments_df_30d.groupby('chan_out', as_index=True).count() + payments_df_30d_sum = payments_df_30d.groupby('chan_out', as_index=True).sum() + channels_df['rebal_out_30day'] = payments_df_30d_count.loc[chan_id].value + channels_df['amt_rebal_out_30day'] = int(payments_df_30d_sum.loc[chan_id].value) + payments_df_7d = payments_df_30d.loc[payments_df_30d['creation_date'] >= filter_7day] + if payments_df_7d.shape[0] > 0: + payments_df_7d_count = payments_df_7d.groupby('chan_out', as_index=True).count() + payments_df_7d_sum = payments_df_7d.groupby('chan_out', as_index=True).sum() + channels_df['rebal_out_7day'] = payments_df_7d_count.loc[chan_id].value + channels_df['amt_rebal_out_7day'] = int(payments_df_7d_sum.loc[chan_id].value) + payments_df_1d = payments_df_7d.loc[payments_df_7d['creation_date'] >= filter_1day] + if payments_df_1d.shape[0] > 0: + payments_df_1d_count = payments_df_1d.groupby('chan_out', as_index=True).count() + payments_df_1d_sum = payments_df_1d.groupby('chan_out', as_index=True).sum() + channels_df['rebal_out_1day'] = payments_df_1d_count.loc[chan_id].value + channels_df['amt_rebal_out_1day'] = int(payments_df_1d_sum.loc[chan_id].value) + if invoices_df.shape[0]> 0: + invoices_df_30d = invoices_df.loc[invoices_df['settle_date'] >= filter_30day] + invoices_df_7d = invoices_df_30d.loc[invoices_df_30d['settle_date'] >= filter_7day] + invoices_df_1d = invoices_df_7d.loc[invoices_df_7d['settle_date'] >= filter_1day] + invoices_df_count = DataFrame() if invoices_df.empty else invoices_df.groupby('chan_in', as_index=True).count() + invoices_df_30d_count = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True).count() + invoices_df_7d_count = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True).count() + invoices_df_1d_count = DataFrame() if invoices_df_1d.empty else invoices_df_1d.groupby('chan_in', as_index=True).count() + invoices_df_sum = DataFrame() if invoices_df.empty else invoices_df.groupby('chan_in', as_index=True).sum() + invoices_df_30d_sum = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True).sum() + invoices_df_7d_sum = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True).sum() + invoices_df_1d_sum = DataFrame() if invoices_df_1d.empty else invoices_df_1d.groupby('chan_in', as_index=True).sum() + channels_df['rebal_in'] = invoices_df_count.loc[chan_id].amt_paid if invoices_df_count.empty == False else 0 + channels_df['rebal_in_30day'] = invoices_df_30d_count.loc[chan_id].amt_paid if invoices_df_30d_count.empty == False else 0 + channels_df['rebal_in_7day'] = invoices_df_7d_count.loc[chan_id].amt_paid if invoices_df_7d_count.empty == False else 0 + channels_df['rebal_in_1day'] = invoices_df_1d_count.loc[chan_id].amt_paid if invoices_df_1d_count.empty == False else 0 + channels_df['amt_rebal_in'] = int(invoices_df_sum.loc[chan_id].amt_paid) if invoices_df_count.empty == False else 0 + channels_df['amt_rebal_in_30day'] = int(invoices_df_30d_sum.loc[chan_id].amt_paid) if invoices_df_30d_count.empty == False else 0 + channels_df['amt_rebal_in_7day'] = int(invoices_df_7d_sum.loc[chan_id].amt_paid) if invoices_df_7d_count.empty == False else 0 + channels_df['amt_rebal_in_1day'] = int(invoices_df_1d_sum.loc[chan_id].amt_paid) if invoices_df_1d_count.empty == False else 0 + rebal_payments_df = DataFrame.from_records(Payments.objects.filter(status=2).filter(rebal_chan=chan_id).values()) + if rebal_payments_df.shape[0] > 0: + rebal_payments_df_30d = rebal_payments_df.loc[rebal_payments_df['creation_date'] >= filter_30day] + rebal_payments_df_7d = rebal_payments_df_30d.loc[rebal_payments_df_30d['creation_date'] >= filter_7day] + rebal_payments_df_1d = rebal_payments_df_7d.loc[rebal_payments_df_7d['creation_date'] >= filter_1day] + invoice_hashes = DataFrame() if invoices_df.empty else invoices_df.groupby('chan_in', as_index=True)['r_hash'].apply(list) + invoice_hashes_30d = DataFrame() if invoices_df_30d.empty else invoices_df_30d.groupby('chan_in', as_index=True)['r_hash'].apply(list) + invoice_hashes_7d = DataFrame() if invoices_df_7d.empty else invoices_df_7d.groupby('chan_in', as_index=True)['r_hash'].apply(list) + invoice_hashes_1d = DataFrame() if invoices_df_1d.empty else invoices_df_1d.groupby('chan_in', as_index=True)['r_hash'].apply(list) + channels_df['costs'] = 0 if channels_df['rebal_in'][0] == 0 or invoice_hashes.empty == True else int(rebal_payments_df.set_index('payment_hash', inplace=False).loc[invoice_hashes[chan_id]]['fee'].sum()) + channels_df['costs_30day'] = 0 if channels_df['rebal_in_30day'][0] == 0 or invoice_hashes_30d.empty == True else int(rebal_payments_df_30d.set_index('payment_hash', inplace=False).loc[invoice_hashes_30d[chan_id]]['fee'].sum()) + channels_df['costs_7day'] = 0 if channels_df['rebal_in_7day'][0] == 0 or invoice_hashes_7d.empty == True else int(rebal_payments_df_7d.set_index('payment_hash', inplace=False).loc[invoice_hashes_7d[chan_id]]['fee'].sum()) + channels_df['costs_1day'] = 0 if channels_df['rebal_in_1day'][0] == 0 or invoice_hashes_1d.empty == True else int(rebal_payments_df_1d.set_index('payment_hash', inplace=False).loc[invoice_hashes_1d[chan_id]]['fee'].sum()) + channels_df['profits'] = channels_df['revenue'] - channels_df['costs'] + channels_df['profits_30day'] = channels_df['revenue_30day'] - channels_df['costs_30day'] + channels_df['profits_7day'] = channels_df['revenue_7day'] - channels_df['costs_7day'] + channels_df['profits_1day'] = channels_df['revenue_1day'] - channels_df['costs_1day'] + channels_df['profits_vol'] = 0 if channels_df['amt_routed_out'][0] == 0 else int(channels_df['profits'] / (channels_df['amt_routed_out']/1000000)) + channels_df['profits_vol_30day'] = 0 if channels_df['amt_routed_out_30day'][0] == 0 else int(channels_df['profits_30day'] / (channels_df['amt_routed_out_30day']/1000000)) + channels_df['profits_vol_7day'] = 0 if channels_df['amt_routed_out_7day'][0] == 0 else int(channels_df['profits_7day'] / (channels_df['amt_routed_out_7day']/1000000)) + channels_df['profits_vol_1day'] = 0 if channels_df['amt_routed_out_1day'][0] == 0 else int(channels_df['profits_1day'] / (channels_df['amt_routed_out_1day']/1000000)) + channels_df['apy'] = 0.0 + channels_df = channels_df.copy() + channels_df['net_routed_7day'] = round((channels_df['amt_routed_out_7day']-channels_df['amt_routed_in_7day'])/channels_df['capacity'], 1) + channels_df['out_rate'] = int((channels_df['revenue_7day']/channels_df['amt_routed_out_7day'])*1000000) if channels_df['amt_routed_out_7day'][0] > 0 else 0 + channels_df['rebal_ppm'] = int((channels_df['costs_7day']/channels_df['amt_rebal_in_7day'])*1000000) if channels_df['amt_rebal_in_7day'][0] > 0 else 0 + channels_df['max_suggestion'] = int((channels_df['out_rate'] if channels_df['out_rate'][0] > 0 else channels_df['local_fee_rate'])*1.15) if channels_df['in_percent'][0] > 25 else int(channels_df['local_fee_rate']) + channels_df['max_suggestion'] = channels_df['local_fee_rate']+25 if channels_df['max_suggestion'][0] > (channels_df['local_fee_rate'][0]+25) else channels_df['max_suggestion'] + channels_df['min_suggestion'] = int((channels_df['out_rate'] if channels_df['out_rate'][0] > 0 else channels_df['local_fee_rate'])*0.75) if channels_df['out_percent'][0] > 25 else int(channels_df['local_fee_rate']) + channels_df['min_suggestion'] = channels_df['local_fee_rate']-50 if channels_df['min_suggestion'][0] < (channels_df['local_fee_rate'][0]-50) else channels_df['min_suggestion'] + channels_df['assisted_ratio'] = round((channels_df['revenue_assist_7day'] if channels_df['revenue_7day'][0] == 0 else channels_df['revenue_assist_7day']/channels_df['revenue_7day']), 2) + channels_df['profit_margin'] = channels_df['out_rate']*((100-channels_df['ar_max_cost'])/100) + channels_df['adjusted_out_rate'] = int(channels_df['out_rate']+channels_df['net_routed_7day']*channels_df['assisted_ratio']) + channels_df['adjusted_rebal_rate'] = int(channels_df['rebal_ppm']+channels_df['profit_margin']) + channels_df['out_rate_only'] = int(channels_df['out_rate']+channels_df['net_routed_7day']*channels_df['out_rate']*0.02) + channels_df['fee_rate_only'] = int(channels_df['local_fee_rate']+channels_df['net_routed_7day']*channels_df['local_fee_rate']*0.05) + channels_df['new_rate'] = channels_df['adjusted_out_rate'] if channels_df['net_routed_7day'][0] != 0 else (channels_df['adjusted_rebal_rate'] if channels_df['rebal_ppm'][0] > 0 and channels_df['out_rate'][0] > 0 else (channels_df['out_rate_only'] if channels_df['out_rate'][0] > 0 else (channels_df['min_suggestion'] if channels_df['net_routed_7day'][0] == 0 and channels_df['in_percent'][0] < 25 else channels_df['fee_rate_only']))) + channels_df['new_rate'] = 0 if channels_df['new_rate'][0] < 0 else channels_df['new_rate'] + channels_df['new_rate'] = channels_df['max_suggestion'] if channels_df['max_suggestion'][0] > 0 and channels_df['new_rate'][0] > channels_df['max_suggestion'][0] else channels_df['new_rate'] + channels_df['new_rate'] = channels_df['min_suggestion'] if channels_df['new_rate'][0] < channels_df['min_suggestion'][0] else channels_df['new_rate'] + channels_df['new_rate'] = int(round(channels_df['new_rate']/5, 0)*5) + channels_df['adjustment'] = int(channels_df['new_rate']-channels_df['local_fee_rate']) + channels_df['inbound_can'] = ((channels_df['remote_balance']*100)/channels_df['capacity'])/channels_df['ar_in_target'] + channels_df['fee_ratio'] = 100 if channels_df['local_fee_rate'][0] == 0 else int(round(((channels_df['remote_fee_rate']/channels_df['local_fee_rate'])*1000)/10, 0)) + channels_df['fee_check'] = 1 if channels_df['ar_max_cost'][0] == 0 else int(round(((channels_df['fee_ratio']/channels_df['ar_max_cost'])*1000)/10, 0)) + channels_df = channels_df.copy() + channels_df['steps'] = 0 if channels_df['inbound_can'][0] < 1 else int(((channels_df['in_percent']-channels_df['ar_in_target'])/((channels_df['ar_amt_target']/channels_df['capacity'])*100))+0.999) + if node_capacity > 0: + outbound_ratio = node_outbound/node_capacity + if start_date is not None: + time_delta = datetime.now() - start_date.to_pydatetime() + days_routing = time_delta.days + (time_delta.seconds/86400) + channels_df['apy'] = round(((channels_df['profits']/days_routing)*36500)/(channels_df['capacity']*outbound_ratio), 2) + channels_df['apy_30day'] = round((channels_df['profits_30day']*1216.6667)/(channels_df['capacity']*outbound_ratio), 2) + channels_df['apy_7day'] = round((channels_df['profits_7day']*5214.2857)/(channels_df['capacity']*outbound_ratio), 2) + channels_df['apy_1day'] = round((channels_df['profits_1day']*36500)/(channels_df['capacity']*outbound_ratio), 2) + else: + channels_df = DataFrame() + forwards_df = DataFrame() + payments_df = DataFrame() + invoices_df = DataFrame() + rebalancer_df = DataFrame() + failed_htlc_df = DataFrame() + context = { + 'chan_id': chan_id, + 'channel': [] if channels_df.empty else channels_df.to_dict(orient='records')[0], + 'incoming_htlcs': PendingHTLCs.objects.filter(chan_id=chan_id).filter(incoming=True).order_by('hash_lock'), + 'outgoing_htlcs': PendingHTLCs.objects.filter(chan_id=chan_id).filter(incoming=False).order_by('hash_lock'), + 'forwards': [] if forwards_df.empty else forwards_df.sort_values(by=['forward_date'], ascending=False).to_dict(orient='records')[:5], + 'payments': [] if payments_df.empty else payments_df.sort_values(by=['creation_date'], ascending=False).to_dict(orient='records')[:5], + 'invoices': [] if invoices_df.empty else invoices_df.sort_values(by=['settle_date'], ascending=False).to_dict(orient='records')[:5], + 'rebalances': [] if rebalancer_df.empty else rebalancer_df.to_dict(orient='records')[:5], + 'failed_htlcs': [] if failed_htlc_df.empty else failed_htlc_df.to_dict(orient='records')[:5], + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'graph_links': graph_links(), + 'network_links': network_links() + } + return render(request, 'channel.html', context) + else: + return redirect('home') + @login_required(login_url='/lndg-admin/login/?next=/') def opens(request): if request.method == 'GET': @@ -421,7 +784,7 @@ def opens(request): @login_required(login_url='/lndg-admin/login/?next=/') def actions(request): if request.method == 'GET': - channels = Channels.objects.filter(is_active=True, is_open=True).annotate(outbound_percent=(Sum('local_balance')*1000)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*1000)/Sum('capacity')) + channels = Channels.objects.filter(is_active=True, is_open=True, private=False).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*1000)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*1000)/Sum('capacity')) filter_7day = datetime.now() - timedelta(days=7) forwards = Forwards.objects.filter(forward_date__gte=filter_7day) action_list = [] @@ -430,8 +793,8 @@ def actions(request): result['chan_id'] = channel.chan_id result['alias'] = channel.alias result['capacity'] = channel.capacity - result['local_balance'] = channel.local_balance - result['remote_balance'] = channel.remote_balance + result['local_balance'] = channel.local_balance + channel.pending_outbound + result['remote_balance'] = channel.remote_balance + channel.pending_inbound result['outbound_percent'] = int(round(channel.outbound_percent/10, 0)) result['inbound_percent'] = int(round(channel.inbound_percent/10, 0)) result['unsettled_balance'] = channel.unsettled_balance @@ -494,7 +857,7 @@ def pending_htlcs(request): def failed_htlcs(request): if request.method == 'GET': context = { - 'failed_htlcs': FailedHTLCs.objects.all().order_by('-timestamp')[:150], + 'failed_htlcs': FailedHTLCs.objects.all().order_by('-id')[:150], } return render(request, 'failed_htlcs.html', context) else: @@ -530,6 +893,37 @@ def forwards(request): else: return redirect('home') +@login_required(login_url='/lndg-admin/login/?next=/') +def rebalancing(request): + if request.method == 'GET': + filter_7day = datetime.now() - timedelta(days=7) + rebalancer_7d_df = DataFrame.from_records(Rebalancer.objects.filter(stop__gte=filter_7day).order_by('-id').values()) + channels_df = DataFrame.from_records(Channels.objects.filter(is_open=True, private=False).annotate(percent_inbound=((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity')).annotate(percent_outbound=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).order_by('-is_active', 'percent_outbound').values()) + channels_df['inbound_can'] = channels_df['percent_inbound'] / channels_df['ar_in_target'] + channels_df['fee_ratio'] = channels_df.apply(lambda row: 100 if row['local_fee_rate'] == 0 else int(round(((row['remote_fee_rate']/row['local_fee_rate'])*1000)/10, 0)), axis=1) + channels_df['fee_check'] = channels_df.apply(lambda row: 1 if row['ar_max_cost'] == 0 else int(round(((row['fee_ratio']/row['ar_max_cost'])*1000)/10, 0)), axis=1) + channels_df['steps'] = channels_df.apply(lambda row: 0 if row['inbound_can'] < 1 else int(((row['percent_inbound']-row['ar_in_target'])/((row['ar_amt_target']/row['capacity'])*100))+0.999), axis=1) + rebalancer_count_7d_df = rebalancer_7d_df[rebalancer_7d_df['status']>=2][rebalancer_7d_df['status']<400] + channels_df['attempts'] = channels_df.apply(lambda row: 0 if rebalancer_count_7d_df.empty else rebalancer_count_7d_df[rebalancer_count_7d_df['last_hop_pubkey']==row.remote_pubkey].shape[0], axis=1) + channels_df['success'] = channels_df.apply(lambda row: 0 if rebalancer_count_7d_df.empty else rebalancer_count_7d_df[rebalancer_count_7d_df['last_hop_pubkey']==row.remote_pubkey][rebalancer_count_7d_df['status']==2].shape[0], axis=1) + channels_df['success_rate'] = channels_df.apply(lambda row: 0 if row['attempts'] == 0 else int((row['success']/row['attempts'])*100), axis=1) + enabled_df = channels_df[channels_df['auto_rebalance']==True] + eligible_df = enabled_df[enabled_df['inbound_can']>=1][enabled_df['fee_check']<100] + eligible_count = eligible_df.shape[0] + enabled_count = enabled_df.shape[0] + context = { + 'eligible_count': eligible_count, + 'enabled_count': enabled_count, + 'channels': channels_df.to_dict(orient='records'), + 'rebalancer': Rebalancer.objects.all().annotate(ppm=(Sum('fee_limit')*1000000)/Sum('value')).order_by('-id')[:20], + 'rebalancer_form': RebalancerForm, + 'local_settings': LocalSettings.objects.filter(key__contains='AR-'), + 'network': 'testnet/' if LND_NETWORK == 'testnet' else '', + 'graph_links': graph_links() + } + return render(request, 'rebalancing.html', context) + else: + return redirect('home') @login_required(login_url='/lndg-admin/login/?next=/') def keysends(request): @@ -545,7 +939,7 @@ def keysends(request): def autopilot(request): if request.method == 'GET': context = { - 'autopilot': Autopilot.objects.all().order_by('-timestamp') + 'autopilot': Autopilot.objects.all().order_by('-id') } return render(request, 'autopilot.html', context) else: @@ -699,9 +1093,13 @@ def rebalance(request): chan_ids = [] for channel in form.cleaned_data['outgoing_chan_ids']: chan_ids.append(channel.chan_id) - target_alias = Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey'])[0].alias if Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey']).exists() else '' - Rebalancer(value=form.cleaned_data['value'], fee_limit=form.cleaned_data['fee_limit'], outgoing_chan_ids=chan_ids, last_hop_pubkey=form.cleaned_data['last_hop_pubkey'], target_alias=target_alias, duration=form.cleaned_data['duration']).save() - messages.success(request, 'Rebalancer request created!') + if len(chan_ids) > 0: + target_alias = Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey'])[0].alias if Channels.objects.filter(is_active=True, is_open=True, remote_pubkey=form.cleaned_data['last_hop_pubkey']).exists() else '' + fee_limit = 0 if form.cleaned_data['value'] == 0 else int(form.cleaned_data['fee_limit']*form.cleaned_data['value']*0.000001) + Rebalancer(value=form.cleaned_data['value'], fee_limit=fee_limit, outgoing_chan_ids=chan_ids, last_hop_pubkey=form.cleaned_data['last_hop_pubkey'], target_alias=target_alias, duration=form.cleaned_data['duration']).save() + messages.success(request, 'Rebalancer request created!') + else: + messages.error(request, 'You must select atleast one outgoing channel.') else: messages.error(request, 'Target peer is invalid or unknown.') except Exception as e: @@ -712,7 +1110,7 @@ def rebalance(request): messages.error(request, 'Error entering rebalancer request! Error: ' + error_msg) else: messages.error(request, 'Invalid Request. Please try again.') - return redirect('home') + return redirect(request.META.get('HTTP_REFERER')) @login_required(login_url='/lndg-admin/login/?next=/') def update_chan_policy(request): diff --git a/htlc_stream.py b/htlc_stream.py index 4c3c43e3..1a41044e 100644 --- a/htlc_stream.py +++ b/htlc_stream.py @@ -4,6 +4,7 @@ from gui.lnd_deps.lnd_connect import lnd_connect from lndg import settings from os import environ +from time import sleep environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings' django.setup() from gui.models import Channels, FailedHTLCs @@ -20,13 +21,16 @@ def main(): out_chan = Channels.objects.filter(chan_id=out_chan_id)[0] if Channels.objects.filter(chan_id=out_chan_id).exists() else None in_chan_alias = in_chan.alias if in_chan is not None else None out_chan_alias = out_chan.alias if out_chan is not None else None + out_chan_liq = out_chan.local_balance if out_chan is not None else None + out_chan_pending = out_chan.pending_outbound if out_chan is not None else None amount = int(response.link_fail_event.info.outgoing_amt_msat/1000) wire_failure = response.link_fail_event.wire_failure failure_detail = response.link_fail_event.failure_detail missed_fee = 0 if out_chan == None else round(((amount/1000000) * out_chan.local_fee_rate) + (out_chan.local_base_fee/1000), 3) - FailedHTLCs(amount=amount, chan_id_in=in_chan_id, chan_id_out=out_chan_id, chan_in_alias=in_chan_alias, chan_out_alias=out_chan_alias, wire_failure=wire_failure, failure_detail=failure_detail, missed_fee=missed_fee).save() + FailedHTLCs(amount=amount, chan_id_in=in_chan_id, chan_id_out=out_chan_id, chan_in_alias=in_chan_alias, chan_out_alias=out_chan_alias, chan_out_liq=out_chan_liq, chan_out_pending=out_chan_pending, wire_failure=wire_failure, failure_detail=failure_detail, missed_fee=missed_fee).save() except Exception as e: print('Error while running failed HTLC stream: ' + str(e)) + sleep(20) if __name__ == '__main__': main() \ No newline at end of file diff --git a/jobs.py b/jobs.py index 0de42748..dfdcfca9 100644 --- a/jobs.py +++ b/jobs.py @@ -3,12 +3,14 @@ from datetime import datetime, timedelta 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.lnd_connect import lnd_connect from lndg import settings from os import environ environ['DJANGO_SETTINGS_MODULE'] = 'lndg.settings' django.setup() -from gui.models import Payments, PaymentHops, Invoices, Forwards, Channels, Peers, Onchain, PendingHTLCs, LocalSettings +from gui.models import Payments, PaymentHops, Invoices, Forwards, Channels, Peers, Onchain, Closures, Resolutions, PendingHTLCs, LocalSettings def update_payments(stub): #Remove anything in-flight so we can get most up to date status @@ -16,6 +18,7 @@ def update_payments(stub): #Get the number of records in the database currently last_index = 0 if Payments.objects.aggregate(Max('index'))['index__max'] == None else Payments.objects.aggregate(Max('index'))['index__max'] payments = stub.ListPayments(ln.ListPaymentsRequest(include_incomplete=True, index_offset=last_index, max_payments=100)).payments + self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey for payment in payments: try: new_payment = Payments(creation_date=datetime.fromtimestamp(payment.creation_date), payment_hash=payment.payment_hash, value=round(payment.value_msat/1000, 3), fee=round(payment.fee_msat/1000, 3), status=payment.status, index=payment.payment_index) @@ -39,13 +42,14 @@ def update_payments(stub): if hop_count == 1: new_payment.chan_out = hop.chan_id new_payment.chan_out_alias = alias - new_payment.save() if hop_count == total_hops and 5482373484 in hop.custom_records: records = hop.custom_records - message = records[34349334].decode('utf-8', errors='ignore')[:255] if 34349334 in records else None + message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None new_payment.keysend_preimage = records[5482373484].hex() new_payment.message = message - new_payment.save() + if hop_count == total_hops and hop.pub_key == self_pubkey: + new_payment.rebal_chan = hop.chan_id + new_payment.save() break except: #Error inserting, try to update instead @@ -79,7 +83,7 @@ def update_payments(stub): db_payment.save() if hop_count == total_hops and 5482373484 in hop.custom_records: records = hop.custom_records - message = records[34349334].decode('utf-8', errors='ignore')[:255] if 34349334 in records else None + message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None db_payment.keysend_preimage = records[5482373484].hex() db_payment.message = message db_payment.save() @@ -92,11 +96,36 @@ def update_invoices(stub): invoices = stub.ListInvoices(ln.ListInvoiceRequest(index_offset=last_index, num_max_invoices=100)).invoices for invoice in invoices: if invoice.state == 1: - alias = Channels.objects.filter(chan_id=invoice.htlcs[0].chan_id)[0].alias if Channels.objects.filter(chan_id=invoice.htlcs[0].chan_id).exists() else None - records = invoice.htlcs[0].custom_records - keysend_preimage = records[5482373484].hex() if 5482373484 in records else None - message = records[34349334].decode('utf-8', errors='ignore')[:500] if 34349334 in records else None - Invoices(creation_date=datetime.fromtimestamp(invoice.creation_date), settle_date=datetime.fromtimestamp(invoice.settle_date), r_hash=invoice.r_hash.hex(), value=round(invoice.value_msat/1000, 3), amt_paid=invoice.amt_paid_sat, state=invoice.state, chan_in=invoice.htlcs[0].chan_id, chan_in_alias=alias, keysend_preimage=keysend_preimage, message=message, index=invoice.add_index).save() + if len(invoice.htlcs) > 0: + chan_in_id = invoice.htlcs[0].chan_id + alias = Channels.objects.filter(chan_id=chan_in_id)[0].alias if Channels.objects.filter(chan_id=chan_in_id).exists() else None + records = invoice.htlcs[0].custom_records + keysend_preimage = records[5482373484].hex() if 5482373484 in records else None + message = records[34349334].decode('utf-8', errors='ignore')[:1000] if 34349334 in records else None + if 34349337 in records and 34349339 in records and 34349343 in records and 34349334 in records: + signerstub = lnsigner.SignerStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey + try: + valid = signerstub.VerifyMessage(lns.VerifyMessageReq(msg=(records[34349339]+bytes.fromhex(self_pubkey)+records[34349343]+records[34349334]), signature=records[34349337], pubkey=records[34349339])).valid + except: + print('Unable to validate signature on invoice: ' + invoice.r_hash.hex()) + valid = False + sender = records[34349339].hex() if valid == True else None + try: + sender_alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=sender, include_channels=False)).node.alias if sender != None else None + except: + sender_alias = None + else: + sender = None + sender_alias = None + else: + chan_in_id = None + alias = None + keysend_preimage = None + message = None + sender = None + sender_alias = None + Invoices(creation_date=datetime.fromtimestamp(invoice.creation_date), settle_date=datetime.fromtimestamp(invoice.settle_date), r_hash=invoice.r_hash.hex(), value=round(invoice.value_msat/1000, 3), amt_paid=invoice.amt_paid_sat, state=invoice.state, chan_in=chan_in_id, chan_in_alias=alias, keysend_preimage=keysend_preimage, message=message, sender=sender, sender_alias=sender_alias, index=invoice.add_index).save() else: Invoices(creation_date=datetime.fromtimestamp(invoice.creation_date), r_hash=invoice.r_hash.hex(), value=round(invoice.value_msat/1000, 3), amt_paid=invoice.amt_paid_sat, state=invoice.state, index=invoice.add_index).save() @@ -119,7 +148,10 @@ def update_channels(stub): db_channel = Channels.objects.filter(chan_id=channel.chan_id)[0] else: #Create a record for this new channel - alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=channel.remote_pubkey, include_channels=False)).node.alias + try: + alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=channel.remote_pubkey, include_channels=False)).node.alias + except: + alias = '' channel_point = channel.channel_point txid, index = channel_point.split(':') db_channel = Channels() @@ -129,6 +161,8 @@ def update_channels(stub): db_channel.alias = alias db_channel.funding_txid = txid db_channel.output_index = index + db_channel.capacity = channel.capacity + db_channel.private = channel.private try: chan_data = stub.GetChanInfo(ln.ChanInfoRequest(chan_id=channel.chan_id)) if chan_data.node1_pub == channel.remote_pubkey: @@ -152,7 +186,6 @@ def update_channels(stub): db_channel.remote_base_fee = 0 db_channel.remote_fee_rate = 0 db_channel.remote_disabled = False - db_channel.capacity = channel.capacity db_channel.local_balance = channel.local_balance db_channel.remote_balance = channel.remote_balance db_channel.unsettled_balance = channel.unsettled_balance @@ -162,9 +195,11 @@ def update_channels(stub): db_channel.last_update = datetime.now() if db_channel.is_active != channel.active else db_channel.last_update db_channel.is_active = channel.active db_channel.is_open = True - db_channel.save() - counter += 1 - chan_list.append(channel.chan_id) + db_channel.total_sent = channel.total_satoshis_sent + db_channel.total_received = channel.total_satoshis_received + pending_out = 0 + pending_in = 0 + htlc_counter = 0 if len(channel.pending_htlcs) > 0: for htlc in channel.pending_htlcs: pending_htlc = PendingHTLCs() @@ -177,6 +212,17 @@ def update_channels(stub): pending_htlc.forwarding_channel = htlc.forwarding_channel pending_htlc.forwarding_alias = Channels.objects.filter(chan_id=htlc.forwarding_channel)[0].alias if Channels.objects.filter(chan_id=htlc.forwarding_channel).exists() else '---' pending_htlc.save() + if htlc.incoming == True: + pending_in += htlc.amount + else: + pending_out += htlc.amount + htlc_counter += 1 + db_channel.pending_outbound = pending_out + db_channel.pending_inbound = pending_in + db_channel.htlc_count = htlc_counter + db_channel.save() + counter += 1 + chan_list.append(channel.chan_id) records = Channels.objects.filter(is_open=True).count() if records > counter: #A channel must have been closed, mark it as closed @@ -199,10 +245,19 @@ def update_peers(stub): db_peer.sat_sent = peer.sat_sent db_peer.sat_recv = peer.sat_recv db_peer.inbound = peer.inbound + if db_peer.connected == False: + try: + db_peer.alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer.pub_key, include_channels=False)).node.alias + except: + db_peer.alias = '' db_peer.connected = True db_peer.save() elif exists == 0: - Peers(pubkey = peer.pub_key, address = peer.address, sat_sent = peer.sat_sent, sat_recv = peer.sat_recv, inbound = peer.inbound, connected = True).save() + try: + alias = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=peer.pub_key, include_channels=False)).node.alias + except: + alias = '' + Peers(pubkey = peer.pub_key, address = peer.address, sat_sent = peer.sat_sent, sat_recv = peer.sat_recv, inbound = peer.inbound, alias=alias, connected = True).save() counter += 1 peer_list.append(peer.pub_key) records = Peers.objects.filter(connected=True).count() @@ -219,8 +274,23 @@ def update_onchain(stub): for tx in onchain_txs: Onchain(tx_hash=tx.tx_hash, time_stamp=datetime.fromtimestamp(tx.time_stamp), amount=tx.amount, fee=tx.total_fees, block_hash=tx.block_hash, block_height=tx.block_height, label=tx.label[:100]).save() +def update_closures(stub): + closures = stub.ClosedChannels(ln.ClosedChannelsRequest()).channels + if len(closures) > Closures.objects.all().count(): + counter = 0 + skip = Closures.objects.all().count() + for closure in closures: + counter += 1 + if counter > skip: + resolution_count = len(closure.resolutions) + db_closure = Closures(chan_id=closure.chan_id, closing_tx=closure.closing_tx_hash, remote_pubkey=closure.remote_pubkey, capacity=closure.capacity, close_height=closure.close_height, settled_balance=closure.settled_balance, time_locked_balance=closure.time_locked_balance, close_type=closure.close_type, open_initiator=closure.open_initiator, close_initiator=closure.close_initiator, resolution_count=resolution_count) + db_closure.save() + if resolution_count > 0: + for resolution in closure.resolutions: + Resolutions(chan_id=db_closure, resolution_type=resolution.resolution_type, outcome=resolution.outcome, outpoint_tx=resolution.outpoint.txid_str, outpoint_index=resolution.outpoint.output_index, amount_sat=resolution.amount_sat, sweep_txid=resolution.sweep_txid).save() + def reconnect_peers(stub): - inactive_peers = Channels.objects.filter(is_open=True, is_active=False).values_list('remote_pubkey', flat=True).distinct() + inactive_peers = Channels.objects.filter(is_open=True, is_active=False, private=False).values_list('remote_pubkey', flat=True).distinct() if len(inactive_peers) > 0: peers = Peers.objects.all() for inactive_peer in inactive_peers: @@ -233,8 +303,12 @@ def reconnect_peers(stub): peer.connected = False peer.save() print('Attempting connection to:', inactive_peer) - node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=inactive_peer, include_channels=False)).node - host = node.addresses[0].addr + try: + node = stub.GetNodeInfo(ln.NodeInfoRequest(pub_key=inactive_peer, include_channels=False)).node + host = node.addresses[0].addr + except: + print('Unable to find node info on graph, using last known value') + host = peer.address address = ln.LightningAddress(pubkey=inactive_peer, host=host) stub.ConnectPeer(request = ln.ConnectPeerRequest(addr=address, perm=True, timeout=5)) peer.last_reconnected = datetime.now() @@ -281,6 +355,7 @@ def main(): update_invoices(stub) update_forwards(stub) update_onchain(stub) + update_closures(stub) reconnect_peers(stub) clean_payments(stub) except Exception as e: diff --git a/keysend.py b/keysend.py index 579850d3..defc22ef 100644 --- a/keysend.py +++ b/keysend.py @@ -1,37 +1,57 @@ -import secrets +import secrets, time, struct from hashlib import sha256 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 router_pb2 as lnr from gui.lnd_deps import router_pb2_grpc as lnrouter +from gui.lnd_deps import signer_pb2 as lns +from gui.lnd_deps import signer_pb2_grpc as lnsigner from gui.lnd_deps.lnd_connect import lnd_connect -def keysend(target_pubkey, msg, amount, fee_limit, timeout): +def keysend(target_pubkey, msg, amount, fee_limit, timeout, sign): #Construct and send - routerstub = lnrouter.RouterStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) - secret = secrets.token_bytes(32) - hashed_secret = sha256(secret).hexdigest() - custom_records = [(5482373484, secret),] - msg = str(msg) - if len(msg) > 0: - custom_records.append((34349334, bytes.fromhex(msg.encode('utf-8').hex()))) - for response in routerstub.SendPaymentV2(lnr.SendPaymentRequest(dest=bytes.fromhex(target_pubkey), dest_custom_records=custom_records, fee_limit_sat=fee_limit, timeout_seconds=timeout, amt=amount, payment_hash=bytes.fromhex(hashed_secret))): - if response.status == 1: - print('In-flight') - if response.status == 2: - print('Succeeded') - if response.status == 3: - if response.failure_reason == 1: - print('Failure - Timeout') - elif response.failure_reason == 2: - print('Failure - No Route') - elif response.failure_reason == 3: - print('Failure - Error') - elif response.failure_reason == 4: - print('Failure - Incorrect Payment Details') - elif response.failure_reason == 5: - print('Failure Insufficient Balance') - if response.status == 0: - print('Unknown Error') + try: + routerstub = lnrouter.RouterStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + secret = secrets.token_bytes(32) + hashed_secret = sha256(secret).hexdigest() + custom_records = [(5482373484, secret),] + msg = str(msg) + if len(msg) > 0: + custom_records.append((34349334, bytes.fromhex(msg.encode('utf-8').hex()))) + if sign == True: + stub = lnrpc.LightningStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + signerstub = lnsigner.SignerStub(lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER)) + self_pubkey = stub.GetInfo(ln.GetInfoRequest()).identity_pubkey + timestamp = struct.pack(">i", int(time.time())) + signature = signerstub.SignMessage(lns.SignMessageReq(msg=(bytes.fromhex(self_pubkey)+bytes.fromhex(target_pubkey)+timestamp+bytes.fromhex(msg.encode('utf-8').hex())), key_loc=lns.KeyLocator(key_family=6, key_index=0))).signature + custom_records.append((34349337, signature)) + custom_records.append((34349339, bytes.fromhex(self_pubkey))) + custom_records.append((34349343, timestamp)) + for response in routerstub.SendPaymentV2(lnr.SendPaymentRequest(dest=bytes.fromhex(target_pubkey), dest_custom_records=custom_records, fee_limit_sat=fee_limit, timeout_seconds=timeout, amt=amount, payment_hash=bytes.fromhex(hashed_secret))): + if response.status == 1: + print('In-flight') + if response.status == 2: + print('Succeeded') + if response.status == 3: + if response.failure_reason == 1: + print('Failure - Timeout') + elif response.failure_reason == 2: + print('Failure - No Route') + elif response.failure_reason == 3: + print('Failure - Error') + elif response.failure_reason == 4: + print('Failure - Incorrect Payment Details') + elif response.failure_reason == 5: + print('Failure Insufficient Balance') + if response.status == 0: + print('Unknown Error') + 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] + print('Error while sending keysend payment! Error: ' + error_msg) def main(): #Ask user for variables @@ -40,13 +60,18 @@ def main(): 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)) if len(msg) > 0: print('Attaching this message to the keysend payment:', msg) - keysend(target_pubkey, msg, amount, fee_limit, timeout) + keysend(target_pubkey, msg, amount, fee_limit, timeout, sign) if __name__ == '__main__': main() \ No newline at end of file diff --git a/rebalancer.py b/rebalancer.py index f74671a7..4bc72539 100644 --- a/rebalancer.py +++ b/rebalancer.py @@ -20,7 +20,6 @@ def run_rebalancer(rebalance): unknown_error.stop = datetime.now() unknown_error.save() rebalance.start = datetime.now() - rebalance.save() try: #Open connection with lnd via grpc connection = lnd_connect(settings.LND_DIR_PATH, settings.LND_NETWORK, settings.LND_RPC_SERVER) @@ -28,33 +27,35 @@ def run_rebalancer(rebalance): routerstub = lnrouter.RouterStub(connection) chan_ids = json.loads(rebalance.outgoing_chan_ids) timeout = rebalance.duration * 60 - response = stub.AddInvoice(ln.Invoice(value=rebalance.value, expiry=timeout)) - for response in routerstub.SendPaymentV2(lnr.SendPaymentRequest(payment_request=str(response.payment_request), fee_limit_sat=rebalance.fee_limit, outgoing_chan_ids=chan_ids, last_hop_pubkey=bytes.fromhex(rebalance.last_hop_pubkey), timeout_seconds=(timeout-5), allow_self_payment=True), timeout=(timeout+60)): - if response.status == 1 and rebalance.status == 0: + invoice_response = stub.AddInvoice(ln.Invoice(value=rebalance.value, expiry=timeout)) + for payment_response in routerstub.SendPaymentV2(lnr.SendPaymentRequest(payment_request=str(invoice_response.payment_request), fee_limit_sat=rebalance.fee_limit, outgoing_chan_ids=chan_ids, last_hop_pubkey=bytes.fromhex(rebalance.last_hop_pubkey), timeout_seconds=(timeout-5), allow_self_payment=True), timeout=(timeout+60)): + if payment_response.status == 1 and rebalance.status == 0: #IN-FLIGHT + rebalance.payment_hash = payment_response.payment_hash rebalance.status = 1 - rebalance.payment_hash = response.payment_hash rebalance.save() - elif response.status == 2: + elif payment_response.status == 2: #SUCCESSFUL rebalance.status = 2 - elif response.status == 3: + elif payment_response.status == 3: #FAILURE - if response.failure_reason == 1: + if payment_response.failure_reason == 1: #FAILURE_REASON_TIMEOUT rebalance.status = 3 - elif response.failure_reason == 2: + elif payment_response.failure_reason == 2: #FAILURE_REASON_NO_ROUTE rebalance.status = 4 - elif response.failure_reason == 3: + elif payment_response.failure_reason == 3: #FAILURE_REASON_ERROR rebalance.status = 5 - elif response.failure_reason == 4: + elif payment_response.failure_reason == 4: #FAILURE_REASON_INCORRECT_PAYMENT_DETAILS rebalance.status = 6 - elif response.failure_reason == 5: + elif payment_response.failure_reason == 5: #FAILURE_REASON_INSUFFICIENT_BALANCE rebalance.status = 7 + elif payment_response.status == 0: + rebalance.status = 400 except Exception as e: if str(e.code()) == 'StatusCode.DEADLINE_EXCEEDED': rebalance.status = 408 @@ -65,6 +66,26 @@ def run_rebalancer(rebalance): finally: rebalance.stop = datetime.now() rebalance.save() + if rebalance.status == 2: + update_channel(stub, rebalance.last_hop_pubkey) + auto_rebalance_channels = Channels.objects.filter(is_active=True, is_open=True, private=False).annotate(percent_outbound=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).annotate(inbound_can=(((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity'))/Sum('ar_in_target')) + inbound_cans = auto_rebalance_channels.filter(remote_pubkey=rebalance.last_hop_pubkey).filter(auto_rebalance=True, inbound_can__gte=1) + if len(inbound_cans) > 0: + outbound_cans = list(auto_rebalance_channels.filter(auto_rebalance=False, percent_outbound__gte=F('ar_out_target')).values_list('chan_id', flat=True)) + next_rebalance = Rebalancer(value=rebalance.value, fee_limit=rebalance.fee_limit, outgoing_chan_ids=str(outbound_cans).replace('\'', ''), last_hop_pubkey=rebalance.last_hop_pubkey, target_alias=rebalance.target_alias, duration=rebalance.duration) + next_rebalance.save() + else: + next_rebalance = None + else: + next_rebalance = None + return next_rebalance + +def update_channel(stub, peer_pubkey): + channel = stub.ListChannels(ln.ListChannelsRequest(peer=bytes.fromhex(peer_pubkey))).channels[0] + db_channel = Channels.objects.filter(chan_id=channel.chan_id)[0] + db_channel.local_balance = channel.local_balance + db_channel.remote_balance = channel.remote_balance + db_channel.save() def auto_schedule(): #No rebalancer jobs have been scheduled, lets look for any channels with an auto_rebalance flag and make the best request if we find one @@ -74,7 +95,7 @@ def auto_schedule(): LocalSettings(key='AR-Enabled', value='0').save() enabled = 0 if enabled == 1: - auto_rebalance_channels = Channels.objects.filter(is_active=True, is_open=True).annotate(percent_outbound=(Sum('local_balance')*100)/Sum('capacity')).annotate(inbound_can=((Sum('remote_balance')*100)/Sum('capacity'))/Sum('ar_in_target')) + auto_rebalance_channels = Channels.objects.filter(is_active=True, is_open=True, private=False).annotate(percent_outbound=((Sum('local_balance')+Sum('pending_outbound'))*100)/Sum('capacity')).annotate(inbound_can=(((Sum('remote_balance')+Sum('pending_inbound'))*100)/Sum('capacity'))/Sum('ar_in_target')) if len(auto_rebalance_channels) > 0: if not LocalSettings.objects.filter(key='AR-Outbound%').exists(): LocalSettings(key='AR-Outbound%', value='0.75').save() @@ -96,17 +117,16 @@ def auto_schedule(): if target_fee_rate > 0 and target_fee_rate > target.remote_fee_rate: value_per_fee = int(1 / (target_fee_rate / 1000000)) if target_fee_rate <= max_fee_rate else int(1 / (max_fee_rate / 1000000)) target_value = int(target.ar_amt_target / value_per_fee) * value_per_fee - if target_value >= value_per_fee: + target_fee = int(target_value * (1 / value_per_fee)) + if target_value >= value_per_fee and target_fee > 0: if LocalSettings.objects.filter(key='AR-Time').exists(): target_time = int(LocalSettings.objects.filter(key='AR-Time')[0].value) else: LocalSettings(key='AR-Time', value='5').save() target_time = 5 - inbound_pubkey = Channels.objects.filter(chan_id=target.chan_id)[0] # TLDR: willing to pay 1 sat for every value_per_fee sats moved - target_fee = int(target_value * (1 / value_per_fee)) - if Rebalancer.objects.filter(last_hop_pubkey=inbound_pubkey.remote_pubkey).exclude(status=0).exists(): - last_rebalance = Rebalancer.objects.filter(last_hop_pubkey=inbound_pubkey.remote_pubkey).exclude(status=0).order_by('-id')[0] + if Rebalancer.objects.filter(last_hop_pubkey=target.remote_pubkey).exclude(status=0).exists(): + last_rebalance = Rebalancer.objects.filter(last_hop_pubkey=target.remote_pubkey).exclude(status=0).order_by('-id')[0] if not (last_rebalance.value != target_value or last_rebalance.status in [2, 6] or (last_rebalance.status in [3, 4, 5, 7, 400, 408] and (int((datetime.now() - last_rebalance.stop).total_seconds() / 60) > 30)) or (last_rebalance.status == 1 and (int((datetime.now() - last_rebalance.start).total_seconds() / 60) > 30))): continue print('Creating Auto Rebalance Request') @@ -115,7 +135,7 @@ def auto_schedule(): print('Target Value:', target.ar_amt_target) print('Target Fee:', target_fee) print('Target Time:', target_time) - Rebalancer(value=target_value, fee_limit=target_fee, outgoing_chan_ids=outbound_cans, last_hop_pubkey=inbound_pubkey.remote_pubkey, target_alias=inbound_pubkey.alias, duration=target_time).save() + Rebalancer(value=target_value, fee_limit=target_fee, outgoing_chan_ids=str(outbound_cans).replace('\'', ''), last_hop_pubkey=target.remote_pubkey, target_alias=target.alias, duration=target_time).save() def auto_enable(): if LocalSettings.objects.filter(key='AR-Autopilot').exists(): @@ -124,7 +144,7 @@ def auto_enable(): LocalSettings(key='AR-Autopilot', value='0').save() enabled = 0 if enabled == 1: - channels = Channels.objects.filter(is_active=True, is_open=True).annotate(outbound_percent=(Sum('local_balance')*1000)/Sum('capacity')).annotate(inbound_percent=(Sum('remote_balance')*1000)/Sum('capacity')) + channels = Channels.objects.filter(is_active=True, is_open=True, private=False).annotate(outbound_percent=((Sum('local_balance')+Sum('pending_outbound'))*1000)/Sum('capacity')).annotate(inbound_percent=((Sum('remote_balance')+Sum('pending_inbound'))*1000)/Sum('capacity')) filter_7day = datetime.now() - timedelta(days=7) forwards = Forwards.objects.filter(forward_date__gte=filter_7day) for channel in channels: @@ -160,7 +180,9 @@ def main(): auto_enable() auto_schedule() else: - run_rebalancer(rebalances[0]) + rebalance = rebalances[0] + while rebalance != None: + rebalance = run_rebalancer(rebalance) if __name__ == '__main__': main() \ No newline at end of file