mirror of
https://github.com/bitromortac/lndmanage.git
synced 2026-08-13 12:33:37 +02:00
Merge pull request #152 from bitromortac/2312-macaroon
default to readonly macaroon, version bump
This commit is contained in:
commit
1aa45a7d1b
13 changed files with 123 additions and 272 deletions
22
README.md
22
README.md
|
|
@ -401,7 +401,7 @@ If you are running an older version of lnd checkout the according
|
|||
[tag](https://github.com/bitromortac/lndmanage/releases).
|
||||
|
||||
### Requirements
|
||||
Installation of lndmanage requires `>=python3.8`, `lnd v0.15.x`, `python3-venv`.
|
||||
Installation of lndmanage requires `>=python3.8`, `lnd v0.16.x`, `python3-venv`.
|
||||
|
||||
#### Optional Requirements
|
||||
Depending on if you want to install from source dependency packages you may
|
||||
|
|
@ -413,11 +413,15 @@ This can be done when compiling with minimal build tags of `make && make install
|
|||
tags="routerrpc signrpc walletrpc"`. If you use precompiled binaries, you can
|
||||
ignore this.
|
||||
|
||||
#### Admin Macaroon and TLS cert needed
|
||||
#### Macaroon and TLS cert needed
|
||||
If you run this tool from a different host than the lnd host,
|
||||
make sure to copy `/path/to/.lnd/data/chain/bitcoin/mainnet/admin.macaroon`
|
||||
and `/path/to/.lnd/tls.cert` to your local machine, which you need for later
|
||||
configuration.
|
||||
make sure to copy `/path/to/.lnd/data/chain/bitcoin/mainnet/readonly.macaroon`
|
||||
and `/path/to/.lnd/tls.cert` to your local machine, which you need for later
|
||||
configuration.
|
||||
|
||||
Note that if you want to run `update-fees` or `openchannels` you will need to
|
||||
create a custom macaroon, see `scripts/bakemacaroon.sh`. Don't use
|
||||
`admin.macaroon`, which is not recommended for best security practices.
|
||||
|
||||
#### Signature verification
|
||||
Commits and releases are signed with key `1965 063F C13B EBE2` available via
|
||||
|
|
@ -435,7 +439,7 @@ You can install lndmanage via three methods:
|
|||
1\. Install from repository:
|
||||
```
|
||||
$ git clone https://github.com/bitromortac/lndmanage && cd lndmanage
|
||||
$ git checkout v0.15.0
|
||||
$ git checkout v0.16.0
|
||||
$ python3 -m venv venv
|
||||
$ git verify-commit HEAD
|
||||
$ source venv/bin/activate
|
||||
|
|
@ -448,11 +452,11 @@ $ pip install .
|
|||
* Download `.whl` and `.whl.asc` files
|
||||
* Verify signature and install:
|
||||
```
|
||||
$ gpg --verify lndmanage-0.15.0-py3-none-any.whl.asc lndmanage-0.15.0-py3-none-any.whl
|
||||
$ gpg --verify lndmanage-0.16.0-py3-none-any.whl.asc lndmanage-0.16.0-py3-none-any.whl
|
||||
$ python3 -m venv venv
|
||||
$ source venv/bin/activate
|
||||
$ pip install --upgrade pip setuptools wheel
|
||||
$ pip install lndmanage-0.15.0-py3-none-any.whl
|
||||
$ pip install lndmanage-0.16.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
3\. Install with pip (deprecated):
|
||||
|
|
@ -475,7 +479,7 @@ You need to set the environment variable `PYTHONIOENCODING` for proper encoding
|
|||
```
|
||||
$ git clone https://github.com/bitromortac/lndmanage
|
||||
$ cd lndmanage
|
||||
$ git checkout v0.15.0
|
||||
$ git checkout v0.16.0
|
||||
$ git verify-commit HEAD
|
||||
$ py -m venv venv
|
||||
$ .\venv\Scripts\activate
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ COPY --from=builder /root/.venv /root/.venv
|
|||
|
||||
ENV PATH="/root/.venv/bin:$PATH:/root"
|
||||
ENV TLS_CERT_FILE /root/aux/tls.cert
|
||||
ENV ADMIN_MACAROON_FILE /root/aux/admin.macaroon
|
||||
ENV MACAROON_FILE /root/aux/readonly.macaroon
|
||||
|
||||
# copy sources under /root/lndmanage
|
||||
WORKDIR /root/lndmanage
|
||||
|
|
|
|||
|
|
@ -8,19 +8,21 @@ To run `lndmanage` from a docker container:
|
|||
# so $HOME directory is /root
|
||||
|
||||
# build the container
|
||||
./build.sh
|
||||
./build.sh
|
||||
|
||||
# if you have local lnd node on host machine, point LND_HOME to your actual lnd directory:
|
||||
export LND_HOME=~/.lnd
|
||||
|
||||
# or alternatively if you have remote lnd node, specify paths to auth files explicitly:
|
||||
# export TLS_CERT_FILE=/path/to/tls.cert
|
||||
# export ADMIN_MACAROON_FILE=/path/to/admin.macaroon
|
||||
# export MACAROON_FILE=/path/to/readonly.macaroon
|
||||
# export LND_GRPC_HOST=<remoteip>:10009
|
||||
# note that in order to have all features available, you will need to create a
|
||||
# custom macaroon, see `scripts/bakemacaroon.sh`
|
||||
|
||||
# look into _settings.sh for more details on container configuration
|
||||
|
||||
# run lndmanage from the container:
|
||||
# run lndmanage from the container:
|
||||
./lndmanage.sh status
|
||||
|
||||
# lndmanage cache will be mapped to host folder at ./_volumes/lndmanage-cache
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# you have two possible ways how to specify ADMIN_MACAROON_FILE and TLS_CERT_FILE
|
||||
# you have two possible ways how to specify MACAROON_FILE and TLS_CERT_FILE
|
||||
# 1. specify LND_HOME if it is located on your local machine, we use default paths from there
|
||||
# 2. specify env variables ADMIN_MACAROON_FILE and TLS_CERT_FILE
|
||||
# 2. specify env variables MACAROON_FILE and TLS_CERT_FILE
|
||||
|
||||
# also you want to specify LND_GRPC_HOST if your node is remote
|
||||
# other config tweaks have to be done by changing lndmanage/home/config_template.ini
|
||||
|
||||
# note: docker uses network_mode: host
|
||||
|
||||
if [[ -z "$ADMIN_MACAROON_FILE" || -z "$TLS_CERT_FILE" ]]; then
|
||||
if [[ -z "$MACAROON_FILE" || -z "$TLS_CERT_FILE" ]]; then
|
||||
if [[ -z "$LND_HOME" ]]; then
|
||||
export LND_HOME="$HOME/.lnd"
|
||||
echo "warning: LND_HOME is not set, assuming '$LND_HOME'"
|
||||
fi
|
||||
fi
|
||||
|
||||
export ADMIN_MACAROON_FILE=${ADMIN_MACAROON_FILE:-$LND_HOME/data/chain/bitcoin/mainnet/admin.macaroon}
|
||||
export MACAROON_FILE=${MACAROON_FILE:-$LND_HOME/data/chain/bitcoin/mainnet/readonly.macaroon}
|
||||
export TLS_CERT_FILE=${TLS_CERT_FILE:-$LND_HOME/tls.cert}
|
||||
export LND_GRPC_HOST=${LND_GRPC_HOST:-127.0.0.1:10009}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# see docker/build.sh
|
||||
lnd_grpc_host = ${LND_GRPC_HOST}
|
||||
tls_cert_file = ${TLS_CERT_FILE}
|
||||
admin_macaroon_file = ${ADMIN_MACAROON_FILE}
|
||||
macaroon_file = ${MACAROON_FILE}
|
||||
|
||||
[logging]
|
||||
loglevel = INFO
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ if [[ ! -e "$LNDMANAGE_AUX_DIR" ]]; then
|
|||
fi
|
||||
LNDMANAGE_AUX_DIR_ABSOLUTE=$(abs_path "$LNDMANAGE_AUX_DIR")
|
||||
|
||||
# we use LNDMANAGE_AUX_DIR as ad-hoc volume to pass admin.macaroon and tls.cert into our container
|
||||
# we use LNDMANAGE_AUX_DIR as ad-hoc volume to pass readonly.macaroon and tls.cert into our container
|
||||
# it is mapped to /root/aux, config_template.ini assumes that
|
||||
cp "$ADMIN_MACAROON_FILE" "$LNDMANAGE_AUX_DIR/admin.macaroon"
|
||||
cp "$MACAROON_FILE" "$LNDMANAGE_AUX_DIR/readonly.macaroon"
|
||||
cp "$TLS_CERT_FILE" "$LNDMANAGE_AUX_DIR/tls.cert"
|
||||
|
||||
if [[ -n "$LNDMANAGE_VERBOSE" ]]; then
|
||||
|
|
@ -36,7 +36,7 @@ exec docker run \
|
|||
-v "$LNDMANAGE_AUX_DIR_ABSOLUTE:/root/aux" \
|
||||
-e "LND_GRPC_HOST=${LND_GRPC_HOST}" \
|
||||
-e "TLS_CERT_FILE=/root/aux/tls.cert" \
|
||||
-e "ADMIN_MACAROON_FILE=/root/aux/admin.macaroon" \
|
||||
-e "MACAROON_FILE=/root/aux/readonly.macaroon" \
|
||||
-ti \
|
||||
lndmanage:local \
|
||||
run-lndmanage "$@"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = '0.15.0'
|
||||
__version__ = '0.16.0'
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ def check_or_create_configuration(home_dir):
|
|||
:type home_dir: str
|
||||
"""
|
||||
if not os.path.exists(home_dir): # user runs for the first time
|
||||
print(f"Running lndmanage for the first time.")
|
||||
print("Running lndmanage for the first time.")
|
||||
print(f"Creating configuration folder at {home_dir}.")
|
||||
print("The default path can be overridden by setting the "
|
||||
"LNDMANAGE_HOME environment variable.")
|
||||
|
|
@ -44,17 +44,18 @@ def check_or_create_configuration(home_dir):
|
|||
lnd_home = os.path.expanduser('~/.lnd')
|
||||
lnd_grpc_host = 'localhost:10009'
|
||||
|
||||
admin_macaroon_path = os.path.join(
|
||||
lnd_home, 'data/chain/bitcoin/mainnet/admin.macaroon')
|
||||
macaroon_path = os.path.join(
|
||||
lnd_home, 'data/chain/bitcoin/mainnet/readonly.macaroon')
|
||||
tls_cert_path = os.path.join(
|
||||
lnd_home, 'tls.cert')
|
||||
if os.path.exists(lnd_home):
|
||||
remote = False
|
||||
print(f"Detected a local lnd configuration folder {lnd_home}.", )
|
||||
print("Will use admin.macaroon and tls.cert from this directory.")
|
||||
print("Will use macaroon and tls.cert from this directory.")
|
||||
else:
|
||||
remote = True
|
||||
print(f"IF LND RUNS ON A REMOTE HOST, CONFIGURE {home_dir}/config.ini.")
|
||||
print(
|
||||
f"IF LND RUNS ON A REMOTE HOST, CONFIGURE {home_dir}/config.ini.")
|
||||
|
||||
# build config file
|
||||
config = configparser.ConfigParser()
|
||||
|
|
@ -64,7 +65,7 @@ def check_or_create_configuration(home_dir):
|
|||
config.read(config_template_path)
|
||||
|
||||
config['network']['lnd_grpc_host'] = str(lnd_grpc_host)
|
||||
config['network']['admin_macaroon_file'] = str(admin_macaroon_path)
|
||||
config['network']['macaroon_file'] = str(macaroon_path)
|
||||
config['network']['tls_cert_file'] = str(tls_cert_path)
|
||||
|
||||
config_path = os.path.join(home_dir, 'config.ini')
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class Lncli(object):
|
|||
|
||||
cert_file = os.path.expanduser(config['network']['tls_cert_file'])
|
||||
macaroon_file = \
|
||||
os.path.expanduser(config['network']['admin_macaroon_file'])
|
||||
os.path.expanduser(config['network']['macaroon_file'])
|
||||
lnd_host = config['network']['lnd_grpc_host']
|
||||
|
||||
# assemble the command for lncli for execution with flags
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import asyncio
|
||||
import binascii
|
||||
import codecs
|
||||
from collections import OrderedDict, defaultdict
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
from typing import List, TYPE_CHECKING, Optional, Dict
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
import grpc
|
||||
from grpc._channel import _Rendezvous
|
||||
|
|
@ -20,11 +19,7 @@ import lndmanage.grpc_compiled.walletkit_pb2 as lndwalletkit
|
|||
import lndmanage.grpc_compiled.walletkit_pb2_grpc as lndwalletkitrpc
|
||||
|
||||
from lndmanage.lib.network import Network
|
||||
from lndmanage.lib.exceptions import PaymentTimeOut, NoRoute, OurNodeFailure
|
||||
from lndmanage.lib import exceptions
|
||||
from lndmanage.lib.ln_utilities import (
|
||||
extract_short_channel_id_from_string,
|
||||
convert_short_channel_id_to_channel_id,
|
||||
convert_channel_id_to_short_channel_id,
|
||||
local_balance_to_unbalancedness
|
||||
)
|
||||
|
|
@ -33,9 +28,6 @@ from lndmanage.lib.user import yes_no_question
|
|||
from lndmanage.lib.utilities import convert_dictionary_number_strings_to_ints
|
||||
from lndmanage import settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lndmanage.lib.routing import Route
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addHandler(logging.NullHandler())
|
||||
|
|
@ -73,7 +65,8 @@ class LndNode:
|
|||
|
||||
def __init__(self, config_file: Optional[str] = None,
|
||||
lnd_home: Optional[str] = None,
|
||||
lnd_host: Optional[str] = None, regtest=False):
|
||||
lnd_host: Optional[str] = None, regtest=False,
|
||||
use_admin=False):
|
||||
"""
|
||||
:param config_file: path to the config file
|
||||
:param lnd_home: path to lnd home folder
|
||||
|
|
@ -93,20 +86,22 @@ class LndNode:
|
|||
# configure lndmanage home: (TODO: separate into config)
|
||||
# if no lnd_home is given, then use the paths from the config,
|
||||
# else override them with default file paths in lnd_home
|
||||
macaroon = 'readonly.macaroon' if not use_admin else 'admin.macaroon'
|
||||
if self.lnd_home is not None:
|
||||
self.cert_file_path = os.path.join(self.lnd_home, 'tls.cert')
|
||||
bitcoin_network = 'regtest' if self.regtest else 'mainnet'
|
||||
self.macaroon_file_path = os.path.join(
|
||||
self.lnd_home, 'data/chain/bitcoin/',
|
||||
bitcoin_network, 'admin.macaroon')
|
||||
bitcoin_network, macaroon)
|
||||
if self.lnd_host is None:
|
||||
raise ValueError('if lnd_home is given, lnd_host must be given')
|
||||
raise ValueError(
|
||||
'if lnd_home is given, lnd_host must be given')
|
||||
else:
|
||||
self.cert_file_path = os.path.expanduser(
|
||||
self.config['network']['tls_cert_file']
|
||||
)
|
||||
self.macaroon_file_path = os.path.expanduser(
|
||||
self.config['network']['admin_macaroon_file']
|
||||
self.config['network']['macaroon_file']
|
||||
)
|
||||
self.lnd_host = self.config['network']['lnd_grpc_host']
|
||||
|
||||
|
|
@ -127,7 +122,7 @@ class LndNode:
|
|||
macaroon_bytes = f.read()
|
||||
macaroon = codecs.encode(macaroon_bytes, 'hex')
|
||||
except FileNotFoundError:
|
||||
logger.error("admin.macaroon not found, please configure %s.",
|
||||
logger.error("macaroon not found, please configure %s.",
|
||||
self.config_file)
|
||||
exit(1)
|
||||
|
||||
|
|
@ -140,8 +135,8 @@ class LndNode:
|
|||
return grpc.composite_channel_credentials(cert_creds, auth_creds)
|
||||
|
||||
async def connect_async_rpcs(self):
|
||||
# This needs to be run within an async context, the loop is being used in the
|
||||
# rpc connections.
|
||||
# This needs to be run within an async context, the loop is being used
|
||||
# in the rpc connections.
|
||||
logger.debug("Connecting async rpcs.")
|
||||
|
||||
self._async_channel = grpc.aio.secure_channel(
|
||||
|
|
@ -202,108 +197,6 @@ class LndNode:
|
|||
channel_dict = convert_dictionary_number_strings_to_ints(channel_dict)
|
||||
return channel_dict
|
||||
|
||||
@staticmethod
|
||||
def lnd_hops(hops) -> List[lnd.Hop]:
|
||||
return [lnd.Hop(**hop) for hop in hops]
|
||||
|
||||
def _to_lnd_route(self, route: 'Route') -> lnd.Route:
|
||||
"""
|
||||
Converts a cleartext route to an lnd route.
|
||||
"""
|
||||
hops = self.lnd_hops(route.hops)
|
||||
lnd_route = lnd.Route(
|
||||
total_time_lock=route.total_time_lock,
|
||||
total_fees=route.total_fee_msat // 1000,
|
||||
total_amt=route.total_amt_msat // 1000,
|
||||
hops=hops,
|
||||
total_fees_msat=route.total_fee_msat,
|
||||
total_amt_msat=route.total_amt_msat
|
||||
)
|
||||
return lnd_route
|
||||
|
||||
def get_invoice(self, amt_msat: int, memo: str) -> lnd.Invoice:
|
||||
"""
|
||||
Creates a new invoice with amt_msat and memo.
|
||||
|
||||
:param amt_msat: int
|
||||
:param memo: str
|
||||
:return: Hash of invoice preimage.
|
||||
"""
|
||||
invoice = self._rpc.AddInvoice(lnd.Invoice(
|
||||
value=amt_msat // 1000, memo=memo))
|
||||
return invoice
|
||||
|
||||
def get_rebalance_invoice(self, memo) -> lnd.Invoice:
|
||||
"""
|
||||
Creates a zero amount invoice and gives back it's hash.
|
||||
|
||||
:param memo: Comment for the invoice.
|
||||
:return: Hash of the invoice preimage.
|
||||
"""
|
||||
invoice = self._rpc.AddInvoice(lnd.Invoice(value=0, memo=memo))
|
||||
return invoice
|
||||
|
||||
def send_to_route(self, route: lnd.Route, payment_hash: bytes):
|
||||
"""Takes a route and sends to it."""
|
||||
|
||||
request = lndrouter.SendToRouteRequest(
|
||||
route=route,
|
||||
payment_hash=payment_hash,
|
||||
)
|
||||
|
||||
try:
|
||||
payment = self._routerrpc.SendToRouteV2(
|
||||
request, timeout=GRPC_TIMEOUT_SEC,
|
||||
)
|
||||
|
||||
except grpc.RpcError as e:
|
||||
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
|
||||
raise PaymentTimeOut
|
||||
|
||||
raise
|
||||
|
||||
if payment.HasField('failure'):
|
||||
failure = payment.failure # type: lnd.Failure.FailureCode
|
||||
logger.debug(f"Routing failure: {failure}")
|
||||
if failure.failure_source_index == 0:
|
||||
raise OurNodeFailure("Not enough funds?")
|
||||
if failure.code == 12:
|
||||
raise exceptions.FeeInsufficient(payment)
|
||||
elif failure.code == 13:
|
||||
raise exceptions.IncorrectCLTVExpiry(payment)
|
||||
elif failure.code == 14:
|
||||
raise exceptions.ChannelDisabled(payment)
|
||||
elif failure.code == 15:
|
||||
raise exceptions.TemporaryChannelFailure(payment)
|
||||
elif failure.code == 18:
|
||||
raise exceptions.UnknownNextPeer(payment)
|
||||
elif failure.code == 19:
|
||||
raise exceptions.TemporaryNodeFailure(payment)
|
||||
else:
|
||||
logger.info(f"Unknown error: code: {failure.code}")
|
||||
raise exceptions.TemporaryChannelFailure(payment)
|
||||
|
||||
return payment
|
||||
|
||||
def build_route(self, amt_msat: int, outgoing_chan_id: int,
|
||||
hop_pubkeys: List[str], payment_addr: bytes) -> lnd.Route:
|
||||
""" Queries the routerrpc endpoint to build a route."""
|
||||
|
||||
final_cltv_delta = 144
|
||||
|
||||
# Convert hop_pubkeys to List[bytes]
|
||||
hop_pubkeys = [bytes.fromhex(n) for n in hop_pubkeys]
|
||||
|
||||
request = lndrouter.BuildRouteRequest(
|
||||
amt_msat = amt_msat,
|
||||
final_cltv_delta = final_cltv_delta,
|
||||
outgoing_chan_id = outgoing_chan_id,
|
||||
hop_pubkeys = hop_pubkeys,
|
||||
payment_addr = payment_addr,
|
||||
)
|
||||
|
||||
return self._routerrpc.BuildRoute(request, timeout=5 * 60).route
|
||||
|
||||
def get_raw_network_graph(self):
|
||||
try:
|
||||
graph = self._rpc.DescribeGraph(lnd.ChannelGraphRequest())
|
||||
|
|
@ -400,11 +293,15 @@ class LndNode:
|
|||
# interested in node2
|
||||
policies = edge_info['policies']
|
||||
if edge_info['node1_pub'] == self.pub_key:
|
||||
policy_peer = policies[edge_info['node2_pub'] > edge_info['node1_pub']]
|
||||
policy_local = policies[edge_info['node1_pub'] > edge_info['node2_pub']]
|
||||
policy_peer = policies[edge_info['node2_pub']
|
||||
> edge_info['node1_pub']]
|
||||
policy_local = policies[edge_info['node1_pub']
|
||||
> edge_info['node2_pub']]
|
||||
else: # interested in node1
|
||||
policy_peer = policies[edge_info['node1_pub'] > edge_info['node2_pub']]
|
||||
policy_local = policies[edge_info['node2_pub'] > edge_info['node1_pub']]
|
||||
policy_peer = policies[edge_info['node1_pub']
|
||||
> edge_info['node2_pub']]
|
||||
policy_local = policies[edge_info['node2_pub']
|
||||
> edge_info['node1_pub']]
|
||||
except KeyError:
|
||||
# if channel is unknown in describegraph
|
||||
# we need to set the fees to some error value
|
||||
|
|
@ -492,7 +389,8 @@ class LndNode:
|
|||
|
||||
def node_id_to_channel_ids(self, open_only=False) -> Dict[str, List[int]]:
|
||||
node_channels_mapping = defaultdict(list)
|
||||
for cid, nid in self.channel_id_to_node_id(open_only=open_only).items():
|
||||
mappings = self.channel_id_to_node_id(open_only=open_only)
|
||||
for cid, nid in mappings.items():
|
||||
node_channels_mapping[nid].append(cid)
|
||||
return node_channels_mapping
|
||||
|
||||
|
|
@ -513,22 +411,29 @@ class LndNode:
|
|||
channels = self.get_open_channels(public_only=False, active_only=False)
|
||||
return channels
|
||||
|
||||
def get_unbalanced_channels(self, unbalancedness_greater_than=0.0, excluded_channels: List[int] = None, public_only=True, active_only=True):
|
||||
def get_unbalanced_channels(
|
||||
self, unbalancedness_greater_than=0.0,
|
||||
excluded_channels: List[int] = None,
|
||||
public_only=True, active_only=True):
|
||||
"""
|
||||
Gets all channels which have an absolute unbalancedness
|
||||
(-1...1, -1 for outbound unbalanced, 1 for inbound unbalanced)
|
||||
larger than unbalancedness_greater_than.
|
||||
|
||||
:param unbalancedness_greater_than: unbalancedness interval, default returns all channels
|
||||
:return: all channels which are more unbalanced than the specified interval
|
||||
:param unbalancedness_greater_than: unbalancedness interval,
|
||||
default returns all channels
|
||||
:return: all channels which are more unbalanced than the
|
||||
specified interval
|
||||
"""
|
||||
self.public_active_channels = \
|
||||
self.get_open_channels(public_only=public_only, active_only=active_only)
|
||||
self.get_open_channels(
|
||||
public_only=public_only, active_only=active_only)
|
||||
channels = {
|
||||
k: c for k, c in self.public_active_channels.items()
|
||||
if abs(c['unbalancedness']) >= unbalancedness_greater_than
|
||||
}
|
||||
channels = {k: v for k, v in channels.items() if k not in (excluded_channels if excluded_channels else [])}
|
||||
channels = {k: v for k, v in channels.items() if k not in (
|
||||
excluded_channels if excluded_channels else [])}
|
||||
return channels
|
||||
|
||||
def get_channel_fee_policies(self):
|
||||
|
|
@ -632,99 +537,6 @@ class LndNode:
|
|||
}
|
||||
return closed_channels_dict
|
||||
|
||||
@staticmethod
|
||||
def handle_payment_error(payment_error):
|
||||
"""
|
||||
Handles payment errors and determines the failed channel.
|
||||
|
||||
:param payment_error:
|
||||
:return: int, channel_id of the failed channel.
|
||||
"""
|
||||
if "TemporaryChannelFailure" in payment_error:
|
||||
logger.error(" Encountered temporary channel failure.")
|
||||
short_channel_groups = extract_short_channel_id_from_string(payment_error)
|
||||
channel_id = convert_short_channel_id_to_channel_id(*short_channel_groups)
|
||||
return channel_id
|
||||
|
||||
def queryroute_external(self, source_pubkey, target_pubkey, amt_msat,
|
||||
ignored_nodes=(), ignored_channels={},
|
||||
use_mc=False):
|
||||
"""
|
||||
Queries the lnd node for a route.
|
||||
|
||||
Channels and nodes can be ignored if they failed before.
|
||||
|
||||
:param source_pubkey: source node public key
|
||||
:type source_pubkey: str
|
||||
:param target_pubkey: target node public key
|
||||
:type target_pubkey: str
|
||||
:param amt_msat: amount to send in msat
|
||||
:type amt_msat: int
|
||||
:param ignored_nodes: ignored node pubilc keys for the route
|
||||
:type ignored_nodes: list[str]
|
||||
:param ignored_channels: ignored channel directions for the route
|
||||
:type ignored_channels: dict
|
||||
:param use_mc: true if mission control should be used to blacklist
|
||||
channels
|
||||
:type use_mc: bool
|
||||
:return: route expressed in terms of short channel ids
|
||||
:rtype: list[int]
|
||||
"""
|
||||
amt_sat = amt_msat // 1000
|
||||
|
||||
# put safety margin when using mc based routing
|
||||
# reason is that routes will not be diverse when sending with a larger
|
||||
# amount later on due to fees
|
||||
# the fees for the route are somewhat accounted for by the margin
|
||||
if use_mc:
|
||||
amt_sat = int(amt_sat * 1.02)
|
||||
|
||||
# have a safety max fee in sat
|
||||
max_fee = 10000
|
||||
|
||||
# convert ignored nodes to api format
|
||||
if ignored_nodes:
|
||||
ignored_nodes_api = [bytes.fromhex(n) for n in ignored_nodes]
|
||||
else:
|
||||
ignored_nodes_api = []
|
||||
|
||||
# convert ignored channels to api format
|
||||
if ignored_channels:
|
||||
ignored_channels_api = []
|
||||
for c, cv in ignored_channels.items():
|
||||
direction_reverse = cv['source'] > cv['target']
|
||||
ignored_channels_api.append(
|
||||
lnd.EdgeLocator(channel_id=c,
|
||||
direction_reverse=direction_reverse))
|
||||
else:
|
||||
ignored_channels_api = []
|
||||
|
||||
logger.debug(f"Ignored for queryroutes: channels: "
|
||||
f"{ignored_channels_api}, nodes: {ignored_nodes_api}")
|
||||
|
||||
request = lnd.QueryRoutesRequest(
|
||||
pub_key=target_pubkey,
|
||||
amt=amt_sat,
|
||||
final_cltv_delta=0,
|
||||
fee_limit=lnd.FeeLimit(fixed=max_fee),
|
||||
ignored_nodes=ignored_nodes_api,
|
||||
ignored_edges=ignored_channels_api,
|
||||
source_pub_key=source_pubkey,
|
||||
use_mission_control=use_mc,
|
||||
)
|
||||
try:
|
||||
response = self._rpc.QueryRoutes(request)
|
||||
except Exception as e:
|
||||
if "unable to find a path" in e.details():
|
||||
raise NoRoute
|
||||
else:
|
||||
raise e
|
||||
|
||||
# We give back only one route, as multiple routes will be deprecated
|
||||
channel_route = [h.chan_id for h in response.routes[0].hops]
|
||||
|
||||
return channel_route
|
||||
|
||||
def get_node_info(self, pub_key):
|
||||
"""
|
||||
Retrieves information on a node with a specific pub key.
|
||||
|
|
@ -778,8 +590,10 @@ class LndNode:
|
|||
balancedness_local = 0
|
||||
balancedness_remote = 0
|
||||
else:
|
||||
balancedness_local = self.total_local_balance / self.total_capacity
|
||||
balancedness_remote = self.total_remote_balance / self.total_capacity
|
||||
balancedness_local = self.total_local_balance \
|
||||
/ self.total_capacity
|
||||
balancedness_remote = self.total_remote_balance \
|
||||
/ self.total_capacity
|
||||
logger.info(f"alias: {self.alias}")
|
||||
logger.info(f"pub key: {self.pub_key}")
|
||||
logger.info(f"blockheight: {self.blockheight}")
|
||||
|
|
@ -788,9 +602,15 @@ class LndNode:
|
|||
logger.info(f"active channels: {self.total_active_channels}")
|
||||
logger.info(f"private channels: {self.total_private_channels}")
|
||||
logger.info(f"capacity: {self.total_capacity}")
|
||||
logger.info(f"balancedness: l:{balancedness_local:.2%} r:{balancedness_remote:.2%}")
|
||||
logger.info(f"total satoshis received (current channels): {self.total_satoshis_received}")
|
||||
logger.info(f"total satoshis sent (current channels): {self.total_satoshis_sent}")
|
||||
logger.info(
|
||||
f"balancedness: l:{balancedness_local:.2%} "
|
||||
f"r:{balancedness_remote:.2%}")
|
||||
logger.info(
|
||||
"total satoshis received (current channels): "
|
||||
f"{self.total_satoshis_received}")
|
||||
logger.info(
|
||||
"total satoshis sent (current channels): "
|
||||
f"{self.total_satoshis_sent}")
|
||||
|
||||
def open_channels(self, pubkeys: List[bytes],
|
||||
amounts_sat: List[int],
|
||||
|
|
@ -808,8 +628,8 @@ class LndNode:
|
|||
private=private,
|
||||
))
|
||||
|
||||
logger.info("\n>>> WARNING: This feature is new, use at your own risk. "
|
||||
"Please check the above output carefully.\n")
|
||||
logger.info("\n>>> WARNING: This feature is new, use at your own "
|
||||
"risk. Please check the above output carefully.\n")
|
||||
logger.info("\n>>> Do you want to open the channel(s) (y/n)?")
|
||||
if not test:
|
||||
if not yes_no_question('no'):
|
||||
|
|
@ -834,7 +654,8 @@ class LndNode:
|
|||
raise ValueError(f"pubkey of unknown format {pubkey}")
|
||||
info = self.get_node_info(pubkey)
|
||||
if not info['addresses']:
|
||||
raise ConnectionRefusedError(f"Could not find connection address for {pubkey}.")
|
||||
raise ConnectionRefusedError(
|
||||
f"Could not find connection address for {pubkey}.")
|
||||
logger.info(">>> Connecting to channel peer candidates.")
|
||||
for pubkey in pubkeys:
|
||||
info = self.get_node_info(pubkey)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# network settings
|
||||
[network]
|
||||
lnd_grpc_host = IP:10009
|
||||
# tls and admin macaroon can be found in .lnd folder
|
||||
# tls and macaroon can be found in .lnd folder
|
||||
tls_cert_file = /path/to/tls.cert
|
||||
admin_macaroon_file = /path/to/admin.macaroon
|
||||
# see `scripts/bakemacaroon.sh` to create an lndmanage macaroon to use
|
||||
# openchannels and update-fees
|
||||
macaroon_file = /path/to/readonly.macaroon
|
||||
|
||||
[logging]
|
||||
loglevel = INFO
|
||||
|
|
@ -19,4 +21,4 @@ loglevel = INFO
|
|||
[excluded-channels-fee-opt]
|
||||
# channels which are excluded from the fee optimization via the update-fees
|
||||
# command can be listed here by their channel ids, e.g.,
|
||||
# 635263839283742663=ignore
|
||||
# 635263839283742663=ignore
|
||||
|
|
|
|||
20
scripts/bakemacaroon.sh
Executable file
20
scripts/bakemacaroon.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This command creates a macaroon containing permissions to call all the
|
||||
# enpoints lndmanage uses. This is more secure than using an admin macaroon.
|
||||
|
||||
lncli bakemacaroon \
|
||||
--save_to lndmanage.macaroon \
|
||||
uri:/lnrpc.Lightning/GetInfo \
|
||||
uri:/lnrpc.Lightning/GetChanInfo \
|
||||
uri:/lnrpc.Lightning/GetNodeInfo \
|
||||
uri:/lnrpc.Lightning/DescribeGraph \
|
||||
uri:/lnrpc.Lightning/ListChannels \
|
||||
uri:/lnrpc.Lightning/FeeReport \
|
||||
uri:/lnrpc.Lightning/UpdateChannelPolicy \
|
||||
uri:/lnrpc.Lightning/ForwardingHistory \
|
||||
uri:/lnrpc.Lightning/ClosedChannels \
|
||||
uri:/lnrpc.Lightning/BatchOpenChannel \
|
||||
uri:/lnrpc.Lightning/ConnectPeer \
|
||||
uri:/walletrpc.WalletKit/ListUnspent \
|
||||
uri:/routerrpc.Router/QueryMissionControl
|
||||
|
|
@ -2,6 +2,15 @@
|
|||
import os
|
||||
import shutil
|
||||
from unittest import TestCase
|
||||
import logging.config
|
||||
|
||||
from lnregtest.lib.network import Network
|
||||
|
||||
from lndmanage import settings
|
||||
from lndmanage.lib.node import LndNode
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# testing base folder
|
||||
test_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
|
@ -14,19 +23,11 @@ test_data_dir = os.path.join(test_dir, 'test_data')
|
|||
lndmanage_home = os.path.join(test_data_dir, 'lndmanage')
|
||||
os.makedirs(lndmanage_home, exist_ok=True)
|
||||
|
||||
# create empty config and set env var to not trigger error when importing settings
|
||||
# create empty config and set env var to not trigger error when importing
|
||||
# settings
|
||||
open(os.path.join(lndmanage_home, 'config.ini'), 'a').close()
|
||||
os.environ.setdefault('LNDMANAGE_HOME', lndmanage_home)
|
||||
|
||||
from lnregtest.lib.network import Network
|
||||
|
||||
from lndmanage.lib.node import LndNode
|
||||
|
||||
import logging.config
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
from lndmanage import settings
|
||||
settings.CACHING_RETENTION_MINUTES = 0
|
||||
|
||||
# constants for testing
|
||||
|
|
@ -95,7 +96,7 @@ class TestNetwork(TestCase):
|
|||
self.lndnode = LndNode(
|
||||
lnd_home=master_node_data_dir,
|
||||
lnd_host='localhost:' + str(master_node_port),
|
||||
regtest=True
|
||||
regtest=True, use_admin=True,
|
||||
)
|
||||
self.graph_test()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue